feat(investment): FD screens - purchase, maturity/premature/renewal, interest entry and reminders

This commit is contained in:
2026-10-16 17:24:13 +05:30
parent e1b33827ea
commit d584af8e54
9 changed files with 1356 additions and 866 deletions
@@ -403,6 +403,21 @@
</div>
</div>
}
@if (preview?.investmentFdPending?.length) {
<div class="bd-warn">
<mat-icon>notifications_active</mat-icon>
<div>
<strong>Reminder: FD investment entries are pending.</strong>
@for (t of preview.investmentFdPending; track t.fdId) {
<div>
{{ t.lineType === 'MATURITY' ? 'Maturity' : t.lineType === 'PAYOUT' ? 'Interest payout' : 'Provision' }}
· {{ t.partyName }} · FD {{ t.dealNo }} · {{ t.intDate }} · ₹{{ t.amount | number: '1.2-2' }}
</div>
}
Post them under Investment → FD Investment. You can still close the day.
</div>
</div>
}
@if (preview?.savingsInterestPending?.length) {
<div class="bd-warn">
<mat-icon>warning</mat-icon>
+22
View File
@@ -64,6 +64,28 @@
</a>
}
<!-- FDs placed with other banks: interest vouchers and maturities due -->
@if (pendingFd.length) {
<a
class="investment-interest-alert"
routerLink="/investment/fd"
[queryParams]="{ tab: 'interest', fd: pendingFd[0].fdId }"
>
<span class="iia-icon">🔔</span>
<span class="iia-text">
<strong>{{ pendingFd.length }} FD investment entry(ies) pending</strong>
@for (t of pendingFd; track t.fdId) {
<span class="iia-item">
{{ t.lineType === 'MATURITY' ? 'Maturity' : t.lineType === 'PAYOUT' ? 'Interest payout' : 'Provision' }} ·
{{ t.partyName }} · FD {{ t.dealNo }} ·
{{ t.overdue ? 'overdue since ' + t.intDate : t.postable ? 'due today' : 'due ' + t.intDate }}
</span>
}
</span>
<span class="iia-action">Open FD Investment →</span>
</a>
}
<!-- Quick Action Cards -->
<div class="quick-actions">
<a class="qa-card" [routerLink]="['/dashboard']" #buttonDashboard>
+7 -1
View File
@@ -26,7 +26,7 @@ import { PopoverService } from '../configuration-wizard/popover/popover.service'
import { ConfigurationWizardService } from '../configuration-wizard/configuration-wizard.service';
import { SettingsService } from 'app/settings/settings.service';
import { DscService } from 'app/users/dsc/dsc.service';
import { InvestmentInterestDueDto, InvestmentService } from 'app/investment/investment.service';
import { InvestmentFdDueDto, InvestmentInterestDueDto, InvestmentService } from 'app/investment/investment.service';
/** Custom Components */
import { NextStepDialogComponent } from '../configuration-wizard/next-step-dialog/next-step-dialog.component';
@@ -122,6 +122,8 @@ export class HomeComponent implements OnInit, AfterViewInit {
/** Investment interest lines due now or within two days, shown on the home screen so nobody has to look for them. */
pendingInvestmentInterest: InvestmentInterestDueDto[] = [];
/** FDs placed with other banks: interest vouchers and maturities due now or within two days. */
pendingFd: InvestmentFdDueDto[] = [];
private readonly investmentService = inject(InvestmentService);
/** Silent when the user cannot see investments or nothing is due — the strip simply does not render. */
@@ -130,6 +132,10 @@ export class HomeComponent implements OnInit, AfterViewInit {
next: (due) => (this.pendingInvestmentInterest = due ?? []),
error: () => (this.pendingInvestmentInterest = [])
});
this.investmentService.getFdDue().subscribe({
next: (due) => (this.pendingFd = due ?? []),
error: () => (this.pendingFd = [])
});
}
/**
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -1906,6 +1906,10 @@ export interface PartyMaster {
glAccountId?: number;
glCode?: string;
glName?: string;
/** Interest income GL of the FDs placed with this bank (Bond Type FD). */
interestGlAccountId?: number;
interestGlCode?: string;
interestGlName?: string;
rbiCode?: string;
/** Exposure limit for this party (₹). */
limit?: number | null;
@@ -162,6 +162,17 @@
}
</select>
</label>
@if (partyForm.bondType === 'FD') {
<label class="inv-form-field">
<span>* Interest GL</span>
<select [(ngModel)]="partyForm.interestGlAccountId">
<option [ngValue]="undefined">-- Select --</option>
@for (g of interestGlOptions; track g.id) {
<option [ngValue]="g.id">{{ g.glCode }} — {{ g.name }}</option>
}
</select>
</label>
}
<label class="inv-form-field"><span>RBI Code</span><input type="text" [(ngModel)]="partyForm.rbiCode" /></label>
<label class="inv-form-field"
><span>Limit (₹)</span><input type="number" min="0" [(ngModel)]="partyForm.limit"
@@ -126,6 +126,8 @@ export class InvestmentMastersComponent implements OnInit {
readonly slrTypeOptions = SLR_TYPE_OPTIONS;
/** Investment GLs (under the 100700 INVESTMENTS head) a party or script can be mapped to — not loans, banks or test GLs. */
investmentGlOptions: InvestmentGlAccount[] = [];
/** Income GLs under 400400 INCOME FROM INVESTMENTS, for an FD bank's interest. */
interestGlOptions: InvestmentGlAccount[] = [];
newSubType: Partial<SubTypeMaster> = {};
newBroker: Partial<BrokerMaster> = {};
newControlParam: Partial<ControlParameter> = {};
@@ -143,6 +145,10 @@ export class InvestmentMastersComponent implements OnInit {
)),
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
});
this.investmentService.getIncomeGlAccounts().subscribe({
next: (gls) => (this.interestGlOptions = gls.filter((g) => g.glCode.startsWith('4004'))),
error: (err) => this.toast(`Could not load income GLs: ${apiErrorMessage(err)}`)
});
this.reloadMasters();
}
@@ -182,6 +188,10 @@ export class InvestmentMastersComponent implements OnInit {
this.toast('Enter Bond Type, Party Name and GL A/c No.');
return;
}
if (f.bondType === 'FD' && !f.interestGlAccountId) {
this.toast('An FD bank needs its Interest GL as well.');
return;
}
const body = {
bondType: f.bondType,
partyName: f.partyName,
@@ -194,6 +204,7 @@ export class InvestmentMastersComponent implements OnInit {
fax: f.fax ?? '',
bankType: f.bankType ?? '',
glAccountId: Number(f.glAccountId),
interestGlAccountId: f.bondType === 'FD' && f.interestGlAccountId ? Number(f.interestGlAccountId) : null,
rbiCode: f.rbiCode ?? '',
limit: f.limit ?? null,
leiNo: f.leiNo ?? '',
+197
View File
@@ -39,6 +39,9 @@ export interface InvestmentPartyDto {
glAccountId: number;
glCode: string;
glName: string;
interestGlAccountId?: number;
interestGlCode?: string;
interestGlName?: string;
rbiCode?: string;
limit?: number;
leiNo?: string;
@@ -49,6 +52,126 @@ export interface InvestmentPartyDto {
active: boolean;
}
/** Where an FD stands against the RBI limits on deposits with other banks (20% overall, 5% with one bank). */
export interface InvestmentFdLimitDto {
asOnDate: string;
totalDeposits?: number;
glTotalDeposits: number;
grossLimit?: number;
fdOutstanding: number;
bankBalances: number;
grossAfter: number;
bankLimit?: number;
partyName?: string;
bankAfter: number;
newAmount: number;
withinLimits: boolean;
message: string;
}
export type FdPayout = 'MATURITY' | 'HALF_YEARLY' | 'YEARLY';
export type FdCalcType = 'SIMPLE' | 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY';
/** An FD the bank has placed with another bank (dates DD-MM-YYYY); also the purchase preview. */
export interface InvestmentFdDto {
id?: number;
dealNo?: string;
partyId?: number;
partyCode?: string;
partyName?: string;
certNo?: string;
certDate?: string;
dealDate?: string;
principal?: number;
intRate?: number;
maturityDate?: string;
days: number;
payout: FdPayout;
calcType: FdCalcType;
expectedInterest?: number;
expectedMaturityAmount?: number;
periodInterest?: number;
fdGlCode?: string;
fdGlName?: string;
interestGlCode?: string;
interestGlName?: string;
bankGlAccountId?: number;
bankGlCode?: string;
bankGlName?: string;
status?: 'ACTIVE' | 'MATURED' | 'PREMATURE' | 'RENEWED';
remarks?: string;
glTransactionId?: string;
voucherNo?: string;
renewedFromDealNo?: string;
closedOnDate?: string;
closureType?: string;
closureAmount?: number;
closureInterest?: number;
closureTds?: number;
closurePenal?: number;
closureVoucherNo?: string;
closureGlTransactionId?: string;
limit?: InvestmentFdLimitDto;
}
export type FdClosureType = 'MATURITY' | 'PREMATURE' | 'RENEWAL';
/** One interest line of an FD: a 31 Mar / 30 Sep provision, a payout, or the maturity. */
export interface InvestmentFdInterestRowDto {
lineType: 'PROVISION' | 'PAYOUT' | 'MATURITY';
intDate: string;
fromDate: string;
toDate: string;
days: number;
grossInterest: number;
alreadyBooked?: number;
tds?: number;
amount: number;
posted: boolean;
voucherNo?: string;
glTransactionId?: string;
postable: boolean;
}
export interface InvestmentFdScheduleDto {
fd: InvestmentFdDto;
provisioned: number;
rows: InvestmentFdInterestRowDto[];
}
/** The next interest voucher (or the maturity) of an FD that is due. */
export interface InvestmentFdDueDto {
fdId: number;
dealNo: string;
partyName: string;
certNo: string;
lineType: 'PROVISION' | 'PAYOUT' | 'MATURITY';
intDate: string;
amount: number;
postable: boolean;
overdue: boolean;
}
/** What closing an FD works out to, and the voucher it will post. */
export interface InvestmentFdClosureDto {
fdId: number;
dealNo: string;
type: FdClosureType;
closeDate: string;
principal: number;
expectedInterest: number;
provisioned: number;
receivedAmount: number;
tds: number;
penalAmount?: number;
interest: number;
income: number;
difference: number;
renewalAmount?: number;
topUp?: number;
lines: Array<{ side: 'Dr' | 'Cr'; glCode: string; glName: string; amount: number }>;
}
/** Investment script as returned by /investment/scripts (dates are DD-MM-YYYY). */
export interface InvestmentScriptDto {
id: number;
@@ -299,6 +422,77 @@ export class InvestmentService {
return this.http.delete(`/investment/purchases/${id}`);
}
// -------------------- FD with other banks --------------------
getFds(): Observable<InvestmentFdDto[]> {
return this.http.get<InvestmentFdDto[]>('/investment/fds');
}
/** Interest, maturity amount and the RBI limit check for an FD before it is saved. */
quoteFd(params: Record<string, string | number>): Observable<InvestmentFdDto> {
return this.http.get<InvestmentFdDto>('/investment/fds/quote', { params });
}
/** Places the FD: Dr the bank's FD GL / Cr our bank GL at once. */
createFd(
body: object
): Observable<{ resourceIdentifier?: string; changes?: { dealNo?: string; voucherNo?: string } }> {
return this.http.post<{ resourceIdentifier?: string; changes?: { dealNo?: string; voucherNo?: string } }>(
'/investment/fds',
body
);
}
closureQuoteFd(fdId: number, params: Record<string, string | number>): Observable<InvestmentFdClosureDto> {
return this.http.get<InvestmentFdClosureDto>(`/investment/fds/${fdId}/closure-quote`, { params });
}
/** Maturity, premature closure or renewal of an FD (voucher posted at once). */
closeFd(
fdId: number,
type: FdClosureType,
body: object
): Observable<{ resourceIdentifier?: string; changes?: { dealNo?: string; voucherNo?: string; interest?: number } }> {
const path = type === 'MATURITY' ? 'mature' : type === 'PREMATURE' ? 'premature' : 'renew';
return this.http.post<{
resourceIdentifier?: string;
changes?: { dealNo?: string; voucherNo?: string; interest?: number };
}>(`/investment/fds/${fdId}/${path}`, body);
}
getFdSchedule(fdId: number): Observable<InvestmentFdScheduleDto> {
return this.http.get<InvestmentFdScheduleDto>(`/investment/fds/${fdId}/interest-schedule`);
}
/** Posts the next interest line of an FD (provision, or payout with the bank's amount and TDS). */
postFdInterest(fdId: number, body: object): Observable<{ resourceIdentifier?: string }> {
return this.http.post<{ resourceIdentifier?: string }>(`/investment/fds/${fdId}/interest`, body);
}
/** FD interest vouchers and maturities due now or within two days. */
getFdDue(): Observable<InvestmentFdDueDto[]> {
return this.http.get<InvestmentFdDueDto[]>('/investment/fds/due');
}
/** Records total deposits as on a 31 March, the base of the RBI limits. */
setDepositBase(body: object): Observable<unknown> {
return this.http.post('/investment/fds/deposit-base', body);
}
/** Enabled INCOME detail GLs — an FD bank's interest GL. */
getIncomeGlAccounts(): Observable<InvestmentGlAccount[]> {
return this.http
.get<
Array<{ id: number; glCode: string; name: string }>
>('/glaccounts', { params: { type: '4', usage: '1', disabled: 'false' } })
.pipe(
map((list) =>
list
.map((g) => ({ id: g.id, glCode: g.glCode, name: g.name }))
.sort((a, b) => a.glCode.localeCompare(b.glCode))
)
);
}
// -------------------- GL accounts --------------------
/** Enabled ASSET detail GLs — the only kind an investment party / script can be booked to. */
getAssetGlAccounts(): Observable<InvestmentGlAccount[]> {
@@ -345,6 +539,9 @@ export function toPartyMaster(p: InvestmentPartyDto): PartyMaster {
glAccountId: p.glAccountId,
glCode: p.glCode,
glName: p.glName,
interestGlAccountId: p.interestGlAccountId,
interestGlCode: p.interestGlCode,
interestGlName: p.interestGlName,
rbiCode: p.rbiCode,
limit: p.limit ?? null,
leiNo: p.leiNo,