feat: add investment reports, bond components, dashboard, and service implementation

This commit is contained in:
2026-10-23 16:49:50 +05:30
parent 731ab5a0e0
commit 8e98077158
6 changed files with 865 additions and 497 deletions
@@ -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;