/** * 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/. */ /** Angular Imports */ import { Component, OnInit, Input, Output, EventEmitter, OnChanges } from '@angular/core'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { MatDivider } from '@angular/material/divider'; import { NgIf, NgSwitch, TitleCasePipe, CurrencyPipe } from '@angular/common'; import { MatTable, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatCellDef, MatCell, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow } from '@angular/material/table'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatStepperPrevious } from '@angular/material/stepper'; import { FindPipe } from '../../../../pipes/find.pipe'; import { DateFormatPipe } from '../../../../pipes/date-format.pipe'; import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; import { YesnoPipe } from '../../../../pipes/yesno.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** * Fixed Deposit Preview Step */ @Component({ selector: 'mifosx-fixed-deposit-account-preview-step', templateUrl: './fixed-deposit-account-preview-step.component.html', styleUrls: ['./fixed-deposit-account-preview-step.component.scss'], animations: [ trigger('expandChartSlab', [ state('collapsed', style({ height: '0px', minHeight: '0' })), state('expanded', style({ height: '*' })), transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')) ]) ], imports: [ ...STANDALONE_SHARED_IMPORTS, MatDivider, MatTable, MatColumnDef, MatHeaderCellDef, MatHeaderCell, MatCellDef, MatCell, FaIconComponent, NgSwitch, MatHeaderRowDef, MatHeaderRow, MatRowDef, MatRow, MatStepperPrevious, TitleCasePipe, CurrencyPipe, FindPipe, DateFormatPipe, FormatNumberPipe, YesnoPipe ] }) export class FixedDepositAccountPreviewStepComponent implements OnChanges { /** Fixed Deposits Account Template */ @Input() fixedDepositsAccountTemplate: any; @Input() hideButtons = false; /** Fixed Deposits Account Product Template */ @Input() fixedDepositsAccountProductTemplate: any; /** Fixed Deposits Account Data */ @Input() fixedDepositAccountData: any; @Input() fdNominees: any[] = []; /** Tenure selected from the interest rate chart step (includes pre-calculated maturity if user used calculator) */ @Input() selectedTenure: { depositPeriod: number; depositPeriodFrequencyId: number; depositAmount?: number; maturityAmount?: number; interestEarned?: number; appliedRate?: number; isSeniorCitizen?: boolean; } | null = null; get appliedInterestRate(): number { if (this.selectedTenure?.appliedRate != null) return this.selectedTenure.appliedRate; if (this.matchedSlabIndex >= 0) return this.interestRateChartData[this.matchedSlabIndex]?.annualInterestRate ?? 0; return 0; } get isSeniorCitizenSelected(): boolean { return !!this.selectedTenure?.isSeniorCitizen; } get seniorCitizenBonus(): number { if (!this.isSeniorCitizenSelected || this.selectedTenure?.appliedRate == null) return 0; if (this.matchedSlabIndex < 0) return 0; const normalRate = this.interestRateChartData[this.matchedSlabIndex]?.annualInterestRate ?? 0; return this.selectedTenure.appliedRate - normalRate; } /** Charges Displayed Columns */ chargesDisplayedColumns: string[] = [ 'name', 'chargeCalculationType', 'amount', 'chargeTimeType', 'date', 'repaymentsEvery' ]; /** Interest Rate Chart Data */ interestRateChartData: any = []; /** Columns to be displayed in interest rate chart table. */ chartSlabsDisplayedColumns: any[] = [ 'period', 'amountRange', 'interest', 'description', 'actions' ]; /** Columns to be displayed in incentives sub-table. */ incentivesDisplayedColumns: string[] = [ 'entityType', 'attributeName', 'conditionType', 'attributeValue', 'incentiveType', 'amount' ]; /** Additional Column to disblac incentives table */ chartSlabsIncentivesDisplayedColumns: string[] = ['incentives']; /** Expand Chart Slab Index used in the view */ expandChartSlabIndex: number; readonly fdTypeLabels: Record = { SINGLE: 'FD Single (Days < 1 Year)', DOUBLE: 'FD Double (≥ 1 Year)', SPECIAL: 'FD Special (Monthly Interest Payout)' }; /** FD Special pays its interest out monthly; the principal alone comes back at maturity. */ get isSpecial(): boolean { return this.fixedDepositsAccountProductTemplate?.fdType === 'SPECIAL'; } getFdTypeLabel(fdType: string): string { return this.fdTypeLabels[fdType] || fdType; } get currencyName(): string { return this.fixedDepositsAccountProductTemplate?.currency?.name || '—'; } get decimalPlaces(): string | number { return this.fixedDepositsAccountProductTemplate?.currency?.decimalPlaces ?? '—'; } get inMultiplesOf(): string | number { return this.fixedDepositsAccountProductTemplate?.currency?.inMultiplesOf ?? '—'; } /** Form submission event */ @Output() submitEvent = new EventEmitter(); /** Maturity/pre-mature-closure choices made in the Terms step -- shown here read-only. */ @Input() closureSettings: any = {}; get closureMaturityInstructionLabel(): string { const options = this.fixedDepositsAccountProductTemplate?.maturityInstructionOptions ?? []; const match = options.find((o: any) => o.id === this.closureSettings?.maturityInstructionId); return match?.value ?? '—'; } get closureTransferToSavingsLabel(): string { const accounts = this.fixedDepositsAccountProductTemplate?.savingsAccounts ?? []; const match = accounts.find((a: any) => a.id === this.closureSettings?.transferToSavingsId); return match ? `(${match.accountNo}) ${match.savingsProductName}` : '—'; } get closurePenalInterestOnTypeLabel(): string { const options = this.fixedDepositsAccountProductTemplate?.preClosurePenalInterestOnTypeOptions ?? []; const match = options.find((o: any) => o.id === this.closureSettings?.preClosurePenalInterestOnTypeId); return match?.value ?? '—'; } ngOnChanges() { if (this.fixedDepositsAccountProductTemplate) { this.interestRateChartData = this.fixedDepositsAccountProductTemplate.accountChart?.chartSlabs ?? []; } } get matchedSlabIndex(): number { if (!this.interestRateChartData?.length) return -1; let depositPeriod: number; let depositPeriodFrequencyId: number; if (this.selectedTenure) { depositPeriod = this.selectedTenure.depositPeriod; depositPeriodFrequencyId = this.selectedTenure.depositPeriodFrequencyId; } else if ( this.fixedDepositsAccountProductTemplate?.minDepositTerm != null && this.fixedDepositsAccountProductTemplate?.minDepositTermType?.id != null ) { depositPeriod = this.fixedDepositsAccountProductTemplate.minDepositTerm; depositPeriodFrequencyId = this.fixedDepositsAccountProductTemplate.minDepositTermType.id; } else { return -1; } // Convert selected tenure to days for comparison const toDays = (v: number, id: number) => { if (id === 1) return v * 7; // Weeks if (id === 2) return v * 30; // Months if (id === 3) return v * 365; // Years return v; // Days (id=0) }; const enteredDays = toDays(depositPeriod, depositPeriodFrequencyId); return this.interestRateChartData.findIndex((slab: any) => { const periodTypeValue = (slab.periodType?.value || '').toLowerCase(); const slabId = periodTypeValue.includes('month') ? 2 : periodTypeValue.includes('year') ? 3 : periodTypeValue.includes('week') ? 1 : 0; const fromDays = toDays(slab.fromPeriod ?? 0, slabId); const toDaysVal = slab.toPeriod != null ? toDays(slab.toPeriod, slabId) : Infinity; return enteredDays >= fromDays && enteredDays <= toDaysVal; }); } get selectedTenureLabel(): string { let depositPeriod: number; let depositPeriodFrequencyId: number; if (this.selectedTenure) { depositPeriod = this.selectedTenure.depositPeriod; depositPeriodFrequencyId = this.selectedTenure.depositPeriodFrequencyId; } else if ( this.fixedDepositsAccountProductTemplate?.minDepositTerm != null && this.fixedDepositsAccountProductTemplate?.minDepositTermType?.id != null ) { depositPeriod = this.fixedDepositsAccountProductTemplate.minDepositTerm; depositPeriodFrequencyId = this.fixedDepositsAccountProductTemplate.minDepositTermType.id; } else { return ''; } const unitMap: Record = { 0: 'Days', 1: 'Weeks', 2: 'Months', 3: 'Years' }; return `${depositPeriod} ${unitMap[depositPeriodFrequencyId] ?? ''}`; } get maturitySummary(): { depositAmount: number; maturityAmount: number; totalInterest: number; interestRate: number; tenureLabel: string; } { let depositPeriod = 0; let depositPeriodFrequencyId = 0; if (this.selectedTenure) { depositPeriod = this.selectedTenure.depositPeriod; depositPeriodFrequencyId = this.selectedTenure.depositPeriodFrequencyId; } else if ( this.fixedDepositsAccountProductTemplate?.minDepositTerm != null && this.fixedDepositsAccountProductTemplate?.minDepositTermType?.id != null ) { depositPeriod = this.fixedDepositsAccountProductTemplate.minDepositTerm; depositPeriodFrequencyId = this.fixedDepositsAccountProductTemplate.minDepositTermType.id; } const depositAmount = this.selectedTenure?.depositAmount ?? this.fixedDepositAccountData?.depositAmount ?? this.fixedDepositsAccountProductTemplate?.depositAmount ?? 0; // If the chart step already calculated maturity, use those values directly if (this.selectedTenure?.maturityAmount != null && this.selectedTenure.maturityAmount > 0) { const interestRate = this.selectedTenure.appliedRate ?? this.fixedDepositsAccountProductTemplate?.nominalAnnualInterestRate ?? 0; return { depositAmount, maturityAmount: this.selectedTenure.maturityAmount, totalInterest: this.selectedTenure.interestEarned ?? this.selectedTenure.maturityAmount - depositAmount, interestRate, tenureLabel: this.selectedTenureLabel }; } // Fallback: recalculate using product template settings const chartRate = this.matchedSlabIndex >= 0 ? this.interestRateChartData[this.matchedSlabIndex]?.annualInterestRate : null; const interestRate = chartRate ?? this.fixedDepositsAccountProductTemplate?.nominalAnnualInterestRate ?? 0; const r = interestRate / 100; // Tenure in years (t) let t = 0; const daysInYear = this.fixedDepositsAccountProductTemplate?.interestCalculationDaysInYearType?.id === 360 ? 360 : 365; if (depositPeriodFrequencyId === 0) { t = depositPeriod / daysInYear; } else if (depositPeriodFrequencyId === 1) { t = (depositPeriod * 7) / daysInYear; } else if (depositPeriodFrequencyId === 2) { t = depositPeriod / 12; } else if (depositPeriodFrequencyId === 3) { t = depositPeriod; } const compPeriodValue = ( this.fixedDepositsAccountProductTemplate?.interestCompoundingPeriodType?.value || '' ).toLowerCase(); let n = 1; if (compPeriodValue.includes('daily')) n = daysInYear; else if (compPeriodValue.includes('monthly')) n = 12; else if (compPeriodValue.includes('quarterly')) n = 4; else if (compPeriodValue.includes('half')) n = 2; else if (compPeriodValue.includes('biannual')) n = 2; else if (compPeriodValue.includes('annual')) n = 1; const calcTypeValue = ( this.fixedDepositsAccountProductTemplate?.interestCalculationType?.value || '' ).toLowerCase(); const isSimple = calcTypeValue.includes('simple'); let maturityAmount = 0; if (isSimple) { maturityAmount = depositAmount + depositAmount * r * t; } else { maturityAmount = depositAmount * Math.pow(1 + r / n, n * t); } const totalInterest = maturityAmount - depositAmount; return { depositAmount, maturityAmount, totalInterest, interestRate, tenureLabel: this.selectedTenureLabel }; } }