Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
314 lines
10 KiB
TypeScript
314 lines
10 KiB
TypeScript
/**
|
|
* 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;
|
|
/** Fixed Deposits Account Product Template */
|
|
@Input() fixedDepositsAccountProductTemplate: any;
|
|
/** Fixed Deposits Account Data */
|
|
@Input() fixedDepositAccountData: 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;
|
|
} | null = null;
|
|
|
|
/** 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<string, string> = {
|
|
SINGLE: 'FD Single (Days < 1 Year)',
|
|
DOUBLE: 'FD Double (≥ 1 Year)',
|
|
SPECIAL: 'FD Special (Monthly Interest Payout)'
|
|
};
|
|
|
|
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();
|
|
|
|
constructor() {}
|
|
|
|
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<number, string> = { 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
|
|
};
|
|
}
|
|
}
|