diff --git a/src/app/investment/bond-investment/bond-investment.component.ts b/src/app/investment/bond-investment/bond-investment.component.ts
index c50ebea8f..e489fceda 100644
--- a/src/app/investment/bond-investment/bond-investment.component.ts
+++ b/src/app/investment/bond-investment/bond-investment.component.ts
@@ -74,7 +74,11 @@ import {
InvestmentNonSlrLimitDto,
InvestmentService,
apiErrorMessage,
- toHolding
+ toHolding,
+ toInterestTxn,
+ toPurchaseTxn,
+ toSaleTxn,
+ toTransferTxn
} from '../investment.service';
type BondType = 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND';
@@ -222,7 +226,7 @@ export class BondInvestmentComponent implements OnInit {
this.investmentService.getPurchases().subscribe({
next: (purchases) => {
this.holdings = purchases.map(toHolding);
- this.txns = purchases.map((d) => this.purchaseTxn(d));
+ this.txns = purchases.map(toPurchaseTxn);
this.purchaseForm = this.blankPurchaseForm();
this.loadNonSlrLimit();
this.loadInterestLog();
@@ -253,115 +257,13 @@ export class BondInvestmentComponent implements OnInit {
);
this.txns = [
...others,
- ...postings.map((x) => this.interestTxn(x)),
- ...sales.map((x) => this.saleTxn(x)),
- ...transfers.map((x) => this.transferTxn(x))
+ ...postings.map(toInterestTxn),
+ ...sales.map(toSaleTxn),
+ ...transfers.map(toTransferTxn)
].sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime());
});
}
- /** A security transfer shows the book value carried to the new security. */
- private transferTxn(x: InvestmentSecurityTransferDto): InvestmentTxn {
- return {
- certNo: x.certNo,
- instrumentType: x.bondType as InstrumentType,
- txnType: 'TRANSFER',
- voucherNo: x.voucherNo ?? `TRF/${x.dealNo}/${x.id}`,
- txnDate: x.transferDate,
- amount: x.bookValue,
- particulars: `GOI category transfer - Deal ${x.dealNo}: ${x.fromScriptCode} ${x.fromScriptDesc} → ${x.toScriptCode} ${x.toScriptDesc}`,
- glCode: x.toGlCode,
- glName: x.glTransactionId
- ? `${x.toGlName} / Cr ${x.fromGlCode} (txn ${x.glTransactionId})`
- : `${x.toGlName} (same GL, no entry)`
- };
- }
-
- /** A sale or redemption shows the money into the bank; a pending sale is not in the GL yet. */
- private saleTxn(x: InvestmentSaleDto): InvestmentTxn {
- if (x.saleType === 'AMORTISE') {
- return {
- certNo: x.certNo,
- instrumentType: x.bondType as InstrumentType,
- txnType: 'AMORTIZE',
- voucherNo: x.voucherNo ?? `AMZ/${x.dealNo}/${x.id}`,
- txnDate: x.settlementDate,
- amount: x.bookValueSold,
- particulars: `${x.remarks ?? 'Premium amortisation'} - Deal ${x.dealNo} - ${x.scriptDesc} (${x.accruedDays} days)`,
- glCode: '500604',
- glName: `Amortisation of Premium / Cr Premium on Investments (txn ${x.glTransactionId})`
- };
- }
- if (x.saleType === 'WRITEOFF') {
- return {
- certNo: x.certNo,
- instrumentType: x.bondType as InstrumentType,
- txnType: 'WRITE_OFF',
- voucherNo: x.voucherNo ?? `WOF/${x.dealNo}/${x.id}`,
- txnDate: x.settlementDate,
- amount: x.bookValueSold,
- particulars: `Written off - Deal ${x.dealNo} - ${x.scriptDesc}${x.remarks ? ' - ' + x.remarks : ''}`,
- glCode: '500605',
- glName: `Loss on Investments / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
- };
- }
- const redeem = x.saleType === 'REDEEM';
- const pl =
- x.profitLoss >= 0
- ? `profit ₹${x.profitLoss}`
- : redeem
- ? `premium written off ₹${-x.profitLoss}`
- : `loss ₹${-x.profitLoss}`;
- return {
- certNo: x.certNo,
- instrumentType: x.bondType as InstrumentType,
- txnType: x.saleType,
- voucherNo: x.voucherNo ?? `SALE/${x.dealNo}/${x.id}`,
- txnDate: x.settlementDate,
- amount: x.netAmount,
- particulars: redeem
- ? `Redeemed at maturity - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`
- : `Sold ${x.units} units @ ${x.saleRate} to ${x.partyName ?? ''} - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`,
- glCode: x.status === 'SETTLED' ? x.bankGlCode : '',
- glName:
- x.status === 'SETTLED'
- ? `${x.bankGlName} / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
- : 'Not posted (pending settlement)'
- };
- }
-
- /** A coupon shows the cash received into the bank; a provision the interest booked as receivable. */
- private interestTxn(x: InvestmentInterestPostingDto): InvestmentTxn {
- const coupon = x.lineType === 'COUPON';
- return {
- certNo: x.certNo,
- instrumentType: x.bondType as InstrumentType,
- txnType: 'INTEREST',
- voucherNo: x.voucherNo,
- txnDate: x.intDate,
- amount: coupon ? x.grossInterest : x.amount,
- particulars: `${coupon ? 'Coupon' : 'Provision'} ${x.fromDate} to ${x.toDate} - Deal ${x.dealNo} - ${x.scriptDesc}`,
- glCode: x.drGlCode,
- glName: `${x.drGlName} / Cr ${x.incomeGlCode} (txn ${x.glTransactionId})`
- };
- }
-
- /** A purchase reaches the GL only when it is settled, dated the settlement date. */
- private purchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
- const posted = !!d.glTransactionId;
- return {
- certNo: d.certNo,
- instrumentType: d.bondType,
- txnType: 'PURCHASE',
- voucherNo: `INV/PUR/${d.dealNo}`,
- txnDate: d.settlementDate,
- amount: d.settlementAmount,
- particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]`,
- glCode: posted ? d.investmentGlCode : '',
- glName: posted ? `${d.investmentGlName} (txn ${d.glTransactionId})` : 'Not posted (pending settlement)'
- };
- }
-
activeTab: BondTab = 'holdings';
/** Purchase Entry shows only the mandatory fields; the rest sit under "More Details". */
showMoreDetails = false;
diff --git a/src/app/investment/investment-dashboard/investment-dashboard.component.html b/src/app/investment/investment-dashboard/investment-dashboard.component.html
index 51673f91b..9d5cdc2d3 100644
--- a/src/app/investment/investment-dashboard/investment-dashboard.component.html
+++ b/src/app/investment/investment-dashboard/investment-dashboard.component.html
@@ -12,34 +12,50 @@
TREASURY · INVESTMENT
Investment Portfolio Dashboard
- Consolidated position across FD, Govt Security, Non-SLR, Bonds, Mutual Fund and Share holdings as on
+ What the bank holds today across Govt Securities, FDs with banks, Non-SLR, Bonds, Mutual Funds and Shares, as on
{{ asOnDate }}.
-
+
+ {{ loading ? 'Loading…' : 'Refresh' }}
+
+
{{ generating ? 'Exporting…' : 'Export to Excel' }}
+ @if (error) {
+
+ }
+
- Total Holdings
+ Holdings
{{ totalHoldings }}
- {{ activeCount }} active · {{ maturedCount }} matured
+ {{ maturingSoon.length }} due within 90 days{{ overdueCount ? ' (' + overdueCount + ' overdue)' : '' }}
- Total Invested (Face Value)
- ₹{{ totalFaceValue | number: '1.2-2' }}
+ Total Invested (Book Value)
+ ₹{{ totalBookValue | number: '1.2-2' }}
+ Face value ₹{{ totalFaceValue | number: '1.2-2' }}
- Accrued Interest / Income
+ Interest Receivable
₹{{ totalAccruedInterest | number: '1.2-2' }}
+ Earned till today, not received · ₹{{ totalProvisioned | number: '1.2-2' }} provisioned
- Projected Value at Maturity
+ Value at Maturity
₹{{ totalMaturityValue | number: '1.2-2' }}
+ Securities at face value, FDs with interest
@@ -51,7 +67,9 @@
Instrument Type
Holdings
Face Value
- Accrued Interest
+ Book Value
+ Share
+ Interest Receivable
Maturity Value
@@ -62,6 +80,8 @@
{{ s.label }}
{{ s.count }}
₹{{ s.faceValue | number: '1.2-2' }}
+ ₹{{ s.bookValue | number: '1.2-2' }}
+ {{ s.sharePct | number: '1.1-1' }}%
₹{{ s.accruedInterest | number: '1.2-2' }}
₹{{ s.maturityValue | number: '1.2-2' }}
@@ -75,6 +95,8 @@
Total
{{ totalHoldings }}
₹{{ totalFaceValue | number: '1.2-2' }}
+ ₹{{ totalBookValue | number: '1.2-2' }}
+ {{ totalBookValue > 0 ? '100%' : '—' }}
₹{{ totalAccruedInterest | number: '1.2-2' }}
₹{{ totalMaturityValue | number: '1.2-2' }}
@@ -84,93 +106,70 @@
-
Portfolio Allocation — Target vs Actual
-
- Govt Security, Non-SLR, Mutual Fund and Share targets trace to RBI's SLR floor (18% of NDTL) and Non-SLR ceiling
- (≤10% of deposits) for Urban Co-operative Banks; Fixed Deposit and Bond are board-approved policy choices within
- the room that framework leaves.
-
-
-
-
- Instrument Type
- Target Basis
- Target %
- Actual %
- Actual Value
- Variance
- Status
-
-
-
- @for (a of allocationVsTarget; track a.type) {
+ RBI Limits
+ @if (limitNote) {
+ {{ limitNote }}
+ }
+ @if (limits.length) {
+
+
- {{ a.label }}
-
-
- {{ a.isRbiLinked ? 'RBI SLR / Non-SLR Norm' : 'Board Policy' }}
-
-
- {{ a.targetPct }}%
- {{ a.actualPct }}%
- ₹{{ a.actualValue | number: '1.2-2' }}
-
- {{ a.variancePct > 0 ? '+' : '' }}{{ a.variancePct }}%
-
-
-
- {{ a.statusLabel }}
-
-
+ Limit
+ Basis
+ Allowed
+ Used
+ Used %
+ Status
- }
-
-
-
- Total
- 100%
- 100%
- ₹{{ totalFaceValue | number: '1.2-2' }}
-
-
-
-
+
+
+ @for (l of limits; track l.label) {
+
+ {{ l.label }}
+ {{ l.basis }}
+ ₹{{ l.limit | number: '1.2-2' }}
+ ₹{{ l.used | number: '1.2-2' }}
+ {{ l.usedPct === null ? '—' : (l.usedPct | number: '1.1-1') + '%' }}
+
+
+ {{ l.within ? 'Within' : 'Above limit' }}
+
+
+
+ }
+
+
+ }
-
Due-Date Diary — Maturing Within 90 Days
+
Maturing Within 90 Days
@if (maturingSoon.length === 0) {
No holdings maturing in the next 90 days.
} @else {
- @for (h of maturingSoon; track h.certNo) {
+ @for (m of maturingSoon; track m.dealNo) {
- {{ h.certNo }}
- {{ h.scriptName }} — {{ h.party }} ({{ instrumentLabel(h.instrumentType) }})
+ {{ m.dealNo }}
+ {{ m.particulars }} ({{ m.instrument }})
- Matures {{ h.maturityDate }} ·
+ Matures {{ m.maturityDate }} · ₹{{ m.amount | number: '1.2-2' }} ·
30"
+ [class.inv-maturity-days--soon]="m.days <= 30"
+ [class.inv-maturity-days--ok]="m.days > 30"
>
- {{ daysLeft(h) }} days
+ {{ m.days < 0 ? 'Overdue ' + -m.days + ' days' : m.days + ' days' }}
}
+
}
@@ -179,19 +178,17 @@
Fixed Deposit
- {{ summaries[0].count }}
- Purchase / Interest / Sell / Write-off
+ {{ countOf('FD') }}
+ Purchase / Interest / Maturity / Renewal
Govt Security / Non-SLR / Bonds / MF
- {{
- summaries[1].count + summaries[2].count + summaries[3].count + summaries[4].count
- }}
+ {{ countOf('GOVT_SECURITY', 'NON_SLR', 'BOND', 'MUTUAL_FUND') }}
Purchase / Interest / Sell / Amortize
Share Entry
- {{ summaries[5].count }}
+ {{ countOf('SHARE') }}
Purchase / Dividend / Sell / Write-off
diff --git a/src/app/investment/investment-dashboard/investment-dashboard.component.ts b/src/app/investment/investment-dashboard/investment-dashboard.component.ts
index 15d6adb79..bbfc8c4d3 100644
--- a/src/app/investment/investment-dashboard/investment-dashboard.component.ts
+++ b/src/app/investment/investment-dashboard/investment-dashboard.component.ts
@@ -6,21 +6,23 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { Component } from '@angular/core';
+import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
-import {
- ALL_HOLDINGS,
- ALLOCATION_TARGET_IS_RBI_LINKED,
- INSTRUMENT_LABELS,
- InstrumentType,
- InvestmentHolding,
- TARGET_ALLOCATION_PCT,
- accruedInterest,
- daysToMaturity,
- maturityAmount
-} from '../investment-data';
+import { forkJoin } from 'rxjs';
+import { INSTRUMENT_LABELS, InstrumentType, parseDDMMYYYY, todayDDMMYYYY } from '../investment-data';
import { exportWorkbook } from '../investment-export';
+import {
+ InvestmentBucketReportDto,
+ InvestmentBucketRowDto,
+ InvestmentFdDto,
+ InvestmentFdLimitDto,
+ InvestmentInterestReportDto,
+ InvestmentNonSlrLimitDto,
+ InvestmentPurchaseDto,
+ InvestmentService,
+ apiErrorMessage
+} from '../investment.service';
interface InstrumentSummary {
type: InstrumentType;
@@ -28,25 +30,45 @@ interface InstrumentSummary {
route: string;
count: number;
faceValue: number;
+ bookValue: number;
accruedInterest: number;
maturityValue: number;
+ sharePct: number;
}
-interface AllocationRow {
- type: InstrumentType;
+interface LimitRow {
label: string;
- targetPct: number;
- actualPct: number;
- actualValue: number;
- variancePct: number;
- statusLabel: 'Within Target' | 'Over-Allocated' | 'Under-Allocated';
- /** True when the target itself traces to an RBI SLR/Non-SLR rule rather than being a
- * board-only policy choice — see TARGET_ALLOCATION_PCT's doc comment in investment-data.ts. */
- isRbiLinked: boolean;
+ basis: string;
+ limit: number | null;
+ used: number;
+ usedPct: number | null;
+ within: boolean;
}
-/** 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. */
+/** A holding maturing soon, or past maturity and not yet redeemed / closed. */
+interface MaturingRow {
+ dealNo: string;
+ particulars: string;
+ instrument: string;
+ maturityDate: string;
+ days: number;
+ amount: number;
+}
+
+const ROUTE: Record = {
+ FD: '/investment/fd',
+ GOVT_SECURITY: '/investment/bonds',
+ NON_SLR: '/investment/bonds',
+ BOND: '/investment/bonds',
+ MUTUAL_FUND: '/investment/bonds',
+ SHARE: '/investment/share'
+};
+
+/**
+ * Cross-instrument portfolio dashboard on live data: what is held today by instrument (face value, book value,
+ * interest earned and not yet received, value at maturity), where the portfolio stands against the RBI limits, and
+ * what matures in the next 90 days.
+ */
@Component({
selector: 'mifosx-investment-dashboard',
templateUrl: './investment-dashboard.component.html',
@@ -60,107 +82,216 @@ interface AllocationRow {
RouterLink
]
})
-export class InvestmentDashboardComponent {
- asOnDate = '';
+export class InvestmentDashboardComponent implements OnInit {
+ private readonly investmentService = inject(InvestmentService);
+
+ asOnDate = todayDDMMYYYY();
+ loading = false;
+ error = '';
generating = false;
toastMsg = '';
toastVisible = false;
private toastTimer: ReturnType | undefined;
- private readonly types: Array<{ type: InstrumentType; route: string }> = [
- { type: 'FD', route: '/investment/fd' },
- { type: 'GOVT_SECURITY', route: '/investment/bonds' },
- { type: 'NON_SLR', route: '/investment/bonds' },
- { type: 'BOND', route: '/investment/bonds' },
- { type: 'MUTUAL_FUND', route: '/investment/bonds' },
- { type: 'SHARE', route: '/investment/share' }
- ];
- constructor() {
- const d = new Date();
- this.asOnDate = `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
+ summaries: InstrumentSummary[] = [];
+ limits: LimitRow[] = [];
+ limitNote = '';
+ maturingSoon: MaturingRow[] = [];
+ totalReceivable = 0;
+ totalProvisioned = 0;
+
+ ngOnInit(): void {
+ this.load();
}
- private holdingsOf(type: InstrumentType): InvestmentHolding[] {
- return ALL_HOLDINGS.filter((h) => h.instrumentType === type);
- }
-
- get summaries(): InstrumentSummary[] {
- return this.types.map(({ type, route }) => {
- const holdings = this.holdingsOf(type);
- return {
- type,
- label: INSTRUMENT_LABELS[type],
- route,
- count: holdings.length,
- faceValue: holdings.reduce((s, h) => s + h.faceValue, 0),
- accruedInterest: holdings.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0),
- maturityValue: holdings.reduce((s, h) => s + maturityAmount(h), 0)
- };
+ load(): void {
+ const today = this.asOnDate;
+ const d = parseDDMMYYYY(today);
+ const fyStart = `01-04-${d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1}`;
+ this.loading = true;
+ this.error = '';
+ forkJoin({
+ bucket: this.investmentService.getBucketReport(today),
+ interest: this.investmentService.getInterestReport(fyStart, today),
+ purchases: this.investmentService.getPurchases(),
+ fds: this.investmentService.getFds(),
+ fdQuote: this.investmentService.quoteFd({ amount: 0, intRate: 0, dealDate: today }),
+ nonSlr: this.investmentService.getNonSlrLimit(null, today, 0)
+ }).subscribe({
+ next: (r) => {
+ this.build(r.bucket, r.interest, r.purchases, r.fds, r.fdQuote.limit ?? null, r.nonSlr);
+ this.loading = false;
+ },
+ error: (err: unknown) => {
+ this.loading = false;
+ this.error = apiErrorMessage(err);
+ }
});
}
- /** 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,
- isRbiLinked: ALLOCATION_TARGET_IS_RBI_LINKED[type]
- };
- });
+ private build(
+ bucket: InvestmentBucketReportDto,
+ interest: InvestmentInterestReportDto,
+ purchases: InvestmentPurchaseDto[],
+ fds: InvestmentFdDto[],
+ fdLimit: InvestmentFdLimitDto | null,
+ nonSlr: InvestmentNonSlrLimitDto
+ ): void {
+ const purchaseById = new Map(
+ purchases.map((p) => [
+ p.id,
+ p
+ ])
+ );
+ const fdById = new Map(
+ fds
+ .filter((f) => f.id != null)
+ .map((f) => [
+ f.id as number,
+ f
+ ])
+ );
+ const receivable = new Map(
+ interest.rows.map((x) => [
+ x.instrument + ':' + x.refId,
+ x.receivable
+ ])
+ );
+ this.totalReceivable = interest.totalReceivable;
+ this.totalProvisioned = interest.totalProvisioned;
+
+ // Bucket rows are the Govt Securities, bonds and FDs held today; the purchase tells a Non-SLR from a Bond.
+ const typeOf = (r: InvestmentBucketRowDto): InstrumentType =>
+ r.instrument === 'FD' ? 'FD' : (purchaseById.get(r.refId)?.bondType ?? 'BOND');
+ const acc = new Map();
+ const sum = (type: InstrumentType): InstrumentSummary => {
+ let s = acc.get(type);
+ if (!s) {
+ s = {
+ type,
+ label: INSTRUMENT_LABELS[type],
+ route: ROUTE[type],
+ count: 0,
+ faceValue: 0,
+ bookValue: 0,
+ accruedInterest: 0,
+ maturityValue: 0,
+ sharePct: 0
+ };
+ acc.set(type, s);
+ }
+ return s;
+ };
+ for (const r of bucket.rows) {
+ const s = sum(typeOf(r));
+ s.count += 1;
+ s.faceValue += r.faceValue;
+ s.bookValue += r.bookValue;
+ s.accruedInterest += receivable.get((r.instrument === 'FD' ? 'FD' : 'SECURITY') + ':' + r.refId) ?? 0;
+ // A security is redeemed at face value; an FD pays its maturity amount (just the principal on a payout FD).
+ s.maturityValue +=
+ r.instrument === 'FD' ? (fdById.get(r.refId)?.expectedMaturityAmount ?? r.faceValue) : r.faceValue;
+ }
+ // Mutual funds and shares have no maturity, so they are not in the buckets; their units are held at cost.
+ for (const p of purchases) {
+ if ((p.bondType === 'MUTUAL_FUND' || p.bondType === 'SHARE') && p.status === 'ACTIVE') {
+ const s = sum(p.bondType);
+ s.count += 1;
+ s.faceValue += p.faceValue;
+ s.bookValue += p.bookValue;
+ s.maturityValue += p.bookValue;
+ }
+ }
+ const order: InstrumentType[] = [
+ 'GOVT_SECURITY',
+ 'FD',
+ 'NON_SLR',
+ 'BOND',
+ 'MUTUAL_FUND',
+ 'SHARE'
+ ];
+ const total = [...acc.values()].reduce((t, s) => t + s.bookValue, 0);
+ this.summaries = order
+ .map((t) => sum(t))
+ .map((s) => ({ ...s, sharePct: total > 0 ? (s.bookValue / total) * 100 : 0 }));
+
+ this.limits = [];
+ this.limitNote = '';
+ if (fdLimit?.grossLimit != null) {
+ const used = fdLimit.fdOutstanding + fdLimit.bankBalances;
+ this.limits.push(
+ this.limitRow(
+ 'Deposits with other banks',
+ 'FDs + current accounts, ≤ 20% of deposits',
+ fdLimit.grossLimit,
+ used
+ )
+ );
+ }
+ if (nonSlr.nonSlrLimit != null) {
+ this.limits.push(
+ this.limitRow(
+ 'Non-SLR investments',
+ 'Bonds, CP / CD, MF, ≤ 10% of deposits',
+ nonSlr.nonSlrLimit,
+ nonSlr.nonSlrOutstanding
+ )
+ );
+ this.limits.push(
+ this.limitRow(
+ 'Unlisted non-SLR',
+ `≤ 10% of non-SLR held on ${nonSlr.asOnDate}`,
+ nonSlr.unlistedLimit,
+ nonSlr.unlistedOutstanding
+ )
+ );
+ } else {
+ this.limitNote = `Total deposits as on ${nonSlr.asOnDate} are not entered yet; enter them on the FD screen to see the RBI limits.`;
+ }
+
+ this.maturingSoon = bucket.rows
+ .filter((r) => r.daysToMaturity <= 90)
+ .map((r) => ({
+ dealNo: r.dealNo,
+ particulars: r.particulars,
+ instrument: INSTRUMENT_LABELS[typeOf(r)],
+ maturityDate: r.maturityDate,
+ days: r.daysToMaturity,
+ amount: r.instrument === 'FD' ? (fdById.get(r.refId)?.expectedMaturityAmount ?? r.faceValue) : r.faceValue
+ }));
+ }
+
+ private limitRow(label: string, basis: string, limit: number, used: number): LimitRow {
+ return { label, basis, limit, used, usedPct: limit > 0 ? (used / limit) * 100 : null, within: used <= limit };
}
get totalHoldings(): number {
- return ALL_HOLDINGS.length;
+ return this.summaries.reduce((t, s) => t + s.count, 0);
}
get totalFaceValue(): number {
- return ALL_HOLDINGS.reduce((s, h) => s + h.faceValue, 0);
+ return this.summaries.reduce((t, s) => t + s.faceValue, 0);
+ }
+
+ get totalBookValue(): number {
+ return this.summaries.reduce((t, s) => t + s.bookValue, 0);
}
get totalAccruedInterest(): number {
- return ALL_HOLDINGS.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0);
+ return this.summaries.reduce((t, s) => t + s.accruedInterest, 0);
}
get totalMaturityValue(): number {
- return ALL_HOLDINGS.reduce((s, h) => s + maturityAmount(h), 0);
+ return this.summaries.reduce((t, s) => t + s.maturityValue, 0);
}
- get activeCount(): number {
- return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE').length;
+ get overdueCount(): number {
+ return this.maturingSoon.filter((m) => m.days < 0).length;
}
- get maturedCount(): number {
- return ALL_HOLDINGS.filter((h) => h.status === 'MATURED').length;
- }
-
- /** Holdings maturing within the next 90 days — a "due-date diary" style widget. */
- get maturingSoon(): InvestmentHolding[] {
- return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE' && daysToMaturity(h, this.asOnDate) <= 90).sort(
- (a, b) => daysToMaturity(a, this.asOnDate) - daysToMaturity(b, this.asOnDate)
- );
- }
-
- daysLeft(h: InvestmentHolding): number {
- return daysToMaturity(h, this.asOnDate);
- }
-
- instrumentLabel(type: InstrumentType): string {
- return INSTRUMENT_LABELS[type];
+ /** Holdings of one or more instrument types, for the Quick Entry cards. */
+ countOf(...types: InstrumentType[]): number {
+ return this.summaries.filter((s) => types.includes(s.type)).reduce((t, s) => t + s.count, 0);
}
async exportExcel(): Promise {
@@ -176,54 +307,97 @@ export class InvestmentDashboardComponent {
'Instrument Type',
'Holdings',
'Face Value',
- 'Accrued Interest',
+ 'Book Value',
+ 'Share %',
+ 'Interest Receivable',
'Maturity Value'
],
widths: [
- 26,
- 12,
+ 22,
+ 10,
18,
18,
+ 10,
+ 18,
18
],
- rows: this.summaries.map((s) => [
- s.label,
- s.count,
- s.faceValue,
- s.accruedInterest,
- s.maturityValue
- ])
+ rows: [
+ ...this.summaries.map((s) => [
+ s.label,
+ s.count,
+ s.faceValue,
+ s.bookValue,
+ Number(s.sharePct.toFixed(2)),
+ s.accruedInterest,
+ s.maturityValue
+ ]),
+ [
+ 'Total',
+ this.totalHoldings,
+ this.totalFaceValue,
+ this.totalBookValue,
+ 100,
+ this.totalAccruedInterest,
+ this.totalMaturityValue
+ ]
+ ]
},
{
- sheetName: 'Portfolio Allocation',
- title: 'Portfolio Allocation — Target vs Actual',
+ sheetName: 'RBI Limits',
+ title: 'RBI Limits',
subtitle: `As on ${this.asOnDate}`,
headers: [
- 'Instrument Type',
- 'Target Basis',
- 'Target %',
- 'Actual %',
- 'Actual Value',
- 'Variance %',
+ 'Limit',
+ 'Basis',
+ 'Allowed',
+ 'Used',
+ 'Used %',
'Status'
],
widths: [
26,
- 20,
- 12,
- 12,
+ 40,
18,
+ 18,
+ 10,
+ 14
+ ],
+ rows: this.limits.map((l) => [
+ l.label,
+ l.basis,
+ l.limit,
+ l.used,
+ l.usedPct === null ? null : Number(l.usedPct.toFixed(2)),
+ l.within ? 'Within' : 'Above limit'
+ ])
+ },
+ {
+ sheetName: 'Maturing in 90 days',
+ title: 'Maturing Within 90 Days (and overdue)',
+ subtitle: `As on ${this.asOnDate}`,
+ headers: [
+ 'Deal No',
+ 'Investment',
+ 'Particulars',
+ 'Maturity',
+ 'Days',
+ 'Amount'
+ ],
+ widths: [
+ 16,
+ 16,
+ 40,
12,
+ 10,
18
],
- rows: this.allocationVsTarget.map((a) => [
- a.label,
- a.isRbiLinked ? 'RBI SLR / Non-SLR Norm' : 'Board Policy',
- a.targetPct,
- a.actualPct,
- a.actualValue,
- a.variancePct,
- a.statusLabel
+ rows: this.maturingSoon.map((m) => [
+ m.dealNo,
+ m.instrument,
+ m.particulars,
+ m.maturityDate,
+ m.days,
+ m.amount
])
}
],
diff --git a/src/app/investment/investment-reports/investment-reports.component.html b/src/app/investment/investment-reports/investment-reports.component.html
index f8c91431b..29c989aee 100644
--- a/src/app/investment/investment-reports/investment-reports.component.html
+++ b/src/app/investment/investment-reports/investment-reports.component.html
@@ -12,17 +12,26 @@
TREASURY · INVESTMENT · REPORTS
Investment Reports
- Balance reports, ledgers, registers, maturity lists and the consolidated portfolio — all derived live from the
- same holdings/transactions used by the Entry screens.
+ Balance reports, ledgers, registers, maturity lists and the consolidated portfolio from the saved deals, FDs and
+ their GL entries, as on {{ asOnDate }}.
-
+
+ {{ loading ? 'Loading…' : 'Refresh' }}
+
+
{{ generating ? 'Exporting…' : 'Export to Excel' }}
+ @if (error) {
+
+ }
+
@@ -138,6 +147,12 @@
{{ h.status }}
+ } @empty {
+
+
+ {{ activeReport === 'shareBalance' ? 'Share entry is not live yet.' : 'No holdings.' }}
+
+
}
@@ -280,7 +295,7 @@
: activeReport === 'amortizeLedger'
? amortizeTxns
: dealSlips;
- track t.voucherNo
+ track $index
) {
{{ t.txnDate }}
@@ -303,7 +318,10 @@
}
@if (activeReport === 'intDueRegister') {
-
Interest Due Register (Non-Cumulative Holdings)
+
+ Interest Due Register — interest earned up to {{ asOnDate }} and not received yet ·
+
Due Date Diary
+
@@ -311,11 +329,11 @@
Script / Party
Frequency
Rate
- Due Amount (as on {{ asOnDate }})
+ Receivable (as on {{ asOnDate }})
- @for (h of interestDueRegister; track h.certNo) {
+ @for (h of interestDueRegister; track h.instrumentType + ':' + h.id) {
{{ h.certNo }}
@@ -325,6 +343,10 @@
{{ h.interestRate }}%
₹{{ h.dueAmount | number: '1.2-2' }}
+ } @empty {
+
+ No interest receivable.
+
}
@@ -344,7 +366,7 @@
- @for (h of slrBondRegister; track h.certNo) {
+ @for (h of slrBondRegister; track h.instrumentType + ':' + h.id) {
{{ h.certNo }}
@@ -372,29 +394,33 @@
- @for (t of receivedInterestTxns; track t.voucherNo) {
+ @for (t of receivedInterestTxns; track $index) {
{{ t.txnDate }}
{{ t.certNo }}
{{ t.particulars }}
₹{{ t.amount | number: '1.2-2' }}
+ } @empty {
+
+ No interest received yet.
+
}
-
Pending Interest (No Interest Posted Yet)
+
Pending Interest — earned up to {{ asOnDate }}, not received yet
Cert No
Script / Party
- Accrued (Pending)
+ Receivable
- @for (h of pendingInterestHoldings; track h.certNo) {
+ @for (h of pendingInterestHoldings; track h.instrumentType + ':' + h.id) {
{{ h.certNo }}
@@ -487,11 +513,11 @@
Cert No
Party
Face Value
- Matured On
+ Maturity Date
- @for (h of maturedFdList; track h.certNo) {
+ @for (h of maturedFdList; track h.instrumentType + ':' + h.id) {
{{ h.certNo }}
@@ -524,7 +550,7 @@
- @for (h of maturingList; track h.certNo) {
+ @for (h of maturingList; track h.instrumentType + ':' + h.id) {
{{ h.certNo }}
@@ -543,24 +569,12 @@
@if (activeReport === 'maturityPattern') {
Maturity Pattern Register
-
-
-
- Bucket
- Holdings
- Face Value
-
-
-
- @for (b of maturityBuckets; track b.label) {
-
- {{ b.label }}
- {{ b.count }}
- ₹{{ b.faceValue | number: '1.2-2' }}
-
- }
-
-
+
+ The maturity pattern (up to 1 year, 1–3, 3–5 and over 5 years) is the Bucket Wise Investment report.
+
+
}
@if (
@@ -601,7 +615,7 @@
: activeReport === 'shareSecList'
? shareSecList
: mutualFundList;
- track h.certNo
+ track h.instrumentType + ':' + h.id
) {
@@ -615,6 +629,12 @@
{{ h.status }}
+ } @empty {
+
+
+ {{ activeReport === 'shareSecList' ? 'Share entry is not live yet.' : 'No holdings.' }}
+
+
}
@@ -622,35 +642,35 @@
}
@if (activeReport === 'shiftingReport') {
-
- Shifting Report — SLR Securities with Market Deviation ≥ 1% (AFS/HTM review candidates)
-
+
Shifting Report — Govt Securities transferred to another script / category
- Cert No
- Script
- Purchase Rate
- Market Price
- Deviation %
+ Date
+ Deal No
+ From
+ To
+ Units
+ Book Value
+ Voucher
- @for (h of shiftingCandidates; track h.certNo) {
+ @for (x of transfers; track x.id) {
+ {{ x.transferDate }}
- {{ h.certNo }}
-
- {{ h.scriptName }}
- {{ h.purchaseRate }}
- {{ h.marketPrice }}
- 0" [class.col-out]="h.deviationPct < 0">
- {{ h.deviationPct }}%
+ {{ x.dealNo }}
+ {{ x.fromScriptCode }} {{ x.fromScriptDesc }}
+ {{ x.toScriptCode }} {{ x.toScriptDesc }}
+ {{ x.units | number: '1.0-4' }}
+ ₹{{ x.bookValue | number: '1.2-2' }}
+ {{ x.voucherNo || '—' }}
} @empty {
- No securities crossing the 1% deviation threshold today.
+ No securities shifted.
}
@@ -687,7 +707,7 @@
Total
-
+ {{ portfolioTotalCount }}
₹{{ portfolioTotalFaceValue | number: '1.2-2' }}
₹{{ portfolioTotalAccrued | number: '1.2-2' }}
₹{{ portfolioTotalMaturity | number: '1.2-2' }}
diff --git a/src/app/investment/investment-reports/investment-reports.component.ts b/src/app/investment/investment-reports/investment-reports.component.ts
index 51eddea92..080227b49 100644
--- a/src/app/investment/investment-reports/investment-reports.component.ts
+++ b/src/app/investment/investment-reports/investment-reports.component.ts
@@ -6,35 +6,44 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { Component } from '@angular/core';
+import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
+import { RouterLink } from '@angular/router';
+import { forkJoin, of } from 'rxjs';
+import { catchError } from 'rxjs/operators';
import {
- ALL_HOLDINGS,
- ALL_TXNS,
- FD_HOLDINGS,
- GOVT_SECURITY_HOLDINGS,
- NON_SLR_HOLDINGS,
- BOND_HOLDINGS,
- MUTUAL_FUND_HOLDINGS,
- SHARE_HOLDINGS,
- SCRIPT_MARKET_DETAIL,
InstrumentType,
InvestmentHolding,
InvestmentTxn,
INSTRUMENT_LABELS,
- accruedInterest,
- maturityAmount,
daysToMaturity,
+ parseDDMMYYYY,
todayDDMMYYYY
} from '../investment-data';
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
+import {
+ InvestmentDailyRateDto,
+ InvestmentFdDto,
+ InvestmentFdScheduleDto,
+ InvestmentSecurityTransferDto,
+ InvestmentService,
+ apiErrorMessage,
+ toFdHolding,
+ toFdTxns,
+ toHolding,
+ toInterestTxn,
+ toPurchaseTxn,
+ toSaleTxn,
+ toTransferTxn
+} from '../investment.service';
type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
/**
- * ~20 legacy Investment reports, grouped into 4 tabs. Every figure here is derived from the
- * same ALL_HOLDINGS/ALL_TXNS arrays used by the Entry screens and Dashboard — so a report can
- * never drift out of sync with what a Purchase/Sell/Interest entry actually recorded.
+ * ~20 legacy Investment reports in 4 tabs, on live data: the saved purchases and FDs as holdings, and their GL
+ * entries (settlements, interest, sales, redemptions, amortisation, transfers, FD placements and closures) as the
+ * ledgers. Interest receivable comes from the Interest Received & Receivable report, market rates from the Daily
+ * Script Master.
*/
@Component({
selector: 'mifosx-investment-reports',
@@ -44,37 +53,176 @@ type ReportGroup = 'balance' | 'ledger' | 'maturity' | 'portfolio';
'../investment-shared.scss'
],
standalone: true,
- imports: [CommonModule]
+ imports: [
+ CommonModule,
+ RouterLink
+ ]
})
-export class InvestmentReportsComponent {
+export class InvestmentReportsComponent implements OnInit {
+ private readonly investmentService = inject(InvestmentService);
+
activeGroup: ReportGroup = 'balance';
activeReport = 'govtSecBalance';
asOnDate = todayDDMMYYYY();
+ loading = false;
+ error = '';
generating = false;
toastMsg = '';
toastVisible = false;
private toastTimer: ReturnType | undefined;
+ /** Saved purchases and FDs as holdings, and their GL entries, newest first. */
+ holdings: InvestmentHolding[] = [];
+ txns: InvestmentTxn[] = [];
+ /** Coupons and FD interest paid out — interest actually received. */
+ receivedTxns: InvestmentTxn[] = [];
+ /** GOI category transfers (security shifted to another script / category). */
+ transfers: InvestmentSecurityTransferDto[] = [];
+ /** FD interest lines need one call per FD, so they load when a report needs them. */
+ fdInterestLoaded = false;
+ private fds: InvestmentFdDto[] = [];
+ private receivable = new Map();
+ private fdMaturity = new Map();
+ private latestRate = new Map();
+
+ ngOnInit(): void {
+ this.load();
+ }
+
+ load(): void {
+ const d = parseDDMMYYYY(this.asOnDate);
+ const fyStart = `01-04-${d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1}`;
+ this.loading = true;
+ this.error = '';
+ this.fdInterestLoaded = false;
+ forkJoin({
+ purchases: this.investmentService.getPurchases(),
+ fds: this.investmentService.getFds(),
+ sales: this.investmentService.getSales(),
+ postings: this.investmentService.getInterestPostings(),
+ transfers: this.investmentService.getTransfers(),
+ interest: this.investmentService.getInterestReport(fyStart, this.asOnDate),
+ rates: this.investmentService.getDailyRates().pipe(catchError(() => of([] as InvestmentDailyRateDto[])))
+ }).subscribe({
+ next: (r) => {
+ this.fds = r.fds;
+ this.transfers = r.transfers;
+ this.holdings = [
+ ...r.purchases.map(toHolding),
+ ...r.fds.map(toFdHolding)
+ ];
+ this.txns = this.byDateDesc([
+ ...r.purchases.map(toPurchaseTxn),
+ ...r.postings.map(toInterestTxn),
+ ...r.sales.map(toSaleTxn),
+ ...r.transfers.map(toTransferTxn),
+ ...r.fds.flatMap((f) => toFdTxns(f, []))
+ ]);
+ this.receivedTxns = this.byDateDesc(r.postings.filter((x) => x.lineType === 'COUPON').map(toInterestTxn));
+ this.receivable = new Map(
+ r.interest.rows.map((x) => [
+ x.instrument + ':' + x.refId,
+ x.receivable
+ ])
+ );
+ this.fdMaturity = new Map(
+ r.fds
+ .filter((f) => f.id != null)
+ .map((f) => [
+ f.id as number,
+ f.expectedMaturityAmount ?? f.principal ?? 0
+ ])
+ );
+ // Latest close on or before the as-on date, per script.
+ const asOn = parseDDMMYYYY(this.asOnDate).getTime();
+ this.latestRate = new Map();
+ [...r.rates]
+ .filter((x) => parseDDMMYYYY(x.rateDate).getTime() <= asOn)
+ .sort((a, b) => parseDDMMYYYY(a.rateDate).getTime() - parseDDMMYYYY(b.rateDate).getTime())
+ .forEach((x) => this.latestRate.set(x.scriptCode, x.closeRate));
+ this.loading = false;
+ if (this.activeReport === 'fdLedger' || this.activeReport === 'pendingReceived') {
+ this.loadFdInterest();
+ }
+ },
+ error: (err: unknown) => {
+ this.loading = false;
+ this.error = apiErrorMessage(err);
+ }
+ });
+ }
+
+ /** Adds each FD's posted interest lines to the ledgers (one schedule call per FD). */
+ private loadFdInterest(): void {
+ if (this.fdInterestLoaded || !this.fds.length) {
+ return;
+ }
+ this.fdInterestLoaded = true;
+ const withId = this.fds.filter((f) => f.id != null);
+ forkJoin(
+ withId.map((f) =>
+ this.investmentService
+ .getFdSchedule(f.id as number)
+ .pipe(catchError(() => of(null as InvestmentFdScheduleDto | null)))
+ )
+ ).subscribe((schedules) => {
+ const lines: InvestmentTxn[] = [];
+ const received: InvestmentTxn[] = [];
+ schedules.forEach((s, i) => {
+ if (!s) {
+ return;
+ }
+ const interest = toFdTxns(withId[i], s.rows).filter((t) => t.txnType === 'INTEREST');
+ lines.push(...interest);
+ received.push(...interest.filter((t) => !t.particulars.startsWith('Provision')));
+ });
+ this.txns = this.byDateDesc([
+ ...this.txns,
+ ...lines
+ ]);
+ this.receivedTxns = this.byDateDesc([
+ ...this.receivedTxns,
+ ...received
+ ]);
+ });
+ }
+
+ private byDateDesc(txns: InvestmentTxn[]): InvestmentTxn[] {
+ return txns
+ .filter((t) => !!t.txnDate)
+ .sort((a, b) => parseDDMMYYYY(b.txnDate).getTime() - parseDDMMYYYY(a.txnDate).getTime());
+ }
+
setGroup(group: ReportGroup, defaultReport: string): void {
this.activeGroup = group;
- this.activeReport = defaultReport;
+ this.setReport(defaultReport);
}
setReport(report: string): void {
this.activeReport = report;
+ if (report === 'fdLedger' || report === 'pendingReceived') {
+ this.loadFdInterest();
+ }
}
instrumentLabel(type: InstrumentType): string {
return INSTRUMENT_LABELS[type];
}
+ /** Interest earned up to the as-on date and not yet received (Interest Received & Receivable report). */
accrued(h: InvestmentHolding): number {
- return accruedInterest(h, this.asOnDate);
+ return this.receivable.get((h.instrumentType === 'FD' ? 'FD' : 'SECURITY') + ':' + h.id) ?? 0;
}
+ /** What comes back at maturity: the FD maturity amount, a security's face value, a fund's cost. */
maturity(h: InvestmentHolding): number {
- return maturityAmount(h);
+ if (h.instrumentType === 'FD') {
+ return this.fdMaturity.get(h.id ?? -1) ?? h.faceValue;
+ }
+ return h.instrumentType === 'MUTUAL_FUND' || h.instrumentType === 'SHARE'
+ ? (h.bookValue ?? h.faceValue)
+ : h.faceValue;
}
daysLeft(h: InvestmentHolding): number {
@@ -85,23 +233,32 @@ export class InvestmentReportsComponent {
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
}
+ /** Latest close in the Daily Script Master on or before the as-on date. */
marketPrice(scriptCode: string): number | null {
- return SCRIPT_MARKET_DETAIL.find((m) => m.scriptCode === scriptCode)?.marketPrice ?? null;
+ return this.latestRate.get(scriptCode) ?? null;
+ }
+
+ private active(type?: InstrumentType): InvestmentHolding[] {
+ return this.holdings.filter((h) => h.status === 'ACTIVE' && (!type || h.instrumentType === type));
+ }
+
+ private ofType(type: InstrumentType): InvestmentHolding[] {
+ return this.holdings.filter((h) => h.instrumentType === type);
}
// -------------------- Balance Reports --------------------
get govtSecHoldings(): InvestmentHolding[] {
- return GOVT_SECURITY_HOLDINGS;
+ return this.active('GOVT_SECURITY');
}
get shareHoldings(): InvestmentHolding[] {
- return SHARE_HOLDINGS;
+ return this.active('SHARE');
}
get fdHoldings(): InvestmentHolding[] {
- return FD_HOLDINGS;
+ return this.active('FD');
}
get bankInvestmentList(): Array<{ party: string; count: number; totalFaceValue: number }> {
const map = new Map();
- for (const h of ALL_HOLDINGS) {
+ for (const h of this.active()) {
const row = map.get(h.party) || { party: h.party, count: 0, totalFaceValue: 0 };
row.count += 1;
row.totalFaceValue += h.faceValue;
@@ -112,112 +269,68 @@ export class InvestmentReportsComponent {
// -------------------- Ledgers & Registers --------------------
get fdTxns(): InvestmentTxn[] {
- return ALL_TXNS.filter((t) => t.instrumentType === 'FD');
+ return this.txns.filter((t) => t.instrumentType === 'FD');
}
get bondTxns(): InvestmentTxn[] {
- return ALL_TXNS.filter((t) => ([
+ return this.txns.filter((t) => ([
'GOVT_SECURITY',
'NON_SLR',
- 'BOND'
+ 'BOND',
+ 'MUTUAL_FUND'
] as InstrumentType[]).includes(t.instrumentType));
}
get shareTxns(): InvestmentTxn[] {
- return ALL_TXNS.filter((t) => t.instrumentType === 'SHARE');
+ return this.txns.filter((t) => t.instrumentType === 'SHARE');
}
get amortizeTxns(): InvestmentTxn[] {
- return ALL_TXNS.filter((t) => t.txnType === 'AMORTIZE');
+ return this.txns.filter((t) => t.txnType === 'AMORTIZE');
}
get dealSlips(): InvestmentTxn[] {
- return ALL_TXNS.filter((t) => t.txnType === 'PURCHASE');
+ return this.txns.filter((t) => t.txnType === 'PURCHASE' && t.instrumentType !== 'FD');
}
get slrBondRegister(): InvestmentHolding[] {
- return GOVT_SECURITY_HOLDINGS.filter((h) => h.subType === 'SLR');
+ return this.active('GOVT_SECURITY');
}
+ /** Holdings with interest earned up to the as-on date and not received yet, with the amount. */
get interestDueRegister(): Array {
- return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE' && h.interestFreq !== 'CUMULATIVE').map((h) => ({
- ...h,
- dueAmount: this.accrued(h)
- }));
+ return this.active()
+ .map((h) => ({ ...h, dueAmount: this.accrued(h) }))
+ .filter((h) => h.dueAmount > 0);
}
get receivedInterestTxns(): InvestmentTxn[] {
- return ALL_TXNS.filter((t) => t.txnType === 'INTEREST' || t.txnType === 'DIVIDEND');
+ return this.receivedTxns;
}
get pendingInterestHoldings(): InvestmentHolding[] {
- const receivedCerts = new Set(this.receivedInterestTxns.map((t) => t.certNo));
- return ALL_HOLDINGS.filter(
- (h) => h.status === 'ACTIVE' && h.interestFreq !== 'CUMULATIVE' && !receivedCerts.has(h.certNo)
- );
+ return this.active().filter((h) => this.accrued(h) > 0);
}
// -------------------- Maturity & Lists --------------------
get maturedFdList(): InvestmentHolding[] {
- return FD_HOLDINGS.filter((h) => h.status === 'MATURED');
+ return this.ofType('FD').filter((h) => h.status === 'MATURED');
}
get maturingList(): InvestmentHolding[] {
return [
- ...FD_HOLDINGS,
- ...GOVT_SECURITY_HOLDINGS
- ]
- .filter((h) => h.status === 'ACTIVE')
- .sort((a, b) => this.daysLeft(a) - this.daysLeft(b));
- }
- get maturityBuckets(): Array<{ label: string; count: number; faceValue: number }> {
- const buckets = [
- { label: '0-1 Year', min: -99999, max: 365, count: 0, faceValue: 0 },
- { label: '1-3 Years', min: 366, max: 1095, count: 0, faceValue: 0 },
- { label: '3-5 Years', min: 1096, max: 1825, count: 0, faceValue: 0 },
- { label: 'Above 5 Years', min: 1826, max: 999999, count: 0, faceValue: 0 }
- ];
- for (const h of ALL_HOLDINGS.filter((x) => x.status === 'ACTIVE')) {
- const days = this.daysLeft(h);
- const bucket = buckets.find((b) => days >= b.min && days <= b.max);
- if (bucket) {
- bucket.count += 1;
- bucket.faceValue += h.faceValue;
- }
- }
- return buckets;
+ ...this.active('FD'),
+ ...this.active('GOVT_SECURITY')
+ ].sort((a, b) => this.daysLeft(a) - this.daysLeft(b));
}
get govtSecList(): InvestmentHolding[] {
- return GOVT_SECURITY_HOLDINGS;
+ return this.ofType('GOVT_SECURITY');
}
get fdSecList(): InvestmentHolding[] {
- return FD_HOLDINGS;
+ return this.ofType('FD');
}
get shareSecList(): InvestmentHolding[] {
- return SHARE_HOLDINGS;
+ return this.ofType('SHARE');
}
get mutualFundList(): InvestmentHolding[] {
- return MUTUAL_FUND_HOLDINGS;
- }
- get nonSlrList(): InvestmentHolding[] {
- return NON_SLR_HOLDINGS;
- }
- get bondList(): InvestmentHolding[] {
- return BOND_HOLDINGS;
- }
- /** Shifting Report — SLR securities whose market value has moved away from book value by
- * more than 1%, a proxy for "category shift" candidates (AFS/HTM reclassification review). */
- get shiftingCandidates(): Array {
- return GOVT_SECURITY_HOLDINGS.map((h) => {
- const mp = this.marketPrice(h.scriptCode);
- if (mp === null) {
- return null;
- }
- const deviationPct = +(((mp - h.purchaseRate) / h.purchaseRate) * 100).toFixed(2);
- return { ...h, marketPrice: mp, deviationPct };
- })
- .filter(
- (x): x is InvestmentHolding & { marketPrice: number; deviationPct: number } =>
- x !== null && Math.abs(x.deviationPct) >= 1
- )
- .sort((a, b) => Math.abs(b.deviationPct) - Math.abs(a.deviationPct));
+ return this.ofType('MUTUAL_FUND');
}
// -------------------- Portfolio --------------------
private readonly portfolioTypes: InstrumentType[] = [
- 'FD',
'GOVT_SECURITY',
+ 'FD',
'NON_SLR',
'BOND',
'MUTUAL_FUND',
@@ -231,7 +344,7 @@ export class InvestmentReportsComponent {
maturityValue: number;
}> {
return this.portfolioTypes.map((type) => {
- const holdings = ALL_HOLDINGS.filter((h) => h.instrumentType === type);
+ const holdings = this.active(type);
return {
label: INSTRUMENT_LABELS[type],
count: holdings.length,
@@ -241,6 +354,9 @@ export class InvestmentReportsComponent {
};
});
}
+ get portfolioTotalCount(): number {
+ return this.portfolioSummary.reduce((s, r) => s + r.count, 0);
+ }
get portfolioTotalFaceValue(): number {
return this.portfolioSummary.reduce((s, r) => s + r.faceValue, 0);
}
@@ -480,26 +596,6 @@ export class InvestmentReportsComponent {
this.holdingsExportWidths,
this.maturingList.map((h) => this.holdingsExportRow(h))
);
- case 'maturityPattern':
- return one(
- 'Maturity Pattern',
- 'Maturity Pattern Register',
- [
- 'Bucket',
- 'Holdings',
- 'Face Value'
- ],
- [
- 22,
- 14,
- 18
- ],
- this.maturityBuckets.map((b) => [
- b.label,
- b.count,
- b.faceValue
- ])
- );
case 'govtSecList':
return one(
'Govt Security List',
@@ -535,27 +631,33 @@ export class InvestmentReportsComponent {
case 'shiftingReport':
return one(
'Shifting Report',
- 'Shifting Report — SLR Market Deviation ≥ 1%',
+ 'Shifting Report — Govt Securities transferred to another script / category',
[
- 'Cert No',
- 'Script',
- 'Purchase Rate',
- 'Market Price',
- 'Deviation %'
+ 'Date',
+ 'Deal No',
+ 'From',
+ 'To',
+ 'Units',
+ 'Book Value',
+ 'Voucher No'
],
[
- 14,
- 30,
+ 12,
16,
+ 34,
+ 34,
+ 12,
16,
- 14
+ 20
],
- this.shiftingCandidates.map((x) => [
- x.certNo,
- x.scriptName,
- x.purchaseRate,
- x.marketPrice,
- x.deviationPct
+ this.transfers.map((x) => [
+ x.transferDate,
+ x.dealNo,
+ `${x.fromScriptCode} ${x.fromScriptDesc}`,
+ `${x.toScriptCode} ${x.toScriptDesc}`,
+ x.units,
+ x.bookValue,
+ x.voucherNo ?? ''
])
);
case 'portfolio':
diff --git a/src/app/investment/investment.service.ts b/src/app/investment/investment.service.ts
index 3ca835bc8..95c582551 100644
--- a/src/app/investment/investment.service.ts
+++ b/src/app/investment/investment.service.ts
@@ -15,6 +15,7 @@ import {
HoldingStatus,
InstrumentType,
InvestmentHolding,
+ InvestmentTxn,
PARTY_MASTER,
PartyMaster,
SCRIPT_MASTER,
@@ -756,6 +757,178 @@ export function toHolding(p: InvestmentPurchaseDto): InvestmentHolding {
};
}
+/** A security transfer shows the book value carried to the new security. */
+export function toTransferTxn(x: InvestmentSecurityTransferDto): InvestmentTxn {
+ return {
+ certNo: x.certNo,
+ instrumentType: x.bondType as InstrumentType,
+ txnType: 'TRANSFER',
+ voucherNo: x.voucherNo ?? `TRF/${x.dealNo}/${x.id}`,
+ txnDate: x.transferDate,
+ amount: x.bookValue,
+ particulars: `GOI category transfer - Deal ${x.dealNo}: ${x.fromScriptCode} ${x.fromScriptDesc} → ${x.toScriptCode} ${x.toScriptDesc}`,
+ glCode: x.toGlCode,
+ glName: x.glTransactionId
+ ? `${x.toGlName} / Cr ${x.fromGlCode} (txn ${x.glTransactionId})`
+ : `${x.toGlName} (same GL, no entry)`
+ };
+}
+
+/** A sale or redemption shows the money into the bank; a pending sale is not in the GL yet. */
+export function toSaleTxn(x: InvestmentSaleDto): InvestmentTxn {
+ if (x.saleType === 'AMORTISE') {
+ return {
+ certNo: x.certNo,
+ instrumentType: x.bondType as InstrumentType,
+ txnType: 'AMORTIZE',
+ voucherNo: x.voucherNo ?? `AMZ/${x.dealNo}/${x.id}`,
+ txnDate: x.settlementDate,
+ amount: x.bookValueSold,
+ particulars: `${x.remarks ?? 'Premium amortisation'} - Deal ${x.dealNo} - ${x.scriptDesc} (${x.accruedDays} days)`,
+ glCode: '500604',
+ glName: `Amortisation of Premium / Cr Premium on Investments (txn ${x.glTransactionId})`
+ };
+ }
+ if (x.saleType === 'WRITEOFF') {
+ return {
+ certNo: x.certNo,
+ instrumentType: x.bondType as InstrumentType,
+ txnType: 'WRITE_OFF',
+ voucherNo: x.voucherNo ?? `WOF/${x.dealNo}/${x.id}`,
+ txnDate: x.settlementDate,
+ amount: x.bookValueSold,
+ particulars: `Written off - Deal ${x.dealNo} - ${x.scriptDesc}${x.remarks ? ' - ' + x.remarks : ''}`,
+ glCode: '500605',
+ glName: `Loss on Investments / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
+ };
+ }
+ const redeem = x.saleType === 'REDEEM';
+ const pl =
+ x.profitLoss >= 0
+ ? `profit ₹${x.profitLoss}`
+ : redeem
+ ? `premium written off ₹${-x.profitLoss}`
+ : `loss ₹${-x.profitLoss}`;
+ return {
+ certNo: x.certNo,
+ instrumentType: x.bondType as InstrumentType,
+ txnType: x.saleType,
+ voucherNo: x.voucherNo ?? `SALE/${x.dealNo}/${x.id}`,
+ txnDate: x.settlementDate,
+ amount: x.netAmount,
+ particulars: redeem
+ ? `Redeemed at maturity - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`
+ : `Sold ${x.units} units @ ${x.saleRate} to ${x.partyName ?? ''} - Deal ${x.dealNo} - ${x.scriptDesc} (${pl})`,
+ glCode: x.status === 'SETTLED' ? x.bankGlCode : '',
+ glName:
+ x.status === 'SETTLED'
+ ? `${x.bankGlName} / Cr ${x.investmentGlCode} (txn ${x.glTransactionId})`
+ : 'Not posted (pending settlement)'
+ };
+}
+
+/** A coupon shows the cash received into the bank; a provision the interest booked as receivable. */
+export function toInterestTxn(x: InvestmentInterestPostingDto): InvestmentTxn {
+ const coupon = x.lineType === 'COUPON';
+ return {
+ certNo: x.certNo,
+ instrumentType: x.bondType as InstrumentType,
+ txnType: 'INTEREST',
+ voucherNo: x.voucherNo,
+ txnDate: x.intDate,
+ amount: coupon ? x.grossInterest : x.amount,
+ particulars: `${coupon ? 'Coupon' : 'Provision'} ${x.fromDate} to ${x.toDate} - Deal ${x.dealNo} - ${x.scriptDesc}`,
+ glCode: x.drGlCode,
+ glName: `${x.drGlName} / Cr ${x.incomeGlCode} (txn ${x.glTransactionId})`
+ };
+}
+
+/** A purchase reaches the GL only when it is settled, dated the settlement date. */
+export function toPurchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
+ const posted = !!d.glTransactionId;
+ return {
+ certNo: d.certNo,
+ instrumentType: d.bondType,
+ txnType: 'PURCHASE',
+ voucherNo: `INV/PUR/${d.dealNo}`,
+ txnDate: d.settlementDate,
+ amount: d.settlementAmount,
+ particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]`,
+ glCode: posted ? d.investmentGlCode : '',
+ glName: posted ? `${d.investmentGlName} (txn ${d.glTransactionId})` : 'Not posted (pending settlement)'
+ };
+}
+
+/** An FD as a row of the holdings registers: principal as face value, closed FDs shown as matured. */
+export function toFdHolding(f: InvestmentFdDto): InvestmentHolding {
+ return {
+ id: f.id,
+ dealNo: f.dealNo,
+ certNo: f.certNo ?? '',
+ instrumentType: 'FD',
+ scriptCode: '',
+ scriptName: `FD @ ${f.intRate ?? 0}%`,
+ party: f.partyName ?? '',
+ partyCode: f.partyCode,
+ subType: '',
+ faceValue: f.principal ?? 0,
+ purchaseRate: 100,
+ units: 1,
+ purchaseDate: f.dealDate ?? '',
+ maturityDate: f.maturityDate ?? '',
+ interestRate: f.intRate ?? 0,
+ interestFreq: f.payout === 'HALF_YEARLY' ? 'HALF_YEARLY' : f.payout === 'YEARLY' ? 'YEARLY' : 'CUMULATIVE',
+ glCode: f.fdGlCode ?? '',
+ glName: f.fdGlName ?? '',
+ crToAc: f.bankGlCode ?? '',
+ brokerCode: '',
+ status: f.status === 'ACTIVE' ? 'ACTIVE' : 'MATURED',
+ bookValue: f.principal
+ };
+}
+
+/** The GL entries of an FD: placing it, each posted interest line, and its closure. */
+export function toFdTxns(f: InvestmentFdDto, rows: InvestmentFdInterestRowDto[]): InvestmentTxn[] {
+ const base = { certNo: f.certNo ?? '', instrumentType: 'FD' as InstrumentType };
+ const out: InvestmentTxn[] = [
+ {
+ ...base,
+ txnType: 'PURCHASE',
+ voucherNo: f.voucherNo ?? `FD/${f.dealNo}`,
+ txnDate: f.dealDate ?? '',
+ amount: f.principal ?? 0,
+ particulars: `FD placed - Deal ${f.dealNo} - ${f.partyName} @ ${f.intRate}% to ${f.maturityDate}`,
+ glCode: f.fdGlCode,
+ glName: `${f.fdGlName} / Cr ${f.bankGlCode} (txn ${f.glTransactionId})`
+ }
+ ];
+ for (const r of rows.filter((x) => x.posted)) {
+ out.push({
+ ...base,
+ txnType: 'INTEREST',
+ voucherNo: r.voucherNo ?? `FDI/${f.dealNo}/${r.intDate}`,
+ txnDate: r.intDate,
+ amount: r.lineType === 'PROVISION' ? r.amount : r.grossInterest,
+ particulars: `${r.lineType === 'PROVISION' ? 'Provision' : 'Interest payout'} ${r.fromDate} to ${r.toDate} - Deal ${f.dealNo} - ${f.partyName}${r.tds ? ` (TDS ₹${r.tds})` : ''}`,
+ glCode: f.interestGlCode,
+ glName: `Cr ${f.interestGlName} (txn ${r.glTransactionId})`
+ });
+ }
+ if (f.closedOnDate) {
+ out.push({
+ ...base,
+ txnType: 'REDEEM',
+ voucherNo: f.closureVoucherNo ?? `FDC/${f.dealNo}`,
+ txnDate: f.closedOnDate,
+ amount: f.closureAmount ?? 0,
+ particulars: `FD ${(f.closureType ?? 'closed').toLowerCase()} - Deal ${f.dealNo} - ${f.partyName} (interest ₹${f.closureInterest ?? 0}${f.closureTds ? `, TDS ₹${f.closureTds}` : ''}${f.closurePenal ? `, penal ₹${f.closurePenal}` : ''})`,
+ glCode: f.bankGlCode,
+ glName: `${f.bankGlName} / Cr ${f.fdGlCode} (txn ${f.closureGlTransactionId})`
+ });
+ }
+ return out;
+}
+
/** One line of a holding's interest schedule (see InvestmentInterestScheduleData in the backend). */
export interface InvestmentInterestRowDto {
type: 'COUPON' | 'PROVISION';