Files
cbsnexora/src/app/investment/bond-investment/bond-investment.component.ts
T
anshulandClaude Opus 5.5 6a04a92113 feat(investment): Daily Script Master, market rate on purchase, broken-period interest as expense
- 09 Daily Script Master now saves to and loads from the backend (sample
  rows removed), with Drop to correct a wrong rate.
- Purchase Entry takes the rate (NAV for a mutual fund) from the newest
  Daily Script Master rate on or before the deal date, shows which rate
  was used, warns when the rate entered is outside the day's low-high,
  and clears the rate when the script has none.
- Broken-period interest paid to the seller is debited to expense GL
  500606 instead of Interest Receivable (RBI UCB Investment Directions,
  para 152, confirmed by Jitendra Sir); Interest Entry labels it so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:27:15 +05:30

1526 lines
48 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
import {
SCRIPT_MASTER,
PARTY_MASTER,
SUBTYPE_MASTER,
BROKER_MASTER,
SGL_BANK_MASTER,
MUTUAL_FUND_PRODUCTS,
LIEN_REASON_MASTER,
CATEGORY_OPTIONS,
DEAL_TYPE_OPTIONS,
MARKET_OPTIONS,
PAYMENT_MODE_OPTIONS,
DELIVERY_MODE_OPTIONS,
BOND_RATING_OPTIONS,
INT_TYPE_OPTIONS,
AUTHORITY_OPTIONS,
INVESTMENT_GL_BY_TYPE,
INVESTMENT_GL_LOOKUP,
INTEREST_RECEIVABLE_GL,
BROKEN_PERIOD_INTEREST_GL,
STAMP_DUTY_GL,
BROKERAGE_GL,
OTHER_CHARGES_GL,
SETTLEMENT_BANK_GL_OPTIONS,
periodBetween,
parseDDMMYYYY,
days360,
lastHalfYearlyCouponDate,
nextHalfYearlyCouponDate,
couponDatesLabel,
InvestmentHolding,
InvestmentTxn,
InstrumentType,
InterestFreq,
INSTRUMENT_LABELS,
accruedInterest,
daysToMaturity,
maturityAmount,
addPeriod,
approxYtm,
yearsBetween,
todayDDMMYYYY
} from '../investment-data';
import { exportWorkbook } from '../investment-export';
import {
InvestmentDailyRateDto,
InvestmentInterestScheduleDto,
InvestmentPurchaseDto,
InvestmentService,
apiErrorMessage,
toHolding
} from '../investment.service';
type BondType = 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND';
type BondTab =
| 'holdings'
| 'purchase'
| 'redemption'
| 'interest'
| 'sell'
| 'writeoff'
| 'amortize'
| 'amortizeYearly'
| 'goiTransfer'
| 'depreciation'
| 'mfTransfer';
interface BondPurchaseForm {
bondType: BondType;
dealNo: string;
dealDate: string;
dealTime: string;
isin: string;
scriptCode: string;
certNo: string;
settlementType: 'T+0' | 'T+1' | 'T+2';
settlementDt: string;
faceValue: number | null;
purchaseRate: number | null;
partyCode: string;
subType: string;
tbillGlAc: string;
crToAc: string;
premiumAmt: number | null;
brokerage: number | null;
brokerName: string;
tradeNo: string;
orderNo: string;
recommAuthority: string;
sanctionAuthority: string;
issueDate: string;
periodYear: number | null;
periodMonth: number | null;
periodDays: number | null;
descInv: string;
folioNo: string;
callOption: string;
meetDt: string;
resoNo: string;
fromDist: string;
toDist: string;
redumMatDt: string;
category: string;
intRate: number | null;
intType: InterestFreq;
intDate: string;
lastIntDt: string;
dealType: string;
market: string;
markCblo: 'YES' | 'NO';
amortizeAmt: number | null;
remarks: string;
lien: 'YES' | 'NO';
lienAmount: number | null;
lienReason: string;
deliveryMode: string;
bondRating: string;
corpBond: 'YES' | 'NO';
paymentMode: string;
sglWithBank: string;
description: string;
registerAdd: string;
regiCity: string;
regiPin: string;
// Mutual Fund specific
productName: string;
considerAmt: number | null;
tradeAmt: number | null;
nav: number | null;
holdingDays: number | null;
stampDuty: number | null;
stampDutyAc: string;
otherCharges: number | null;
}
interface AccountingPreviewLine {
side: 'Dr' | 'Cr';
glCode: string;
glName: string;
amount: number;
}
interface BondSellForm {
/** Legacy screen has SELL / REDEEM / MUTUAL FUND SELL radio at the top. */
mode: 'SELL' | 'REDEEM' | 'MUTUAL_FUND_SELL';
/** Purchase Search — locate the original purchase by Deal No or ISIN No. */
searchBy: 'DEAL_NO' | 'ISIN_NO';
searchValue: string;
bondType: BondType;
scriptCode: string;
certNo: string;
buyerCode: string;
subType: string;
isin: string;
tbillGlAc: string;
dealDate: string;
dealTime: string;
settlementType: 'T+0' | 'T+1' | 'T+2';
settlementDt: string;
faceValue: number | null;
sellRate: number | null;
tdsAmt: number | null;
unitVal: number | null;
payAmt: number | null;
sellUnit: number | null;
brokerage: number | null;
dbToAc: string;
amortizeAmt: number | null;
recommAuthority: string;
sanctionAuthority: string;
callMoney: boolean;
}
/**
* Combined Govt Security / Non-SLR / Bond / Mutual Fund treasury screen, mirroring the legacy
* "03 Govt./Bond Entry" submenu in full: Purchase, Redemption, Interest Calculation, Sell,
* Write-off, Amortize, Amortize Yearly Process, GOI Category Transfer, Investment Depreciation
* and Mutual Fund Transfer. Dummy in-memory data; Save appends to the local arrays.
*/
@Component({
selector: 'mifosx-bond-investment',
templateUrl: './bond-investment.component.html',
styleUrls: [
'../../cashier/cashier-shared.scss',
'../investment-shared.scss'
],
standalone: true,
imports: [
CommonModule,
FormsModule,
DateDdmmyyyyPickerComponent
]
})
export class BondInvestmentComponent implements OnInit {
private readonly investmentService = inject(InvestmentService);
/** Script and party dropdowns offer the masters saved in the backend. */
ngOnInit(): void {
this.investmentService.syncSharedMasters().subscribe({
error: (err) => this.toast(`Could not load Script / Party masters: ${apiErrorMessage(err)}`)
});
this.investmentService.getAssetGlAccounts().subscribe({
next: (gls) =>
(this.glIdByCode = new Map(
gls.map((g) => [
g.glCode,
g.id
])
)),
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
});
this.loadPurchases();
}
/** GL id for a GL code — the purchase API takes ids, the form works with codes. */
private glIdByCode = new Map<string, number>();
/** The Holdings register and transaction log are the purchases saved in the backend. */
private loadPurchases(): void {
this.investmentService.getPurchases().subscribe({
next: (purchases) => {
this.holdings = purchases.map(toHolding);
this.txns = purchases.map((d) => this.purchaseTxn(d));
this.purchaseForm = this.blankPurchaseForm();
},
error: (err) => this.toast(`Could not load purchases: ${apiErrorMessage(err)}`)
});
}
private purchaseTxn(d: InvestmentPurchaseDto): InvestmentTxn {
return {
certNo: d.certNo,
instrumentType: d.bondType,
txnType: 'PURCHASE',
voucherNo: `INV/PUR/${d.dealNo}`,
txnDate: d.dealDate,
amount: d.settlementAmount,
particulars: `Purchase Deal ${d.dealNo} - ${d.scriptDesc} - ${d.partyName} [${d.category}]`,
glCode: d.investmentGlCode,
glName: d.investmentGlName
};
}
activeTab: BondTab = 'holdings';
/** Purchase Entry shows only the mandatory fields; the rest sit under "More Details". */
showMoreDetails = false;
/** Cert No typed in the Retrieve box (Cert No itself is system-generated and read-only). */
retrieveCertNo = '';
bondType: BondType = 'GOVT_SECURITY';
asOnDate = todayDDMMYYYY();
/** Loaded from the backend (saved purchases) in ngOnInit. */
holdings: InvestmentHolding[] = [];
txns: InvestmentTxn[] = [];
// Master-driven dropdown sources
readonly scriptOptions = SCRIPT_MASTER;
readonly partyOptions = PARTY_MASTER;
readonly subTypeOptions = SUBTYPE_MASTER;
readonly brokerOptions = BROKER_MASTER;
readonly sglBankOptions = SGL_BANK_MASTER;
readonly mfProductOptions = MUTUAL_FUND_PRODUCTS;
readonly lienReasonOptions = LIEN_REASON_MASTER;
readonly categoryOptions = CATEGORY_OPTIONS;
readonly dealTypeOptions = DEAL_TYPE_OPTIONS;
readonly marketOptions = MARKET_OPTIONS;
readonly paymentModeOptions = PAYMENT_MODE_OPTIONS;
readonly deliveryModeOptions = DELIVERY_MODE_OPTIONS;
readonly bondRatingOptions = BOND_RATING_OPTIONS;
readonly intTypeOptions = INT_TYPE_OPTIONS;
readonly authorityOptions = AUTHORITY_OPTIONS;
generating = false;
toastMsg = '';
toastVisible = false;
private toastTimer: ReturnType<typeof setTimeout> | undefined;
purchaseForm: BondPurchaseForm = this.blankPurchaseForm();
sellForm: BondSellForm = this.blankSellForm();
readonly settlementBankOptions = SETTLEMENT_BANK_GL_OPTIONS;
// Redemption Entry
redeemCertNo = '';
redeemDate = this.asOnDate;
redeemRemarks = '';
// 03 Interest Entry (legacy: Type / Script / Deal No + Retrieve) — the backend works out every
// half-yearly coupon and 31 Mar / 30 Sep closing provision of the holding. Display only:
// nothing is posted to GL until the posting rules are approved.
intType: BondType | '' = '';
intScript = '';
intPurchaseId: number | null = null;
intAsOnDate = this.asOnDate;
intSchedule: InvestmentInterestScheduleDto | null = null;
intLoading = false;
readonly interestReceivableGl = INTEREST_RECEIVABLE_GL;
// Write-off
writeoffCertNo = '';
writeoffReason = '';
// Amortize
amortizeCertNo = '';
amortizeAmt: number | null = null;
// Amortize Yearly Process
amortizeYear = String(new Date().getFullYear());
// GOI Category Transfer
goiCertNo = '';
goiFromCategory = '';
goiToCategory = '';
goiTransferDate = this.asOnDate;
// Investment Depreciation
depreciationDate = this.asOnDate;
depreciationCategory = '';
// Mutual Fund Transfer
mfFromCertNo = '';
mfToProduct = '';
mfTransferUnits: number | null = null;
mfTransferDate = this.asOnDate;
readonly bondTypeOptions: Array<{ value: BondType; label: string }> = [
{ value: 'GOVT_SECURITY', label: 'GOVT SECURITY' },
{ value: 'NON_SLR', label: 'NON SLR' },
{ value: 'BOND', label: 'BOND' },
{ value: 'MUTUAL_FUND', label: 'MUTUAL FUND' }
];
setTab(tab: BondTab): void {
this.activeTab = tab;
}
setBondType(type: BondType): void {
this.bondType = type;
this.purchaseForm.bondType = type;
}
get holdingsForType(): InvestmentHolding[] {
return this.holdings.filter((h) => h.instrumentType === this.bondType);
}
get activeHoldingsForType(): InvestmentHolding[] {
return this.holdingsForType.filter((h) => h.status === 'ACTIVE');
}
get txnsForType(): InvestmentTxn[] {
return this.txns.filter((t) => t.instrumentType === this.bondType);
}
get scriptsForType(): typeof SCRIPT_MASTER {
return this.scriptOptions.filter((s) => s.instrumentType === this.purchaseForm.bondType);
}
instrumentLabel(type: InstrumentType): string {
return INSTRUMENT_LABELS[type];
}
/** Preview of the deal number the backend will assign: FY prefix (e.g. 202627) + running number. */
private nextDealNo(): string {
const d = parseDDMMYYYY(this.asOnDate);
const fyStart = d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1;
const prefix = `${fyStart}${String((fyStart + 1) % 100).padStart(2, '0')}`;
const nums = (this.holdings ?? [])
.map((h) => h.dealNo)
.filter((n): n is string => !!n && n.startsWith(prefix))
.map((n) => parseInt(n.slice(prefix.length), 10));
return `${prefix}${String((nums.length ? Math.max(...nums) : 0) + 1).padStart(6, '0')}`;
}
/** Certificate No is system-generated, as in the existing CBS (greyed there). */
/** Preview of the certificate number the backend will assign (running number). */
private nextCertNo(): string {
const nums = (this.holdings ?? []).map((h) => parseInt(h.certNo, 10)).filter((n) => !isNaN(n));
return String((nums.length ? Math.max(...nums) : 0) + 1);
}
private blankPurchaseForm(): BondPurchaseForm {
const type = this.bondType ?? 'GOVT_SECURITY';
this.showMoreDetails = false;
this.retrieveCertNo = '';
this.purchaseMarketRate = undefined;
return {
bondType: type,
dealNo: this.nextDealNo(),
dealDate: this.asOnDate,
dealTime: new Date().toTimeString().slice(0, 8),
isin: '',
scriptCode: '',
certNo: this.nextCertNo(),
settlementType: 'T+0',
settlementDt: this.asOnDate,
faceValue: null,
purchaseRate: 100,
partyCode: '',
subType: type === 'GOVT_SECURITY' ? 'SLR' : type === 'NON_SLR' ? 'NON SLR' : 'CORPORATE BOND',
tbillGlAc: '',
crToAc: SETTLEMENT_BANK_GL_OPTIONS[0]?.glCode ?? '',
premiumAmt: null,
brokerage: null,
brokerName: '',
tradeNo: '',
orderNo: '',
recommAuthority: '',
sanctionAuthority: '',
issueDate: this.asOnDate,
periodYear: 3,
periodMonth: 0,
periodDays: 0,
descInv: '',
folioNo: '',
callOption: '',
meetDt: '',
resoNo: '',
fromDist: '',
toDist: '',
redumMatDt: '',
category: '',
intRate: null,
intType: 'HALF_YEARLY',
intDate: this.asOnDate,
lastIntDt: this.asOnDate,
dealType: 'OUTRIGHT',
market: 'NDS-OM',
markCblo: 'NO',
amortizeAmt: null,
remarks: '',
lien: 'NO',
lienAmount: null,
lienReason: '',
deliveryMode: '',
bondRating: type === 'GOVT_SECURITY' ? 'SOVEREIGN' : 'AAA',
corpBond: type === 'BOND' ? 'YES' : 'NO',
paymentMode: '',
sglWithBank: '',
description: '',
registerAdd: '',
regiCity: '',
regiPin: '',
productName: '',
considerAmt: null,
tradeAmt: null,
nav: null,
holdingDays: null,
stampDuty: null,
stampDutyAc: '',
otherCharges: null
};
}
private blankSellForm(): BondSellForm {
return {
mode: 'SELL',
searchBy: 'DEAL_NO',
searchValue: '',
bondType: this.bondType ?? 'GOVT_SECURITY',
scriptCode: '',
certNo: '',
buyerCode: '',
subType: '',
isin: '',
tbillGlAc: '',
dealDate: this.asOnDate,
dealTime: '',
settlementType: 'T+0',
settlementDt: this.asOnDate,
faceValue: null,
sellRate: null,
tdsAmt: null,
unitVal: 100,
payAmt: null,
sellUnit: null,
brokerage: null,
dbToAc: '310070',
amortizeAmt: null,
recommAuthority: '',
sanctionAuthority: '',
callMoney: false
};
}
get isMutualFund(): boolean {
return this.purchaseForm.bondType === 'MUTUAL_FUND';
}
/** Face value for the purchase — Mutual Fund screens label it "Consider Amt" instead. */
get purchaseBaseAmount(): number {
const f = this.purchaseForm;
return (this.isMutualFund ? f.considerAmt : f.faceValue) || 0;
}
get purchaseMaturityDate(): string {
const f = this.purchaseForm;
if (f.redumMatDt) {
return f.redumMatDt;
}
if (!f.issueDate) {
return '';
}
return addPeriod(f.issueDate, f.periodYear || 0, f.periodMonth || 0, f.periodDays || 0);
}
get purchaseUnits(): number {
const f = this.purchaseForm;
const amt = this.purchaseBaseAmount;
if (this.isMutualFund) {
return f.nav ? +(amt / f.nav).toFixed(3) : 0;
}
return f.purchaseRate ? Math.round((amt / f.purchaseRate) * (f.purchaseRate / 100)) : 0;
}
/** Unit Val — the per-unit face value used by the legacy form (100 for G-Sec/Bonds, NAV for MF). */
get purchaseUnitVal(): number {
return this.isMutualFund ? this.purchaseForm.nav || 0 : 100;
}
/** Book Value — the price paid for the security. Charges are expenses and stay out of it (Jitendra Sir). */
get purchaseBookValue(): number {
const f = this.purchaseForm;
if (this.isMutualFund) {
return this.purchaseBaseAmount;
}
const rate = f.purchaseRate || 100;
return Math.round((this.purchaseBaseAmount * rate) / 100);
}
/** Stamp duty + brokerage + other charges — paid with the settlement, booked to expense GLs. */
get purchaseTotalCharges(): number {
const f = this.purchaseForm;
return (f.stampDuty || 0) + (f.brokerage || 0) + (f.otherCharges || 0);
}
// Interest cycle of the security being bought: its two coupon days and the next coupon after the deal.
get purchaseCouponDates(): string {
return this.purchaseMaturityDate ? couponDatesLabel(this.purchaseMaturityDate) : '';
}
get purchaseNextCouponDate(): string {
const f = this.purchaseForm;
return this.purchaseMaturityDate && f.dealDate
? nextHalfYearlyCouponDate(this.purchaseMaturityDate, f.dealDate)
: '';
}
get purchaseNextCouponAmount(): number {
const f = this.purchaseForm;
return f.intRate ? Math.round((this.purchaseBaseAmount * f.intRate) / 200) : 0;
}
get purchaseDaysToNextCoupon(): number {
return this.purchaseNextCouponDate ? days360(this.purchaseForm.dealDate, this.purchaseNextCouponDate) : 0;
}
get purchaseYtm(): number {
const f = this.purchaseForm;
if (!f.intRate || !this.purchaseMaturityDate) {
return 0;
}
return approxYtm(f.intRate, f.purchaseRate || 100, yearsBetween(f.issueDate, this.purchaseMaturityDate));
}
/** Accrued Interest at purchase — interest earned by the seller since the last coupon date. */
/** Interest owed to the seller from the last coupon date up to the deal date, on a 30/360 basis —
* interest is counted to the event date, not the date the money moves (Jitendra Sir). */
get purchaseAccruedInterest(): number {
const f = this.purchaseForm;
if (!f.intRate || !f.lastIntDt || !f.dealDate) {
return 0;
}
return Math.round((this.purchaseBaseAmount * (f.intRate / 100) * days360(f.lastIntDt, f.dealDate)) / 360);
}
get purchaseAccruedDays(): number {
const f = this.purchaseForm;
return f.lastIntDt && f.dealDate ? days360(f.lastIntDt, f.dealDate) : 0;
}
/** Premium (+) or discount (−) paid over face value. */
get purchasePremiumDiscount(): number {
const f = this.purchaseForm;
if (this.isMutualFund || !f.faceValue || !f.purchaseRate) {
return 0;
}
return Math.round((f.faceValue * (f.purchaseRate - 100)) / 100);
}
/** Re-suggests the last coupon date when the deal date changes. */
onDealDateChange(): void {
this.suggestLastIntDate();
this.loadMarketRate();
}
private suggestLastIntDate(): void {
const f = this.purchaseForm;
const s = this.purchaseScript;
if (!this.isMutualFund && s?.maturityDate && f.dealDate) {
f.lastIntDt = lastHalfYearlyCouponDate(s.maturityDate, f.dealDate) || f.lastIntDt;
}
}
/** Settlement Amt of Purchase — book value, accrued interest payable to the seller and the charges. */
get purchaseSettlementAmount(): number {
return this.purchaseBookValue + this.purchaseAccruedInterest + this.purchaseTotalCharges;
}
get purchaseMaturityAmount(): number {
const f = this.purchaseForm;
if (!this.purchaseBaseAmount || !this.purchaseMaturityDate) {
return this.purchaseBaseAmount;
}
return maturityAmount({
certNo: f.certNo,
instrumentType: f.bondType,
scriptCode: f.scriptCode,
scriptName: '',
party: f.partyCode,
subType: f.subType,
faceValue: this.purchaseBaseAmount,
purchaseRate: f.purchaseRate || 100,
units: this.purchaseUnits,
purchaseDate: f.issueDate,
maturityDate: this.purchaseMaturityDate,
interestRate: f.intRate || 0,
interestFreq: this.isMutualFund ? 'CUMULATIVE' : f.intType,
glCode: '',
glName: '',
crToAc: f.crToAc,
brokerCode: f.brokerName,
status: 'ACTIVE'
});
}
get purchaseScript(): (typeof SCRIPT_MASTER)[number] | undefined {
return this.scriptOptions.find((x) => x.scriptCode === this.purchaseForm.scriptCode);
}
/** Picking a script pulls its master details into the deal, as the existing CBS does. */
onScriptChange(): void {
const s = this.purchaseScript;
if (!s) {
return;
}
const f = this.purchaseForm;
f.isin = s.isin;
if (s.couponRate != null) {
f.intRate = s.couponRate;
}
if (s.issueDate) {
f.issueDate = s.issueDate;
}
if (s.maturityDate) {
f.redumMatDt = s.maturityDate;
const p = periodBetween(s.issueDate, s.maturityDate);
f.periodYear = p.years;
f.periodMonth = p.months;
f.periodDays = p.days;
}
if (s.slrType) {
f.subType = s.slrType === 'NON_SLR' ? 'NON SLR' : 'SLR';
}
if (this.isMutualFund) {
f.productName = s.scriptName;
}
this.suggestLastIntDate();
this.loadMarketRate();
}
/**
* Market rate of the script for the deal date from the Daily Script Master (newest rate on or before it). It fills
* the purchase rate (NAV for a mutual fund); the dealer may still change it. undefined = not looked up yet,
* null = no rate entered for the script.
*/
purchaseMarketRate: InvestmentDailyRateDto | null | undefined;
private loadMarketRate(): void {
const s = this.purchaseScript;
const f = this.purchaseForm;
if (!s?.id || !f.dealDate) {
this.purchaseMarketRate = undefined;
return;
}
this.investmentService.getLatestDailyRate(s.id, f.dealDate).subscribe({
next: (rate) => {
this.purchaseMarketRate = rate;
// No market rate: clear the field rather than leave another script's rate in it.
if (this.isMutualFund) {
f.nav = rate ? rate.closeRate : null;
} else {
f.purchaseRate = rate ? rate.closeRate : null;
}
},
error: (err) => this.toast(`Could not read the market rate: ${apiErrorMessage(err)}`)
});
}
/** True when the rate entered is outside the day's low–high in the Daily Script Master. */
get purchaseRateOutOfRange(): boolean {
const m = this.purchaseMarketRate;
const f = this.purchaseForm;
const rate = this.isMutualFund ? f.nav : f.purchaseRate;
if (!m || rate == null) {
return false;
}
return (m.lowRate != null && rate < m.lowRate) || (m.highRate != null && rate > m.highRate);
}
/** Settlement Date = Deal Date + T+n days (greyed / auto in the existing CBS). */
get purchaseSettlementDate(): string {
const f = this.purchaseForm;
if (!f.dealDate) {
return '';
}
const n = parseInt(f.settlementType.replace('T+', ''), 10) || 0;
return addPeriod(f.dealDate, 0, 0, n);
}
/** Parties offered for a new deal — inactive parties are hidden. */
get activePartyOptions(): typeof PARTY_MASTER {
return this.partyOptions.filter((p) => p.status !== 'INACTIVE');
}
get purchaseInvestmentGlCode(): string {
return this.purchaseScript?.glCode || INVESTMENT_GL_BY_TYPE[this.purchaseForm.bondType];
}
glNameOf(glCode: string): string {
return INVESTMENT_GL_LOOKUP.find((g) => g.glCode === glCode)?.glName ?? '';
}
/** The journal entry this purchase will post once the checker approves it. */
get purchaseAccountingPreview(): AccountingPreviewLine[] {
const book = this.purchaseBookValue;
const accrued = this.purchaseAccruedInterest;
const total = this.purchaseSettlementAmount;
const crGl = this.purchaseForm.crToAc;
if (!book || !crGl) {
return [];
}
const invGl = this.purchaseInvestmentGlCode;
const lines: AccountingPreviewLine[] = [
{ side: 'Dr', glCode: invGl, glName: this.glNameOf(invGl), amount: book }];
// Broken-period interest paid to the seller is an expense (RBI para 152, confirmed by Jitendra Sir).
if (accrued > 0) {
lines.push({
side: 'Dr',
glCode: BROKEN_PERIOD_INTEREST_GL.glCode,
glName: BROKEN_PERIOD_INTEREST_GL.glName,
amount: accrued
});
}
const f = this.purchaseForm;
for (const [
gl,
amount
] of [
[
STAMP_DUTY_GL,
f.stampDuty
],
[
BROKERAGE_GL,
f.brokerage
],
[
OTHER_CHARGES_GL,
f.otherCharges
]
] as const) {
if (amount && amount > 0) {
lines.push({ side: 'Dr', glCode: gl.glCode, glName: gl.glName, amount });
}
}
lines.push({ side: 'Cr', glCode: crGl, glName: this.glNameOf(crGl), amount: total });
return lines;
}
savePurchase(): void {
const f = this.purchaseForm;
const missing: string[] = [];
if (!f.scriptCode) missing.push('Script Code');
if (!f.partyCode) missing.push('Party Code');
if (!f.dealDate) missing.push('Deal Date');
if (!f.dealTime) missing.push('Deal Time');
if (this.isMutualFund) {
if (!f.considerAmt) missing.push('Consider Amt');
if (!f.nav) missing.push('NAV');
} else {
if (!f.faceValue) missing.push('Face Value');
if (!f.purchaseRate) missing.push('Purchase Rate');
if (!f.lastIntDt) missing.push('Last Interest Date');
}
if (!f.category) missing.push('Category');
if (!f.crToAc) missing.push('CR. To A/c');
if (missing.length) {
this.toast(`Please fill: ${missing.join(', ')}.`);
return;
}
const script = this.purchaseScript;
const party = this.partyOptions.find((p) => p.partyCode === f.partyCode);
const crGlAccountId = this.glIdByCode.get(f.crToAc);
if (!script?.id || !party?.id) {
this.toast('Script and Party must be saved in Investment Masters first.');
return;
}
if (!crGlAccountId) {
this.toast(`GL ${f.crToAc} was not found in the chart of accounts.`);
return;
}
const body = {
bondType: f.bondType,
scriptId: script.id,
partyId: party.id,
dealDate: f.dealDate,
dealTime: f.dealTime || '',
settlementType: f.settlementType,
...(this.isMutualFund
? { considerAmt: f.considerAmt, nav: f.nav }
: { faceValue: f.faceValue, purchaseRate: f.purchaseRate, lastIntDate: f.lastIntDt }),
stampDuty: f.stampDuty ?? null,
brokerage: f.brokerage ?? null,
otherCharges: f.otherCharges ?? null,
category: f.category,
crGlAccountId,
subType: f.subType || '',
otherDetails: this.purchaseOtherDetails(),
dateFormat: 'dd-MM-yyyy',
locale: 'en'
};
this.investmentService.createPurchase(body).subscribe({
next: (res) => {
this.toast(`Deal ${res.resourceIdentifier} saved. GL posting will follow once accounting is approved.`);
this.activeTab = 'holdings';
this.loadPurchases();
},
error: (err) => this.toast(apiErrorMessage(err))
});
}
/** The optional "More Details" fields, kept with the deal as they were entered. */
private purchaseOtherDetails(): Record<string, string | number> {
const f = this.purchaseForm;
const fields: Record<string, string | number | null> = {
tradeNo: f.tradeNo,
orderNo: f.orderNo,
brokerCode: f.brokerName,
recommAuthority: f.recommAuthority,
sanctionAuthority: f.sanctionAuthority,
meetDate: f.meetDt,
resoNo: f.resoNo,
dealType: f.dealType,
market: f.market,
markCblo: f.markCblo,
paymentMode: f.paymentMode,
deliveryMode: f.deliveryMode,
lien: f.lien,
lienAmount: f.lienAmount,
lienReason: f.lienReason,
bondRating: f.bondRating,
corpBond: f.corpBond,
sglWithBank: f.sglWithBank,
tbillGlAc: f.tbillGlAc,
intType: f.intType,
intDate: f.intDate,
folioNo: f.folioNo,
callOption: f.callOption,
descInv: f.descInv,
fromDist: f.fromDist,
toDist: f.toDist,
amortizeAmt: f.amortizeAmt,
registerAddress: f.registerAdd,
registerCity: f.regiCity,
registerPin: f.regiPin,
remarks: f.remarks,
description: f.description,
tradeAmt: f.tradeAmt,
holdingDays: f.holdingDays,
stampDutyAc: f.stampDutyAc
};
const out: Record<string, string | number> = {};
for (const [
k,
v
] of Object.entries(fields)) {
if (v !== null && v !== undefined && v !== '') {
out[k] = v;
}
}
return out;
}
cancelPurchase(): void {
this.purchaseForm = this.blankPurchaseForm();
this.toast('Entry cancelled — form reset.');
}
/** Legacy "Retrieve" button — loads an existing deal back into the form by Cert No. */
retrievePurchase(): void {
const certNo = this.retrieveCertNo.trim();
const h = this.holdings.find((x) => x.certNo === certNo);
if (!h) {
this.toast(`Cert No ${certNo || '(blank)'} not found.`);
return;
}
this.purchaseForm = {
...this.blankPurchaseForm(),
bondType: h.instrumentType as BondType,
certNo: h.certNo,
scriptCode: h.scriptCode,
isin: this.scriptOptions.find((s) => s.scriptCode === h.scriptCode)?.isin || '',
dealNo: h.dealNo ?? '',
partyCode: h.partyCode ?? '',
subType: h.subType,
faceValue: h.faceValue,
considerAmt: h.faceValue,
purchaseRate: h.purchaseRate,
nav: h.purchaseRate,
issueDate: h.purchaseDate,
redumMatDt: h.maturityDate,
intRate: h.interestRate,
intType: h.interestFreq,
brokerName: h.brokerCode,
crToAc: h.crToAc,
productName: h.scriptName,
category: h.category ?? ''
};
this.bondType = h.instrumentType as BondType;
this.toast(`Deal for Cert No ${h.certNo} retrieved.`);
}
dropPurchase(): void {
const h = this.holdings.find((x) => x.certNo === this.purchaseForm.certNo);
if (!h?.id) {
this.toast('Retrieve a saved deal first, then Drop.');
return;
}
this.investmentService.deletePurchase(h.id).subscribe({
next: () => {
this.toast(`Deal ${h.dealNo} (Cert No ${h.certNo}) dropped.`);
this.loadPurchases();
},
error: (err) => this.toast(apiErrorMessage(err))
});
}
exitPurchase(): void {
this.purchaseForm = this.blankPurchaseForm();
this.activeTab = 'holdings';
}
// -------------------- Redemption Entry --------------------
get redeemHolding(): InvestmentHolding | undefined {
return this.holdingsForType.find((h) => h.certNo === this.redeemCertNo);
}
get redeemAmount(): number {
const h = this.redeemHolding;
return h ? maturityAmount(h) + accruedInterest(h, this.redeemDate) - h.faceValue + h.faceValue : 0;
}
saveRedemption(): void {
const h = this.redeemHolding;
if (!h) {
this.toast('Select a certificate to redeem.');
return;
}
h.status = 'MATURED';
this.txns = [
{
certNo: h.certNo,
instrumentType: h.instrumentType,
txnType: 'REDEEM',
voucherNo: `INV/RED/${h.certNo}`,
txnDate: this.redeemDate,
amount: this.redeemAmount,
particulars: `Redemption on maturity - ${h.scriptName}${this.redeemRemarks ? ' - ' + this.redeemRemarks : ''}`
},
...this.txns
];
this.toast(`${h.certNo} redeemed. Proceeds ₹${this.redeemAmount.toLocaleString('en-IN')}.`);
this.redeemCertNo = '';
this.redeemRemarks = '';
this.activeTab = 'holdings';
}
// -------------------- Interest calculation --------------------
/** Saved holdings for the Deal No dropdown, narrowed by Type and Script. */
get intDealOptions(): InvestmentHolding[] {
return this.holdings.filter(
(h) =>
h.id != null &&
h.status === 'ACTIVE' &&
(!this.intType || h.instrumentType === this.intType) &&
(!this.intScript || h.scriptCode === this.intScript)
);
}
get intScriptOptions(): typeof SCRIPT_MASTER {
return this.scriptOptions.filter((s) => !this.intType || s.instrumentType === this.intType);
}
onIntFilterChange(): void {
if (this.intScript && !this.intScriptOptions.some((s) => s.scriptCode === this.intScript)) {
this.intScript = '';
}
if (this.intPurchaseId != null && !this.intDealOptions.some((h) => h.id === this.intPurchaseId)) {
this.intPurchaseId = null;
}
this.intSchedule = null;
}
retrieveInterestSchedule(): void {
if (this.intPurchaseId == null) {
this.toast('Select a Deal No first.');
return;
}
this.intLoading = true;
this.investmentService.getInterestSchedule(this.intPurchaseId, this.intAsOnDate || undefined).subscribe({
next: (schedule) => {
this.intSchedule = schedule;
this.intLoading = false;
},
error: (err) => {
this.intSchedule = null;
this.intLoading = false;
this.toast(apiErrorMessage(err));
}
});
}
/** Total of the lines falling due up to the as-on date. */
get intDueTotal(): number {
return (this.intSchedule?.rows ?? []).filter((r) => r.due).reduce((sum, r) => sum + r.amount, 0);
}
// -------------------- Sell / Redeem Entry --------------------
/** Locates the original purchase by Deal No / Cert No or ISIN and fills the sell form. */
searchPurchaseForSell(): void {
const val = this.sellForm.searchValue.trim();
if (!val) {
this.toast('Enter a Deal No / Cert No or ISIN to search.');
return;
}
const h =
this.sellForm.searchBy === 'DEAL_NO'
? this.holdings.find((x) => x.certNo === val)
: this.holdings.find((x) => this.scriptOptions.find((s) => s.scriptCode === x.scriptCode)?.isin === val);
if (!h) {
this.toast('No matching purchase found.');
return;
}
this.sellForm = {
...this.sellForm,
bondType: h.instrumentType as BondType,
scriptCode: h.scriptCode,
certNo: h.certNo,
subType: h.subType,
isin: this.scriptOptions.find((s) => s.scriptCode === h.scriptCode)?.isin || '',
faceValue: h.faceValue,
unitVal: h.instrumentType === 'MUTUAL_FUND' ? h.purchaseRate : 100,
sellUnit: h.units
};
this.toast(`Purchase ${h.certNo} loaded.`);
}
get sellHolding(): InvestmentHolding | undefined {
return this.holdings.find((h) => h.certNo === this.sellForm.certNo);
}
/** Consideration = units sold × sell rate, scaled by the instrument's unit convention. */
get sellConsideration(): number {
const f = this.sellForm;
if (!f.sellUnit || !f.sellRate) {
return 0;
}
const perUnitDivisor = f.mode === 'MUTUAL_FUND_SELL' ? 1 : 100;
return Math.round((f.sellUnit * f.sellRate) / perUnitDivisor);
}
/** Sale Diff — profit/loss versus what the holding was bought at. */
get sellSaleDiff(): number {
const h = this.sellHolding;
const f = this.sellForm;
if (!h || !f.sellUnit || !f.sellRate) {
return 0;
}
const perUnitDivisor = f.mode === 'MUTUAL_FUND_SELL' ? 1 : 100;
const cost = Math.round((f.sellUnit * h.purchaseRate) / perUnitDivisor);
return this.sellConsideration - cost;
}
/** Premium Loss — the unamortised premium written off when a holding is sold early. */
get sellPremiumLoss(): number {
const h = this.sellHolding;
if (!h || h.purchaseRate <= 100 || h.instrumentType === 'MUTUAL_FUND') {
return 0;
}
return Math.round((h.faceValue * (h.purchaseRate - 100)) / 100);
}
get sellAccruedInterest(): number {
const h = this.sellHolding;
return h ? accruedInterest(h, this.sellForm.dealDate || this.asOnDate) : 0;
}
get sellAccruedDays(): number {
const h = this.sellHolding;
if (!h) {
return 0;
}
return Math.max(0, Math.round(yearsBetween(h.purchaseDate, this.sellForm.dealDate || this.asOnDate) * 365));
}
/** Pay Amt — net amount receivable: consideration + accrued interest − TDS − brokerage. */
get sellPayAmount(): number {
const f = this.sellForm;
return this.sellConsideration + this.sellAccruedInterest - (f.tdsAmt || 0) - (f.brokerage || 0);
}
saveSell(): void {
const f = this.sellForm;
const h = this.sellHolding;
if (!h || !f.sellRate || !f.sellUnit) {
this.toast('Load a purchase, then enter Sell Rate and Sell Unit.');
return;
}
if (f.sellUnit > h.units) {
this.toast(`Sell Unit cannot exceed units held (${h.units}).`);
return;
}
h.units -= f.sellUnit;
h.faceValue = Math.round(h.units * (f.unitVal || 100));
if (h.units <= 0) {
h.status = f.mode === 'REDEEM' ? 'MATURED' : 'SOLD';
}
this.txns = [
{
certNo: h.certNo,
instrumentType: h.instrumentType,
txnType: f.mode === 'REDEEM' ? 'REDEEM' : 'SELL',
voucherNo: `INV/${f.mode === 'REDEEM' ? 'RED' : 'SEL'}/${h.certNo}-${this.txns.length + 1}`,
txnDate: f.dealDate,
amount: this.sellPayAmount,
particulars: `${f.mode.replace(/_/g, ' ')} - ${h.scriptName} @ ${f.sellRate}${f.buyerCode ? ' to ' + f.buyerCode : ''}${f.callMoney ? ' [CALL MONEY]' : ''}`
},
...this.txns
];
this.toast(
`${h.certNo} ${f.mode === 'REDEEM' ? 'redeemed' : 'sold'}. Pay Amt ₹${this.sellPayAmount.toLocaleString('en-IN')}.`
);
this.sellForm = this.blankSellForm();
this.activeTab = 'holdings';
}
cancelSell(): void {
this.sellForm = this.blankSellForm();
this.toast('Sell entry cancelled — form reset.');
}
exitSell(): void {
this.sellForm = this.blankSellForm();
this.activeTab = 'holdings';
}
// -------------------- Write-off --------------------
get writeoffHolding(): InvestmentHolding | undefined {
return this.holdingsForType.find((h) => h.certNo === this.writeoffCertNo);
}
saveWriteoff(): void {
const h = this.writeoffHolding;
if (!h || !this.writeoffReason) {
this.toast('Select a certificate and enter a write-off reason.');
return;
}
h.status = 'WRITTEN_OFF';
this.txns = [
{
certNo: h.certNo,
instrumentType: h.instrumentType,
txnType: 'WRITE_OFF',
voucherNo: `INV/WOF/${h.certNo}`,
txnDate: this.asOnDate,
amount: h.faceValue,
particulars: `Written off - ${this.writeoffReason}`
},
...this.txns
];
this.toast(`${h.certNo} written off.`);
this.writeoffCertNo = '';
this.writeoffReason = '';
this.activeTab = 'holdings';
}
// -------------------- Amortize --------------------
get amortizeHolding(): InvestmentHolding | undefined {
return this.holdingsForType.find((h) => h.certNo === this.amortizeCertNo);
}
/** Straight-line premium amortisation for one year on the selected holding. */
get suggestedAmortizeAmt(): number {
const h = this.amortizeHolding;
if (!h || h.purchaseRate <= 100) {
return 0;
}
const years = Math.max(1, yearsBetween(h.purchaseDate, h.maturityDate));
return Math.round((h.faceValue * (h.purchaseRate - 100)) / 100 / years);
}
saveAmortize(): void {
const h = this.amortizeHolding;
const amt = this.amortizeAmt ?? this.suggestedAmortizeAmt;
if (!h || !amt) {
this.toast('Select a certificate and enter the amortization amount.');
return;
}
this.txns = [
{
certNo: h.certNo,
instrumentType: h.instrumentType,
txnType: 'AMORTIZE',
voucherNo: `INV/AMZ/${h.certNo}-${this.txns.length + 1}`,
txnDate: this.asOnDate,
amount: amt,
particulars: `Premium/discount amortized - ${h.scriptName}`
},
...this.txns
];
this.toast(`Amortization of ₹${amt.toLocaleString('en-IN')} booked for ${h.certNo}.`);
this.amortizeCertNo = '';
this.amortizeAmt = null;
}
// -------------------- Amortize Yearly Process --------------------
/** All premium-carrying holdings the yearly batch would amortise. */
get amortizeYearlyTargets(): Array<{ holding: InvestmentHolding; amount: number }> {
return this.holdings
.filter((h) => h.status === 'ACTIVE' && h.purchaseRate > 100 && h.instrumentType !== 'MUTUAL_FUND')
.map((h) => {
const years = Math.max(1, yearsBetween(h.purchaseDate, h.maturityDate));
return { holding: h, amount: Math.round((h.faceValue * (h.purchaseRate - 100)) / 100 / years) };
});
}
get amortizeYearlyTotal(): number {
return this.amortizeYearlyTargets.reduce((s, t) => s + t.amount, 0);
}
runAmortizeYearly(): void {
const targets = this.amortizeYearlyTargets;
if (targets.length === 0) {
this.toast('No premium-carrying holdings to amortise.');
return;
}
const newTxns: InvestmentTxn[] = targets.map((t, i) => ({
certNo: t.holding.certNo,
instrumentType: t.holding.instrumentType,
txnType: 'AMORTIZE',
voucherNo: `INV/AMZ-YR/${this.amortizeYear}-${i + 1}`,
txnDate: this.asOnDate,
amount: t.amount,
particulars: `Yearly amortization process ${this.amortizeYear} - ${t.holding.scriptName}`
}));
this.txns = [
...newTxns,
...this.txns
];
this.toast(
`Yearly amortization run for ${targets.length} holding(s). Total ₹${this.amortizeYearlyTotal.toLocaleString('en-IN')}.`
);
}
// -------------------- GOI Category Transfer --------------------
get goiHolding(): InvestmentHolding | undefined {
return this.holdings.find((h) => h.certNo === this.goiCertNo);
}
saveGoiTransfer(): void {
const h = this.goiHolding;
if (!h || !this.goiFromCategory || !this.goiToCategory) {
this.toast('Select a certificate and both From / To categories.');
return;
}
if (this.goiFromCategory === this.goiToCategory) {
this.toast('From and To categories must differ.');
return;
}
this.txns = [
{
certNo: h.certNo,
instrumentType: h.instrumentType,
txnType: 'AMORTIZE',
voucherNo: `INV/GOI-TRF/${h.certNo}-${this.txns.length + 1}`,
txnDate: this.goiTransferDate,
amount: h.faceValue,
particulars: `GOI category transfer ${this.goiFromCategory} → ${this.goiToCategory} - ${h.scriptName}`
},
...this.txns
];
this.toast(`${h.certNo} transferred from ${this.goiFromCategory} to ${this.goiToCategory}.`);
this.goiCertNo = '';
this.goiFromCategory = '';
this.goiToCategory = '';
}
// -------------------- Investment Depreciation --------------------
/** AFS/HFT holdings whose market price is below book value must be marked down (RBI norms). */
get depreciationTargets(): Array<{ holding: InvestmentHolding; marketPrice: number; depreciation: number }> {
return this.holdings
.filter((h) => h.status === 'ACTIVE' && h.instrumentType !== 'MUTUAL_FUND')
.map((h) => {
const md = this.scriptOptions.find((s) => s.scriptCode === h.scriptCode);
const marketPrice = md ? h.purchaseRate * 0.995 : h.purchaseRate;
const depreciation =
marketPrice < h.purchaseRate ? Math.round((h.faceValue * (h.purchaseRate - marketPrice)) / 100) : 0;
return { holding: h, marketPrice: +marketPrice.toFixed(2), depreciation };
})
.filter((r) => r.depreciation > 0);
}
get depreciationTotal(): number {
return this.depreciationTargets.reduce((s, r) => s + r.depreciation, 0);
}
runDepreciation(): void {
const targets = this.depreciationTargets;
if (targets.length === 0) {
this.toast('No holdings require depreciation provisioning.');
return;
}
const newTxns: InvestmentTxn[] = targets.map((t, i) => ({
certNo: t.holding.certNo,
instrumentType: t.holding.instrumentType,
txnType: 'AMORTIZE',
voucherNo: `INV/DEPR/${this.depreciationDate.replace(/-/g, '')}-${i + 1}`,
txnDate: this.depreciationDate,
amount: t.depreciation,
particulars: `Investment depreciation provision - ${t.holding.scriptName} (market ${t.marketPrice})`
}));
this.txns = [
...newTxns,
...this.txns
];
this.toast(
`Depreciation provisioned for ${targets.length} holding(s). Total ₹${this.depreciationTotal.toLocaleString('en-IN')}.`
);
}
// -------------------- Mutual Fund Transfer --------------------
get mfHoldings(): InvestmentHolding[] {
return this.holdings.filter((h) => h.instrumentType === 'MUTUAL_FUND' && h.status === 'ACTIVE');
}
get mfFromHolding(): InvestmentHolding | undefined {
return this.holdings.find((h) => h.certNo === this.mfFromCertNo);
}
get mfTransferValue(): number {
const h = this.mfFromHolding;
if (!h || !this.mfTransferUnits) {
return 0;
}
return Math.round(this.mfTransferUnits * h.purchaseRate);
}
saveMfTransfer(): void {
const h = this.mfFromHolding;
if (!h || !this.mfToProduct || !this.mfTransferUnits) {
this.toast('Select source holding, target product and units to transfer.');
return;
}
if (this.mfTransferUnits > h.units) {
this.toast(`Units cannot exceed units held (${h.units}).`);
return;
}
const value = this.mfTransferValue;
h.units = +(h.units - this.mfTransferUnits).toFixed(3);
h.faceValue = Math.round(h.units * h.purchaseRate);
if (h.units <= 0) {
h.status = 'SOLD';
}
const product = this.mfProductOptions.find((p) => p.productName === this.mfToProduct);
const newCert = `MF-${5000 + this.holdings.filter((x) => x.instrumentType === 'MUTUAL_FUND').length + 1}`;
this.holdings = [
{
certNo: newCert,
instrumentType: 'MUTUAL_FUND',
scriptCode: product?.productCode || this.mfToProduct,
scriptName: this.mfToProduct,
party: product?.amcName || '',
subType: product?.category || 'DEBT',
faceValue: value,
purchaseRate: h.purchaseRate,
units: this.mfTransferUnits,
purchaseDate: this.mfTransferDate,
maturityDate: addPeriod(this.mfTransferDate, 1, 0),
interestRate: h.interestRate,
interestFreq: 'CUMULATIVE',
glCode: '310050',
glName: 'INVESTMENT IN MUTUAL FUNDS',
crToAc: h.crToAc,
brokerCode: '',
status: 'ACTIVE'
},
...this.holdings
];
this.txns = [
{
certNo: newCert,
instrumentType: 'MUTUAL_FUND',
txnType: 'PURCHASE',
voucherNo: `INV/MF-TRF/${newCert}`,
txnDate: this.mfTransferDate,
amount: value,
particulars: `Mutual Fund switch from ${h.scriptName} to ${this.mfToProduct} (${this.mfTransferUnits} units)`
},
...this.txns
];
this.toast(`Transferred ${this.mfTransferUnits} units to ${this.mfToProduct} as ${newCert}.`);
this.mfFromCertNo = '';
this.mfToProduct = '';
this.mfTransferUnits = null;
}
// -------------------- Helpers --------------------
accrued(h: InvestmentHolding): number {
return accruedInterest(h, this.asOnDate);
}
maturity(h: InvestmentHolding): number {
return maturityAmount(h);
}
daysLeft(h: InvestmentHolding): number {
return daysToMaturity(h, this.asOnDate);
}
statusClass(h: InvestmentHolding): string {
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
}
// -------------------- Export to Excel (Holdings register) --------------------
async exportExcel(): Promise<void> {
this.generating = true;
try {
await exportWorkbook(
[
{
sheetName: this.instrumentLabel(this.bondType),
title: `${this.instrumentLabel(this.bondType)} Holdings Register`,
subtitle: `As on ${this.asOnDate}`,
headers: [
'Cert No',
'Script',
'Party',
'Units',
'Face Value',
'Rate',
'Purchase Date',
'Maturity Date',
'Accrued Interest',
'Maturity Value',
'Status'
],
widths: [
14,
26,
22,
12,
16,
10,
14,
14,
16,
16,
12
],
rows: this.holdingsForType.map((h) => [
h.certNo,
h.scriptName,
h.party,
h.units,
h.faceValue,
h.interestRate,
h.purchaseDate,
h.maturityDate,
this.accrued(h),
this.maturity(h),
h.status
])
}
],
`${this.instrumentLabel(this.bondType)}-Holdings-${this.asOnDate}`
);
this.toast(`${this.instrumentLabel(this.bondType)} Holdings exported.`);
} catch (err) {
console.error('Bond Holdings export failed', err);
this.toast('Could not generate the workbook — please retry.');
} finally {
this.generating = false;
}
}
private toast(msg: string): void {
this.toastMsg = msg;
this.toastVisible = true;
if (this.toastTimer) {
clearTimeout(this.toastTimer);
}
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
}
}