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.
This commit is contained in:
@@ -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<typeof setTimeout> | 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user