feat(investment): add portfolio allocation targets and interest receivable/post/receive lifecycle
Adds a Portfolio Allocation (Target vs Actual %) section to the Investment Dashboard, and reworks Investment Interest into the full accrual lifecycle: Interest Receivable, FD/Govt./Bond Interest Calculation & Post, and Interest Receive, plus the existing registers. Interest Calculation & Post now tracks cumulative posted/received amounts per holding instead of recomputing full accrued interest on every click (which previously double-counted on repeat posting). Holdings classified Non-Performing (Sub-Standard/Doubtful/Loss) are excluded from accrual posting per RBI norms — their interest accrues in memo/suspense only and is recognised solely on actual cash receipt via Interest Receive. Also bumps the production initial-bundle budget in angular.json (13/14mb -> 15/17mb) since the growing set of eagerly-loaded feature modules (Investment, NPA, Reconciliation, GL Reports, OSS Returns) pushed the build past the old limit.
This commit is contained in:
@@ -22,6 +22,7 @@ export type InterestFreq = 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY' |
|
||||
export type TxnType =
|
||||
| 'PURCHASE'
|
||||
| 'INTEREST'
|
||||
| 'INTEREST_RECEIVED'
|
||||
| 'DIVIDEND'
|
||||
| 'SELL'
|
||||
| 'REDEEM'
|
||||
@@ -38,6 +39,32 @@ export const INSTRUMENT_LABELS: Record<InstrumentType, string> = {
|
||||
SHARE: 'Share'
|
||||
};
|
||||
|
||||
/** RBI investment-provisioning classification. A holding with no `provisionStatus` (or
|
||||
* explicitly 'STANDARD') is performing normally. Once classified Sub-Standard/Doubtful/Loss —
|
||||
* a Non-Performing Investment (NPI), the loan-book equivalent of an NPA — interest can no
|
||||
* longer be recognised on accrual basis (see canAccrueInterest()) and a provision must be held
|
||||
* against the investment (see provisionRequiredInv()). */
|
||||
export type InvestmentProvisionStatus = 'STANDARD' | 'SUB_STANDARD' | 'DOUBTFUL' | 'LOSS';
|
||||
|
||||
export const PROVISION_STATUS_LABELS: Record<InvestmentProvisionStatus, string> = {
|
||||
STANDARD: 'Standard',
|
||||
SUB_STANDARD: 'Sub-Standard (NPI)',
|
||||
DOUBTFUL: 'Doubtful (NPI)',
|
||||
LOSS: 'Loss (NPI)'
|
||||
};
|
||||
|
||||
/** Board-approved portfolio-policy target — % of the total investment corpus each instrument
|
||||
* type should hold. Compared against actual holdings on the Dashboard so a treasury desk can
|
||||
* see at a glance where the book has drifted from policy. */
|
||||
export const TARGET_ALLOCATION_PCT: Record<InstrumentType, number> = {
|
||||
FD: 15,
|
||||
GOVT_SECURITY: 35,
|
||||
NON_SLR: 10,
|
||||
BOND: 20,
|
||||
MUTUAL_FUND: 12,
|
||||
SHARE: 8
|
||||
};
|
||||
|
||||
export interface InvestmentHolding {
|
||||
certNo: string;
|
||||
instrumentType: InstrumentType;
|
||||
@@ -57,6 +84,12 @@ export interface InvestmentHolding {
|
||||
crToAc: string;
|
||||
brokerCode: string;
|
||||
status: HoldingStatus;
|
||||
/** Absent/'STANDARD' for a healthy holding. Drives canAccrueInterest() and provisioning. */
|
||||
provisionStatus?: InvestmentProvisionStatus;
|
||||
/** Cumulative interest posted (booked as an accrual receivable/income entry) to date. */
|
||||
interestPosted?: number;
|
||||
/** Cumulative interest actually received in cash to date. */
|
||||
interestReceived?: number;
|
||||
}
|
||||
|
||||
export interface InvestmentTxn {
|
||||
@@ -109,6 +142,67 @@ export function daysToMaturity(h: InvestmentHolding, asOnStr: string): number {
|
||||
return Math.round((mat - asOn) / (24 * 3600 * 1000));
|
||||
}
|
||||
|
||||
// -------------------- Provisioning & interest-lifecycle helpers --------------------
|
||||
|
||||
export function isNpi(h: InvestmentHolding): boolean {
|
||||
return (h.provisionStatus ?? 'STANDARD') !== 'STANDARD';
|
||||
}
|
||||
|
||||
export function provisionRatePctInv(status: InvestmentProvisionStatus): number {
|
||||
switch (status) {
|
||||
case 'STANDARD':
|
||||
return 0;
|
||||
case 'SUB_STANDARD':
|
||||
return 25;
|
||||
case 'DOUBTFUL':
|
||||
return 50;
|
||||
case 'LOSS':
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
|
||||
/** Provision required against the investment itself (principal), same idea as NPA provisioning
|
||||
* on a loan book — computed on face value since that is this module's carrying-value figure. */
|
||||
export function provisionRequiredInv(h: InvestmentHolding): number {
|
||||
return Math.round(h.faceValue * (provisionRatePctInv(h.provisionStatus ?? 'STANDARD') / 100));
|
||||
}
|
||||
|
||||
/** Interest can only be recognised on an accrual basis (posted as a receivable/income entry)
|
||||
* while the investment is Standard. Once it is classified Non-Performing (Sub-Standard/
|
||||
* Doubtful/Loss), RBI income-recognition norms require interest to move to cash basis — i.e.
|
||||
* it is recognised only when actually received, never posted as an accrual receivable. */
|
||||
export function canAccrueInterest(h: InvestmentHolding): boolean {
|
||||
return !isNpi(h);
|
||||
}
|
||||
|
||||
/** Interest still to be calculated & posted this cycle. Always 0 for an NPI holding — posting
|
||||
* an accrual on a non-performing investment would overstate income. */
|
||||
export function interestToPost(h: InvestmentHolding, asOnStr: string): number {
|
||||
if (!canAccrueInterest(h)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, accruedInterest(h, asOnStr) - (h.interestPosted ?? 0));
|
||||
}
|
||||
|
||||
/** Outstanding "Interest Receivable" GL balance — interest already posted (booked as income)
|
||||
* but not yet actually received in cash. Always 0 for an NPI holding, since nothing is ever
|
||||
* posted as a receivable on one. */
|
||||
export function interestReceivableBalance(h: InvestmentHolding): number {
|
||||
if (!canAccrueInterest(h)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, (h.interestPosted ?? 0) - (h.interestReceived ?? 0));
|
||||
}
|
||||
|
||||
/** For an NPI holding, interest keeps accruing notionally but is held in memo/suspense — never
|
||||
* posted as an asset or recognised as income — until it is actually collected in cash. */
|
||||
export function suspenseInterest(h: InvestmentHolding, asOnStr: string): number {
|
||||
if (canAccrueInterest(h)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, accruedInterest(h, asOnStr) - (h.interestReceived ?? 0));
|
||||
}
|
||||
|
||||
export function formatDDMMYYYY(d: Date): string {
|
||||
return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
|
||||
}
|
||||
@@ -208,7 +302,9 @@ export const FD_HOLDINGS: InvestmentHolding[] = [
|
||||
glName: 'FIXED DEPO. CO-OPER. BANK MAIN',
|
||||
crToAc: '310070',
|
||||
brokerCode: '',
|
||||
status: 'ACTIVE'
|
||||
status: 'ACTIVE',
|
||||
interestPosted: 42000,
|
||||
interestReceived: 36000
|
||||
},
|
||||
{
|
||||
certNo: '15624',
|
||||
@@ -268,7 +364,10 @@ export const FD_HOLDINGS: InvestmentHolding[] = [
|
||||
glName: 'FIXED DEPO. CO-OPER. BANK MAIN',
|
||||
crToAc: '310070',
|
||||
brokerCode: '',
|
||||
status: 'ACTIVE'
|
||||
status: 'ACTIVE',
|
||||
provisionStatus: 'DOUBTFUL',
|
||||
interestPosted: 115500,
|
||||
interestReceived: 50000
|
||||
},
|
||||
{
|
||||
certNo: '15627',
|
||||
@@ -311,7 +410,9 @@ export const GOVT_SECURITY_HOLDINGS: InvestmentHolding[] = [
|
||||
glName: 'INVESTMENT IN GOVT. SECURITIES',
|
||||
crToAc: '310070',
|
||||
brokerCode: 'BRK-002',
|
||||
status: 'ACTIVE'
|
||||
status: 'ACTIVE',
|
||||
interestPosted: 181500,
|
||||
interestReceived: 150000
|
||||
},
|
||||
{
|
||||
certNo: 'GSEC-2002',
|
||||
@@ -351,7 +452,9 @@ export const GOVT_SECURITY_HOLDINGS: InvestmentHolding[] = [
|
||||
glName: 'INVESTMENT IN GOVT. SECURITIES',
|
||||
crToAc: '310070',
|
||||
brokerCode: 'BRK-002',
|
||||
status: 'ACTIVE'
|
||||
status: 'ACTIVE',
|
||||
interestPosted: 67900,
|
||||
interestReceived: 67900
|
||||
},
|
||||
{
|
||||
certNo: 'GSEC-2004',
|
||||
@@ -514,7 +617,9 @@ export const NON_SLR_HOLDINGS: InvestmentHolding[] = [
|
||||
glName: 'INVESTMENT IN NON SLR SECURITIES',
|
||||
crToAc: '310070',
|
||||
brokerCode: 'BRK-003',
|
||||
status: 'ACTIVE'
|
||||
status: 'ACTIVE',
|
||||
interestPosted: 89400,
|
||||
interestReceived: 60000
|
||||
}
|
||||
];
|
||||
|
||||
@@ -597,7 +702,10 @@ export const BOND_HOLDINGS: InvestmentHolding[] = [
|
||||
glName: 'INVESTMENT IN CORPORATE BONDS',
|
||||
crToAc: '310070',
|
||||
brokerCode: 'BRK-006',
|
||||
status: 'ACTIVE'
|
||||
status: 'ACTIVE',
|
||||
provisionStatus: 'SUB_STANDARD',
|
||||
interestPosted: 66000,
|
||||
interestReceived: 40000
|
||||
},
|
||||
{
|
||||
certNo: 'BOND-4005',
|
||||
|
||||
Reference in New Issue
Block a user