feat(investment): FD screens - purchase, maturity/premature/renewal, interest entry and reminders
This commit is contained in:
@@ -403,6 +403,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</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) {
|
@if (preview?.savingsInterestPending?.length) {
|
||||||
<div class="bd-warn">
|
<div class="bd-warn">
|
||||||
<mat-icon>warning</mat-icon>
|
<mat-icon>warning</mat-icon>
|
||||||
|
|||||||
@@ -64,6 +64,28 @@
|
|||||||
</a>
|
</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 -->
|
<!-- Quick Action Cards -->
|
||||||
<div class="quick-actions">
|
<div class="quick-actions">
|
||||||
<a class="qa-card" [routerLink]="['/dashboard']" #buttonDashboard>
|
<a class="qa-card" [routerLink]="['/dashboard']" #buttonDashboard>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { PopoverService } from '../configuration-wizard/popover/popover.service'
|
|||||||
import { ConfigurationWizardService } from '../configuration-wizard/configuration-wizard.service';
|
import { ConfigurationWizardService } from '../configuration-wizard/configuration-wizard.service';
|
||||||
import { SettingsService } from 'app/settings/settings.service';
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
import { DscService } from 'app/users/dsc/dsc.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 */
|
/** Custom Components */
|
||||||
import { NextStepDialogComponent } from '../configuration-wizard/next-step-dialog/next-step-dialog.component';
|
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. */
|
/** Investment interest lines due now or within two days, shown on the home screen so nobody has to look for them. */
|
||||||
pendingInvestmentInterest: InvestmentInterestDueDto[] = [];
|
pendingInvestmentInterest: InvestmentInterestDueDto[] = [];
|
||||||
|
/** FDs placed with other banks: interest vouchers and maturities due now or within two days. */
|
||||||
|
pendingFd: InvestmentFdDueDto[] = [];
|
||||||
private readonly investmentService = inject(InvestmentService);
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
/** Silent when the user cannot see investments or nothing is due — the strip simply does not render. */
|
/** 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 ?? []),
|
next: (due) => (this.pendingInvestmentInterest = due ?? []),
|
||||||
error: () => (this.pendingInvestmentInterest = [])
|
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
@@ -1906,6 +1906,10 @@ export interface PartyMaster {
|
|||||||
glAccountId?: number;
|
glAccountId?: number;
|
||||||
glCode?: string;
|
glCode?: string;
|
||||||
glName?: string;
|
glName?: string;
|
||||||
|
/** Interest income GL of the FDs placed with this bank (Bond Type FD). */
|
||||||
|
interestGlAccountId?: number;
|
||||||
|
interestGlCode?: string;
|
||||||
|
interestGlName?: string;
|
||||||
rbiCode?: string;
|
rbiCode?: string;
|
||||||
/** Exposure limit for this party (₹). */
|
/** Exposure limit for this party (₹). */
|
||||||
limit?: number | null;
|
limit?: number | null;
|
||||||
|
|||||||
@@ -162,6 +162,17 @@
|
|||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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>RBI Code</span><input type="text" [(ngModel)]="partyForm.rbiCode" /></label>
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Limit (₹)</span><input type="number" min="0" [(ngModel)]="partyForm.limit"
|
><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;
|
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. */
|
/** Investment GLs (under the 100700 INVESTMENTS head) a party or script can be mapped to — not loans, banks or test GLs. */
|
||||||
investmentGlOptions: InvestmentGlAccount[] = [];
|
investmentGlOptions: InvestmentGlAccount[] = [];
|
||||||
|
/** Income GLs under 400400 INCOME FROM INVESTMENTS, for an FD bank's interest. */
|
||||||
|
interestGlOptions: InvestmentGlAccount[] = [];
|
||||||
newSubType: Partial<SubTypeMaster> = {};
|
newSubType: Partial<SubTypeMaster> = {};
|
||||||
newBroker: Partial<BrokerMaster> = {};
|
newBroker: Partial<BrokerMaster> = {};
|
||||||
newControlParam: Partial<ControlParameter> = {};
|
newControlParam: Partial<ControlParameter> = {};
|
||||||
@@ -143,6 +145,10 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
)),
|
)),
|
||||||
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
|
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();
|
this.reloadMasters();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +188,10 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
this.toast('Enter Bond Type, Party Name and GL A/c No.');
|
this.toast('Enter Bond Type, Party Name and GL A/c No.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (f.bondType === 'FD' && !f.interestGlAccountId) {
|
||||||
|
this.toast('An FD bank needs its Interest GL as well.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const body = {
|
const body = {
|
||||||
bondType: f.bondType,
|
bondType: f.bondType,
|
||||||
partyName: f.partyName,
|
partyName: f.partyName,
|
||||||
@@ -194,6 +204,7 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
fax: f.fax ?? '',
|
fax: f.fax ?? '',
|
||||||
bankType: f.bankType ?? '',
|
bankType: f.bankType ?? '',
|
||||||
glAccountId: Number(f.glAccountId),
|
glAccountId: Number(f.glAccountId),
|
||||||
|
interestGlAccountId: f.bondType === 'FD' && f.interestGlAccountId ? Number(f.interestGlAccountId) : null,
|
||||||
rbiCode: f.rbiCode ?? '',
|
rbiCode: f.rbiCode ?? '',
|
||||||
limit: f.limit ?? null,
|
limit: f.limit ?? null,
|
||||||
leiNo: f.leiNo ?? '',
|
leiNo: f.leiNo ?? '',
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export interface InvestmentPartyDto {
|
|||||||
glAccountId: number;
|
glAccountId: number;
|
||||||
glCode: string;
|
glCode: string;
|
||||||
glName: string;
|
glName: string;
|
||||||
|
interestGlAccountId?: number;
|
||||||
|
interestGlCode?: string;
|
||||||
|
interestGlName?: string;
|
||||||
rbiCode?: string;
|
rbiCode?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
leiNo?: string;
|
leiNo?: string;
|
||||||
@@ -49,6 +52,126 @@ export interface InvestmentPartyDto {
|
|||||||
active: boolean;
|
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). */
|
/** Investment script as returned by /investment/scripts (dates are DD-MM-YYYY). */
|
||||||
export interface InvestmentScriptDto {
|
export interface InvestmentScriptDto {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -299,6 +422,77 @@ export class InvestmentService {
|
|||||||
return this.http.delete(`/investment/purchases/${id}`);
|
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 --------------------
|
// -------------------- GL accounts --------------------
|
||||||
/** Enabled ASSET detail GLs — the only kind an investment party / script can be booked to. */
|
/** Enabled ASSET detail GLs — the only kind an investment party / script can be booked to. */
|
||||||
getAssetGlAccounts(): Observable<InvestmentGlAccount[]> {
|
getAssetGlAccounts(): Observable<InvestmentGlAccount[]> {
|
||||||
@@ -345,6 +539,9 @@ export function toPartyMaster(p: InvestmentPartyDto): PartyMaster {
|
|||||||
glAccountId: p.glAccountId,
|
glAccountId: p.glAccountId,
|
||||||
glCode: p.glCode,
|
glCode: p.glCode,
|
||||||
glName: p.glName,
|
glName: p.glName,
|
||||||
|
interestGlAccountId: p.interestGlAccountId,
|
||||||
|
interestGlCode: p.interestGlCode,
|
||||||
|
interestGlName: p.interestGlName,
|
||||||
rbiCode: p.rbiCode,
|
rbiCode: p.rbiCode,
|
||||||
limit: p.limit ?? null,
|
limit: p.limit ?? null,
|
||||||
leiNo: p.leiNo,
|
leiNo: p.leiNo,
|
||||||
|
|||||||
Reference in New Issue
Block a user