+ FD Holdings Register — {{ activeFds.length }} active, ₹{{ totalPlaced | number: '1.2-2' }} placed
+
- Cert No
- Party
- Type
- Face Value
+ Deal No
+ Bank
+ Certificate
+ Deal Date
+ Amount
Rate
- Purchase Date
Maturity Date
- Accrued Interest
- Maturity Value
+ Days
+ Interest
+ Interest (expected)
+ Maturity Amount
Status
+ Voucher
+
- @for (h of holdings; track h.certNo) {
+ @for (f of fds; track f.id) {
- {{ h.certNo }}
+ {{ f.dealNo }}
- {{ 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' }}
+ {{ f.partyName }}
+ {{ f.certNo }}
+ {{ f.dealDate }}
+ ₹{{ f.principal | number: '1.2-2' }}
+ {{ f.intRate }}%
+ {{ f.maturityDate }}
+ {{ f.days }}
- {{ h.status }}
+ {{ payoutLabel(f.payout) }}
+ @if (f.payout === 'MATURITY') {
+ {{ calcLabel(f.calcType) }}
+ }
+ ₹{{ f.expectedInterest | number: '1.2-2' }}
+ ₹{{ f.expectedMaturityAmount | number: '1.2-2' }}
+
+ {{ f.status }}
+ @if (f.closedOnDate) {
+ {{ f.closedOnDate }} · interest ₹{{ f.closureInterest | number: '1.2-2' }}
+ @if (f.closureTds) {
+ · TDS ₹{{ f.closureTds | number: '1.2-2' }}
+ }
+
+ }
+ @if (f.renewedFromDealNo) {
+ renewal of {{ f.renewedFromDealNo }}
+ }
+
+
+ {{ f.voucherNo }}
+ @if (f.closureVoucherNo) {
+ closed: {{ f.closureVoucherNo }}
+ }
+
+
+ @if (f.status === 'ACTIVE') {
+ Interest
+ Close / Renew
+ }
+
+
+ } @empty {
+
+ No FDs placed yet. Use 01 FD Purchase Entry.
}
@@ -119,375 +135,556 @@
}
-
+
@if (activeTab === 'purchase') {
}
-
+
@if (activeTab === 'interest') {
- }
-
-
- @if (activeTab === 'sell') {
-
- }
-
-
- @if (activeTab === 'writeoff') {
-
-
Investment - FD Write Off Entry
-
-
- Certificate No *
-
- -- Select --
- @for (h of activeHoldings; track h.certNo) {
- {{ h.certNo }} — {{ h.party }}
- }
-
-
- @if (writeoffHolding; as h) {
- Face Value to be Written Off
- }
- Reason *
-
-
- Save
-
-
- }
-
-
- @if (activeTab === 'partpayment') {
-
-
Investment - FD Part Payment Entry
-
-
- Certificate No *
-
- -- Select --
- @for (h of activeHoldings; track h.certNo) {
- {{ h.certNo }} — {{ h.party }}
- }
-
-
- @if (partPayHolding; as h) {
- Current Face Value
- }
- Part Payment Amount *
-
-
- Save
-
-
- }
-
- @if (activeTab !== 'holdings') {
-
-
FD Transaction Log
-
-
-
- Date
- Voucher No
- Cert No
- Type
- Particulars
- GL Posted
- Amount
-
-
-
- @for (t of txns; track t.voucherNo) {
+ @if (schedule; as s) {
+
+ Days are actual / 365. A provision books the interest earned since the last payout (Dr 100603 Interest
+ Receivable / Cr {{ s.fd.interestGlCode }}); a payout clears what was provisioned and books the rest. Post the
+ lines in date order, on or after their date. The interest of the last period comes with the maturity (02
+ Maturity tab).
+
+
+ }
+
+ Exit
+
+
+ }
+
+
+ @if (activeTab === 'closure') {
+
-
+
{{
+ closure.type === 'PREMATURE'
+ ? '* Amount Received from Bank (₹)'
+ : closure.type === 'RENEWAL'
+ ? 'Maturity Amount as per Bank (₹)'
+ : 'Amount Received from Bank (₹)'
+ }}
+
TDS deducted by Bank (₹)
+ @if (closure.type === 'PREMATURE') {
+
Penal Interest (₹)
+ }
+ @if (closure.type !== 'RENEWAL') {
+
+ Principal received into
+
+ @for (g of bankGlOptions; track g.id) {
+ {{ g.glCode }} — {{ g.name }}
+ }
+
+
+
+ Interest received into
+
+ @for (g of bankGlOptions; track g.id) {
+ {{ g.glCode }} — {{ g.name }}
+ }
+
+
+ }
+ }
+
+
+ @if (closureFd && closure.type === 'RENEWAL') {
+ New Certificate
+
+ * New Certificate No
+ Certificate Date
+ * New Interest Rate %
+ Period – Years
+ Period – Months
+ Period – Days
+ * New Maturity Date
+
+ Interest Paid
+
+ @for (o of payoutOptions; track o.value) {
+ {{ o.label }}
+ }
+
+
+ @if (closure.newPayout === 'MATURITY') {
+
+ Maturity Calculation
+
+ @for (o of calcTypeOptions; track o.value) {
+ {{ o.label }}
+ }
+
+
+ }
+ Renewal Amount (₹) — blank = full maturity amount
+ @if (closureTopUp !== 0) {
+
+ * {{ closureTopUp > 0 ? 'Extra' : 'Taken out' }} ₹{{
+ (closureTopUp > 0 ? closureTopUp : -closureTopUp) | number: '1.2-2'
+ }}
+ — {{ closureTopUp > 0 ? 'paid from' : 'credited to' }}
+
+ -- Select --
+ @for (g of bankGlOptions; track g.id) {
+ {{ g.glCode }} — {{ g.name }}
+ }
+
+
+ }
+
+ }
+
+ @if (closureQuote; as q) {
+ Interest — system vs bank
+
+ Interest as per System
+ Interest as per Bank (received + TDS − principal)
+ Difference
+ Already Provisioned
+
+ @if (q.difference !== 0 && closure.type !== 'PREMATURE') {
+
+ The bank and the system differ by ₹{{ q.difference | number: '1.2-2' }}: the bank may compound differently
+ (daily / monthly / quarterly). The bank's figure is what gets posted.
+
+ }
+
+
+
+ Dr / Cr
+ GL
+ Amount
+
+
+
+ @for (l of q.lines; track $index) {
+
+ {{ l.side }}
+ {{ l.glCode }} {{ l.glName }}
+ ₹{{ l.amount | number: '1.2-2' }}
+
+ } @empty {
+
+ No voucher needed.
+
+ }
+
+
+
+ Voucher dated {{ q.closeDate }}. The lines follow the amounts entered above.
+
+ }
+ @if (closureError) {
+ {{ closureError }}
+ }
+ @if (closureFd) {
+
+ Remarks
+
+ }
+
+
+ {{
+ closure.type === 'RENEWAL'
+ ? 'Renew'
+ : closure.type === 'PREMATURE'
+ ? 'Close Premature'
+ : 'Close at Maturity'
+ }}
+
+ Exit
+
}
diff --git a/src/app/investment/fd-investment/fd-investment.component.ts b/src/app/investment/fd-investment/fd-investment.component.ts
index 04643d0c0..1643c7485 100644
--- a/src/app/investment/fd-investment/fd-investment.component.ts
+++ b/src/app/investment/fd-investment/fd-investment.component.ts
@@ -6,77 +6,73 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
-import { Component } from '@angular/core';
+import { Component, OnInit, inject } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
+import { addPeriod, parseDDMMYYYY, todayDDMMYYYY } from '../investment-data';
import {
- FD_HOLDINGS,
- ALL_TXNS,
- PARTY_MASTER,
- SUBTYPE_MASTER,
- FD_TYPE_OPTIONS,
- INT_TYPE_OPTIONS,
- LIEN_REASON_MASTER,
- INTEREST_INCOME_GL_MASTER,
- INTEREST_INCOME_GL_BY_TYPE,
- InvestmentHolding,
- InvestmentTxn,
- InterestFreq,
- accruedInterest,
- accruedInterestBetween,
- canAccrueInterest,
- daysToMaturity,
- maturityAmount,
- addPeriod,
- todayDDMMYYYY
-} from '../investment-data';
-import { exportWorkbook } from '../investment-export';
+ FdCalcType,
+ FdClosureType,
+ FdPayout,
+ InvestmentFdClosureDto,
+ InvestmentFdInterestRowDto,
+ InvestmentFdScheduleDto,
+ InvestmentFdDto,
+ InvestmentGlAccount,
+ InvestmentPartyDto,
+ InvestmentService,
+ apiErrorMessage
+} from '../investment.service';
-type FdTab = 'holdings' | 'purchase' | 'interest' | 'sell' | 'writeoff' | 'partpayment';
+type FdTab = 'holdings' | 'purchase' | 'closure' | 'interest';
+
+interface FdClosureForm {
+ fdId: number | null;
+ type: FdClosureType;
+ closeDate: string;
+ /** Maturity / premature: money credited by the bank. Renewal: the maturity amount as per the bank. */
+ receivedAmount: number | null;
+ tds: number | null;
+ penalAmount: number | null;
+ principalBankGlAccountId: number | null;
+ interestBankGlAccountId: number | null;
+ newCertNo: string;
+ newCertDate: string;
+ newIntRate: number | null;
+ newPeriodYears: number | null;
+ newPeriodMonths: number | null;
+ newPeriodDays: number | null;
+ newMaturityDate: string;
+ newPayout: FdPayout;
+ newCalcType: FdCalcType;
+ renewalAmount: number | null;
+ diffBankGlAccountId: number | null;
+ remarks: string;
+}
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';
+ partyId: number | null;
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;
+ dealDate: string;
+ amount: number | null;
+ intRate: number | null;
+ periodYears: number | null;
+ periodMonths: 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';
+ maturityDate: string;
+ payout: FdPayout;
+ calcType: FdCalcType;
+ bankGlAccountId: number | null;
+ remarks: string;
}
/**
- * 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.
+ * FDs the bank places with other banks (Jitendra Sir): the bank is an FD party with its own FD GL and interest GL;
+ * placing an FD posts Dr the bank's FD GL / Cr our bank GL at once. The RBI limits (20% of total deposits as on the
+ * previous 31 March with all banks, 5% with one bank) are checked before saving.
*/
@Component({
selector: 'mifosx-fd-investment',
@@ -92,463 +88,494 @@ interface FdPurchaseForm {
DateDdmmyyyyPickerComponent
]
})
-export class FdInvestmentComponent {
+export class FdInvestmentComponent implements OnInit {
+ private readonly investmentService = inject(InvestmentService);
+ private readonly route = inject(ActivatedRoute);
+
activeTab: FdTab = 'holdings';
- asOnDate = todayDDMMYYYY();
+ fds: InvestmentFdDto[] = [];
+ fdParties: InvestmentPartyDto[] = [];
+ /** Our bank accounts (current accounts under 100200) the money can be paid from. */
+ bankGlOptions: InvestmentGlAccount[] = [];
- 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 interestIncomeGlOptions = INTEREST_INCOME_GL_MASTER;
- readonly branchOptions = [
- 'HEAD OFFICE',
- 'BRANCH 1',
- 'BRANCH 2'
+ readonly payoutOptions: Array<{ value: FdPayout; label: string }> = [
+ { value: 'MATURITY', label: 'At Maturity' },
+ { value: 'HALF_YEARLY', label: 'Half-Yearly' },
+ { value: 'YEARLY', label: 'Yearly' }
+ ];
+ readonly calcTypeOptions: Array<{ value: FdCalcType; label: string }> = [
+ { value: 'QUARTERLY', label: 'Compound – Quarterly' },
+ { value: 'MONTHLY', label: 'Compound – Monthly' },
+ { value: 'HALF_YEARLY', label: 'Compound – Half-Yearly' },
+ { value: 'YEARLY', label: 'Compound – Yearly' },
+ { value: 'SIMPLE', label: 'Simple Interest' }
];
- generating = false;
+ form: FdPurchaseForm = this.blankForm();
+ quote: InvestmentFdDto | null = null;
+ quoteError = '';
+ saving = false;
+
+ // 03 Interest Entry
+ interestFdId: number | null = null;
+ schedule: InvestmentFdScheduleDto | null = null;
+ payoutReceived: number | null = null;
+ payoutTds: number | null = null;
+ payoutBankGlAccountId: number | null = null;
+ postingInterest = false;
+
+ closure: FdClosureForm = this.blankClosure();
+ closureQuote: InvestmentFdClosureDto | null = null;
+ closureError = '';
+ closing = false;
+
+ /** Total deposits as on the 31 March the limits use, when it still has to be entered. */
+ baseAmount: number | null = null;
+ savingBase = false;
+
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 = '';
- interestFromDate = addPeriod(this.asOnDate, 0, -3);
- interestToDate = this.asOnDate;
- interestGlCode = INTEREST_INCOME_GL_BY_TYPE.FD;
- sellCertNo = '';
- sellDate = this.asOnDate;
- sellRemark = '';
- writeoffCertNo = '';
- writeoffReason = '';
- partPayCertNo = '';
- partPayAmount: number | null = null;
+ ngOnInit(): void {
+ this.investmentService.getParties().subscribe({
+ next: (ps) => (this.fdParties = ps.filter((p) => p.bondType === 'FD' && p.active)),
+ error: (err) => this.toast(`Could not load FD banks: ${apiErrorMessage(err)}`)
+ });
+ this.investmentService.getAssetGlAccounts().subscribe({
+ next: (gls) => (this.bankGlOptions = gls.filter((g) => g.glCode.startsWith('1002'))),
+ error: (err) => this.toast(`Could not load bank GLs: ${apiErrorMessage(err)}`)
+ });
+ this.loadFds();
+ // Opened from a reminder (home screen / day close): straight to the FD's interest lines.
+ const fdId = Number(this.route.snapshot.queryParamMap.get('fd'));
+ if (this.route.snapshot.queryParamMap.get('tab') === 'interest') {
+ this.activeTab = 'interest';
+ if (fdId) {
+ this.interestFdId = fdId;
+ this.loadSchedule();
+ }
+ }
+ }
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 (tab === 'purchase') {
+ this.refreshQuote();
}
- 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'
+ private loadFds(): void {
+ this.investmentService.getFds().subscribe({
+ next: (list) => (this.fds = list),
+ error: (err) => this.toast(`Could not load FDs: ${apiErrorMessage(err)}`)
});
}
+ get selectedParty(): InvestmentPartyDto | undefined {
+ return this.fdParties.find((p) => p.id === this.form.partyId);
+ }
+
+ get activeFds(): InvestmentFdDto[] {
+ return this.fds.filter((f) => f.status === 'ACTIVE');
+ }
+
+ get totalPlaced(): number {
+ return this.activeFds.reduce((sum, f) => sum + (f.principal ?? 0), 0);
+ }
+
+ payoutLabel(p?: string): string {
+ return this.payoutOptions.find((o) => o.value === p)?.label ?? p ?? '';
+ }
+
+ calcLabel(c?: string): string {
+ return this.calcTypeOptions.find((o) => o.value === c)?.label ?? c ?? '';
+ }
+
+ // -------------------- Purchase --------------------
+ /** Period (years / months / days) sets the maturity date; the maturity date can also be typed directly. */
+ onPeriodChange(): void {
+ const f = this.form;
+ if (f.dealDate && (f.periodYears || f.periodMonths || f.periodDays)) {
+ f.maturityDate = addPeriod(f.dealDate, f.periodYears ?? 0, f.periodMonths ?? 0, f.periodDays ?? 0);
+ }
+ this.refreshQuote();
+ }
+
+ /** Asks the backend for the interest, maturity amount and the RBI limit position of the FD being entered. */
+ refreshQuote(): void {
+ const f = this.form;
+ this.quoteError = '';
+ const params: Record = {
+ dealDate: f.dealDate || todayDDMMYYYY(),
+ payout: f.payout,
+ calcType: f.calcType
+ };
+ if (f.partyId) {
+ params['partyId'] = f.partyId;
+ }
+ if (f.amount) {
+ params['amount'] = f.amount;
+ }
+ if (f.intRate) {
+ params['intRate'] = f.intRate;
+ }
+ if (f.maturityDate) {
+ params['maturityDate'] = f.maturityDate;
+ }
+ this.investmentService.quoteFd(params).subscribe({
+ next: (q) => {
+ this.quote = q;
+ if (q.limit && q.limit.totalDeposits == null && this.baseAmount == null) {
+ this.baseAmount = q.limit.glTotalDeposits || null;
+ }
+ },
+ error: (err) => {
+ this.quote = null;
+ this.quoteError = apiErrorMessage(err);
+ }
+ });
+ }
+
+ saveBase(): void {
+ const asOn = this.quote?.limit?.asOnDate;
+ if (!asOn || !this.baseAmount || this.baseAmount <= 0) {
+ this.toast('Enter the total deposits as on 31 March.');
+ return;
+ }
+ this.savingBase = true;
+ this.investmentService
+ .setDepositBase({ asOnDate: asOn, totalDeposits: this.baseAmount, locale: 'en', dateFormat: 'dd-MM-yyyy' })
+ .subscribe({
+ next: () => {
+ this.savingBase = false;
+ this.toast(`Total deposits as on ${asOn} saved.`);
+ this.refreshQuote();
+ },
+ error: (err) => {
+ this.savingBase = false;
+ this.toast(apiErrorMessage(err));
+ }
+ });
+ }
+
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 --------------------
- /** Certificates eligible for accrual-basis posting — same rule as the Investment Interest
- * module: a Non-Performing holding never appears here, only through Interest Receive. */
- get interestEligibleHoldings(): InvestmentHolding[] {
- return this.activeHoldings.filter((h) => canAccrueInterest(h));
- }
-
- get interestHolding(): InvestmentHolding | undefined {
- return this.holdings.find((h) => h.certNo === this.interestCertNo);
- }
-
- /** Interest accrued strictly within [interestFromDate, interestToDate] — a period-based
- * Interest Calculation & Post run, same basis as the Investment Interest module. */
- interestAccrued(h: InvestmentHolding): number {
- return accruedInterestBetween(h, this.interestFromDate || h.purchaseDate, this.interestToDate || this.asOnDate);
- }
-
- glName(glCode: string): string {
- return this.interestIncomeGlOptions.find((g) => g.glCode === glCode)?.glName ?? glCode;
- }
-
- postInterest(): void {
- const h = this.interestHolding;
- if (!h) {
- this.toast('Select a certificate first.');
- return;
- }
- const amt = this.interestAccrued(h);
- if (amt <= 0) {
- this.toast('No interest due to be posted for this period.');
- return;
- }
- const toDate = this.interestToDate || this.asOnDate;
- const fromDate = this.interestFromDate || toDate;
- const glCode = this.interestGlCode;
- const glName = this.glName(glCode);
- h.interestPosted = (h.interestPosted ?? 0) + amt;
- h.interestPostedTillDate = toDate;
- this.interestFromDate = toDate;
- this.txns = [
- {
- certNo: h.certNo,
- instrumentType: 'FD',
- txnType: 'INTEREST',
- voucherNo: `INV/INT/FD-${h.certNo}-${this.txns.length + 1}`,
- txnDate: toDate,
- amount: amt,
- particulars: `Interest posted (accrual) for period ${fromDate} to ${toDate} - ${h.party} (${h.interestFreq})`,
- glCode,
- glName
- },
- ...this.txns
- ];
- this.toast(
- `Interest of ₹${amt.toLocaleString('en-IN')} posted to ${glCode} - ${glName} 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()}`;
- }
-
- // -------------------- Export to Excel (Holdings register) --------------------
- async exportExcel(): Promise {
- this.generating = true;
- try {
- await exportWorkbook(
- [
- {
- sheetName: 'FD Holdings',
- title: 'FD Holdings Register',
- subtitle: `As on ${this.asOnDate}`,
- headers: [
- 'Cert No',
- 'Party',
- 'Type',
- 'Face Value',
- 'Rate',
- 'Frequency',
- 'Purchase Date',
- 'Maturity Date',
- 'Accrued Interest',
- 'Maturity Value',
- 'Status'
- ],
- widths: [
- 14,
- 28,
- 18,
- 16,
- 10,
- 14,
- 14,
- 14,
- 16,
- 16,
- 12
- ],
- rows: this.holdings.map((h) => [
- h.certNo,
- h.party,
- h.interestFreq === 'CUMULATIVE' ? 'Cumulative' : 'Payout',
- h.faceValue,
- h.interestRate,
- h.interestFreq,
- h.purchaseDate,
- h.maturityDate,
- this.accrued(h),
- this.maturity(h),
- h.status
- ])
- }
- ],
- `FD-Holdings-${this.asOnDate}`
+ const f = this.form;
+ if (
+ !f.partyId ||
+ !f.certNo.trim() ||
+ !f.dealDate ||
+ !f.amount ||
+ !f.intRate ||
+ !f.maturityDate ||
+ !f.bankGlAccountId
+ ) {
+ this.toast(
+ 'Enter the bank, certificate no, deal date, amount, rate, maturity date and the bank account paid from.'
);
- this.toast('FD Holdings exported.');
- } catch (err) {
- console.error('FD Holdings export failed', err);
- this.toast('Could not generate the workbook — please retry.');
- } finally {
- this.generating = false;
+ return;
}
+ if (this.quote?.limit && !this.quote.limit.withinLimits) {
+ this.toast(this.quote.limit.message);
+ return;
+ }
+ this.saving = true;
+ this.investmentService
+ .createFd({
+ partyId: f.partyId,
+ certNo: f.certNo.trim(),
+ ...(f.certDate ? { certDate: f.certDate } : {}),
+ dealDate: f.dealDate,
+ amount: f.amount,
+ intRate: f.intRate,
+ maturityDate: f.maturityDate,
+ payout: f.payout,
+ calcType: f.calcType,
+ bankGlAccountId: f.bankGlAccountId,
+ ...(f.remarks.trim() ? { remarks: f.remarks.trim() } : {}),
+ locale: 'en',
+ dateFormat: 'dd-MM-yyyy'
+ })
+ .subscribe({
+ next: (res) => {
+ this.saving = false;
+ this.toast(
+ `FD ${res.changes?.dealNo ?? ''} placed — voucher ${res.changes?.voucherNo ?? res.resourceIdentifier ?? ''}.`
+ );
+ this.form = this.blankForm();
+ this.quote = null;
+ this.loadFds();
+ this.setTab('holdings');
+ },
+ error: (err) => {
+ this.saving = false;
+ this.toast(apiErrorMessage(err));
+ }
+ });
+ }
+
+ resetPurchase(): void {
+ this.form = this.blankForm();
+ this.refreshQuote();
+ }
+
+ // -------------------- Interest Entry --------------------
+ openInterest(f: InvestmentFdDto): void {
+ this.interestFdId = f.id ?? null;
+ this.setTab('interest');
+ this.loadSchedule();
+ }
+
+ loadSchedule(): void {
+ this.schedule = null;
+ this.payoutReceived = null;
+ this.payoutTds = null;
+ if (!this.interestFdId) {
+ return;
+ }
+ this.investmentService.getFdSchedule(this.interestFdId).subscribe({
+ next: (s) => {
+ this.schedule = s;
+ this.payoutBankGlAccountId = s.fd.bankGlAccountId ?? null;
+ },
+ error: (err) => this.toast(apiErrorMessage(err))
+ });
+ }
+
+ lineLabel(t: string): string {
+ return t === 'PROVISION' ? 'Provision' : t === 'PAYOUT' ? 'Interest Payout' : 'Maturity';
+ }
+
+ /** Posts the next line: a provision as worked out, a payout with the bank's amount and TDS if entered. */
+ postInterest(r: InvestmentFdInterestRowDto): void {
+ const fdId = this.interestFdId;
+ if (!fdId) {
+ return;
+ }
+ const body: Record = {
+ lineType: r.lineType,
+ intDate: r.intDate,
+ locale: 'en',
+ dateFormat: 'dd-MM-yyyy'
+ };
+ if (r.lineType === 'PAYOUT') {
+ if (this.payoutReceived) {
+ body['receivedAmount'] = this.payoutReceived;
+ }
+ if (this.payoutTds) {
+ body['tds'] = this.payoutTds;
+ }
+ if (this.payoutBankGlAccountId) {
+ body['bankGlAccountId'] = this.payoutBankGlAccountId;
+ }
+ }
+ this.postingInterest = true;
+ this.investmentService.postFdInterest(fdId, body).subscribe({
+ next: (res) => {
+ this.postingInterest = false;
+ this.toast(`${this.lineLabel(r.lineType)} of ${r.intDate} posted — voucher ${res.resourceIdentifier ?? ''}.`);
+ this.loadSchedule();
+ },
+ error: (err) => {
+ this.postingInterest = false;
+ this.toast(apiErrorMessage(err));
+ }
+ });
+ }
+
+ // -------------------- Maturity / Premature / Renewal --------------------
+ get closureFd(): InvestmentFdDto | undefined {
+ return this.fds.find((f) => f.id === this.closure.fdId);
+ }
+
+ /** Maturity and renewal happen on or after the maturity date; before it only a premature closure is possible. */
+ get closureFdMatured(): boolean {
+ const f = this.closureFd;
+ return !!f?.maturityDate && parseDDMMYYYY(f.maturityDate) <= parseDDMMYYYY(todayDDMMYYYY());
+ }
+
+ /** The renewal top-up (positive) or amount taken out (negative), as worked out by the backend. */
+ get closureTopUp(): number {
+ return this.closureQuote?.topUp ?? 0;
+ }
+
+ openClosure(f: InvestmentFdDto): void {
+ this.closure = this.blankClosure();
+ this.closure.fdId = f.id ?? null;
+ this.onClosureFdChange();
+ this.setTab('closure');
+ }
+
+ onClosureFdChange(): void {
+ const f = this.closureFd;
+ const c = this.closure;
+ c.receivedAmount = null;
+ c.renewalAmount = null;
+ c.type = this.closureFdMatured ? 'MATURITY' : 'PREMATURE';
+ c.principalBankGlAccountId = f?.bankGlAccountId ?? null;
+ c.interestBankGlAccountId = f?.bankGlAccountId ?? null;
+ c.newPayout = f?.payout ?? 'MATURITY';
+ c.newCalcType = f?.calcType ?? 'QUARTERLY';
+ this.refreshClosureQuote();
+ }
+
+ onNewPeriodChange(): void {
+ const c = this.closure;
+ const start = this.closureFd?.maturityDate;
+ if (start && (c.newPeriodYears || c.newPeriodMonths || c.newPeriodDays)) {
+ c.newMaturityDate = addPeriod(start, c.newPeriodYears ?? 0, c.newPeriodMonths ?? 0, c.newPeriodDays ?? 0);
+ }
+ }
+
+ refreshClosureQuote(): void {
+ const c = this.closure;
+ this.closureError = '';
+ if (!c.fdId) {
+ this.closureQuote = null;
+ return;
+ }
+ const params: Record = { type: c.type };
+ if (c.type === 'PREMATURE' && c.closeDate) {
+ params['closeDate'] = c.closeDate;
+ }
+ if (c.receivedAmount) {
+ params['receivedAmount'] = c.receivedAmount;
+ }
+ if (c.tds) {
+ params['tds'] = c.tds;
+ }
+ if (c.type === 'RENEWAL' && c.renewalAmount) {
+ params['renewalAmount'] = c.renewalAmount;
+ }
+ this.investmentService.closureQuoteFd(c.fdId, params).subscribe({
+ next: (q) => (this.closureQuote = q),
+ error: (err) => {
+ this.closureQuote = null;
+ this.closureError = apiErrorMessage(err);
+ }
+ });
+ }
+
+ saveClosure(): void {
+ const c = this.closure;
+ const f = this.closureFd;
+ if (!f?.id) {
+ this.toast('Select the FD.');
+ return;
+ }
+ const base = { locale: 'en', dateFormat: 'dd-MM-yyyy', ...(c.remarks.trim() ? { remarks: c.remarks.trim() } : {}) };
+ const tds = c.tds ? { tds: c.tds } : {};
+ let body: object;
+ if (c.type === 'RENEWAL') {
+ if (!c.newCertNo.trim() || !c.newIntRate || !c.newMaturityDate) {
+ this.toast('Enter the new certificate no, interest rate and maturity date.');
+ return;
+ }
+ const topUp = this.closureTopUp;
+ if (topUp !== 0 && !c.diffBankGlAccountId) {
+ this.toast(
+ topUp > 0 ? 'Choose the bank the extra amount is paid from.' : 'Choose the bank the amount taken out goes to.'
+ );
+ return;
+ }
+ body = {
+ ...base,
+ ...tds,
+ newCertNo: c.newCertNo.trim(),
+ ...(c.newCertDate ? { newCertDate: c.newCertDate } : {}),
+ newIntRate: c.newIntRate,
+ newMaturityDate: c.newMaturityDate,
+ newPayout: c.newPayout,
+ newCalcType: c.newCalcType,
+ ...(c.receivedAmount ? { maturityAmount: c.receivedAmount } : {}),
+ ...(c.renewalAmount ? { renewalAmount: c.renewalAmount } : {}),
+ ...(topUp !== 0 && c.diffBankGlAccountId ? { diffBankGlAccountId: c.diffBankGlAccountId } : {})
+ };
+ } else {
+ if (c.type === 'PREMATURE' && (!c.closeDate || !c.receivedAmount)) {
+ this.toast('Enter the closure date and the amount received from the bank.');
+ return;
+ }
+ body = {
+ ...base,
+ ...tds,
+ ...(c.type === 'PREMATURE' ? { closeDate: c.closeDate } : {}),
+ ...(c.receivedAmount ? { receivedAmount: c.receivedAmount } : {}),
+ ...(c.type === 'PREMATURE' && c.penalAmount ? { penalAmount: c.penalAmount } : {}),
+ ...(c.principalBankGlAccountId ? { principalBankGlAccountId: c.principalBankGlAccountId } : {}),
+ ...(c.interestBankGlAccountId ? { interestBankGlAccountId: c.interestBankGlAccountId } : {})
+ };
+ }
+ this.closing = true;
+ this.investmentService.closeFd(f.id, c.type, body).subscribe({
+ next: (res) => {
+ this.closing = false;
+ const what =
+ c.type === 'RENEWAL'
+ ? `renewed as ${res.changes?.dealNo ?? ''}`
+ : c.type === 'PREMATURE'
+ ? 'closed'
+ : 'matured';
+ this.toast(`FD ${f.dealNo} ${what} — voucher ${res.changes?.voucherNo ?? res.resourceIdentifier ?? ''}.`);
+ this.closure = this.blankClosure();
+ this.closureQuote = null;
+ this.loadFds();
+ this.setTab('holdings');
+ },
+ error: (err) => {
+ this.closing = false;
+ this.toast(apiErrorMessage(err));
+ }
+ });
+ }
+
+ private blankClosure(): FdClosureForm {
+ return {
+ fdId: null,
+ type: 'MATURITY',
+ closeDate: todayDDMMYYYY(),
+ receivedAmount: null,
+ tds: null,
+ penalAmount: null,
+ principalBankGlAccountId: null,
+ interestBankGlAccountId: null,
+ newCertNo: '',
+ newCertDate: '',
+ newIntRate: null,
+ newPeriodYears: null,
+ newPeriodMonths: null,
+ newPeriodDays: null,
+ newMaturityDate: '',
+ newPayout: 'MATURITY',
+ newCalcType: 'QUARTERLY',
+ renewalAmount: null,
+ diffBankGlAccountId: null,
+ remarks: ''
+ };
+ }
+
+ bankGlName(id: number | null): string {
+ const g = this.bankGlOptions.find((x) => x.id === id);
+ return g ? `${g.glCode} ${g.name}` : '';
+ }
+
+ private blankForm(): FdPurchaseForm {
+ return {
+ partyId: null,
+ certNo: '',
+ certDate: '',
+ dealDate: todayDDMMYYYY(),
+ amount: null,
+ intRate: null,
+ periodYears: null,
+ periodMonths: null,
+ periodDays: null,
+ maturityDate: '',
+ payout: 'MATURITY',
+ calcType: 'QUARTERLY',
+ bankGlAccountId: null,
+ remarks: ''
+ };
}
private toast(msg: string): void {
@@ -557,6 +584,6 @@ export class FdInvestmentComponent {
if (this.toastTimer) {
clearTimeout(this.toastTimer);
}
- this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
+ this.toastTimer = setTimeout(() => (this.toastVisible = false), 4000);
}
}
diff --git a/src/app/investment/investment-data.ts b/src/app/investment/investment-data.ts
index 77d7e95a7..0310b6698 100644
--- a/src/app/investment/investment-data.ts
+++ b/src/app/investment/investment-data.ts
@@ -1906,6 +1906,10 @@ export interface PartyMaster {
glAccountId?: number;
glCode?: string;
glName?: string;
+ /** Interest income GL of the FDs placed with this bank (Bond Type FD). */
+ interestGlAccountId?: number;
+ interestGlCode?: string;
+ interestGlName?: string;
rbiCode?: string;
/** Exposure limit for this party (₹). */
limit?: number | null;
diff --git a/src/app/investment/investment-masters/investment-masters.component.html b/src/app/investment/investment-masters/investment-masters.component.html
index f4e835753..10b5458ca 100644
--- a/src/app/investment/investment-masters/investment-masters.component.html
+++ b/src/app/investment/investment-masters/investment-masters.component.html
@@ -162,6 +162,17 @@
}
+ @if (partyForm.bondType === 'FD') {
+
+ * Interest GL
+
+ -- Select --
+ @for (g of interestGlOptions; track g.id) {
+ {{ g.glCode }} — {{ g.name }}
+ }
+
+
+ }
RBI Code
Limit (₹) = {};
newBroker: Partial = {};
newControlParam: Partial = {};
@@ -143,6 +145,10 @@ export class InvestmentMastersComponent implements OnInit {
)),
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
});
+ this.investmentService.getIncomeGlAccounts().subscribe({
+ next: (gls) => (this.interestGlOptions = gls.filter((g) => g.glCode.startsWith('4004'))),
+ error: (err) => this.toast(`Could not load income GLs: ${apiErrorMessage(err)}`)
+ });
this.reloadMasters();
}
@@ -182,6 +188,10 @@ export class InvestmentMastersComponent implements OnInit {
this.toast('Enter Bond Type, Party Name and GL A/c No.');
return;
}
+ if (f.bondType === 'FD' && !f.interestGlAccountId) {
+ this.toast('An FD bank needs its Interest GL as well.');
+ return;
+ }
const body = {
bondType: f.bondType,
partyName: f.partyName,
@@ -194,6 +204,7 @@ export class InvestmentMastersComponent implements OnInit {
fax: f.fax ?? '',
bankType: f.bankType ?? '',
glAccountId: Number(f.glAccountId),
+ interestGlAccountId: f.bondType === 'FD' && f.interestGlAccountId ? Number(f.interestGlAccountId) : null,
rbiCode: f.rbiCode ?? '',
limit: f.limit ?? null,
leiNo: f.leiNo ?? '',
diff --git a/src/app/investment/investment.service.ts b/src/app/investment/investment.service.ts
index 4a450142e..d1b16434b 100644
--- a/src/app/investment/investment.service.ts
+++ b/src/app/investment/investment.service.ts
@@ -39,6 +39,9 @@ export interface InvestmentPartyDto {
glAccountId: number;
glCode: string;
glName: string;
+ interestGlAccountId?: number;
+ interestGlCode?: string;
+ interestGlName?: string;
rbiCode?: string;
limit?: number;
leiNo?: string;
@@ -49,6 +52,126 @@ export interface InvestmentPartyDto {
active: boolean;
}
+/** Where an FD stands against the RBI limits on deposits with other banks (20% overall, 5% with one bank). */
+export interface InvestmentFdLimitDto {
+ asOnDate: string;
+ totalDeposits?: number;
+ glTotalDeposits: number;
+ grossLimit?: number;
+ fdOutstanding: number;
+ bankBalances: number;
+ grossAfter: number;
+ bankLimit?: number;
+ partyName?: string;
+ bankAfter: number;
+ newAmount: number;
+ withinLimits: boolean;
+ message: string;
+}
+
+export type FdPayout = 'MATURITY' | 'HALF_YEARLY' | 'YEARLY';
+export type FdCalcType = 'SIMPLE' | 'MONTHLY' | 'QUARTERLY' | 'HALF_YEARLY' | 'YEARLY';
+
+/** An FD the bank has placed with another bank (dates DD-MM-YYYY); also the purchase preview. */
+export interface InvestmentFdDto {
+ id?: number;
+ dealNo?: string;
+ partyId?: number;
+ partyCode?: string;
+ partyName?: string;
+ certNo?: string;
+ certDate?: string;
+ dealDate?: string;
+ principal?: number;
+ intRate?: number;
+ maturityDate?: string;
+ days: number;
+ payout: FdPayout;
+ calcType: FdCalcType;
+ expectedInterest?: number;
+ expectedMaturityAmount?: number;
+ periodInterest?: number;
+ fdGlCode?: string;
+ fdGlName?: string;
+ interestGlCode?: string;
+ interestGlName?: string;
+ bankGlAccountId?: number;
+ bankGlCode?: string;
+ bankGlName?: string;
+ status?: 'ACTIVE' | 'MATURED' | 'PREMATURE' | 'RENEWED';
+ remarks?: string;
+ glTransactionId?: string;
+ voucherNo?: string;
+ renewedFromDealNo?: string;
+ closedOnDate?: string;
+ closureType?: string;
+ closureAmount?: number;
+ closureInterest?: number;
+ closureTds?: number;
+ closurePenal?: number;
+ closureVoucherNo?: string;
+ closureGlTransactionId?: string;
+ limit?: InvestmentFdLimitDto;
+}
+
+export type FdClosureType = 'MATURITY' | 'PREMATURE' | 'RENEWAL';
+
+/** One interest line of an FD: a 31 Mar / 30 Sep provision, a payout, or the maturity. */
+export interface InvestmentFdInterestRowDto {
+ lineType: 'PROVISION' | 'PAYOUT' | 'MATURITY';
+ intDate: string;
+ fromDate: string;
+ toDate: string;
+ days: number;
+ grossInterest: number;
+ alreadyBooked?: number;
+ tds?: number;
+ amount: number;
+ posted: boolean;
+ voucherNo?: string;
+ glTransactionId?: string;
+ postable: boolean;
+}
+
+export interface InvestmentFdScheduleDto {
+ fd: InvestmentFdDto;
+ provisioned: number;
+ rows: InvestmentFdInterestRowDto[];
+}
+
+/** The next interest voucher (or the maturity) of an FD that is due. */
+export interface InvestmentFdDueDto {
+ fdId: number;
+ dealNo: string;
+ partyName: string;
+ certNo: string;
+ lineType: 'PROVISION' | 'PAYOUT' | 'MATURITY';
+ intDate: string;
+ amount: number;
+ postable: boolean;
+ overdue: boolean;
+}
+
+/** What closing an FD works out to, and the voucher it will post. */
+export interface InvestmentFdClosureDto {
+ fdId: number;
+ dealNo: string;
+ type: FdClosureType;
+ closeDate: string;
+ principal: number;
+ expectedInterest: number;
+ provisioned: number;
+ receivedAmount: number;
+ tds: number;
+ penalAmount?: number;
+ interest: number;
+ income: number;
+ difference: number;
+ renewalAmount?: number;
+ topUp?: number;
+ lines: Array<{ side: 'Dr' | 'Cr'; glCode: string; glName: string; amount: number }>;
+}
+
/** Investment script as returned by /investment/scripts (dates are DD-MM-YYYY). */
export interface InvestmentScriptDto {
id: number;
@@ -299,6 +422,77 @@ export class InvestmentService {
return this.http.delete(`/investment/purchases/${id}`);
}
+ // -------------------- FD with other banks --------------------
+ getFds(): Observable {
+ return this.http.get('/investment/fds');
+ }
+
+ /** Interest, maturity amount and the RBI limit check for an FD before it is saved. */
+ quoteFd(params: Record): Observable {
+ return this.http.get('/investment/fds/quote', { params });
+ }
+
+ /** Places the FD: Dr the bank's FD GL / Cr our bank GL at once. */
+ createFd(
+ body: object
+ ): Observable<{ resourceIdentifier?: string; changes?: { dealNo?: string; voucherNo?: string } }> {
+ return this.http.post<{ resourceIdentifier?: string; changes?: { dealNo?: string; voucherNo?: string } }>(
+ '/investment/fds',
+ body
+ );
+ }
+
+ closureQuoteFd(fdId: number, params: Record): Observable {
+ return this.http.get(`/investment/fds/${fdId}/closure-quote`, { params });
+ }
+
+ /** Maturity, premature closure or renewal of an FD (voucher posted at once). */
+ closeFd(
+ fdId: number,
+ type: FdClosureType,
+ body: object
+ ): Observable<{ resourceIdentifier?: string; changes?: { dealNo?: string; voucherNo?: string; interest?: number } }> {
+ const path = type === 'MATURITY' ? 'mature' : type === 'PREMATURE' ? 'premature' : 'renew';
+ return this.http.post<{
+ resourceIdentifier?: string;
+ changes?: { dealNo?: string; voucherNo?: string; interest?: number };
+ }>(`/investment/fds/${fdId}/${path}`, body);
+ }
+
+ getFdSchedule(fdId: number): Observable {
+ return this.http.get(`/investment/fds/${fdId}/interest-schedule`);
+ }
+
+ /** Posts the next interest line of an FD (provision, or payout with the bank's amount and TDS). */
+ postFdInterest(fdId: number, body: object): Observable<{ resourceIdentifier?: string }> {
+ return this.http.post<{ resourceIdentifier?: string }>(`/investment/fds/${fdId}/interest`, body);
+ }
+
+ /** FD interest vouchers and maturities due now or within two days. */
+ getFdDue(): Observable {
+ return this.http.get('/investment/fds/due');
+ }
+
+ /** Records total deposits as on a 31 March, the base of the RBI limits. */
+ setDepositBase(body: object): Observable {
+ return this.http.post('/investment/fds/deposit-base', body);
+ }
+
+ /** Enabled INCOME detail GLs — an FD bank's interest GL. */
+ getIncomeGlAccounts(): Observable {
+ return this.http
+ .get<
+ Array<{ id: number; glCode: string; name: string }>
+ >('/glaccounts', { params: { type: '4', usage: '1', disabled: 'false' } })
+ .pipe(
+ map((list) =>
+ list
+ .map((g) => ({ id: g.id, glCode: g.glCode, name: g.name }))
+ .sort((a, b) => a.glCode.localeCompare(b.glCode))
+ )
+ );
+ }
+
// -------------------- GL accounts --------------------
/** Enabled ASSET detail GLs — the only kind an investment party / script can be booked to. */
getAssetGlAccounts(): Observable {
@@ -345,6 +539,9 @@ export function toPartyMaster(p: InvestmentPartyDto): PartyMaster {
glAccountId: p.glAccountId,
glCode: p.glCode,
glName: p.glName,
+ interestGlAccountId: p.interestGlAccountId,
+ interestGlCode: p.interestGlCode,
+ interestGlName: p.interestGlName,
rbiCode: p.rbiCode,
limit: p.limit ?? null,
leiNo: p.leiNo,