From 63ac900e42ac1446d30e456645fb3b7b502eafe3 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Sat, 1 Aug 2026 09:17:06 +0530 Subject: [PATCH] feat(investment): add Treasury Investment module (FD, Govt Sec/Non-SLR/Bond/MF, Share) Adds full frontend-only Investment module mirroring the legacy AutoBank CBS Investment menu: Dashboard, FD/Bond/Share entry screens (purchase, interest, sell/redeem, write-off, amortize and more), 10 master screens, an Interest calculation/register screen, and ~21 reports. All figures are derived from a single holdings/transactions dataset so entry, interest, and reports can never drift out of sync. Wires the new "Investment" mega-menu into the shell and routes the previously inert Investment stub buttons. --- src/app/app.module.ts | 2 + src/app/core/shell/shell.component.html | 176 +- src/app/core/shell/shell.component.ts | 14 +- .../bond-investment.component.html | 1093 +++++++++++++ .../bond-investment.component.ts | 1103 +++++++++++++ .../fd-investment.component.html | 472 ++++++ .../fd-investment/fd-investment.component.ts | 461 ++++++ .../investment-dashboard.component.html | 146 ++ .../investment-dashboard.component.ts | 120 ++ src/app/investment/investment-data.ts | 1438 +++++++++++++++++ .../investment-interest.component.html | 352 ++++ .../investment-interest.component.ts | 247 +++ .../investment-masters.component.html | 557 +++++++ .../investment-masters.component.ts | 262 +++ .../investment-reports.component.html | 694 ++++++++ .../investment-reports.component.ts | 247 +++ .../investment/investment-routing.module.ts | 71 + src/app/investment/investment-shared.scss | 225 +++ src/app/investment/investment.module.ts | 34 + .../share-investment.component.html | 276 ++++ .../share-investment.component.ts | 263 +++ src/environments/.env.ts | 2 +- 22 files changed, 8249 insertions(+), 6 deletions(-) create mode 100644 src/app/investment/bond-investment/bond-investment.component.html create mode 100644 src/app/investment/bond-investment/bond-investment.component.ts create mode 100644 src/app/investment/fd-investment/fd-investment.component.html create mode 100644 src/app/investment/fd-investment/fd-investment.component.ts create mode 100644 src/app/investment/investment-dashboard/investment-dashboard.component.html create mode 100644 src/app/investment/investment-dashboard/investment-dashboard.component.ts create mode 100644 src/app/investment/investment-data.ts create mode 100644 src/app/investment/investment-interest/investment-interest.component.html create mode 100644 src/app/investment/investment-interest/investment-interest.component.ts create mode 100644 src/app/investment/investment-masters/investment-masters.component.html create mode 100644 src/app/investment/investment-masters/investment-masters.component.ts create mode 100644 src/app/investment/investment-reports/investment-reports.component.html create mode 100644 src/app/investment/investment-reports/investment-reports.component.ts create mode 100644 src/app/investment/investment-routing.module.ts create mode 100644 src/app/investment/investment-shared.scss create mode 100644 src/app/investment/investment.module.ts create mode 100644 src/app/investment/share-investment/share-investment.component.html create mode 100644 src/app/investment/share-investment/share-investment.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 24a8d269b..f27549c9a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -65,6 +65,7 @@ import { DeafModule } from './deaf/deaf.module'; import { DicgcModule } from './dicgc/dicgc.module'; import { FiuModule } from './fiu/fiu.module'; import { SftModule } from './sft/sft.module'; +import { InvestmentModule } from './investment/investment.module'; import { DigitalModule } from './digital/digital.module'; import { LetterMgmtModule } from './letter-mgmt/letter-mgmt.module'; import { AccountsModule } from './accounts/accounts.module'; @@ -172,6 +173,7 @@ export function HttpLoaderFactory(http: HttpClient) { DicgcModule, FiuModule, SftModule, + InvestmentModule, DigitalModule, LetterMgmtModule, AccountsModule, diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index 603883534..064459f79 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -362,6 +362,10 @@ RBI + + Investment + + Digital @@ -492,6 +496,15 @@ RBI + + Investment + + Reports @@ -1166,7 +1179,7 @@ Non-Customer Entry - - - + + + + + + + + + + + + + + @if (activeTab === 'holdings') { +
+ + + + + + + + + + + + + + + + + + + @for (h of holdingsForType; track h.certNo) { + + + + + + + + + + + + + + } @empty { + + + + } + +
Cert NoScriptPartyUnitsFace ValueRatePurchase DateMaturity DateAccrued InterestMaturity ValueStatus
+ {{ h.certNo }} + {{ h.scriptName }}{{ h.party }}{{ h.units | number }}₹{{ h.faceValue | number: '1.2-2' }}{{ h.interestRate }}%{{ h.purchaseDate }}{{ h.maturityDate }}₹{{ accrued(h) | number: '1.2-2' }}₹{{ maturity(h) | number: '1.2-2' }} + {{ h.status }} +
No {{ instrumentLabel(bondType) }} holdings yet.
+
+ } + + + @if (activeTab === 'purchase') { +
+ +
+ + + + + + + + + + + + + @if (isMutualFund) { + + + + + + + + } @else { + + + + + + } + + + + + + + + + + + + + + + + + + + + + + + @if (!isMutualFund) { + + + + + + + + + + + + + + + + + + } + + @if (purchaseForm.lien === 'YES') { + + + } + + + + + +
+
+ + + + + +
+
+ } + + + @if (activeTab === 'redemption') { +
+ +
+ + @if (redeemHolding; as h) { + + + + + } + + + +
+
+ + +
+
+ } + + + @if (activeTab === 'interest') { +
+ +
+ + + + + +
+
+ + +
+ + + + + + + + + + + + + + @for (h of intCalcTargets; track h.certNo) { + + + + + + + + + } @empty { + + + + } + +
Cert NoScriptTypeRateFrequencyInterest to Post
+ {{ h.certNo }} + {{ h.scriptName }}{{ instrumentLabel(h.instrumentType) }}{{ h.interestRate }}%{{ h.interestFreq }}₹{{ interestAccrued(h) | number: '1.2-2' }}
No holdings match the selected filters.
+
+ } + + + @if (activeTab === 'sell') { +
+ +
+ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+
+ } + + + @if (activeTab === 'writeoff') { +
+ +
+ + @if (writeoffHolding; as h) { + + } + +
+
+ + +
+
+ } + + + @if (activeTab === 'amortize') { +
+ +
+ + @if (amortizeHolding; as h) { + + + } + +
+
+ + +
+
+ } + + + @if (activeTab === 'amortizeYearly') { +
+ +
+ + + +
+
+ + +
+ + + + + + + + + + + + + @for (t of amortizeYearlyTargets; track t.holding.certNo) { + + + + + + + + + } @empty { + + + + } + +
Cert NoScriptFace ValuePurchase RateMaturity DateYearly Amortization
+ {{ t.holding.certNo }} + {{ t.holding.scriptName }}₹{{ t.holding.faceValue | number: '1.2-2' }}{{ t.holding.purchaseRate }}{{ t.holding.maturityDate }}₹{{ t.amount | number: '1.2-2' }}
No premium-carrying holdings require amortization.
+
+ } + + + @if (activeTab === 'goiTransfer') { +
+ +
+ + @if (goiHolding; as h) { + + + } + + + +
+
+ + +
+
+ } + + + @if (activeTab === 'depreciation') { +
+ +
+ + + + +
+
+ + +
+ + + + + + + + + + + + @for (t of depreciationTargets; track t.holding.certNo) { + + + + + + + + } @empty { + + + + } + +
Cert NoScriptBook RateMarket PriceDepreciation
+ {{ t.holding.certNo }} + {{ t.holding.scriptName }}{{ t.holding.purchaseRate }}{{ t.marketPrice }}₹{{ t.depreciation | number: '1.2-2' }}
No holdings require depreciation provisioning.
+
+ } + + + @if (activeTab === 'mfTransfer') { +
+ +
+ + @if (mfFromHolding; as h) { + + + } + + + + +
+
+ + +
+
+ } + + + @if (activeTab !== 'holdings') { +
+ + + + + + + + + + + + + + @for (t of txnsForType; track t.voucherNo) { + + + + + + + + + } @empty { + + + + } + +
DateVoucher NoCert NoTypeParticularsAmount
{{ t.txnDate }} + {{ t.voucherNo }} + {{ t.certNo }}{{ t.txnType }}{{ t.particulars }}₹{{ t.amount | number: '1.2-2' }}
No transactions yet.
+
+ } + + +
{{ toastMsg }}
diff --git a/src/app/investment/bond-investment/bond-investment.component.ts b/src/app/investment/bond-investment/bond-investment.component.ts new file mode 100644 index 000000000..de971e980 --- /dev/null +++ b/src/app/investment/bond-investment/bond-investment.component.ts @@ -0,0 +1,1103 @@ +/** + * 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 { + 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, + InvestmentHolding, + InvestmentTxn, + InstrumentType, + InterestFreq, + INSTRUMENT_LABELS, + accruedInterest, + daysToMaturity, + maturityAmount, + addPeriod, + approxYtm, + yearsBetween, + todayDDMMYYYY +} from '../investment-data'; + +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 + ] +}) +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; + + 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: Date / Type / Script / Deal No / ALL + Process) + intCalcDate = this.asOnDate; + intCalcType: BondType | '' = ''; + intCalcScript = ''; + intCalcDealNo = ''; + intCalcAll = false; + + // 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. */ + get intCalcTargets(): InvestmentHolding[] { + if (this.intCalcAll) { + return this.holdings.filter((h) => h.status === 'ACTIVE'); + } + return this.holdings.filter((h) => { + if (h.status !== 'ACTIVE') return false; + 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; + }); + } + + interestAccrued(h: InvestmentHolding): number { + return accruedInterest(h, this.intCalcDate || this.asOnDate); + } + + processInterest(): void { + const targets = this.intCalcTargets; + if (targets.length === 0) { + this.toast('No holdings match the selected filters.'); + return; + } + const newTxns: InvestmentTxn[] = targets.map((h, i) => ({ + certNo: h.certNo, + instrumentType: h.instrumentType, + txnType: 'INTEREST', + voucherNo: `INV/INT/${h.certNo}-${this.txns.length + i + 1}`, + txnDate: this.intCalcDate || this.asOnDate, + amount: this.interestAccrued(h), + particulars: `Interest calculated - ${h.scriptName}` + })); + this.txns = [ + ...newTxns, + ...this.txns + ]; + const total = newTxns.reduce((s, t) => s + t.amount, 0); + this.toast(`Interest processed for ${targets.length} holding(s). 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()}`; + } + + private toast(msg: string): void { + this.toastMsg = msg; + this.toastVisible = true; + if (this.toastTimer) { + clearTimeout(this.toastTimer); + } + this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200); + } +} diff --git a/src/app/investment/fd-investment/fd-investment.component.html b/src/app/investment/fd-investment/fd-investment.component.html new file mode 100644 index 000000000..bac98204f --- /dev/null +++ b/src/app/investment/fd-investment/fd-investment.component.html @@ -0,0 +1,472 @@ + + +
+
+
+
TREASURY · INVESTMENT · FIXED DEPOSIT
+

FD Investment

+

+ Bank's own Fixed Deposit investments with other banks — Purchase, Interest, Sell/Premature, Write-off and + Part-Payment entries. +

+
+
+ +
+ + + + + + +
+ + + @if (activeTab === 'holdings') { +
+ + + + + + + + + + + + + + + + + + @for (h of holdings; track h.certNo) { + + + + + + + + + + + + + } + +
Cert NoPartyTypeFace ValueRatePurchase DateMaturity DateAccrued InterestMaturity ValueStatus
+ {{ h.certNo }} + {{ h.party }}{{ h.interestFreq === 'CUMULATIVE' ? 'Cumulative' : 'Payout' }}₹{{ h.faceValue | number: '1.2-2' }}{{ h.interestRate }}%{{ h.purchaseDate }}{{ h.maturityDate }}₹{{ accrued(h) | number: '1.2-2' }}₹{{ maturity(h) | number: '1.2-2' }} + {{ h.status }} +
+
+ } + + + @if (activeTab === 'purchase') { +
+ +
+ + @if (purchaseForm.tranType === 'RENEWAL') { + + } + + + + + + + + + + + + + + + + + @if (purchaseForm.matureAmtCalcType === 'GENERAL') { + + + + } @else { + + } + + + + + + + @if (purchaseForm.lien === 'YES') { + + + } + + +
+ +
+ + + +
+
+ + + + + +
+
+ } + + + @if (activeTab === 'interest') { +
+ +
+ + @if (interestHolding; as h) { + + + + + } +
+
+ +
+
+ } + + + @if (activeTab === 'sell') { +
+ +
+ + @if (sellHolding; as h) { + + + } + + +
+
+ +
+
+ } + + + @if (activeTab === 'writeoff') { +
+ +
+ + @if (writeoffHolding; as h) { + + } + +
+
+ +
+
+ } + + + @if (activeTab === 'partpayment') { +
+ +
+ + @if (partPayHolding; as h) { + + } + +
+
+ +
+
+ } + + @if (activeTab !== 'holdings') { +
+ + + + + + + + + + + + + + @for (t of txns; track t.voucherNo) { + + + + + + + + + } + +
DateVoucher NoCert NoTypeParticularsAmount
{{ t.txnDate }} + {{ t.voucherNo }} + {{ t.certNo }}{{ t.txnType }}{{ t.particulars }}₹{{ t.amount | number: '1.2-2' }}
+
+ } +
+ +
{{ toastMsg }}
diff --git a/src/app/investment/fd-investment/fd-investment.component.ts b/src/app/investment/fd-investment/fd-investment.component.ts new file mode 100644 index 000000000..739adf032 --- /dev/null +++ b/src/app/investment/fd-investment/fd-investment.component.ts @@ -0,0 +1,461 @@ +/** + * 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 { + FD_HOLDINGS, + ALL_TXNS, + PARTY_MASTER, + SUBTYPE_MASTER, + FD_TYPE_OPTIONS, + INT_TYPE_OPTIONS, + LIEN_REASON_MASTER, + InvestmentHolding, + InvestmentTxn, + InterestFreq, + accruedInterest, + daysToMaturity, + maturityAmount, + addPeriod, + todayDDMMYYYY +} from '../investment-data'; + +type FdTab = 'holdings' | 'purchase' | 'interest' | 'sell' | 'writeoff' | 'partpayment'; + +interface FdPurchaseForm { + tranType: 'FRESH' | 'RENEWAL'; + oldCertNo: string; + fdType: string; + /** GENERAL uses the period (Y/M/D) fields; DATEWISE lets the user type the maturity date directly. */ + matureAmtCalcType: 'GENERAL' | 'DATEWISE'; + certNo: string; + certDate: string; + party: string; + subParty: string; + glAcNo: string; + glAcName: string; + subType: string; + receiptAdvice: 'RECEIPT' | 'ADVICE'; + transactionDate: string; + depositDate: string; + depositAmount: number | null; + interestFreq: InterestFreq; + interestDate: string; + interestRate: number | null; + periodYear: number | null; + periodMonth: number | null; + periodDays: number | null; + /** Only used when matureAmtCalcType === 'DATEWISE'. */ + manualMaturityDate: string; + resolutionNo: string; + crBranchCode: string; + crToAc: string; + crToAcName: string; + intReceivableCode: string; + lien: 'YES' | 'NO'; + lienAmt: number | null; + lienReason: string; + classification: string; + collable: 'YES' | 'NO'; +} + +/** + * FD (Fixed Deposit) treasury investment screen — Holdings register + Purchase / Interest / + * Sell-Premature / Write-off / Part-Payment entry tabs, modelled on the legacy "Investment - FD + * Purchase Entry" form. Dummy in-memory data only; Save appends to the local holdings/txns + * arrays so the demo feels interactive without any backend. + */ +@Component({ + selector: 'mifosx-fd-investment', + templateUrl: './fd-investment.component.html', + styleUrls: [ + '../../cashier/cashier-shared.scss', + '../investment-shared.scss' + ], + standalone: true, + imports: [ + CommonModule, + FormsModule + ] +}) +export class FdInvestmentComponent { + activeTab: FdTab = 'holdings'; + asOnDate = todayDDMMYYYY(); + + holdings: InvestmentHolding[] = [...FD_HOLDINGS]; + txns: InvestmentTxn[] = ALL_TXNS.filter((t) => t.instrumentType === 'FD'); + + readonly partyOptions = PARTY_MASTER; + readonly subTypeOptions = SUBTYPE_MASTER; + readonly fdTypeOptions = FD_TYPE_OPTIONS; + readonly intTypeOptions = INT_TYPE_OPTIONS; + readonly lienReasonOptions = LIEN_REASON_MASTER; + readonly branchOptions = [ + 'HEAD OFFICE', + 'BRANCH 1', + 'BRANCH 2' + ]; + + toastMsg = ''; + toastVisible = false; + private toastTimer: ReturnType | undefined; + + purchaseForm: FdPurchaseForm = this.blankPurchaseForm(); + /** Certificate No typed into the Purchase Entry's Search box (legacy "Search" button). */ + searchCertNo = ''; + + interestCertNo = ''; + sellCertNo = ''; + sellDate = this.asOnDate; + sellRemark = ''; + writeoffCertNo = ''; + writeoffReason = ''; + partPayCertNo = ''; + partPayAmount: number | null = null; + + setTab(tab: FdTab): void { + this.activeTab = tab; + } + + get activeHoldings(): InvestmentHolding[] { + return this.holdings.filter((h) => h.status === 'ACTIVE'); + } + + private nextCertNo(): string { + const nums = this.holdings.map((h) => parseInt(h.certNo, 10)).filter((n) => !isNaN(n)); + return String((nums.length ? Math.max(...nums) : 15619) + 1); + } + + private blankPurchaseForm(): FdPurchaseForm { + return { + tranType: 'FRESH', + oldCertNo: '', + fdType: 'CUMMULATIVE', + matureAmtCalcType: 'GENERAL', + certNo: this.nextCertNo(), + certDate: this.asOnDate, + party: '', + subParty: '', + glAcNo: '320020', + glAcName: 'FIXED DEPO. CO-OPER. BANK MAIN', + subType: 'NON SLR', + receiptAdvice: 'RECEIPT', + transactionDate: this.asOnDate, + depositDate: this.asOnDate, + depositAmount: null, + interestFreq: 'CUMULATIVE', + interestDate: this.asOnDate, + interestRate: null, + periodYear: 2, + periodMonth: 0, + periodDays: 0, + manualMaturityDate: '', + resolutionNo: '', + crBranchCode: 'HEAD OFFICE', + crToAc: '310070', + crToAcName: 'CURRENT A/C CO-OP. BANK MAIN BR.41', + intReceivableCode: '320020', + lien: 'NO', + lienAmt: null, + lienReason: '', + classification: '', + collable: 'NO' + }; + } + + get purchaseMaturityDate(): string { + const f = this.purchaseForm; + if (f.matureAmtCalcType === 'DATEWISE') { + return f.manualMaturityDate; + } + if (!f.depositDate) { + return ''; + } + const base = addPeriod(f.depositDate, f.periodYear || 0, f.periodMonth || 0); + return f.periodDays ? addPeriod(base, 0, 0, f.periodDays) : base; + } + + get purchaseMaturityAmount(): number { + const f = this.purchaseForm; + if (!f.depositAmount || !f.interestRate || !this.purchaseMaturityDate) { + return 0; + } + return maturityAmount({ + certNo: f.certNo, + instrumentType: 'FD', + scriptCode: '', + scriptName: '', + party: f.party, + subType: f.subType, + faceValue: f.depositAmount, + purchaseRate: 100, + units: 1, + purchaseDate: f.depositDate, + maturityDate: this.purchaseMaturityDate, + interestRate: f.interestRate, + interestFreq: f.interestFreq, + glCode: f.glAcNo, + glName: '', + crToAc: f.crToAc, + brokerCode: '', + status: 'ACTIVE' + }); + } + + savePurchase(): void { + const f = this.purchaseForm; + if (!f.party || !f.depositAmount || !f.interestRate || !this.purchaseMaturityDate) { + this.toast('Please fill Party, Deposit Amount, Interest Rate and Period.'); + return; + } + const holding: InvestmentHolding = { + certNo: f.certNo, + instrumentType: 'FD', + scriptCode: f.interestFreq === 'CUMULATIVE' ? 'FD-CUM' : 'FD-MIP', + scriptName: f.fdType ? `${f.fdType} Fixed Deposit` : 'Fixed Deposit', + party: f.party, + subType: f.subType, + faceValue: f.depositAmount, + purchaseRate: 100, + units: 1, + purchaseDate: f.depositDate, + maturityDate: this.purchaseMaturityDate, + interestRate: f.interestRate, + interestFreq: f.interestFreq, + glCode: f.glAcNo, + glName: f.glAcName, + crToAc: f.crToAc, + brokerCode: '', + status: 'ACTIVE' + }; + this.holdings = [ + holding, + ...this.holdings + ]; + this.txns = [ + { + certNo: holding.certNo, + instrumentType: 'FD', + txnType: 'PURCHASE', + voucherNo: `INV/PUR/FD-${holding.certNo}`, + txnDate: f.transactionDate, + amount: holding.faceValue, + particulars: `${f.tranType === 'RENEWAL' ? 'Renewal' : 'Fresh'} FD Purchase - ${holding.party}` + }, + ...this.txns + ]; + this.toast(`FD Certificate No. ${holding.certNo} saved successfully.`); + this.purchaseForm = this.blankPurchaseForm(); + this.activeTab = 'holdings'; + } + + cancelPurchase(): void { + this.purchaseForm = this.blankPurchaseForm(); + this.toast('Entry cancelled — form reset.'); + } + + /** Legacy "Search" button — loads an existing certificate's values back into the form. */ + searchPurchase(): void { + const h = this.holdings.find((x) => x.certNo === this.searchCertNo.trim()); + if (!h) { + this.toast(`Certificate No. ${this.searchCertNo} not found.`); + return; + } + this.purchaseForm = { + ...this.blankPurchaseForm(), + tranType: 'FRESH', + certNo: h.certNo, + certDate: h.purchaseDate, + party: h.party, + subType: h.subType, + glAcNo: h.glCode, + glAcName: h.glName, + transactionDate: h.purchaseDate, + depositDate: h.purchaseDate, + depositAmount: h.faceValue, + interestFreq: h.interestFreq, + interestDate: h.purchaseDate, + interestRate: h.interestRate, + matureAmtCalcType: 'DATEWISE', + manualMaturityDate: h.maturityDate, + crToAc: h.crToAc + }; + this.toast(`Certificate ${h.certNo} loaded.`); + } + + /** Legacy "Drop" button — removes the loaded certificate from the register. */ + dropPurchase(): void { + const certNo = this.purchaseForm.certNo; + const exists = this.holdings.some((h) => h.certNo === certNo); + if (!exists) { + this.toast('Nothing to drop — this certificate 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(`Certificate ${certNo} dropped.`); + } + + /** Legacy "Exit" button — closes the entry form and returns to the register. */ + exitPurchase(): void { + this.purchaseForm = this.blankPurchaseForm(); + this.activeTab = 'holdings'; + } + + // -------------------- Interest -------------------- + get interestHolding(): InvestmentHolding | undefined { + return this.holdings.find((h) => h.certNo === this.interestCertNo); + } + + interestAccrued(h: InvestmentHolding): number { + return accruedInterest(h, this.asOnDate); + } + + postInterest(): void { + const h = this.interestHolding; + if (!h) { + this.toast('Select a certificate first.'); + return; + } + const amt = this.interestAccrued(h); + this.txns = [ + { + certNo: h.certNo, + instrumentType: 'FD', + txnType: 'INTEREST', + voucherNo: `INV/INT/FD-${h.certNo}-${this.txns.length + 1}`, + txnDate: this.asOnDate, + amount: amt, + particulars: `Interest posted - ${h.party} (${h.interestFreq})` + }, + ...this.txns + ]; + this.toast(`Interest of ₹${amt.toLocaleString('en-IN')} posted for Certificate ${h.certNo}.`); + this.interestCertNo = ''; + } + + // -------------------- Sell / Premature -------------------- + get sellHolding(): InvestmentHolding | undefined { + return this.holdings.find((h) => h.certNo === this.sellCertNo); + } + + saveSell(): void { + const h = this.sellHolding; + if (!h || !this.sellDate) { + this.toast('Select a certificate and sell/premature date.'); + return; + } + const payout = h.faceValue + accruedInterest(h, this.sellDate); + h.status = 'SOLD'; + this.txns = [ + { + certNo: h.certNo, + instrumentType: 'FD', + txnType: 'SELL', + voucherNo: `INV/SEL/FD-${h.certNo}`, + txnDate: this.sellDate, + amount: payout, + particulars: `Premature closure - ${h.party}${this.sellRemark ? ' - ' + this.sellRemark : ''}` + }, + ...this.txns + ]; + this.toast(`Certificate ${h.certNo} closed. Payout ₹${payout.toLocaleString('en-IN')}.`); + this.sellCertNo = ''; + this.sellRemark = ''; + this.activeTab = 'holdings'; + } + + // -------------------- Write-off -------------------- + get writeoffHolding(): InvestmentHolding | undefined { + return this.holdings.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: 'FD', + txnType: 'WRITE_OFF', + voucherNo: `INV/WOF/FD-${h.certNo}`, + txnDate: this.asOnDate, + amount: h.faceValue, + particulars: `Written off - ${this.writeoffReason}` + }, + ...this.txns + ]; + this.toast(`Certificate ${h.certNo} written off.`); + this.writeoffCertNo = ''; + this.writeoffReason = ''; + this.activeTab = 'holdings'; + } + + // -------------------- Part Payment -------------------- + get partPayHolding(): InvestmentHolding | undefined { + return this.holdings.find((h) => h.certNo === this.partPayCertNo); + } + + savePartPayment(): void { + const h = this.partPayHolding; + if (!h || !this.partPayAmount || this.partPayAmount <= 0 || this.partPayAmount >= h.faceValue) { + this.toast('Select a certificate and enter a valid part-payment amount (less than face value).'); + return; + } + h.faceValue -= this.partPayAmount; + this.txns = [ + { + certNo: h.certNo, + instrumentType: 'FD', + txnType: 'PART_PAYMENT', + voucherNo: `INV/PP/FD-${h.certNo}-${this.txns.length + 1}`, + txnDate: this.asOnDate, + amount: this.partPayAmount, + particulars: `Part payment withdrawn - ${h.party}, balance ₹${h.faceValue.toLocaleString('en-IN')}` + }, + ...this.txns + ]; + this.toast(`Part payment of ₹${this.partPayAmount.toLocaleString('en-IN')} recorded for Certificate ${h.certNo}.`); + this.partPayCertNo = ''; + this.partPayAmount = null; + this.activeTab = 'holdings'; + } + + // -------------------- 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()}`; + } + + private toast(msg: string): void { + this.toastMsg = msg; + this.toastVisible = true; + if (this.toastTimer) { + clearTimeout(this.toastTimer); + } + this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200); + } +} diff --git a/src/app/investment/investment-dashboard/investment-dashboard.component.html b/src/app/investment/investment-dashboard/investment-dashboard.component.html new file mode 100644 index 000000000..43309fba5 --- /dev/null +++ b/src/app/investment/investment-dashboard/investment-dashboard.component.html @@ -0,0 +1,146 @@ + + +
+
+
+
TREASURY · INVESTMENT
+

Investment Portfolio Dashboard

+

+ Consolidated position across FD, Govt Security, Non-SLR, Bonds, Mutual Fund and Share holdings as on + {{ asOnDate }}. +

+
+
+ +
+
+ Total Holdings + {{ totalHoldings }} + {{ activeCount }} active · {{ maturedCount }} matured +
+
+ Total Invested (Face Value) + ₹{{ totalFaceValue | number: '1.2-2' }} +
+
+ Accrued Interest / Income + ₹{{ totalAccruedInterest | number: '1.2-2' }} +
+
+ Projected Value at Maturity + ₹{{ totalMaturityValue | number: '1.2-2' }} +
+
+ +
+ + + + + + + + + + + + + + @for (s of summaries; track s.type) { + + + + + + + + + } + + + + + + + + + + + +
Instrument TypeHoldingsFace ValueAccrued InterestMaturity Value
{{ s.label }}{{ s.count }}₹{{ s.faceValue | number: '1.2-2' }}₹{{ s.accruedInterest | number: '1.2-2' }}₹{{ s.maturityValue | number: '1.2-2' }} + Open +
Total{{ totalHoldings }}₹{{ totalFaceValue | number: '1.2-2' }}₹{{ totalAccruedInterest | number: '1.2-2' }}₹{{ totalMaturityValue | number: '1.2-2' }}
+
+ +
+ + @if (maturingSoon.length === 0) { +
No holdings maturing in the next 90 days.
+ } @else { +
+ @for (h of maturingSoon; track h.certNo) { +
+ + {{ h.certNo }} + {{ h.scriptName }} — {{ h.party }} ({{ instrumentLabel(h.instrumentType) }}) + + + Matures {{ h.maturityDate }} · + + {{ daysLeft(h) }} days + + +
+ } +
+ } +
+ + +
diff --git a/src/app/investment/investment-dashboard/investment-dashboard.component.ts b/src/app/investment/investment-dashboard/investment-dashboard.component.ts new file mode 100644 index 000000000..650d5f568 --- /dev/null +++ b/src/app/investment/investment-dashboard/investment-dashboard.component.ts @@ -0,0 +1,120 @@ +/** + * 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 { RouterLink } from '@angular/router'; +import { + ALL_HOLDINGS, + INSTRUMENT_LABELS, + InstrumentType, + InvestmentHolding, + accruedInterest, + daysToMaturity, + maturityAmount +} from '../investment-data'; + +interface InstrumentSummary { + type: InstrumentType; + label: string; + route: string; + count: number; + faceValue: number; + accruedInterest: number; + maturityValue: number; +} + +/** Cross-instrument portfolio dashboard — every figure is derived from ALL_HOLDINGS, never + * hand-duplicated, so it can never drift from the per-instrument screens or Reports. */ +@Component({ + selector: 'mifosx-investment-dashboard', + templateUrl: './investment-dashboard.component.html', + styleUrls: [ + '../../cashier/cashier-shared.scss', + '../investment-shared.scss' + ], + standalone: true, + imports: [ + CommonModule, + RouterLink + ] +}) +export class InvestmentDashboardComponent { + asOnDate = ''; + 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()}`; + } + + 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) + }; + }); + } + + get totalHoldings(): number { + return ALL_HOLDINGS.length; + } + + get totalFaceValue(): number { + return ALL_HOLDINGS.reduce((s, h) => s + h.faceValue, 0); + } + + get totalAccruedInterest(): number { + return ALL_HOLDINGS.reduce((s, h) => s + accruedInterest(h, this.asOnDate), 0); + } + + get totalMaturityValue(): number { + return ALL_HOLDINGS.reduce((s, h) => s + maturityAmount(h), 0); + } + + get activeCount(): number { + return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE').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]; + } +} diff --git a/src/app/investment/investment-data.ts b/src/app/investment/investment-data.ts new file mode 100644 index 000000000..a2cdd253c --- /dev/null +++ b/src/app/investment/investment-data.ts @@ -0,0 +1,1438 @@ +/** + * 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/. + */ + +/** + * Sample Investment (Treasury) data — dummy static data modelled on a legacy CBS "Investment" + * menu (FD / Govt Security / Non-SLR / Bond / Mutual Fund / Share), covering Purchase, Interest, + * Sell/Redeem, Write-off and Amortize entries, master data, and reports. + * + * Maturity amount and accrued interest are always DERIVED from principal/rate/dates via the + * helper functions below — never hand-duplicated — so the Dashboard, per-instrument Holdings + * lists, and Reports can never drift out of sync with each other. + */ + +export type InstrumentType = 'FD' | 'GOVT_SECURITY' | 'NON_SLR' | 'BOND' | 'MUTUAL_FUND' | 'SHARE'; +export type HoldingStatus = 'ACTIVE' | 'MATURED' | 'SOLD' | 'WRITTEN_OFF'; +export type InterestFreq = 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY' | 'CUMULATIVE'; +export type TxnType = + | 'PURCHASE' + | 'INTEREST' + | 'DIVIDEND' + | 'SELL' + | 'REDEEM' + | 'WRITE_OFF' + | 'AMORTIZE' + | 'PART_PAYMENT'; + +export const INSTRUMENT_LABELS: Record = { + FD: 'Fixed Deposit', + GOVT_SECURITY: 'Govt Security', + NON_SLR: 'Non-SLR', + BOND: 'Bond', + MUTUAL_FUND: 'Mutual Fund', + SHARE: 'Share' +}; + +export interface InvestmentHolding { + certNo: string; + instrumentType: InstrumentType; + scriptCode: string; + scriptName: string; + party: string; + subType: string; + faceValue: number; + purchaseRate: number; + units: number; + purchaseDate: string; + maturityDate: string; + interestRate: number; + interestFreq: InterestFreq; + glCode: string; + glName: string; + crToAc: string; + brokerCode: string; + status: HoldingStatus; +} + +export interface InvestmentTxn { + certNo: string; + instrumentType: InstrumentType; + txnType: TxnType; + voucherNo: string; + txnDate: string; + amount: number; + particulars: string; +} + +// -------------------- Date / interest helpers -------------------- + +export function parseDDMMYYYY(s: string): Date { + const [ + dd, + mm, + yyyy + ] = s.split('-').map(Number); + return new Date(yyyy, mm - 1, dd); +} + +export function yearsBetween(d1: string, d2: string): number { + const a = parseDDMMYYYY(d1).getTime(); + const b = parseDDMMYYYY(d2).getTime(); + return (b - a) / (365.25 * 24 * 3600 * 1000); +} + +/** Maturity amount — compounded annually for cumulative instruments, principal-only otherwise + * (non-cumulative interest is paid out periodically instead of being added to the corpus). */ +export function maturityAmount(h: InvestmentHolding): number { + const yrs = yearsBetween(h.purchaseDate, h.maturityDate); + if (h.interestFreq === 'CUMULATIVE') { + return Math.round(h.faceValue * Math.pow(1 + h.interestRate / 100, yrs)); + } + return h.faceValue; +} + +/** Interest accrued from purchase up to `asOnStr`, capped at the maturity date. */ +export function accruedInterest(h: InvestmentHolding, asOnStr: string): number { + const cap = Math.min(yearsBetween(h.purchaseDate, asOnStr), yearsBetween(h.purchaseDate, h.maturityDate)); + const yrs = Math.max(0, cap); + return Math.round(h.faceValue * (h.interestRate / 100) * yrs); +} + +export function daysToMaturity(h: InvestmentHolding, asOnStr: string): number { + const asOn = parseDDMMYYYY(asOnStr).getTime(); + const mat = parseDDMMYYYY(h.maturityDate).getTime(); + return Math.round((mat - asOn) / (24 * 3600 * 1000)); +} + +export function formatDDMMYYYY(d: Date): string { + return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`; +} + +/** Adds a period (years/months/days) to a DD-MM-YYYY date string — used to compute Maturity Date + * from an entry form's Deposit/Deal Date + Period Year/Month/Days fields. */ +export function addPeriod(dateStr: string, years: number, months: number, days = 0): string { + const d = parseDDMMYYYY(dateStr); + d.setFullYear(d.getFullYear() + (years || 0)); + d.setMonth(d.getMonth() + (months || 0)); + d.setDate(d.getDate() + (days || 0)); + return formatDDMMYYYY(d); +} + +export function todayDDMMYYYY(): string { + return formatDDMMYYYY(new Date()); +} + +// -------------------- Holdings (dummy portfolio) -------------------- + +export const FD_HOLDINGS: InvestmentHolding[] = [ + { + certNo: '15620', + instrumentType: 'FD', + scriptCode: 'FD-CUM', + scriptName: 'Cumulative Fixed Deposit', + party: 'Jila Sahakari Kendriya Bank', + subType: 'NON SLR', + faceValue: 2401711, + purchaseRate: 100, + units: 1, + purchaseDate: '23-06-2026', + maturityDate: '23-12-2028', + interestRate: 6.75, + interestFreq: 'CUMULATIVE', + glCode: '320020', + glName: 'FIXED DEPO. CO-OPER. BANK MAIN', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + }, + { + certNo: '15621', + instrumentType: 'FD', + scriptCode: 'FD-CUM', + scriptName: 'Cumulative Fixed Deposit', + party: 'ABC Cooperative Bank Ltd', + subType: 'NON SLR', + faceValue: 1500000, + purchaseRate: 100, + units: 1, + purchaseDate: '10-01-2026', + maturityDate: '10-01-2029', + interestRate: 7.0, + interestFreq: 'CUMULATIVE', + glCode: '320020', + glName: 'FIXED DEPO. CO-OPER. BANK MAIN', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + }, + { + certNo: '15622', + instrumentType: 'FD', + scriptCode: 'FD-CUM', + scriptName: 'Cumulative Fixed Deposit', + party: 'State Cooperative Bank Ltd', + subType: 'NON SLR', + faceValue: 5000000, + purchaseRate: 100, + units: 1, + purchaseDate: '05-03-2024', + maturityDate: '05-03-2026', + interestRate: 6.5, + interestFreq: 'YEARLY', + glCode: '320020', + glName: 'FIXED DEPO. CO-OPER. BANK MAIN', + crToAc: '310070', + brokerCode: '', + status: 'MATURED' + }, + { + certNo: '15623', + instrumentType: 'FD', + scriptCode: 'FD-MIP', + scriptName: 'Monthly Interest Payout FD', + party: 'District Central Cooperative Bank', + subType: 'NON SLR', + faceValue: 1000000, + purchaseRate: 100, + units: 1, + purchaseDate: '15-05-2026', + maturityDate: '15-05-2027', + interestRate: 7.25, + interestFreq: 'MONTHLY', + glCode: '320020', + glName: 'FIXED DEPO. CO-OPER. BANK MAIN', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + }, + { + certNo: '15624', + instrumentType: 'FD', + scriptCode: 'FD-CUM', + scriptName: 'Cumulative Fixed Deposit', + party: 'Urban Cooperative Bank Ltd', + subType: 'NON SLR', + faceValue: 2500000, + purchaseRate: 100, + units: 1, + purchaseDate: '20-02-2024', + maturityDate: '20-02-2026', + interestRate: 6.9, + interestFreq: 'QUARTERLY', + glCode: '320020', + glName: 'FIXED DEPO. CO-OPER. BANK MAIN', + crToAc: '310070', + brokerCode: '', + status: 'MATURED' + }, + { + certNo: '15625', + instrumentType: 'FD', + scriptCode: 'FD-CUM', + scriptName: 'Cumulative Fixed Deposit', + party: 'Nagrik Sahakari Bank Ltd', + subType: 'NON SLR', + faceValue: 800000, + purchaseRate: 100, + units: 1, + purchaseDate: '01-04-2026', + maturityDate: '01-04-2028', + interestRate: 7.1, + interestFreq: 'HALF_YEARLY', + glCode: '320020', + glName: 'FIXED DEPO. CO-OPER. BANK MAIN', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + }, + { + certNo: '15626', + instrumentType: 'FD', + scriptCode: 'FD-CUM', + scriptName: 'Cumulative Fixed Deposit', + party: 'Maharashtra State Cooperative Bank', + subType: 'NON SLR', + faceValue: 3500000, + purchaseRate: 100, + units: 1, + purchaseDate: '12-07-2025', + maturityDate: '12-07-2027', + interestRate: 6.6, + interestFreq: 'YEARLY', + glCode: '320020', + glName: 'FIXED DEPO. CO-OPER. BANK MAIN', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + }, + { + certNo: '15627', + instrumentType: 'FD', + scriptCode: 'FD-CUM', + scriptName: 'Cumulative Fixed Deposit', + party: 'Rajasthan State Cooperative Bank', + subType: 'NON SLR', + faceValue: 1250000, + purchaseRate: 100, + units: 1, + purchaseDate: '18-06-2026', + maturityDate: '18-06-2029', + interestRate: 7.0, + interestFreq: 'QUARTERLY', + glCode: '320020', + glName: 'FIXED DEPO. CO-OPER. BANK MAIN', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + } +]; + +export const GOVT_SECURITY_HOLDINGS: InvestmentHolding[] = [ + { + certNo: 'GSEC-2001', + instrumentType: 'GOVT_SECURITY', + scriptCode: 'IN0020230012', + scriptName: '7.26% GOI 2033', + party: 'RBI - SGL', + subType: 'SLR', + faceValue: 5000000, + purchaseRate: 99.5, + units: 50000, + purchaseDate: '15-01-2026', + maturityDate: '15-01-2033', + interestRate: 7.26, + interestFreq: 'HALF_YEARLY', + glCode: '310020', + glName: 'INVESTMENT IN GOVT. SECURITIES', + crToAc: '310070', + brokerCode: 'BRK-002', + status: 'ACTIVE' + }, + { + certNo: 'GSEC-2002', + instrumentType: 'GOVT_SECURITY', + scriptCode: 'IN0020220089', + scriptName: '7.10% GOI 2029', + party: 'RBI - SGL', + subType: 'SLR', + faceValue: 3000000, + purchaseRate: 100.25, + units: 30000, + purchaseDate: '10-03-2025', + maturityDate: '10-03-2029', + interestRate: 7.1, + interestFreq: 'HALF_YEARLY', + glCode: '310020', + glName: 'INVESTMENT IN GOVT. SECURITIES', + crToAc: '310070', + brokerCode: 'BRK-001', + status: 'ACTIVE' + }, + { + certNo: 'GSEC-2003', + instrumentType: 'GOVT_SECURITY', + scriptCode: 'IN0020210056', + scriptName: '6.79% GOI 2027', + party: 'RBI - SGL', + subType: 'SLR', + faceValue: 2000000, + purchaseRate: 101.1, + units: 20000, + purchaseDate: '05-06-2024', + maturityDate: '05-06-2027', + interestRate: 6.79, + interestFreq: 'HALF_YEARLY', + glCode: '310020', + glName: 'INVESTMENT IN GOVT. SECURITIES', + crToAc: '310070', + brokerCode: 'BRK-002', + status: 'ACTIVE' + }, + { + certNo: 'GSEC-2004', + instrumentType: 'GOVT_SECURITY', + scriptCode: 'IN0020240034', + scriptName: '7.18% GOI 2037', + party: 'RBI - SGL', + subType: 'SLR', + faceValue: 4000000, + purchaseRate: 98.75, + units: 40000, + purchaseDate: '22-04-2026', + maturityDate: '22-04-2037', + interestRate: 7.18, + interestFreq: 'HALF_YEARLY', + glCode: '310020', + glName: 'INVESTMENT IN GOVT. SECURITIES', + crToAc: '310070', + brokerCode: 'BRK-001', + status: 'ACTIVE' + }, + { + certNo: 'GSEC-2005', + instrumentType: 'GOVT_SECURITY', + scriptCode: 'TBILL-364D', + scriptName: '364 Days Treasury Bill', + party: 'RBI - SGL', + subType: 'SLR', + faceValue: 1500000, + purchaseRate: 96.4, + units: 15000, + purchaseDate: '02-07-2026', + maturityDate: '02-07-2027', + interestRate: 6.4, + interestFreq: 'CUMULATIVE', + glCode: '310020', + glName: 'INVESTMENT IN GOVT. SECURITIES', + crToAc: '310070', + brokerCode: 'BRK-003', + status: 'ACTIVE' + }, + { + certNo: 'GSEC-2006', + instrumentType: 'GOVT_SECURITY', + scriptCode: 'MH-SDL-2031', + scriptName: '8.05% Maharashtra SDL 2031', + party: 'RBI - SGL', + subType: 'SLR', + faceValue: 2500000, + purchaseRate: 100.5, + units: 25000, + purchaseDate: '15-02-2025', + maturityDate: '15-02-2031', + interestRate: 8.05, + interestFreq: 'HALF_YEARLY', + glCode: '310020', + glName: 'INVESTMENT IN GOVT. SECURITIES', + crToAc: '310070', + brokerCode: 'BRK-002', + status: 'ACTIVE' + } +]; + +export const NON_SLR_HOLDINGS: InvestmentHolding[] = [ + { + certNo: 'NSLR-3001', + instrumentType: 'NON_SLR', + scriptCode: 'REC-7.5-2031', + scriptName: 'REC Ltd Bond 7.50% 2031', + party: 'REC Limited', + subType: 'NON SLR', + faceValue: 2000000, + purchaseRate: 100, + units: 20000, + purchaseDate: '11-08-2025', + maturityDate: '11-08-2031', + interestRate: 7.5, + interestFreq: 'YEARLY', + glCode: '310030', + glName: 'INVESTMENT IN NON SLR SECURITIES', + crToAc: '310070', + brokerCode: 'BRK-004', + status: 'ACTIVE' + }, + { + certNo: 'NSLR-3002', + instrumentType: 'NON_SLR', + scriptCode: 'NABARD-7.35-2030', + scriptName: 'NABARD Bond 7.35% 2030', + party: 'NABARD', + subType: 'NON SLR', + faceValue: 1500000, + purchaseRate: 100, + units: 15000, + purchaseDate: '20-09-2025', + maturityDate: '20-09-2030', + interestRate: 7.35, + interestFreq: 'YEARLY', + glCode: '310030', + glName: 'INVESTMENT IN NON SLR SECURITIES', + crToAc: '310070', + brokerCode: 'BRK-004', + status: 'ACTIVE' + }, + { + certNo: 'NSLR-3003', + instrumentType: 'NON_SLR', + scriptCode: 'SIDBI-7.4-2029', + scriptName: 'SIDBI Bond 7.40% 2029', + party: 'SIDBI', + subType: 'NON SLR', + faceValue: 1000000, + purchaseRate: 100, + units: 10000, + purchaseDate: '02-11-2025', + maturityDate: '02-11-2029', + interestRate: 7.4, + interestFreq: 'YEARLY', + glCode: '310030', + glName: 'INVESTMENT IN NON SLR SECURITIES', + crToAc: '310070', + brokerCode: 'BRK-003', + status: 'ACTIVE' + }, + { + certNo: 'NSLR-3004', + instrumentType: 'NON_SLR', + scriptCode: 'HUDCO-7.6-2032', + scriptName: 'HUDCO Bond 7.60% 2032', + party: 'HUDCO', + subType: 'NON SLR', + faceValue: 1800000, + purchaseRate: 100, + units: 18000, + purchaseDate: '14-12-2025', + maturityDate: '14-12-2032', + interestRate: 7.6, + interestFreq: 'YEARLY', + glCode: '310030', + glName: 'INVESTMENT IN NON SLR SECURITIES', + crToAc: '310070', + brokerCode: 'BRK-004', + status: 'ACTIVE' + }, + { + certNo: 'NSLR-3005', + instrumentType: 'NON_SLR', + scriptCode: 'PFC-7.45-2028', + scriptName: 'PFC Bond 7.45% 2028', + party: 'Power Finance Corporation', + subType: 'NON SLR', + faceValue: 1200000, + purchaseRate: 100, + units: 12000, + purchaseDate: '08-01-2026', + maturityDate: '08-01-2028', + interestRate: 7.45, + interestFreq: 'YEARLY', + glCode: '310030', + glName: 'INVESTMENT IN NON SLR SECURITIES', + crToAc: '310070', + brokerCode: 'BRK-003', + status: 'ACTIVE' + } +]; + +export const BOND_HOLDINGS: InvestmentHolding[] = [ + { + certNo: 'BOND-4001', + instrumentType: 'BOND', + scriptCode: 'LTF-8.1-2028', + scriptName: 'L&T Finance NCD 8.10% 2028', + party: 'L&T Finance Ltd', + subType: 'CORPORATE BOND', + faceValue: 1000000, + purchaseRate: 100, + units: 10000, + purchaseDate: '05-02-2026', + maturityDate: '05-02-2028', + interestRate: 8.1, + interestFreq: 'YEARLY', + glCode: '310040', + glName: 'INVESTMENT IN CORPORATE BONDS', + crToAc: '310070', + brokerCode: 'BRK-005', + status: 'ACTIVE' + }, + { + certNo: 'BOND-4002', + instrumentType: 'BOND', + scriptCode: 'BAF-7.95-2027', + scriptName: 'Bajaj Finance NCD 7.95% 2027', + party: 'Bajaj Finance Ltd', + subType: 'CORPORATE BOND', + faceValue: 1500000, + purchaseRate: 100, + units: 15000, + purchaseDate: '18-03-2026', + maturityDate: '18-03-2027', + interestRate: 7.95, + interestFreq: 'YEARLY', + glCode: '310040', + glName: 'INVESTMENT IN CORPORATE BONDS', + crToAc: '310070', + brokerCode: 'BRK-005', + status: 'ACTIVE' + }, + { + certNo: 'BOND-4003', + instrumentType: 'BOND', + scriptCode: 'TCL-8.05-2029', + scriptName: 'Tata Capital NCD 8.05% 2029', + party: 'Tata Capital Ltd', + subType: 'CORPORATE BOND', + faceValue: 1200000, + purchaseRate: 100, + units: 12000, + purchaseDate: '25-04-2026', + maturityDate: '25-04-2029', + interestRate: 8.05, + interestFreq: 'YEARLY', + glCode: '310040', + glName: 'INVESTMENT IN CORPORATE BONDS', + crToAc: '310070', + brokerCode: 'BRK-006', + status: 'ACTIVE' + }, + { + certNo: 'BOND-4004', + instrumentType: 'BOND', + scriptCode: 'MUT-8.25-2026', + scriptName: 'Muthoot Finance NCD 8.25% 2026', + party: 'Muthoot Finance Ltd', + subType: 'CORPORATE BOND', + faceValue: 800000, + purchaseRate: 100, + units: 8000, + purchaseDate: '10-08-2024', + maturityDate: '10-08-2026', + interestRate: 8.25, + interestFreq: 'YEARLY', + glCode: '310040', + glName: 'INVESTMENT IN CORPORATE BONDS', + crToAc: '310070', + brokerCode: 'BRK-006', + status: 'ACTIVE' + }, + { + certNo: 'BOND-4005', + instrumentType: 'BOND', + scriptCode: 'STF-8.15-2030', + scriptName: 'Shriram Transport NCD 8.15% 2030', + party: 'Shriram Transport Finance', + subType: 'CORPORATE BOND', + faceValue: 2000000, + purchaseRate: 100, + units: 20000, + purchaseDate: '30-05-2026', + maturityDate: '30-05-2030', + interestRate: 8.15, + interestFreq: 'YEARLY', + glCode: '310040', + glName: 'INVESTMENT IN CORPORATE BONDS', + crToAc: '310070', + brokerCode: 'BRK-005', + status: 'ACTIVE' + } +]; + +export const MUTUAL_FUND_HOLDINGS: InvestmentHolding[] = [ + { + certNo: 'MF-5001', + instrumentType: 'MUTUAL_FUND', + scriptCode: 'SBI-OVN', + scriptName: 'SBI Overnight Fund', + party: 'SBI Mutual Fund', + subType: 'DEBT - OVERNIGHT', + faceValue: 1000000, + purchaseRate: 1200.5, + units: 833.33, + purchaseDate: '12-01-2026', + maturityDate: '12-01-2027', + interestRate: 6.8, + interestFreq: 'CUMULATIVE', + glCode: '310050', + glName: 'INVESTMENT IN MUTUAL FUNDS', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + }, + { + certNo: 'MF-5002', + instrumentType: 'MUTUAL_FUND', + scriptCode: 'HDFC-LIQ', + scriptName: 'HDFC Liquid Fund', + party: 'HDFC Mutual Fund', + subType: 'DEBT - LIQUID', + faceValue: 1500000, + purchaseRate: 3800.2, + units: 394.72, + purchaseDate: '05-02-2026', + maturityDate: '05-02-2027', + interestRate: 6.9, + interestFreq: 'CUMULATIVE', + glCode: '310050', + glName: 'INVESTMENT IN MUTUAL FUNDS', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + }, + { + certNo: 'MF-5003', + instrumentType: 'MUTUAL_FUND', + scriptCode: 'ICICI-MMF', + scriptName: 'ICICI Pru Money Market Fund', + party: 'ICICI Prudential Mutual Fund', + subType: 'DEBT - MONEY MARKET', + faceValue: 800000, + purchaseRate: 350.75, + units: 2280.87, + purchaseDate: '18-03-2026', + maturityDate: '18-03-2027', + interestRate: 7.0, + interestFreq: 'CUMULATIVE', + glCode: '310050', + glName: 'INVESTMENT IN MUTUAL FUNDS', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + }, + { + certNo: 'MF-5004', + instrumentType: 'MUTUAL_FUND', + scriptCode: 'AXIS-BPSU', + scriptName: 'Axis Banking & PSU Debt Fund', + party: 'Axis Mutual Fund', + subType: 'DEBT - BANKING & PSU', + faceValue: 1200000, + purchaseRate: 22.4, + units: 53571.43, + purchaseDate: '22-04-2026', + maturityDate: '22-04-2027', + interestRate: 7.2, + interestFreq: 'CUMULATIVE', + glCode: '310050', + glName: 'INVESTMENT IN MUTUAL FUNDS', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + }, + { + certNo: 'MF-5005', + instrumentType: 'MUTUAL_FUND', + scriptCode: 'KOTAK-OVN', + scriptName: 'Kotak Overnight Fund', + party: 'Kotak Mutual Fund', + subType: 'DEBT - OVERNIGHT', + faceValue: 600000, + purchaseRate: 1150.3, + units: 521.6, + purchaseDate: '02-06-2026', + maturityDate: '02-06-2027', + interestRate: 6.7, + interestFreq: 'CUMULATIVE', + glCode: '310050', + glName: 'INVESTMENT IN MUTUAL FUNDS', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + } +]; + +export const SHARE_HOLDINGS: InvestmentHolding[] = [ + { + certNo: 'SH-6001', + instrumentType: 'SHARE', + scriptCode: 'SBIN', + scriptName: 'State Bank of India', + party: 'NSE/BSE', + subType: 'EQUITY', + faceValue: 375000, + purchaseRate: 750, + units: 500, + purchaseDate: '10-01-2026', + maturityDate: '10-01-2036', + interestRate: 2.5, + interestFreq: 'YEARLY', + glCode: '310060', + glName: 'INVESTMENT IN SHARES', + crToAc: '310070', + brokerCode: 'BRK-007', + status: 'ACTIVE' + }, + { + certNo: 'SH-6002', + instrumentType: 'SHARE', + scriptCode: 'HDFCBANK', + scriptName: 'HDFC Bank Ltd', + party: 'NSE/BSE', + subType: 'EQUITY', + faceValue: 495000, + purchaseRate: 1650, + units: 300, + purchaseDate: '15-02-2026', + maturityDate: '15-02-2036', + interestRate: 1.8, + interestFreq: 'YEARLY', + glCode: '310060', + glName: 'INVESTMENT IN SHARES', + crToAc: '310070', + brokerCode: 'BRK-007', + status: 'ACTIVE' + }, + { + certNo: 'SH-6003', + instrumentType: 'SHARE', + scriptCode: 'NCDC', + scriptName: 'National Coop Development Corp', + party: 'NCDC', + subType: 'CO-OP SHARE', + faceValue: 100000, + purchaseRate: 100, + units: 1000, + purchaseDate: '01-04-2025', + maturityDate: '01-04-2035', + interestRate: 5.0, + interestFreq: 'YEARLY', + glCode: '310060', + glName: 'INVESTMENT IN SHARES', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + }, + { + certNo: 'SH-6004', + instrumentType: 'SHARE', + scriptCode: 'DCCB', + scriptName: 'District Cooperative Bank Shares', + party: 'District Cooperative Bank', + subType: 'CO-OP SHARE', + faceValue: 100000, + purchaseRate: 50, + units: 2000, + purchaseDate: '12-05-2025', + maturityDate: '12-05-2035', + interestRate: 5.5, + interestFreq: 'YEARLY', + glCode: '310060', + glName: 'INVESTMENT IN SHARES', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + }, + { + certNo: 'SH-6005', + instrumentType: 'SHARE', + scriptCode: 'SCF', + scriptName: 'State Coop Federation Shares', + party: 'State Coop Federation', + subType: 'CO-OP SHARE', + faceValue: 90000, + purchaseRate: 60, + units: 1500, + purchaseDate: '20-06-2025', + maturityDate: '20-06-2035', + interestRate: 5.0, + interestFreq: 'YEARLY', + glCode: '310060', + glName: 'INVESTMENT IN SHARES', + crToAc: '310070', + brokerCode: '', + status: 'ACTIVE' + } +]; + +export const ALL_HOLDINGS: InvestmentHolding[] = [ + ...FD_HOLDINGS, + ...GOVT_SECURITY_HOLDINGS, + ...NON_SLR_HOLDINGS, + ...BOND_HOLDINGS, + ...MUTUAL_FUND_HOLDINGS, + ...SHARE_HOLDINGS +]; + +// -------------------- Transaction log -------------------- + +/** A Purchase txn for every holding (so every Holdings-list row has a matching ledger entry), + * plus a handful of hand-authored Interest/Sell/Write-off entries for realism. */ +function purchaseTxns(): InvestmentTxn[] { + return ALL_HOLDINGS.map((h, i) => ({ + certNo: h.certNo, + instrumentType: h.instrumentType, + txnType: 'PURCHASE' as TxnType, + voucherNo: `INV/PUR/${1000 + i}`, + txnDate: h.purchaseDate, + amount: h.faceValue, + particulars: `Purchase of ${h.scriptName} - ${h.party}` + })); +} + +export const EXTRA_TXNS: InvestmentTxn[] = [ + { + certNo: '15622', + instrumentType: 'FD', + txnType: 'REDEEM', + voucherNo: 'INV/RED/2001', + txnDate: '05-03-2026', + amount: 5000000 + Math.round(5000000 * 0.065 * 2), + particulars: 'Maturity redemption - State Cooperative Bank Ltd FD' + }, + { + certNo: '15624', + instrumentType: 'FD', + txnType: 'REDEEM', + voucherNo: 'INV/RED/2002', + txnDate: '20-02-2026', + amount: 2500000, + particulars: 'Maturity redemption (non-cumulative) - Urban Cooperative Bank Ltd FD' + }, + { + certNo: 'GSEC-2003', + instrumentType: 'GOVT_SECURITY', + txnType: 'INTEREST', + voucherNo: 'INV/INT/3001', + txnDate: '05-06-2026', + amount: Math.round(2000000 * 0.0679 * 0.5), + particulars: 'Half-yearly coupon - 6.79% GOI 2027' + }, + { + certNo: 'GSEC-2001', + instrumentType: 'GOVT_SECURITY', + txnType: 'INTEREST', + voucherNo: 'INV/INT/3002', + txnDate: '15-07-2026', + amount: Math.round(5000000 * 0.0726 * 0.5), + particulars: 'Half-yearly coupon - 7.26% GOI 2033' + }, + { + certNo: 'NSLR-3005', + instrumentType: 'NON_SLR', + txnType: 'INTEREST', + voucherNo: 'INV/INT/3003', + txnDate: '08-01-2027', + amount: Math.round(1200000 * 0.0745), + particulars: 'Annual coupon - PFC Bond 7.45% 2028' + }, + { + certNo: 'BOND-4004', + instrumentType: 'BOND', + txnType: 'INTEREST', + voucherNo: 'INV/INT/3004', + txnDate: '10-08-2025', + amount: Math.round(800000 * 0.0825), + particulars: 'Annual coupon - Muthoot Finance NCD 8.25% 2026' + }, + { + certNo: 'MF-5001', + instrumentType: 'MUTUAL_FUND', + txnType: 'SELL', + voucherNo: 'INV/SEL/4001', + txnDate: '20-06-2026', + amount: 250000, + particulars: 'Partial redemption - SBI Overnight Fund (200 units)' + }, + { + certNo: 'SH-6002', + instrumentType: 'SHARE', + txnType: 'DIVIDEND', + voucherNo: 'INV/DIV/5001', + txnDate: '25-06-2026', + amount: Math.round(300 * 19), + particulars: 'Dividend received - HDFC Bank Ltd (Rs 19/share)' + }, + { + certNo: 'SH-6001', + instrumentType: 'SHARE', + txnType: 'DIVIDEND', + voucherNo: 'INV/DIV/5002', + txnDate: '18-06-2026', + amount: Math.round(500 * 13.7), + particulars: 'Dividend received - State Bank of India (Rs 13.70/share)' + }, + { + certNo: 'BOND-4004', + instrumentType: 'BOND', + txnType: 'WRITE_OFF', + voucherNo: 'INV/WOF/6001', + txnDate: '15-07-2026', + amount: 0, + particulars: 'Reserved - no write-offs booked this year (sample entry)' + } +]; + +export const ALL_TXNS: InvestmentTxn[] = [ + ...purchaseTxns(), + ...EXTRA_TXNS +]; + +// -------------------- Masters -------------------- + +export interface ScriptMaster { + scriptCode: string; + scriptName: string; + instrumentType: InstrumentType; + isin: string; + faceValuePerUnit: number; +} + +export const SCRIPT_MASTER: ScriptMaster[] = [ + { scriptCode: 'FD-CUM', scriptName: 'Cumulative Fixed Deposit', instrumentType: 'FD', isin: '', faceValuePerUnit: 1 }, + { + scriptCode: 'FD-MIP', + scriptName: 'Monthly Interest Payout FD', + instrumentType: 'FD', + isin: '', + faceValuePerUnit: 1 + }, + { + scriptCode: 'IN0020230012', + scriptName: '7.26% GOI 2033', + instrumentType: 'GOVT_SECURITY', + isin: 'IN0020230012', + faceValuePerUnit: 100 + }, + { + scriptCode: 'IN0020220089', + scriptName: '7.10% GOI 2029', + instrumentType: 'GOVT_SECURITY', + isin: 'IN0020220089', + faceValuePerUnit: 100 + }, + { + scriptCode: 'TBILL-364D', + scriptName: '364 Days Treasury Bill', + instrumentType: 'GOVT_SECURITY', + isin: '', + faceValuePerUnit: 100 + }, + { + scriptCode: 'REC-7.5-2031', + scriptName: 'REC Ltd Bond 7.50% 2031', + instrumentType: 'NON_SLR', + isin: 'INE020B08JN0', + faceValuePerUnit: 100 + }, + { + scriptCode: 'NABARD-7.35-2030', + scriptName: 'NABARD Bond 7.35% 2030', + instrumentType: 'NON_SLR', + isin: 'INE261F09EU1', + faceValuePerUnit: 100 + }, + { + scriptCode: 'LTF-8.1-2028', + scriptName: 'L&T Finance NCD 8.10% 2028', + instrumentType: 'BOND', + isin: 'INE027E07BR5', + faceValuePerUnit: 100 + }, + { + scriptCode: 'BAF-7.95-2027', + scriptName: 'Bajaj Finance NCD 7.95% 2027', + instrumentType: 'BOND', + isin: 'INE296A07SP0', + faceValuePerUnit: 100 + }, + { + scriptCode: 'SBI-OVN', + scriptName: 'SBI Overnight Fund', + instrumentType: 'MUTUAL_FUND', + isin: 'INF200KA1Q40', + faceValuePerUnit: 1000 + }, + { + scriptCode: 'HDFC-LIQ', + scriptName: 'HDFC Liquid Fund', + instrumentType: 'MUTUAL_FUND', + isin: 'INF179KA1WR9', + faceValuePerUnit: 1000 + }, + { + scriptCode: 'SBIN', + scriptName: 'State Bank of India', + instrumentType: 'SHARE', + isin: 'INE062A01020', + faceValuePerUnit: 1 + }, + { + scriptCode: 'HDFCBANK', + scriptName: 'HDFC Bank Ltd', + instrumentType: 'SHARE', + isin: 'INE040A01034', + faceValuePerUnit: 1 + } +]; + +export interface BrokerMaster { + brokerCode: string; + brokerName: string; + contactNo: string; + empanelledOn: string; + status: 'ACTIVE' | 'INACTIVE'; +} + +export const BROKER_MASTER: BrokerMaster[] = [ + { + brokerCode: 'BRK-001', + brokerName: 'ICICI Securities Primary Dealership', + contactNo: '022-40001234', + empanelledOn: '01-04-2020', + status: 'ACTIVE' + }, + { + brokerCode: 'BRK-002', + brokerName: 'SBI-DFHI Ltd', + contactNo: '022-40005678', + empanelledOn: '01-04-2020', + status: 'ACTIVE' + }, + { + brokerCode: 'BRK-003', + brokerName: 'STCI Primary Dealer Ltd', + contactNo: '022-40009012', + empanelledOn: '15-06-2021', + status: 'ACTIVE' + }, + { + brokerCode: 'BRK-004', + brokerName: 'A.K. Capital Services Ltd', + contactNo: '022-67543210', + empanelledOn: '10-09-2021', + status: 'ACTIVE' + }, + { + brokerCode: 'BRK-005', + brokerName: 'Trust Investment Advisors', + contactNo: '022-40103200', + empanelledOn: '05-01-2022', + status: 'ACTIVE' + }, + { + brokerCode: 'BRK-006', + brokerName: 'JM Financial Services Ltd', + contactNo: '022-66303030', + empanelledOn: '12-11-2022', + status: 'ACTIVE' + }, + { + brokerCode: 'BRK-007', + brokerName: 'Zerodha Broking Ltd', + contactNo: '080-47184718', + empanelledOn: '01-01-2023', + status: 'ACTIVE' + } +]; + +export interface PartyMaster { + partyCode: string; + partyName: string; + partyType: 'BANK' | 'CORPORATE' | 'GOVT' | 'AMC' | 'EXCHANGE' | 'COOP_SOCIETY'; + panNo: string; +} + +export const PARTY_MASTER: PartyMaster[] = [ + { partyCode: 'PTY-001', partyName: 'Jila Sahakari Kendriya Bank', partyType: 'BANK', panNo: 'AABCJ1234K' }, + { partyCode: 'PTY-002', partyName: 'State Cooperative Bank Ltd', partyType: 'BANK', panNo: 'AABCS5678L' }, + { partyCode: 'PTY-003', partyName: 'RBI - SGL', partyType: 'GOVT', panNo: 'AAACR4849R' }, + { partyCode: 'PTY-004', partyName: 'REC Limited', partyType: 'CORPORATE', panNo: 'AAACR0641G' }, + { partyCode: 'PTY-005', partyName: 'NABARD', partyType: 'CORPORATE', panNo: 'AAACN0272B' }, + { partyCode: 'PTY-006', partyName: 'L&T Finance Ltd', partyType: 'CORPORATE', panNo: 'AAACL0140P' }, + { partyCode: 'PTY-007', partyName: 'SBI Mutual Fund', partyType: 'AMC', panNo: 'AAATS4325L' }, + { partyCode: 'PTY-008', partyName: 'HDFC Mutual Fund', partyType: 'AMC', panNo: 'AAATH1234M' }, + { partyCode: 'PTY-009', partyName: 'NSE/BSE', partyType: 'EXCHANGE', panNo: 'AAACN0028G' }, + { partyCode: 'PTY-010', partyName: 'District Cooperative Bank', partyType: 'COOP_SOCIETY', panNo: 'AABCD9988K' } +]; + +export interface SubTypeMaster { + subTypeCode: string; + subTypeName: string; + instrumentType: InstrumentType; +} + +export const SUBTYPE_MASTER: SubTypeMaster[] = [ + { subTypeCode: '1', subTypeName: 'SLR', instrumentType: 'GOVT_SECURITY' }, + { subTypeCode: '2', subTypeName: 'NON SLR', instrumentType: 'NON_SLR' }, + { subTypeCode: '3', subTypeName: 'CORPORATE BOND', instrumentType: 'BOND' }, + { subTypeCode: '4', subTypeName: 'EQUITY', instrumentType: 'SHARE' }, + { subTypeCode: '5', subTypeName: 'CO-OP SHARE', instrumentType: 'SHARE' }, + { subTypeCode: '6', subTypeName: 'DEBT - OVERNIGHT', instrumentType: 'MUTUAL_FUND' }, + { subTypeCode: '7', subTypeName: 'DEBT - LIQUID', instrumentType: 'MUTUAL_FUND' } +]; + +export interface ControlParameter { + paramName: string; + paramValue: string; + description: string; +} + +export const CONTROL_PARAMETERS: ControlParameter[] = [ + { + paramName: 'SLR_MIN_PERCENT', + paramValue: '18.00', + description: 'Minimum SLR holding as % of NDTL (RBI requirement)' + }, + { + paramName: 'NON_SLR_MAX_PERCENT', + paramValue: '10.00', + description: 'Maximum Non-SLR exposure as % of total investments' + }, + { + paramName: 'FD_AUTO_RENEW_DAYS', + paramValue: '7', + description: 'Days before maturity to flag FD for auto-renewal review' + }, + { + paramName: 'BROKERAGE_CEILING_BPS', + paramValue: '25', + description: 'Maximum brokerage allowed (basis points) per deal' + }, + { + paramName: 'SINGLE_PARTY_EXPOSURE_LIMIT', + paramValue: '5000000', + description: 'Maximum investment exposure to a single counterparty (Rs)' + }, + { + paramName: 'AMORTIZATION_METHOD', + paramValue: 'STRAIGHT_LINE', + description: 'Default amortization method for premium/discount on bonds' + } +]; + +export interface MutualFundProductMaster { + productCode: string; + productName: string; + amcName: string; + category: string; + riskLevel: 'LOW' | 'MODERATE' | 'HIGH'; +} + +export const MUTUAL_FUND_PRODUCTS: MutualFundProductMaster[] = [ + { + productCode: 'MFP-01', + productName: 'SBI Overnight Fund', + amcName: 'SBI Mutual Fund', + category: 'Debt - Overnight', + riskLevel: 'LOW' + }, + { + productCode: 'MFP-02', + productName: 'HDFC Liquid Fund', + amcName: 'HDFC Mutual Fund', + category: 'Debt - Liquid', + riskLevel: 'LOW' + }, + { + productCode: 'MFP-03', + productName: 'ICICI Pru Money Market Fund', + amcName: 'ICICI Prudential Mutual Fund', + category: 'Debt - Money Market', + riskLevel: 'LOW' + }, + { + productCode: 'MFP-04', + productName: 'Axis Banking & PSU Debt Fund', + amcName: 'Axis Mutual Fund', + category: 'Debt - Banking & PSU', + riskLevel: 'MODERATE' + }, + { + productCode: 'MFP-05', + productName: 'Kotak Overnight Fund', + amcName: 'Kotak Mutual Fund', + category: 'Debt - Overnight', + riskLevel: 'LOW' + } +]; + +export interface SglBankMaster { + sglCode: string; + bankName: string; + branch: string; +} + +export const SGL_BANK_MASTER: SglBankMaster[] = [ + { sglCode: 'SGL-RBI-01', bankName: 'Reserve Bank of India', branch: 'Mumbai (PDO)' }, + { sglCode: 'SGL-SBI-01', bankName: 'State Bank of India', branch: 'Fort, Mumbai' }, + { sglCode: 'SGL-HDFC-01', bankName: 'HDFC Bank Ltd', branch: 'Treasury Branch, Mumbai' }, + { sglCode: 'SGL-CCIL-01', bankName: 'Clearing Corporation of India Ltd', branch: 'Mumbai' } +]; + +export interface ScriptMarketDetail { + scriptCode: string; + marketPrice: number; + asOnDate: string; + ytm: number; +} + +export const SCRIPT_MARKET_DETAIL: ScriptMarketDetail[] = [ + { scriptCode: 'IN0020230012', marketPrice: 99.85, asOnDate: '31-07-2026', ytm: 7.31 }, + { scriptCode: 'IN0020220089', marketPrice: 100.6, asOnDate: '31-07-2026', ytm: 7.02 }, + { scriptCode: 'IN0020210056', marketPrice: 101.4, asOnDate: '31-07-2026', ytm: 6.71 }, + { scriptCode: 'IN0020240034', marketPrice: 99.1, asOnDate: '31-07-2026', ytm: 7.24 }, + { scriptCode: 'REC-7.5-2031', marketPrice: 100.35, asOnDate: '31-07-2026', ytm: 7.44 }, + { scriptCode: 'LTF-8.1-2028', marketPrice: 100.1, asOnDate: '31-07-2026', ytm: 8.08 } +]; + +export interface LienReason { + reasonCode: string; + reasonDescription: string; +} + +export const LIEN_REASON_MASTER: LienReason[] = [ + { reasonCode: 'LR-01', reasonDescription: 'Lien marked for loan collateral' }, + { reasonCode: 'LR-02', reasonDescription: 'Lien marked for court order / litigation' }, + { reasonCode: 'LR-03', reasonDescription: 'Lien marked for SGL/CSGL margin requirement' }, + { reasonCode: 'LR-04', reasonDescription: 'Lien marked for guarantee issued' } +]; + +/** Daily Script Master (legacy menu "09") — the day's quoted rate per script, entered/imported + * every working day. Distinct from Script Market Detail ("13") which also carries YTM/valuation. */ +export interface DailyScriptRate { + rateDate: string; + scriptCode: string; + scriptName: string; + openRate: number; + closeRate: number; + highRate: number; + lowRate: number; +} + +export const DAILY_SCRIPT_RATES: DailyScriptRate[] = [ + { + rateDate: '31-07-2026', + scriptCode: 'IN0020230012', + scriptName: '7.26% GOI 2033', + openRate: 99.7, + closeRate: 99.85, + highRate: 99.95, + lowRate: 99.6 + }, + { + rateDate: '31-07-2026', + scriptCode: 'IN0020220089', + scriptName: '7.10% GOI 2029', + openRate: 100.5, + closeRate: 100.6, + highRate: 100.7, + lowRate: 100.45 + }, + { + rateDate: '31-07-2026', + scriptCode: 'IN0020210056', + scriptName: '6.79% GOI 2027', + openRate: 101.3, + closeRate: 101.4, + highRate: 101.5, + lowRate: 101.25 + }, + { + rateDate: '31-07-2026', + scriptCode: 'SBIN', + scriptName: 'State Bank of India', + openRate: 790, + closeRate: 802.5, + highRate: 808, + lowRate: 787 + }, + { + rateDate: '31-07-2026', + scriptCode: 'HDFCBANK', + scriptName: 'HDFC Bank Ltd', + openRate: 1685, + closeRate: 1702.4, + highRate: 1710, + lowRate: 1680 + }, + { + rateDate: '30-07-2026', + scriptCode: 'IN0020230012', + scriptName: '7.26% GOI 2033', + openRate: 99.55, + closeRate: 99.7, + highRate: 99.8, + lowRate: 99.5 + }, + { + rateDate: '30-07-2026', + scriptCode: 'SBIN', + scriptName: 'State Bank of India', + openRate: 782, + closeRate: 790, + highRate: 794, + lowRate: 780 + } +]; + +// -------------------- Dropdown option lists (legacy entry-form selects) -------------------- + +/** RBI investment classification categories — drive HTM/AFS/HFT valuation treatment. */ +export const CATEGORY_OPTIONS = [ + 'HTM (Held To Maturity)', + 'AFS (Available For Sale)', + 'HFT (Held For Trading)' +]; + +export const DEAL_TYPE_OPTIONS = [ + 'OUTRIGHT', + 'REPO', + 'REVERSE REPO', + 'CBLO / TREPS', + 'SWITCH' +]; + +export const MARKET_OPTIONS = [ + 'NDS-OM', + 'NDS-CALL', + 'OTC', + 'BSE', + 'NSE', + 'PRIMARY AUCTION' +]; + +export const PAYMENT_MODE_OPTIONS = [ + 'RTGS', + 'NEFT', + 'CHEQUE', + 'DD / PAY ORDER', + 'INTERNAL TRANSFER' +]; + +export const DELIVERY_MODE_OPTIONS = [ + 'SGL', + 'CSGL', + 'DEMAT', + 'PHYSICAL', + 'CONSTITUENT SGL' +]; + +export const BOND_RATING_OPTIONS = [ + 'AAA', + 'AA+', + 'AA', + 'AA-', + 'A+', + 'A', + 'A-', + 'BBB+', + 'BBB', + 'SOVEREIGN' +]; + +export const INT_TYPE_OPTIONS: Array<{ value: InterestFreq; label: string }> = [ + { value: 'MONTHLY', label: 'MONTHLY' }, + { value: 'QUARTERLY', label: 'QUARTERLY' }, + { value: 'HALF_YEARLY', label: 'HALF YEARLY' }, + { value: 'YEARLY', label: 'YEARLY' }, + { value: 'CUMULATIVE', label: 'CUMULATIVE' } +]; + +export const AUTHORITY_OPTIONS = [ + 'Board of Directors', + 'Investment Committee', + 'Chief Executive Officer', + 'General Manager - Treasury', + 'Asset Liability Committee (ALCO)' +]; + +export const FD_TYPE_OPTIONS = [ + 'CUMMULATIVE', + 'NON-CUMMULATIVE', + 'RECURRING', + 'CALL DEPOSIT' +]; + +/** Approximate YTM for a bond bought at `purchaseRate` per 100 face — coupon adjusted for + * the premium/discount amortised over the remaining life. Used to fill the read-only YTM + * field on the Bond entry form. */ +export function approxYtm(couponRate: number, purchaseRate: number, yearsToMaturity: number): number { + if (!purchaseRate || !yearsToMaturity) { + return couponRate; + } + const annualAmortisation = (100 - purchaseRate) / yearsToMaturity; + return +(((couponRate + annualAmortisation) / purchaseRate) * 100).toFixed(2); +} diff --git a/src/app/investment/investment-interest/investment-interest.component.html b/src/app/investment/investment-interest/investment-interest.component.html new file mode 100644 index 000000000..94a3060bf --- /dev/null +++ b/src/app/investment/investment-interest/investment-interest.component.html @@ -0,0 +1,352 @@ + + +
+
+
+
TREASURY · INVESTMENT · INTEREST
+

Investment Interest

+

+ FD and Govt./Bond interest calculation batch runs, plus the Government Bond Interest, FD Interest and FD + registers. +

+
+
+ +
+ + + + + +
+ + + @if (activeTab === 'fdCalc') { +
+ +
+ + + + +
+
+ +
+ + + + + + + + + + + + + @for (h of fdCalcTargets; track h.certNo) { + + + + + + + + + } @empty { + + + + } + + + + + + + +
Cert NoPartyFace ValueRateFrequencyInterest
+ {{ h.certNo }} + {{ h.party }}₹{{ h.faceValue | number: '1.2-2' }}{{ h.interestRate }}%{{ h.interestFreq }}₹{{ fdInterest(h) | number: '1.2-2' }}
No FD certificates match the filter.
Total₹{{ fdCalcTotal | number: '1.2-2' }}
+
+ } + + + @if (activeTab === 'bondCalc') { +
+ +
+ + + + + + +
+
+ +
+ + + + + + + + + + + + + + @for (h of bondCalcTargets; track h.certNo) { + + + + + + + + + + } @empty { + + + + } + + + + + + + +
Cert NoScriptTypeFace ValueRateFrequencyInterest
+ {{ h.certNo }} + {{ h.scriptName }}{{ instrumentLabel(h.instrumentType) }}₹{{ h.faceValue | number: '1.2-2' }}{{ h.interestRate }}%{{ h.interestFreq }}₹{{ bondInterest(h) | number: '1.2-2' }}
No holdings match the filter.
Total₹{{ bondCalcTotal | number: '1.2-2' }}
+
+ } + + + @if (activeTab === 'bondIntRegister') { +
+ + + + + + + + + + + + + + @for (t of bondIntRegister; track t.voucherNo) { + + + + + + + + + } @empty { + + + + } + + + + + + + +
DateVoucher NoCert NoInstrumentParticularsInterest Amount
{{ t.txnDate }} + {{ t.voucherNo }} + {{ t.certNo }}{{ instrumentLabel(t.instrumentType) }}{{ t.particulars }}₹{{ t.amount | number: '1.2-2' }}
No bond interest entries recorded yet.
Total₹{{ bondIntRegisterTotal | number: '1.2-2' }}
+
+ } + + + @if (activeTab === 'fdIntRegister') { +
+ + + + + + + + + + + + + @for (t of fdIntRegister; track t.voucherNo) { + + + + + + + + } @empty { + + + + } + + + + + + + +
DateVoucher NoCert NoParticularsInterest Amount
{{ t.txnDate }} + {{ t.voucherNo }} + {{ t.certNo }}{{ t.particulars }}₹{{ t.amount | number: '1.2-2' }}
No FD interest entries recorded yet.
Total₹{{ fdIntRegisterTotal | number: '1.2-2' }}
+
+ } + + + @if (activeTab === 'fdRegister') { +
+ + + + + + + + + + + + + + + + + + + + @for (h of fdRegister; track h.certNo) { + + + + + + + + + + + + + + + } + + + + + + + + + + + +
Cert NoPartySub TypeFace ValueRateFrequencyDeposit DateMaturity DateDays LeftAccrued InterestMaturity ValueStatus
+ {{ h.certNo }} + {{ h.party }}{{ h.subType }}₹{{ h.faceValue | number: '1.2-2' }}{{ h.interestRate }}%{{ h.interestFreq }}{{ h.purchaseDate }}{{ h.maturityDate }}{{ daysLeft(h) }}₹{{ accrued(h) | number: '1.2-2' }}₹{{ maturity(h) | number: '1.2-2' }} + {{ h.status }} +
Total₹{{ fdRegisterFaceTotal | number: '1.2-2' }}₹{{ fdRegisterAccruedTotal | number: '1.2-2' }}₹{{ fdRegisterMaturityTotal | number: '1.2-2' }}
+
+ } +
+ +
{{ toastMsg }}
diff --git a/src/app/investment/investment-interest/investment-interest.component.ts b/src/app/investment/investment-interest/investment-interest.component.ts new file mode 100644 index 000000000..8910706cb --- /dev/null +++ b/src/app/investment/investment-interest/investment-interest.component.ts @@ -0,0 +1,247 @@ +/** + * 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 { + FD_HOLDINGS, + GOVT_SECURITY_HOLDINGS, + NON_SLR_HOLDINGS, + BOND_HOLDINGS, + ALL_TXNS, + SCRIPT_MASTER, + InvestmentHolding, + InvestmentTxn, + InstrumentType, + INSTRUMENT_LABELS, + accruedInterest, + daysToMaturity, + maturityAmount, + todayDDMMYYYY +} from '../investment-data'; + +type InterestTab = 'fdCalc' | 'bondCalc' | 'bondIntRegister' | 'fdIntRegister' | 'fdRegister'; + +/** + * The legacy "Interest" top-menu for Investment — 5 screens: + * 01 FD Interest Calculation, 02 Govt./Bond Int. Calculation (both batch "Process" runs), + * 03 Government Bond Int Register, 04 FD Interest Register, 05 FD Register (listings). + * + * Interest figures are derived from the same holdings used by the Entry screens and Reports, + * so a register here always agrees with what a Purchase/Interest entry recorded. + */ +@Component({ + selector: 'mifosx-investment-interest', + templateUrl: './investment-interest.component.html', + styleUrls: [ + '../../cashier/cashier-shared.scss', + '../investment-shared.scss' + ], + standalone: true, + imports: [ + CommonModule, + FormsModule + ] +}) +export class InvestmentInterestComponent { + activeTab: InterestTab = 'fdCalc'; + asOnDate = todayDDMMYYYY(); + + fdHoldings: InvestmentHolding[] = [...FD_HOLDINGS]; + bondHoldings: InvestmentHolding[] = [ + ...GOVT_SECURITY_HOLDINGS, + ...NON_SLR_HOLDINGS, + ...BOND_HOLDINGS + ]; + txns: InvestmentTxn[] = [...ALL_TXNS]; + + readonly scriptOptions = SCRIPT_MASTER; + + toastMsg = ''; + toastVisible = false; + private toastTimer: ReturnType | undefined; + + // 01 FD Interest Calculation + fdCalcDate = this.asOnDate; + fdCalcCertNo = ''; + fdCalcAll = false; + + // 02 Govt./Bond Int. Calculation + bondCalcDate = this.asOnDate; + bondCalcType: InstrumentType | '' = ''; + bondCalcScript = ''; + bondCalcDealNo = ''; + bondCalcAll = false; + + // Register filters + registerFromDate = ''; + registerToDate = ''; + + setTab(tab: InterestTab): void { + this.activeTab = tab; + } + + instrumentLabel(type: InstrumentType): string { + return INSTRUMENT_LABELS[type]; + } + + statusClass(h: InvestmentHolding): string { + return `inv-badge inv-badge--${h.status.toLowerCase()}`; + } + + maturity(h: InvestmentHolding): number { + return maturityAmount(h); + } + + daysLeft(h: InvestmentHolding): number { + return daysToMaturity(h, this.asOnDate); + } + + // -------------------- 01 FD Interest Calculation -------------------- + get fdCalcTargets(): InvestmentHolding[] { + if (this.fdCalcAll) { + return this.fdHoldings.filter((h) => h.status === 'ACTIVE'); + } + return this.fdHoldings.filter( + (h) => h.status === 'ACTIVE' && (!this.fdCalcCertNo || h.certNo === this.fdCalcCertNo) + ); + } + + fdInterest(h: InvestmentHolding): number { + return accruedInterest(h, this.fdCalcDate || this.asOnDate); + } + + get fdCalcTotal(): number { + return this.fdCalcTargets.reduce((s, h) => s + this.fdInterest(h), 0); + } + + processFdInterest(): void { + const targets = this.fdCalcTargets; + if (targets.length === 0) { + this.toast('No FD holdings match the selected filters.'); + return; + } + const newTxns: InvestmentTxn[] = targets.map((h, i) => ({ + certNo: h.certNo, + instrumentType: 'FD', + txnType: 'INTEREST', + voucherNo: `INV/FD-INT/${(this.fdCalcDate || this.asOnDate).replace(/-/g, '')}-${i + 1}`, + txnDate: this.fdCalcDate || this.asOnDate, + amount: this.fdInterest(h), + particulars: `FD interest calculated - ${h.party}` + })); + this.txns = [ + ...newTxns, + ...this.txns + ]; + this.toast( + `FD interest processed for ${targets.length} certificate(s). Total ₹${this.fdCalcTotal.toLocaleString('en-IN')}.` + ); + } + + // -------------------- 02 Govt./Bond Int. Calculation -------------------- + get bondCalcTargets(): InvestmentHolding[] { + if (this.bondCalcAll) { + return this.bondHoldings.filter((h) => h.status === 'ACTIVE'); + } + return this.bondHoldings.filter((h) => { + if (h.status !== 'ACTIVE') return false; + if (this.bondCalcType && h.instrumentType !== this.bondCalcType) return false; + if (this.bondCalcScript && h.scriptCode !== this.bondCalcScript) return false; + if (this.bondCalcDealNo && h.certNo !== this.bondCalcDealNo.trim()) return false; + return true; + }); + } + + bondInterest(h: InvestmentHolding): number { + return accruedInterest(h, this.bondCalcDate || this.asOnDate); + } + + get bondCalcTotal(): number { + return this.bondCalcTargets.reduce((s, h) => s + this.bondInterest(h), 0); + } + + processBondInterest(): void { + const targets = this.bondCalcTargets; + if (targets.length === 0) { + this.toast('No Govt/Bond holdings match the selected filters.'); + return; + } + const newTxns: InvestmentTxn[] = targets.map((h, i) => ({ + certNo: h.certNo, + instrumentType: h.instrumentType, + txnType: 'INTEREST', + voucherNo: `INV/BOND-INT/${(this.bondCalcDate || this.asOnDate).replace(/-/g, '')}-${i + 1}`, + txnDate: this.bondCalcDate || this.asOnDate, + amount: this.bondInterest(h), + particulars: `Coupon/interest calculated - ${h.scriptName}` + })); + this.txns = [ + ...newTxns, + ...this.txns + ]; + this.toast( + `Interest processed for ${targets.length} holding(s). Total ₹${this.bondCalcTotal.toLocaleString('en-IN')}.` + ); + } + + // -------------------- 03 Government Bond Int Register -------------------- + get bondIntRegister(): InvestmentTxn[] { + return this.txns.filter( + (t) => t.txnType === 'INTEREST' && ([ + 'GOVT_SECURITY', + 'NON_SLR', + 'BOND' + ] as InstrumentType[]).includes(t.instrumentType) + ); + } + + get bondIntRegisterTotal(): number { + return this.bondIntRegister.reduce((s, t) => s + t.amount, 0); + } + + // -------------------- 04 FD Interest Register -------------------- + get fdIntRegister(): InvestmentTxn[] { + return this.txns.filter((t) => t.txnType === 'INTEREST' && t.instrumentType === 'FD'); + } + + get fdIntRegisterTotal(): number { + return this.fdIntRegister.reduce((s, t) => s + t.amount, 0); + } + + // -------------------- 05 FD Register -------------------- + get fdRegister(): InvestmentHolding[] { + return this.fdHoldings; + } + + get fdRegisterFaceTotal(): number { + return this.fdRegister.reduce((s, h) => s + h.faceValue, 0); + } + + get fdRegisterMaturityTotal(): number { + return this.fdRegister.reduce((s, h) => s + this.maturity(h), 0); + } + + accrued(h: InvestmentHolding): number { + return accruedInterest(h, this.asOnDate); + } + + get fdRegisterAccruedTotal(): number { + return this.fdRegister.reduce((s, h) => s + this.accrued(h), 0); + } + + private toast(msg: string): void { + this.toastMsg = msg; + this.toastVisible = true; + if (this.toastTimer) { + clearTimeout(this.toastTimer); + } + this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200); + } +} diff --git a/src/app/investment/investment-masters/investment-masters.component.html b/src/app/investment/investment-masters/investment-masters.component.html new file mode 100644 index 000000000..d44cbf788 --- /dev/null +++ b/src/app/investment/investment-masters/investment-masters.component.html @@ -0,0 +1,557 @@ + + +
+
+
+
TREASURY · INVESTMENT · MASTERS
+

Investment Masters

+

+ Bank/Party, Script, SubType, Broker, Control Parameter, LIEN Reason, Mutual Fund Product, SGL Bank and Script + Market Detail masters. +

+
+
+ +
+ + + + + + + + + + +
+ + + @if (activeTab === 'party') { +
+ +
+ + + +
+
+ +
+ + + + + + + + + + + @for (p of parties; track p.partyCode) { + + + + + + + } + +
Party CodeParty NameTypePAN No
+ {{ p.partyCode }} + {{ p.partyName }}{{ p.partyType }}{{ p.panNo }}
+
+ } + + + @if (activeTab === 'script') { +
+ +
+ + + + + +
+
+ +
+ + + + + + + + + + + + @for (s of scripts; track s.scriptCode) { + + + + + + + + } + +
Script CodeScript NameInstrumentISINFace Value/Unit
+ {{ s.scriptCode }} + {{ s.scriptName }}{{ s.instrumentType }}{{ s.isin || '—' }}{{ s.faceValuePerUnit }}
+
+ } + + + @if (activeTab === 'subtype') { +
+ +
+ + +
+
+ +
+ + + + + + + + + + @for (s of subTypes; track s.subTypeCode + s.subTypeName) { + + + + + + } + +
CodeSub TypeInstrument
+ {{ s.subTypeCode }} + {{ s.subTypeName }}{{ s.instrumentType }}
+
+ } + + + @if (activeTab === 'broker') { +
+ +
+ + + +
+
+ +
+ + + + + + + + + + + + @for (b of brokers; track b.brokerCode) { + + + + + + + + } + +
Broker CodeBroker NameContactEmpanelled OnStatus
+ {{ b.brokerCode }} + {{ b.brokerName }}{{ b.contactNo }}{{ b.empanelledOn }} + {{ b.status }} +
+
+ } + + + @if (activeTab === 'control') { +
+ +
+ + + +
+
+ +
+ + + + + + + + + + @for (c of controlParams; track c.paramName) { + + + + + + } + +
ParameterValueDescription
+ {{ c.paramName }} + {{ c.paramValue }}{{ c.description }}
+
+ } + + + @if (activeTab === 'dailyscript') { +
+ +
+ + + + + + +
+
+ +
+ + + + + + + + + + + + + + @for (r of dailyScriptRates; track r.rateDate + r.scriptCode) { + + + + + + + + + + } + +
Rate DateScript CodeScript NameOpenHighLowClose
{{ r.rateDate }} + {{ r.scriptCode }} + {{ r.scriptName }}{{ r.openRate | number: '1.2-2' }}{{ r.highRate | number: '1.2-2' }}{{ r.lowRate | number: '1.2-2' }}{{ r.closeRate | number: '1.2-2' }}
+
+ } + + + @if (activeTab === 'lien') { +
+ +
+ +
+
+ +
+ + + + + + + + + @for (l of lienReasons; track l.reasonCode) { + + + + + } + +
Reason CodeDescription
+ {{ l.reasonCode }} + {{ l.reasonDescription }}
+
+ } + + + @if (activeTab === 'mfproduct') { +
+ +
+ + + + +
+
+ +
+ + + + + + + + + + + + @for (m of mfProducts; track m.productCode) { + + + + + + + + } + +
Product CodeProduct NameAMCCategoryRisk
+ {{ m.productCode }} + {{ m.productName }}{{ m.amcName }}{{ m.category }}{{ m.riskLevel }}
+
+ } + + + @if (activeTab === 'sglbank') { +
+ +
+ + +
+
+ +
+ + + + + + + + + + @for (s of sglBanks; track s.sglCode) { + + + + + + } + +
SGL CodeBank NameBranch
+ {{ s.sglCode }} + {{ s.bankName }}{{ s.branch }}
+
+ } + + + @if (activeTab === 'marketdetail') { +
+ + + + + + + + + + + + @for (m of marketDetails; track m.scriptCode) { + + + + + + + } + +
Script CodeMarket PriceAs OnYTM (%)
+ {{ m.scriptCode }} + ₹{{ m.marketPrice | number: '1.2-2' }}{{ m.asOnDate }}{{ m.ytm }}%
+
+ } +
+ +
{{ toastMsg }}
diff --git a/src/app/investment/investment-masters/investment-masters.component.ts b/src/app/investment/investment-masters/investment-masters.component.ts new file mode 100644 index 000000000..de324cc4d --- /dev/null +++ b/src/app/investment/investment-masters/investment-masters.component.ts @@ -0,0 +1,262 @@ +/** + * 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 { + SCRIPT_MASTER, + BROKER_MASTER, + PARTY_MASTER, + SUBTYPE_MASTER, + CONTROL_PARAMETERS, + MUTUAL_FUND_PRODUCTS, + SGL_BANK_MASTER, + SCRIPT_MARKET_DETAIL, + DAILY_SCRIPT_RATES, + LIEN_REASON_MASTER, + ScriptMaster, + BrokerMaster, + PartyMaster, + SubTypeMaster, + ControlParameter, + MutualFundProductMaster, + SglBankMaster, + ScriptMarketDetail, + DailyScriptRate, + LienReason, + InstrumentType +} from '../investment-data'; + +type MasterTab = + | 'party' + | 'script' + | 'subtype' + | 'broker' + | 'control' + | 'dailyscript' + | 'lien' + | 'mfproduct' + | 'sglbank' + | 'marketdetail'; + +/** + * Investment master-data screens (legacy "04-13" Entry submenu masters) — 9 simple dummy + * lists, each with an inline Add-row form so the demo stays interactive without a backend. + */ +@Component({ + selector: 'mifosx-investment-masters', + templateUrl: './investment-masters.component.html', + styleUrls: [ + '../../cashier/cashier-shared.scss', + '../investment-shared.scss' + ], + standalone: true, + imports: [ + CommonModule, + FormsModule + ] +}) +export class InvestmentMastersComponent { + activeTab: MasterTab = 'party'; + + scripts: ScriptMaster[] = [...SCRIPT_MASTER]; + brokers: BrokerMaster[] = [...BROKER_MASTER]; + parties: PartyMaster[] = [...PARTY_MASTER]; + subTypes: SubTypeMaster[] = [...SUBTYPE_MASTER]; + controlParams: ControlParameter[] = [...CONTROL_PARAMETERS]; + mfProducts: MutualFundProductMaster[] = [...MUTUAL_FUND_PRODUCTS]; + sglBanks: SglBankMaster[] = [...SGL_BANK_MASTER]; + marketDetails: ScriptMarketDetail[] = [...SCRIPT_MARKET_DETAIL]; + dailyScriptRates: DailyScriptRate[] = [...DAILY_SCRIPT_RATES]; + lienReasons: LienReason[] = [...LIEN_REASON_MASTER]; + + readonly instrumentTypes: InstrumentType[] = [ + 'FD', + 'GOVT_SECURITY', + 'NON_SLR', + 'BOND', + 'MUTUAL_FUND', + 'SHARE' + ]; + + toastMsg = ''; + toastVisible = false; + private toastTimer: ReturnType | undefined; + + newParty: Partial = {}; + newScript: Partial = {}; + newSubType: Partial = {}; + newBroker: Partial = {}; + newControlParam: Partial = {}; + newLienReason: Partial = {}; + newMfProduct: Partial = {}; + newSglBank: Partial = {}; + newDailyRate: Partial = {}; + + setTab(tab: MasterTab): void { + this.activeTab = tab; + } + + addParty(): void { + if (!this.newParty.partyName || !this.newParty.partyType) { + this.toast('Enter Party Name and Type.'); + return; + } + this.parties = [ + { + partyCode: `PTY-${String(this.parties.length + 1).padStart(3, '0')}`, + panNo: '', + ...this.newParty + } as PartyMaster, + ...this.parties + ]; + this.newParty = {}; + this.toast('Party/Bank master added.'); + } + + addScript(): void { + if (!this.newScript.scriptName || !this.newScript.instrumentType) { + this.toast('Enter Script Name and Instrument Type.'); + return; + } + this.scripts = [ + { + scriptCode: this.newScript.scriptCode || `SCR-${this.scripts.length + 1}`, + isin: '', + faceValuePerUnit: 100, + ...this.newScript + } as ScriptMaster, + ...this.scripts + ]; + this.newScript = {}; + this.toast('Script master added.'); + } + + addSubType(): void { + if (!this.newSubType.subTypeName || !this.newSubType.instrumentType) { + this.toast('Enter Sub Type Name and Instrument Type.'); + return; + } + this.subTypes = [ + { subTypeCode: String(this.subTypes.length + 1), ...this.newSubType } as SubTypeMaster, + ...this.subTypes + ]; + this.newSubType = {}; + this.toast('Sub Type master added.'); + } + + addBroker(): void { + if (!this.newBroker.brokerName) { + this.toast('Enter Broker Name.'); + return; + } + this.brokers = [ + { + brokerCode: `BRK-${String(this.brokers.length + 1).padStart(3, '0')}`, + contactNo: '', + empanelledOn: '', + status: 'ACTIVE', + ...this.newBroker + } as BrokerMaster, + ...this.brokers + ]; + this.newBroker = {}; + this.toast('Broker master added.'); + } + + addControlParam(): void { + if (!this.newControlParam.paramName || !this.newControlParam.paramValue) { + this.toast('Enter Parameter Name and Value.'); + return; + } + this.controlParams = [ + { description: '', ...this.newControlParam } as ControlParameter, + ...this.controlParams + ]; + this.newControlParam = {}; + this.toast('Control parameter added.'); + } + + addLienReason(): void { + if (!this.newLienReason.reasonDescription) { + this.toast('Enter Reason Description.'); + return; + } + this.lienReasons = [ + { reasonCode: `LR-${String(this.lienReasons.length + 1).padStart(2, '0')}`, ...this.newLienReason } as LienReason, + ...this.lienReasons + ]; + this.newLienReason = {}; + this.toast('LIEN reason added.'); + } + + addMfProduct(): void { + if (!this.newMfProduct.productName || !this.newMfProduct.amcName) { + this.toast('Enter Product Name and AMC Name.'); + return; + } + this.mfProducts = [ + { + productCode: `MFP-${String(this.mfProducts.length + 1).padStart(2, '0')}`, + category: '', + riskLevel: 'MODERATE', + ...this.newMfProduct + } as MutualFundProductMaster, + ...this.mfProducts + ]; + this.newMfProduct = {}; + this.toast('Mutual Fund product added.'); + } + + addSglBank(): void { + if (!this.newSglBank.bankName) { + this.toast('Enter Bank Name.'); + return; + } + this.sglBanks = [ + { + sglCode: `SGL-${String(this.sglBanks.length + 1).padStart(2, '0')}`, + branch: '', + ...this.newSglBank + } as SglBankMaster, + ...this.sglBanks + ]; + this.newSglBank = {}; + this.toast('SGL Bank master added.'); + } + + addDailyRate(): void { + if (!this.newDailyRate.scriptCode || !this.newDailyRate.rateDate || this.newDailyRate.closeRate == null) { + this.toast('Enter Rate Date, Script Code and Close Rate.'); + return; + } + const script = this.scripts.find((s) => s.scriptCode === this.newDailyRate.scriptCode); + this.dailyScriptRates = [ + { + scriptName: script?.scriptName || '', + openRate: this.newDailyRate.closeRate, + highRate: this.newDailyRate.closeRate, + lowRate: this.newDailyRate.closeRate, + ...this.newDailyRate + } as DailyScriptRate, + ...this.dailyScriptRates + ]; + this.newDailyRate = {}; + this.toast('Daily script rate added.'); + } + + private toast(msg: string): void { + this.toastMsg = msg; + this.toastVisible = true; + if (this.toastTimer) { + clearTimeout(this.toastTimer); + } + this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200); + } +} diff --git a/src/app/investment/investment-reports/investment-reports.component.html b/src/app/investment/investment-reports/investment-reports.component.html new file mode 100644 index 000000000..cae27e4a5 --- /dev/null +++ b/src/app/investment/investment-reports/investment-reports.component.html @@ -0,0 +1,694 @@ + + +
+
+
+
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. +

+
+
+ +
+ + + + +
+ + + @if (activeGroup === 'balance') { +
+ + + + +
+ + @if (activeReport === 'govtSecBalance' || activeReport === 'shareBalance' || activeReport === 'fdrBalance') { +
+ + + + + + + + + + + + + + @for ( + h of activeReport === 'govtSecBalance' + ? govtSecHoldings + : activeReport === 'shareBalance' + ? shareHoldings + : fdHoldings; + track h.certNo + ) { + + + + + + + + + } + +
Cert NoScript / PartyUnitsFace ValueMarket/Purchase RateStatus
+ {{ h.certNo }} + {{ h.scriptName }} — {{ h.party }}{{ h.units | number }}₹{{ h.faceValue | number: '1.2-2' }}{{ marketPrice(h.scriptCode) || h.purchaseRate }} + {{ h.status }} +
+
+ } + @if (activeReport === 'bankInvList') { +
+ + + + + + + + + + + @for (r of bankInvestmentList; track r.party) { + + + + + + } + +
Party / BankNo. of HoldingsTotal Face Value
{{ r.party }}{{ r.count }}₹{{ r.totalFaceValue | number: '1.2-2' }}
+
+ } + } + + + @if (activeGroup === 'ledger') { +
+ + + + + + + + +
+ + @if ( + activeReport === 'fdLedger' || + activeReport === 'bondLedger' || + activeReport === 'shareLedger' || + activeReport === 'amortizeLedger' || + activeReport === 'dealSlip' + ) { +
+ + + + + + + + + + + + + + @for ( + t of activeReport === 'fdLedger' + ? fdTxns + : activeReport === 'bondLedger' + ? bondTxns + : activeReport === 'shareLedger' + ? shareTxns + : activeReport === 'amortizeLedger' + ? amortizeTxns + : dealSlips; + track t.voucherNo + ) { + + + + + + + + + } @empty { + + + + } + +
DateVoucher NoCert NoTypeParticularsAmount
{{ t.txnDate }} + {{ t.voucherNo }} + {{ t.certNo }}{{ t.txnType }}{{ t.particulars }}₹{{ t.amount | number: '1.2-2' }}
No entries.
+
+ } + @if (activeReport === 'intDueRegister') { +
+ + + + + + + + + + + + + @for (h of interestDueRegister; track h.certNo) { + + + + + + + + } + +
Cert NoScript / PartyFrequencyRateDue Amount (as on {{ asOnDate }})
+ {{ h.certNo }} + {{ h.scriptName }} — {{ h.party }}{{ h.interestFreq }}{{ h.interestRate }}%₹{{ h.dueAmount | number: '1.2-2' }}
+
+ } + @if (activeReport === 'slrRegister') { +
+ + + + + + + + + + + + + @for (h of slrBondRegister; track h.certNo) { + + + + + + + + } + +
Cert NoScriptFace ValueRateMaturity Date
+ {{ h.certNo }} + {{ h.scriptName }}₹{{ h.faceValue | number: '1.2-2' }}{{ h.interestRate }}%{{ h.maturityDate }}
+
+ } + @if (activeReport === 'pendingReceived') { +
+ + + + + + + + + + + + @for (t of receivedInterestTxns; track t.voucherNo) { + + + + + + + } + +
DateCert NoParticularsAmount
{{ t.txnDate }}{{ t.certNo }}{{ t.particulars }}₹{{ t.amount | number: '1.2-2' }}
+
+
+ + + + + + + + + + + @for (h of pendingInterestHoldings; track h.certNo) { + + + + + + } + +
Cert NoScript / PartyAccrued (Pending)
+ {{ h.certNo }} + {{ h.scriptName }} — {{ h.party }}₹{{ accrued(h) | number: '1.2-2' }}
+
+ } + } + + + @if (activeGroup === 'maturity') { +
+ + + + + + + + +
+ + @if (activeReport === 'maturedFd') { +
+ + + + + + + + + + + + @for (h of maturedFdList; track h.certNo) { + + + + + + + } @empty { + + + + } + +
Cert NoPartyFace ValueMatured On
+ {{ h.certNo }} + {{ h.party }}₹{{ h.faceValue | number: '1.2-2' }}{{ h.maturityDate }}
No matured FDs.
+
+ } + @if (activeReport === 'maturingList') { +
+ + + + + + + + + + + + + + @for (h of maturingList; track h.certNo) { + + + + + + + + + } + +
Cert NoInstrumentScript / PartyMaturity DateDays LeftMaturity Value
+ {{ h.certNo }} + {{ instrumentLabel(h.instrumentType) }}{{ h.scriptName }} — {{ h.party }}{{ h.maturityDate }}{{ daysLeft(h) }}₹{{ maturity(h) | number: '1.2-2' }}
+
+ } + @if (activeReport === 'maturityPattern') { +
+ + + + + + + + + + + @for (b of maturityBuckets; track b.label) { + + + + + + } + +
BucketHoldingsFace Value
{{ b.label }}{{ b.count }}₹{{ b.faceValue | number: '1.2-2' }}
+
+ } + @if ( + activeReport === 'govtSecList' || + activeReport === 'fdSecList' || + activeReport === 'shareSecList' || + activeReport === 'mfList' + ) { +
+ + + + + + + + + + + + + + @for ( + h of activeReport === 'govtSecList' + ? govtSecList + : activeReport === 'fdSecList' + ? fdSecList + : activeReport === 'shareSecList' + ? shareSecList + : mutualFundList; + track h.certNo + ) { + + + + + + + + + } + +
Cert NoScriptPartyFace ValueRateStatus
+ {{ h.certNo }} + {{ h.scriptName }}{{ h.party }}₹{{ h.faceValue | number: '1.2-2' }}{{ h.interestRate }}% + {{ h.status }} +
+
+ } + @if (activeReport === 'shiftingReport') { +
+ + + + + + + + + + + + + @for (h of shiftingCandidates; track h.certNo) { + + + + + + + + } @empty { + + + + } + +
Cert NoScriptPurchase RateMarket PriceDeviation %
+ {{ h.certNo }} + {{ h.scriptName }}{{ h.purchaseRate }}{{ h.marketPrice }} + {{ h.deviationPct }}% +
No securities crossing the 1% deviation threshold today.
+
+ } + } + + + @if (activeGroup === 'portfolio') { +
+ + + + + + + + + + + + + @for (r of portfolioSummary; track r.label) { + + + + + + + + } + + + + + + + + + + +
Instrument TypeHoldingsFace ValueAccrued InterestMaturity Value
{{ r.label }}{{ r.count }}₹{{ r.faceValue | number: '1.2-2' }}₹{{ r.accrued | number: '1.2-2' }}₹{{ r.maturityValue | number: '1.2-2' }}
Total₹{{ 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 new file mode 100644 index 000000000..551a5423a --- /dev/null +++ b/src/app/investment/investment-reports/investment-reports.component.ts @@ -0,0 +1,247 @@ +/** + * 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 { + 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, + todayDDMMYYYY +} from '../investment-data'; + +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. + */ +@Component({ + selector: 'mifosx-investment-reports', + templateUrl: './investment-reports.component.html', + styleUrls: [ + '../../cashier/cashier-shared.scss', + '../investment-shared.scss' + ], + standalone: true, + imports: [CommonModule] +}) +export class InvestmentReportsComponent { + activeGroup: ReportGroup = 'balance'; + activeReport = 'govtSecBalance'; + asOnDate = todayDDMMYYYY(); + + setGroup(group: ReportGroup, defaultReport: string): void { + this.activeGroup = group; + this.activeReport = defaultReport; + } + + setReport(report: string): void { + this.activeReport = report; + } + + instrumentLabel(type: InstrumentType): string { + return INSTRUMENT_LABELS[type]; + } + + 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()}`; + } + + marketPrice(scriptCode: string): number | null { + return SCRIPT_MARKET_DETAIL.find((m) => m.scriptCode === scriptCode)?.marketPrice ?? null; + } + + // -------------------- Balance Reports -------------------- + get govtSecHoldings(): InvestmentHolding[] { + return GOVT_SECURITY_HOLDINGS; + } + get shareHoldings(): InvestmentHolding[] { + return SHARE_HOLDINGS; + } + get fdHoldings(): InvestmentHolding[] { + return FD_HOLDINGS; + } + get bankInvestmentList(): Array<{ party: string; count: number; totalFaceValue: number }> { + const map = new Map(); + for (const h of ALL_HOLDINGS) { + const row = map.get(h.party) || { party: h.party, count: 0, totalFaceValue: 0 }; + row.count += 1; + row.totalFaceValue += h.faceValue; + map.set(h.party, row); + } + return [...map.values()].sort((a, b) => b.totalFaceValue - a.totalFaceValue); + } + + // -------------------- Ledgers & Registers -------------------- + get fdTxns(): InvestmentTxn[] { + return ALL_TXNS.filter((t) => t.instrumentType === 'FD'); + } + get bondTxns(): InvestmentTxn[] { + return ALL_TXNS.filter((t) => ([ + 'GOVT_SECURITY', + 'NON_SLR', + 'BOND' + ] as InstrumentType[]).includes(t.instrumentType)); + } + get shareTxns(): InvestmentTxn[] { + return ALL_TXNS.filter((t) => t.instrumentType === 'SHARE'); + } + get amortizeTxns(): InvestmentTxn[] { + return ALL_TXNS.filter((t) => t.txnType === 'AMORTIZE'); + } + get dealSlips(): InvestmentTxn[] { + return ALL_TXNS.filter((t) => t.txnType === 'PURCHASE'); + } + get slrBondRegister(): InvestmentHolding[] { + return GOVT_SECURITY_HOLDINGS.filter((h) => h.subType === 'SLR'); + } + get interestDueRegister(): Array { + return ALL_HOLDINGS.filter((h) => h.status === 'ACTIVE' && h.interestFreq !== 'CUMULATIVE').map((h) => ({ + ...h, + dueAmount: this.accrued(h) + })); + } + get receivedInterestTxns(): InvestmentTxn[] { + return ALL_TXNS.filter((t) => t.txnType === 'INTEREST' || t.txnType === 'DIVIDEND'); + } + 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) + ); + } + + // -------------------- Maturity & Lists -------------------- + get maturedFdList(): InvestmentHolding[] { + return FD_HOLDINGS.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; + } + get govtSecList(): InvestmentHolding[] { + return GOVT_SECURITY_HOLDINGS; + } + get fdSecList(): InvestmentHolding[] { + return FD_HOLDINGS; + } + get shareSecList(): InvestmentHolding[] { + return SHARE_HOLDINGS; + } + 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)); + } + + // -------------------- Portfolio -------------------- + private readonly portfolioTypes: InstrumentType[] = [ + 'FD', + 'GOVT_SECURITY', + 'NON_SLR', + 'BOND', + 'MUTUAL_FUND', + 'SHARE' + ]; + get portfolioSummary(): Array<{ + label: string; + count: number; + faceValue: number; + accrued: number; + maturityValue: number; + }> { + return this.portfolioTypes.map((type) => { + const holdings = ALL_HOLDINGS.filter((h) => h.instrumentType === type); + return { + label: INSTRUMENT_LABELS[type], + count: holdings.length, + faceValue: holdings.reduce((s, h) => s + h.faceValue, 0), + accrued: holdings.reduce((s, h) => s + this.accrued(h), 0), + maturityValue: holdings.reduce((s, h) => s + this.maturity(h), 0) + }; + }); + } + get portfolioTotalFaceValue(): number { + return this.portfolioSummary.reduce((s, r) => s + r.faceValue, 0); + } + get portfolioTotalAccrued(): number { + return this.portfolioSummary.reduce((s, r) => s + r.accrued, 0); + } + get portfolioTotalMaturity(): number { + return this.portfolioSummary.reduce((s, r) => s + r.maturityValue, 0); + } +} diff --git a/src/app/investment/investment-routing.module.ts b/src/app/investment/investment-routing.module.ts new file mode 100644 index 000000000..bfe085efb --- /dev/null +++ b/src/app/investment/investment-routing.module.ts @@ -0,0 +1,71 @@ +/** + * 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 { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { Route } from '../core/route/route.service'; +import { InvestmentDashboardComponent } from './investment-dashboard/investment-dashboard.component'; +import { FdInvestmentComponent } from './fd-investment/fd-investment.component'; +import { BondInvestmentComponent } from './bond-investment/bond-investment.component'; +import { ShareInvestmentComponent } from './share-investment/share-investment.component'; +import { InvestmentMastersComponent } from './investment-masters/investment-masters.component'; +import { InvestmentReportsComponent } from './investment-reports/investment-reports.component'; +import { InvestmentInterestComponent } from './investment-interest/investment-interest.component'; + +const routes: Routes = [ + Route.withShell([ + { + path: 'investment', + data: { title: 'Investment', breadcrumb: 'Investment' }, + children: [ + { + path: 'dashboard', + component: InvestmentDashboardComponent, + data: { title: 'Investment Dashboard', breadcrumb: 'Investment Dashboard' } + }, + { + path: 'fd', + component: FdInvestmentComponent, + data: { title: 'FD Investment', breadcrumb: 'FD Investment' } + }, + { + path: 'bonds', + component: BondInvestmentComponent, + data: { title: 'Govt Security / Bond Investment', breadcrumb: 'Govt Security / Bond Investment' } + }, + { + path: 'share', + component: ShareInvestmentComponent, + data: { title: 'Share Investment', breadcrumb: 'Share Investment' } + }, + { + path: 'masters', + component: InvestmentMastersComponent, + data: { title: 'Investment Masters', breadcrumb: 'Investment Masters' } + }, + { + path: 'interest', + component: InvestmentInterestComponent, + data: { title: 'Investment Interest', breadcrumb: 'Investment Interest' } + }, + { + path: 'reports', + component: InvestmentReportsComponent, + data: { title: 'Investment Reports', breadcrumb: 'Investment Reports' } + } + ] + } + ]) +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class InvestmentRoutingModule {} diff --git a/src/app/investment/investment-shared.scss b/src/app/investment/investment-shared.scss new file mode 100644 index 000000000..57a554a2e --- /dev/null +++ b/src/app/investment/investment-shared.scss @@ -0,0 +1,225 @@ +/** + * 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/. + */ + +// Extra styles layered on top of ../cashier/cashier-shared.scss (pg-*, cctr-*) for the +// Investment module — instrument nav cards, status badges, KPI tiles, entry-form layout. + +.inv-kpis { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; + + @media (width <= 1100px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.inv-kpi { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 4px; + + &__label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: #64748b; + } + + &__value { + font-size: 20px; + font-weight: 800; + font-variant-numeric: tabular-nums; + color: #0f172a; + } + + &__sub { + font-size: 11px; + color: #94a3b8; + } +} + +.inv-nav-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 12px; +} + +.inv-nav-card { + display: flex; + flex-direction: column; + gap: 6px; + padding: 16px; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 12px; + text-decoration: none; + color: inherit; + transition: + border-color 0.15s, + box-shadow 0.15s; + + &:hover { + border-color: #0f4f3c; + box-shadow: 0 2px 10px rgb(15 79 60 / 10%); + } + + &__label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #64748b; + } + + &__count { + font-size: 22px; + font-weight: 800; + color: #0f172a; + } + + &__value { + font-size: 13px; + font-weight: 600; + color: #0f4f3c; + } +} + +.inv-badge { + display: inline-flex; + align-items: center; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + + &--active { + background: #f0fdf4; + color: #15803d; + border: 1px solid #bbf7d0; + } + + &--matured { + background: #eff6ff; + color: #1d4ed8; + border: 1px solid #bfdbfe; + } + + &--sold { + background: #fefce8; + color: #a16207; + border: 1px solid #fde68a; + } + + &--written_off { + background: #fef2f2; + color: #b91c1c; + border: 1px solid #fecaca; + } +} + +.inv-form-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 14px 20px; + padding: 18px; +} + +.inv-form-field { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 11px; + font-weight: 600; + color: #64748b; + + input, + select { + height: 36px; + padding: 0 10px; + border: 1px solid #e2e8f0; + border-radius: 8px; + font-size: 13px; + color: #0d1b2a; + font-family: inherit; + background: #fff; + } + + // Computed / lookup-filled fields are rendered read-only and greyed out so they read as + // derived values rather than something the user is expected to type into. + /* stylelint-disable-next-line selector-no-qualifying-type -- [readonly] only applies to inputs */ + input[readonly] { + background: #f8fafc; + color: #64748b; + } +} + +.inv-form-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 0 18px 18px; +} + +.inv-maturity-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + border-bottom: 1px dotted #eef2f7; + font-size: 13px; + + &:last-child { + border-bottom: none; + } +} + +.inv-maturity-days { + font-weight: 800; + font-variant-numeric: tabular-nums; + + &--soon { + color: #dc2626; + } + + &--ok { + color: #16a34a; + } +} + +.inv-toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%) translateY(20px); + background: #0f172a; + color: #fff; + padding: 12px 20px; + border-radius: 10px; + font-size: 13px; + font-weight: 600; + box-shadow: 0 8px 24px rgb(0 0 0 / 25%); + opacity: 0; + pointer-events: none; + transition: + opacity 0.2s, + transform 0.2s; + z-index: 2000; + + &.show { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} diff --git a/src/app/investment/investment.module.ts b/src/app/investment/investment.module.ts new file mode 100644 index 000000000..a60f44544 --- /dev/null +++ b/src/app/investment/investment.module.ts @@ -0,0 +1,34 @@ +/** + * 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 { NgModule } from '@angular/core'; + +import { SharedModule } from '../shared/shared.module'; +import { InvestmentRoutingModule } from './investment-routing.module'; +import { InvestmentDashboardComponent } from './investment-dashboard/investment-dashboard.component'; +import { FdInvestmentComponent } from './fd-investment/fd-investment.component'; +import { BondInvestmentComponent } from './bond-investment/bond-investment.component'; +import { ShareInvestmentComponent } from './share-investment/share-investment.component'; +import { InvestmentMastersComponent } from './investment-masters/investment-masters.component'; +import { InvestmentReportsComponent } from './investment-reports/investment-reports.component'; +import { InvestmentInterestComponent } from './investment-interest/investment-interest.component'; + +@NgModule({ + imports: [ + SharedModule, + InvestmentRoutingModule, + InvestmentDashboardComponent, + FdInvestmentComponent, + BondInvestmentComponent, + ShareInvestmentComponent, + InvestmentMastersComponent, + InvestmentReportsComponent, + InvestmentInterestComponent + ] +}) +export class InvestmentModule {} diff --git a/src/app/investment/share-investment/share-investment.component.html b/src/app/investment/share-investment/share-investment.component.html new file mode 100644 index 000000000..7312ec74c --- /dev/null +++ b/src/app/investment/share-investment/share-investment.component.html @@ -0,0 +1,276 @@ + + +
+
+
+
TREASURY · INVESTMENT · SHARE
+

Share Entry

+

+ Equity and cooperative-society share holdings — Purchase, Dividend, Sell and Write-off entries. +

+
+
+ +
+ + + + + +
+ + + @if (activeTab === 'holdings') { +
+ + + + + + + + + + + + + + + + @for (h of holdings; track h.certNo) { + + + + + + + + + + + } + +
Cert NoScriptPartyUnitsRateFace ValuePurchase DateStatus
+ {{ h.certNo }} + {{ h.scriptName }}{{ h.party }}{{ h.units | number }}₹{{ h.purchaseRate | number: '1.2-2' }}₹{{ h.faceValue | number: '1.2-2' }}{{ h.purchaseDate }} + {{ h.status }} +
+
+ } + + + @if (activeTab === 'purchase') { +
+ +
+ + + + + + + + + + +
+
+ + +
+
+ } + + + @if (activeTab === 'dividend') { +
+ +
+ + @if (dividendHolding; as h) { + + } + + +
+
+ +
+
+ } + + + @if (activeTab === 'sell') { +
+ +
+ + @if (sellHolding; as h) { + + } + + + +
+
+ +
+
+ } + + + @if (activeTab === 'writeoff') { +
+ +
+ + @if (writeoffHolding; as h) { + + } + +
+
+ +
+
+ } + + @if (activeTab !== 'holdings') { +
+ + + + + + + + + + + + + + @for (t of txns; track t.voucherNo) { + + + + + + + + + } + +
DateVoucher NoCert NoTypeParticularsAmount
{{ t.txnDate }} + {{ t.voucherNo }} + {{ t.certNo }}{{ t.txnType }}{{ t.particulars }}₹{{ t.amount | number: '1.2-2' }}
+
+ } +
+ +
{{ toastMsg }}
diff --git a/src/app/investment/share-investment/share-investment.component.ts b/src/app/investment/share-investment/share-investment.component.ts new file mode 100644 index 000000000..f5835ee0d --- /dev/null +++ b/src/app/investment/share-investment/share-investment.component.ts @@ -0,0 +1,263 @@ +/** + * 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 { SHARE_HOLDINGS, ALL_TXNS, InvestmentHolding, InvestmentTxn, todayDDMMYYYY } from '../investment-data'; + +type ShareTab = 'holdings' | 'purchase' | 'dividend' | 'sell' | 'writeoff'; + +interface SharePurchaseForm { + certNo: string; + scriptCode: string; + scriptName: string; + party: string; + subType: string; + units: number | null; + purchaseRate: number | null; + purchaseDate: string; + brokerCode: string; + crToAc: string; +} + +/** + * Share Entry treasury screen — Holdings register + Purchase / Dividend / Sell / Write-off + * entry tabs (legacy "02 Share Entry" menu). Dummy in-memory data; Save appends locally. + */ +@Component({ + selector: 'mifosx-share-investment', + templateUrl: './share-investment.component.html', + styleUrls: [ + '../../cashier/cashier-shared.scss', + '../investment-shared.scss' + ], + standalone: true, + imports: [ + CommonModule, + FormsModule + ] +}) +export class ShareInvestmentComponent { + activeTab: ShareTab = 'holdings'; + asOnDate = todayDDMMYYYY(); + + holdings: InvestmentHolding[] = [...SHARE_HOLDINGS]; + txns: InvestmentTxn[] = ALL_TXNS.filter((t) => t.instrumentType === 'SHARE'); + + toastMsg = ''; + toastVisible = false; + private toastTimer: ReturnType | undefined; + + purchaseForm: SharePurchaseForm = this.blankPurchaseForm(); + dividendCertNo = ''; + dividendPerShare: number | null = null; + sellCertNo = ''; + sellUnits: number | null = null; + sellRate: number | null = null; + writeoffCertNo = ''; + writeoffReason = ''; + + setTab(tab: ShareTab): void { + this.activeTab = tab; + } + + get activeHoldings(): InvestmentHolding[] { + return this.holdings.filter((h) => h.status === 'ACTIVE'); + } + + private blankPurchaseForm(): SharePurchaseForm { + return { + certNo: `SH-${6000 + this.holdings.length + 1}`, + scriptCode: '', + scriptName: '', + party: 'NSE/BSE', + subType: 'EQUITY', + units: null, + purchaseRate: null, + purchaseDate: this.asOnDate, + brokerCode: '', + crToAc: '310070' + }; + } + + savePurchase(): void { + const f = this.purchaseForm; + if (!f.scriptName || !f.units || !f.purchaseRate) { + this.toast('Please fill Script Name, No. of Units and Purchase Rate.'); + return; + } + const faceValue = f.units * f.purchaseRate; + const holding: InvestmentHolding = { + certNo: f.certNo, + instrumentType: 'SHARE', + scriptCode: f.scriptCode || f.scriptName, + scriptName: f.scriptName, + party: f.party, + subType: f.subType, + faceValue, + purchaseRate: f.purchaseRate, + units: f.units, + purchaseDate: f.purchaseDate, + maturityDate: f.purchaseDate, + interestRate: 0, + interestFreq: 'YEARLY', + glCode: '310060', + glName: 'INVESTMENT IN SHARES', + crToAc: f.crToAc, + brokerCode: f.brokerCode, + status: 'ACTIVE' + }; + this.holdings = [ + holding, + ...this.holdings + ]; + this.txns = [ + { + certNo: holding.certNo, + instrumentType: 'SHARE', + txnType: 'PURCHASE', + voucherNo: `INV/PUR/${holding.certNo}`, + txnDate: f.purchaseDate, + amount: faceValue, + particulars: `Purchase of ${f.units} shares - ${holding.scriptName}` + }, + ...this.txns + ]; + this.toast(`Share holding ${holding.certNo} saved successfully.`); + this.purchaseForm = this.blankPurchaseForm(); + this.activeTab = 'holdings'; + } + + cancelPurchase(): void { + this.purchaseForm = this.blankPurchaseForm(); + } + + // -------------------- Dividend -------------------- + get dividendHolding(): InvestmentHolding | undefined { + return this.holdings.find((h) => h.certNo === this.dividendCertNo); + } + + get dividendAmount(): number { + const h = this.dividendHolding; + if (!h || !this.dividendPerShare) { + return 0; + } + return Math.round(h.units * this.dividendPerShare); + } + + saveDividend(): void { + const h = this.dividendHolding; + if (!h || !this.dividendPerShare) { + this.toast('Select a holding and enter dividend per share.'); + return; + } + const amt = this.dividendAmount; + this.txns = [ + { + certNo: h.certNo, + instrumentType: 'SHARE', + txnType: 'DIVIDEND', + voucherNo: `INV/DIV/${h.certNo}-${this.txns.length + 1}`, + txnDate: this.asOnDate, + amount: amt, + particulars: `Dividend received - ${h.scriptName} (₹${this.dividendPerShare}/share)` + }, + ...this.txns + ]; + this.toast(`Dividend of ₹${amt.toLocaleString('en-IN')} recorded for ${h.certNo}.`); + this.dividendCertNo = ''; + this.dividendPerShare = null; + } + + // -------------------- Sell -------------------- + get sellHolding(): InvestmentHolding | undefined { + return this.holdings.find((h) => h.certNo === this.sellCertNo); + } + + get sellConsideration(): number { + if (!this.sellUnits || !this.sellRate) { + return 0; + } + return Math.round(this.sellUnits * this.sellRate); + } + + saveSell(): void { + const h = this.sellHolding; + if (!h || !this.sellUnits || !this.sellRate || this.sellUnits > h.units) { + this.toast('Select a holding and enter valid units (≤ held) and sell rate.'); + return; + } + const consideration = this.sellConsideration; + h.units -= this.sellUnits; + h.faceValue = Math.round(h.units * h.purchaseRate); + if (h.units === 0) { + h.status = 'SOLD'; + } + this.txns = [ + { + certNo: h.certNo, + instrumentType: 'SHARE', + txnType: 'SELL', + voucherNo: `INV/SEL/${h.certNo}-${this.txns.length + 1}`, + txnDate: this.asOnDate, + amount: consideration, + particulars: `Sold ${this.sellUnits} shares of ${h.scriptName} @ ${this.sellRate}` + }, + ...this.txns + ]; + this.toast(`Sold ${this.sellUnits} shares. Consideration ₹${consideration.toLocaleString('en-IN')}.`); + this.sellCertNo = ''; + this.sellUnits = null; + this.sellRate = null; + this.activeTab = 'holdings'; + } + + // -------------------- Write-off -------------------- + get writeoffHolding(): InvestmentHolding | undefined { + return this.holdings.find((h) => h.certNo === this.writeoffCertNo); + } + + saveWriteoff(): void { + const h = this.writeoffHolding; + if (!h || !this.writeoffReason) { + this.toast('Select a holding and enter a write-off reason.'); + return; + } + h.status = 'WRITTEN_OFF'; + this.txns = [ + { + certNo: h.certNo, + instrumentType: 'SHARE', + 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'; + } + + statusClass(h: InvestmentHolding): string { + return `inv-badge inv-badge--${h.status.toLowerCase()}`; + } + + private toast(msg: string): void { + this.toastMsg = msg; + this.toastVisible = true; + if (this.toastTimer) { + clearTimeout(this.toastTimer); + } + this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200); + } +} diff --git a/src/environments/.env.ts b/src/environments/.env.ts index f94dce0fc..52cc1da1e 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -10,7 +10,7 @@ export default { 'mifos_x': { 'version': '260731', - 'hash': 'ca85bae4' + 'hash': 'fd28788d' }, 'allow_switching_backend_instance': true };