@if (maturingSoon.length === 0) {
diff --git a/src/app/investment/investment-dashboard/investment-dashboard.component.ts b/src/app/investment/investment-dashboard/investment-dashboard.component.ts
index 650d5f568..8640f4cff 100644
--- a/src/app/investment/investment-dashboard/investment-dashboard.component.ts
+++ b/src/app/investment/investment-dashboard/investment-dashboard.component.ts
@@ -14,6 +14,7 @@ import {
INSTRUMENT_LABELS,
InstrumentType,
InvestmentHolding,
+ TARGET_ALLOCATION_PCT,
accruedInterest,
daysToMaturity,
maturityAmount
@@ -29,6 +30,16 @@ interface InstrumentSummary {
maturityValue: number;
}
+interface AllocationRow {
+ type: InstrumentType;
+ label: string;
+ targetPct: number;
+ actualPct: number;
+ actualValue: number;
+ variancePct: number;
+ statusLabel: 'Within Target' | 'Over-Allocated' | 'Under-Allocated';
+}
+
/** Cross-instrument portfolio dashboard — every figure is derived from ALL_HOLDINGS, never
* hand-duplicated, so it can never drift from the per-instrument screens or Reports. */
@Component({
@@ -79,6 +90,30 @@ export class InvestmentDashboardComponent {
});
}
+ /** Board-approved policy target % per instrument type vs the portfolio's actual allocation
+ * (by invested/face value) — a ±2 percentage-point band is treated as "Within Target" since
+ * a treasury desk can't rebalance to the exact decimal every day. */
+ get allocationVsTarget(): AllocationRow[] {
+ const total = this.totalFaceValue;
+ return this.types.map(({ type }) => {
+ const value = this.holdingsOf(type).reduce((s, h) => s + h.faceValue, 0);
+ const actualPct = total ? (value / total) * 100 : 0;
+ const targetPct = TARGET_ALLOCATION_PCT[type];
+ const variancePct = +(actualPct - targetPct).toFixed(1);
+ const statusLabel: AllocationRow['statusLabel'] =
+ Math.abs(variancePct) <= 2 ? 'Within Target' : variancePct > 0 ? 'Over-Allocated' : 'Under-Allocated';
+ return {
+ type,
+ label: INSTRUMENT_LABELS[type],
+ targetPct,
+ actualPct: +actualPct.toFixed(1),
+ actualValue: value,
+ variancePct,
+ statusLabel
+ };
+ });
+ }
+
get totalHoldings(): number {
return ALL_HOLDINGS.length;
}
diff --git a/src/app/investment/investment-data.ts b/src/app/investment/investment-data.ts
index a2cdd253c..980093c86 100644
--- a/src/app/investment/investment-data.ts
+++ b/src/app/investment/investment-data.ts
@@ -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 = {
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 = {
+ 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 = {
+ 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',
diff --git a/src/app/investment/investment-interest/investment-interest.component.html b/src/app/investment/investment-interest/investment-interest.component.html
index 94a3060bf..3423802ea 100644
--- a/src/app/investment/investment-interest/investment-interest.component.html
+++ b/src/app/investment/investment-interest/investment-interest.component.html
@@ -12,20 +12,28 @@
TREASURY · INVESTMENT · INTEREST
Investment Interest
- FD and Govt./Bond interest calculation batch runs, plus the Government Bond Interest, FD Interest and FD
- registers.
+ Interest Receivable, FD and Govt./Bond interest calculation-and-post batch runs, Interest Receive, plus the
+ Government Bond Interest, FD Interest and FD registers.
+
+
+
+ @if (activeTab === 'receivable') {
+
+
+ Interest Receivable — Posted but Not Yet Collected (₹{{ totalReceivable | number: '1.2-2' }})
+
+ Non-Performing Investments — Interest Held in Suspense (₹{{ totalSuspense | number: '1.2-2' }})
+
+
+ These holdings are classified Non-Performing (Sub-Standard/Doubtful/Loss). Per RBI income-recognition norms,
+ interest is not posted as an accrual receivable — it accrues in memo only, and is recognised as income solely
+ on actual cash receipt via the Interest Receive tab.
+
+ {{ fdCalcExcludedNpi.length }} certificate(s) excluded — classified Non-Performing, interest is not posted on
+ accrual basis (see Interest Receivable tab for suspense amount).
+
+ }
}
@if (activeTab === 'bondCalc') {
-
Investment - Govt./Bond Interest Calculation
+
Investment - Govt./Bond Interest Calculation & Post
-
+
@@ -204,6 +324,100 @@
+ @if (bondCalcExcludedNpi.length > 0) {
+
+ {{ bondCalcExcludedNpi.length }} holding(s) excluded — classified Non-Performing, interest is not posted on
+ accrual basis (see Interest Receivable tab for suspense amount).
+