/** * 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 } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component'; import { GOVT_SECURITY_HOLDINGS, NON_SLR_HOLDINGS, BOND_HOLDINGS, MUTUAL_FUND_HOLDINGS, ALL_TXNS, 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, INTEREST_INCOME_GL_MASTER, INTEREST_INCOME_GL_BY_TYPE, InvestmentHolding, InvestmentTxn, InstrumentType, InterestFreq, INSTRUMENT_LABELS, accruedInterest, accruedInterestBetween, canAccrueInterest, daysToMaturity, maturityAmount, addPeriod, approxYtm, yearsBetween, todayDDMMYYYY } from '../investment-data'; import { exportWorkbook } from '../investment-export'; 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; } 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 { activeTab: BondTab = 'holdings'; bondType: BondType = 'GOVT_SECURITY'; asOnDate = todayDDMMYYYY(); holdings: InvestmentHolding[] = [ ...GOVT_SECURITY_HOLDINGS, ...NON_SLR_HOLDINGS, ...BOND_HOLDINGS, ...MUTUAL_FUND_HOLDINGS ]; txns: InvestmentTxn[] = ALL_TXNS.filter((t) => ([ 'GOVT_SECURITY', 'NON_SLR', 'BOND', 'MUTUAL_FUND' ] as InstrumentType[]).includes(t.instrumentType)); // 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 | undefined; purchaseForm: BondPurchaseForm = this.blankPurchaseForm(); sellForm: BondSellForm = this.blankSellForm(); // Redemption Entry redeemCertNo = ''; redeemDate = this.asOnDate; redeemRemarks = ''; // Interest Calculation (legacy: From/To Date / Type / Script / Deal No / ALL + Process) — // posts interest accrued strictly within [intCalcFromDate, intCalcToDate], same period-based // basis as the Investment Interest module's Govt./Bond Interest Calculation. intCalcFromDate = addPeriod(this.asOnDate, 0, -3); intCalcToDate = this.asOnDate; intCalcType: BondType | '' = ''; intCalcScript = ''; intCalcDealNo = ''; intCalcAll = false; intCalcGlCode = INTEREST_INCOME_GL_BY_TYPE.GOVT_SECURITY; readonly interestIncomeGlOptions = INTEREST_INCOME_GL_MASTER; // 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]; } private nextDealNo(): string { return `2026${String(27000039 + this.holdings.length).padStart(9, '0')}`; } private blankPurchaseForm(): BondPurchaseForm { const type = this.bondType ?? 'GOVT_SECURITY'; return { bondType: type, dealNo: this.nextDealNo(), dealDate: this.asOnDate, dealTime: '', isin: '', scriptCode: '', certNo: '', 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: '310070', 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: '' }; } 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 — what the investment is carried at, i.e. amount actually paid. */ get purchaseBookValue(): number { const f = this.purchaseForm; if (this.isMutualFund) { return this.purchaseBaseAmount + (f.stampDuty || 0) + (f.brokerage || 0); } const rate = f.purchaseRate || 100; return Math.round((this.purchaseBaseAmount * rate) / 100) + (f.brokerage || 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. */ get purchaseAccruedInterest(): number { const f = this.purchaseForm; if (!f.intRate || !f.lastIntDt || !f.dealDate) { return 0; } const yrs = Math.max(0, yearsBetween(f.lastIntDt, f.dealDate)); return Math.round(this.purchaseBaseAmount * (f.intRate / 100) * yrs); } /** Settlement Amt of Purchase — book value plus accrued interest payable to the seller. */ get purchaseSettlementAmount(): number { return this.purchaseBookValue + this.purchaseAccruedInterest; } 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' }); } /** Fills ISIN + interest rate when a script is picked, mirroring the legacy form's lookup. */ onScriptChange(): void { const s = this.scriptOptions.find((x) => x.scriptCode === this.purchaseForm.scriptCode); if (s) { this.purchaseForm.isin = s.isin; } } savePurchase(): void { const f = this.purchaseForm; if (!f.certNo || !this.purchaseBaseAmount || !f.partyCode) { this.toast('Please fill Cert No, Face Value / Consider Amt and Party Code.'); return; } const holding: InvestmentHolding = { certNo: f.certNo, instrumentType: f.bondType, scriptCode: f.scriptCode || f.isin || f.productName, scriptName: this.isMutualFund ? f.productName : this.scriptOptions.find((s) => s.scriptCode === f.scriptCode)?.scriptName || f.scriptCode || f.isin, party: f.partyCode, subType: f.subType, faceValue: this.purchaseBaseAmount, purchaseRate: this.isMutualFund ? f.nav || 0 : f.purchaseRate || 100, units: this.purchaseUnits, purchaseDate: f.issueDate, maturityDate: this.purchaseMaturityDate || f.issueDate, interestRate: f.intRate || 0, interestFreq: this.isMutualFund ? 'CUMULATIVE' : f.intType, glCode: '31' + (f.bondType === 'GOVT_SECURITY' ? '0020' : f.bondType === 'NON_SLR' ? '0030' : f.bondType === 'BOND' ? '0040' : '0050'), glName: `INVESTMENT IN ${INSTRUMENT_LABELS[f.bondType].toUpperCase()}`, crToAc: f.crToAc, brokerCode: f.brokerName, status: 'ACTIVE' }; this.holdings = [ holding, ...this.holdings ]; this.txns = [ { certNo: holding.certNo, instrumentType: f.bondType, txnType: 'PURCHASE', voucherNo: `INV/PUR/${f.dealNo}`, txnDate: f.dealDate, amount: this.purchaseSettlementAmount, particulars: `Purchase Deal ${f.dealNo} - ${holding.scriptName} - ${holding.party}${f.category ? ' [' + f.category + ']' : ''}` }, ...this.txns ]; this.toast(`${INSTRUMENT_LABELS[f.bondType]} Certificate ${holding.certNo} saved successfully.`); this.purchaseForm = this.blankPurchaseForm(); this.activeTab = 'holdings'; } 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 h = this.holdings.find((x) => x.certNo === this.purchaseForm.certNo.trim()); if (!h) { this.toast(`Cert No ${this.purchaseForm.certNo} 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 || '', partyCode: h.party, 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 }; this.bondType = h.instrumentType as BondType; this.toast(`Deal for Cert No ${h.certNo} retrieved.`); } dropPurchase(): void { const certNo = this.purchaseForm.certNo; if (!this.holdings.some((h) => h.certNo === certNo)) { this.toast('Nothing to drop — this deal is not yet saved.'); return; } this.holdings = this.holdings.filter((h) => h.certNo !== certNo); this.txns = this.txns.filter((t) => t.certNo !== certNo); this.purchaseForm = this.blankPurchaseForm(); this.toast(`Cert No ${certNo} dropped.`); } 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 -------------------- /** Holdings the Interest Calculation "Process" run would cover, given the filters. A * Non-Performing holding is always excluded — same accrual-basis rule as the Investment * Interest module (it's still collectible on cash basis via Interest Receive there). */ get intCalcTargets(): InvestmentHolding[] { const base = this.holdings.filter((h) => h.status === 'ACTIVE' && canAccrueInterest(h)); if (this.intCalcAll) { return base; } return base.filter((h) => { if (this.intCalcType && h.instrumentType !== this.intCalcType) return false; if (this.intCalcScript && h.scriptCode !== this.intCalcScript) return false; if (this.intCalcDealNo && h.certNo !== this.intCalcDealNo.trim()) return false; return true; }); } get intCalcExcludedNpi(): InvestmentHolding[] { return this.holdings.filter((h) => h.status === 'ACTIVE' && !canAccrueInterest(h)); } /** Re-suggests the Interest Income GL when the Type filter changes — still overridable. */ onIntCalcTypeChange(): void { if (this.intCalcType) { this.intCalcGlCode = INTEREST_INCOME_GL_BY_TYPE[this.intCalcType]; } } glName(glCode: string): string { return this.interestIncomeGlOptions.find((g) => g.glCode === glCode)?.glName ?? glCode; } /** Interest accrued strictly within [intCalcFromDate, intCalcToDate] — a period-based * Interest Calculation & Post run, same basis as the Investment Interest module. */ interestAccrued(h: InvestmentHolding): number { return accruedInterestBetween(h, this.intCalcFromDate || h.purchaseDate, this.intCalcToDate || this.asOnDate); } processInterest(): void { const targets = this.intCalcTargets.filter((h) => this.interestAccrued(h) > 0); if (targets.length === 0) { this.toast('No holdings have interest due to be posted for this period.'); return; } const toDate = this.intCalcToDate || this.asOnDate; const fromDate = this.intCalcFromDate || toDate; const glCode = this.intCalcGlCode; const glName = this.glName(glCode); const newTxns: InvestmentTxn[] = targets.map((h, i) => { const amt = this.interestAccrued(h); h.interestPosted = (h.interestPosted ?? 0) + amt; h.interestPostedTillDate = toDate; return { certNo: h.certNo, instrumentType: h.instrumentType, txnType: 'INTEREST', voucherNo: `INV/INT/${h.certNo}-${this.txns.length + i + 1}`, txnDate: toDate, amount: amt, particulars: `Interest calculated (accrual) for period ${fromDate} to ${toDate} - ${h.scriptName}`, glCode, glName }; }); this.txns = [ ...newTxns, ...this.txns ]; this.intCalcFromDate = toDate; const total = newTxns.reduce((s, t) => s + t.amount, 0); this.toast( `Interest processed for ${targets.length} holding(s) to ${glCode} - ${glName}. Total ₹${total.toLocaleString('en-IN')}.` ); } // -------------------- 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 { 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); } }