diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.html index 8eab0d0bc..a38b4a58e 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.html @@ -331,12 +331,14 @@ {{ matchResult.appliedRate | number: '1.2-2' }}% p.a.
- Interest Earned + {{ + isSpecial ? 'Total Interest (paid out monthly)' : 'Interest Earned' + }} ₹ {{ matchResult.interestEarned | number: '1.2-2' }}
@if (isSpecial && matchResult.estimatedMonthlyPayout) {
- Monthly Payout + Monthly Payout (approx., by days in the month) ₹ {{ matchResult.estimatedMonthlyPayout | number: '1.2-2' }} / month @@ -351,13 +353,64 @@ } - ₹ {{ (isSpecial ? principalAmount : matchResult.maturityAmount) | number: '1.2-2' }} + ₹ {{ matchResult.maturityAmount | number: '1.2-2' }}
+ @if (isSpecial && specialPreview) { +
+ Maturity Date + {{ + specialPreview.maturityDate | date: 'dd MMM yyyy' + }} +
+
+ Monthly payouts + {{ specialPreview.numberOfPayouts }} +
+
+ Principal + all payouts + ₹ {{ specialPreview.totalValueAtMaturity | number: '1.2-2' }} +
+ } + + @if (isSpecial && specialPreview) { +
+
+ Monthly Interest Payout Schedule + +
+
+ From {{ specialPreview.startDate | date: 'dd MMM yyyy' }} (the day the account is opened) to + {{ specialPreview.maturityDate | date: 'dd MMM yyyy' }}, worked out by the account engine from the + product. +
+
+ } } - @if (!matchResult.maturityAmount) { + @if (specialPreviewLoading) { +
+ + Working out the maturity from the product… +
+ } + @if (specialPreviewError) { +
+ + {{ specialPreviewError }} +
+ } + @if (!matchResult.maturityAmount && !specialPreviewLoading && !specialPreviewError) {
Enter a deposit amount above to see the maturity calculation. diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.scss b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.scss index 79daea4d9..73759cd9c 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.scss +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.scss @@ -1229,3 +1229,72 @@ $red-light: #fee2e2; font-weight: 700; } } + +/* FD Special: payout schedule from the backend preview */ +.fdrc-schedule { + margin-top: 12px; + border: 1px solid #bbf7d0; + border-radius: 10px; + background: #fff; + overflow: hidden; + + &__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px 14px; + background: #f0fdf4; + font-size: 12px; + font-weight: 700; + color: #166534; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + &__note { + padding: 6px 14px; + font-size: 12px; + color: #475569; + } + + &__wrap { + max-height: 360px; + overflow: auto; + } + + &__table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + th, + td { + padding: 6px 14px; + border-top: 1px solid #e2e8f0; + text-align: left; + } + + th { + position: sticky; + top: 0; + background: #f8fafc; + font-size: 11px; + text-transform: uppercase; + color: #64748b; + } + + .num { + text-align: right; + } + + tfoot td { + font-weight: 700; + background: #f0fdf4; + } + } +} + +.fdrc-payout-hint--error { + color: #b91c1c; +} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts index 666f126d5..7bd845d30 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component.ts @@ -36,12 +36,39 @@ import { FormsModule } from '@angular/forms'; import { DateFormatPipe } from '../../../../pipes/date-format.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { FixedDepositsService } from '../../fixed-deposits.service'; +import { MatDialog } from '@angular/material/dialog'; +import { InterestScheduleDialogComponent } from '../../fixed-deposit-account-view/custom-dialogs/interest-schedule-dialog/interest-schedule-dialog.component'; import { ClientsService } from 'app/clients/clients.service'; import { SettingsService } from 'app/settings/settings.service'; import { SENIOR_CITIZEN_AGE_THRESHOLD, deriveSeniorCitizenStatus } from 'app/clients/shared/client-age.util'; export type FdPeriodUnit = 'Days' | 'Months' | 'Years'; +/** + * yyyy-MM-dd of the calendar day as the user sees it. toISOString() gives the UTC day instead, which in India is the day + * before for any date at local midnight - so a deposit opened on the 21st was quoted from the 20th. + */ +function localIsoDate(d: Date): string { + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; +} + +/** FD quote from POST fixeddepositaccounts/account-preview (worked out by the account engine, every FD type). */ +export interface SpecialPayoutPreview { + fdType: string; + depositAmount: number; + annualInterestRate: number; + seniorCitizen: boolean; + startDate: string; + maturityDate: string; + numberOfPayouts: number; + fullMonthPayout: number; + totalInterest: number; + maturityAmount: number; + totalValueAtMaturity: number; + periods: { period: number; fromDate: string; toDate: string; days: number; interestAmount: number }[]; +} + export interface SlabMatchResult { slab: any; slabIndex: number; @@ -87,6 +114,7 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan private clientsService = inject(ClientsService); private settingsService = inject(SettingsService); private cdr = inject(ChangeDetectorRef); + private dialog = inject(MatDialog); @Input() fixedDepositsAccountTemplate: any; @Input() fixedDepositsAccountProductTemplate: any; @@ -163,6 +191,10 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan } matchResult: SlabMatchResult | null = null; + /** FD Special only: the backend's quote and payout schedule for the tenure and amount entered. */ + specialPreview: SpecialPayoutPreview | null = null; + specialPreviewLoading = false; + specialPreviewError: string | null = null; validationWarning: string | null = null; // ── FD Type labels ────────────────────────────────────────────────────── @@ -270,6 +302,8 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan calculateRate() { this.validationWarning = null; this.matchResult = null; + this.specialPreview = null; + this.specialPreviewError = null; if (!this.tenureValue || this.tenureValue <= 0) { this.validationWarning = 'Please enter a valid tenure value greater than 0.'; @@ -334,30 +368,6 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan const frequencyId = this.resolveFrequencyId(this.tenurePeriodUnit); if (this.principalAmount && this.principalAmount > 0) { - // Years and Months convert exactly; a Days tenure is sent as days and never converted. - let tenureInMonths: number | undefined; - let tenureInDays: number | undefined; - if (this.tenurePeriodUnit === 'Years') { - tenureInMonths = this.tenureValue * 12; - } else if (this.tenurePeriodUnit === 'Months') { - tenureInMonths = this.tenureValue; - } else { - // Sent as days, whatever the count. Converting to whole months silently dropped the - // remainder — a 200-day deposit quoted as six months lost 20 days of interest, and the - // screen still said "200 Days" above the figure. The backend takes days directly and - // works the calendar out itself. - tenureInDays = enteredDays; - } - - // Map compounding period type id → months (0=Daily, 1=Daily, 4=Monthly, 5=Quarterly, 6=SemiAnnual, 7=Annual) - const compMap: Record = { 1: 0, 4: 1, 5: 3, 6: 6, 7: 12 }; - const compId = this.fixedDepositsAccountProductTemplate?.interestCompoundingPeriodType?.id ?? 4; - const compMonths = compMap[compId] ?? 1; - - // Map posting period type id → months - const postId = this.fixedDepositsAccountProductTemplate?.interestPostingPeriodType?.id ?? 4; - const postMonths = compMap[postId] ?? 1; - const emitResult = () => { if (frequencyId !== null) { this.tenureSelected.emit({ @@ -375,7 +385,7 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan // Get submittedOnDate — Input takes priority over template data let submittedOnDateStr: string; if (this.submittedOnDate) { - submittedOnDateStr = this.submittedOnDate.toISOString().split('T')[0]; + submittedOnDateStr = localIsoDate(this.submittedOnDate); } else { const submittedOnDateRaw = this.fixedDepositsAccountTemplate?.timeline?.submittedOnDate ?? @@ -384,63 +394,18 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan const d = Array.isArray(submittedOnDateRaw) ? new Date(submittedOnDateRaw[0], submittedOnDateRaw[1] - 1, submittedOnDateRaw[2]) : new Date(submittedOnDateRaw); - submittedOnDateStr = !isNaN(d.getTime()) - ? d.toISOString().split('T')[0] - : new Date().toISOString().split('T')[0]; + submittedOnDateStr = !isNaN(d.getTime()) ? localIsoDate(d) : localIsoDate(new Date()); } else { - submittedOnDateStr = new Date().toISOString().split('T')[0]; + submittedOnDateStr = localIsoDate(new Date()); } } - const daysInYearVal = - this.fixedDepositsAccountProductTemplate?.interestCalculationDaysInYearType?.id === 360 ? 360 : 365; - - // Always use backend API for accurate calculation (including daily compounding with 0) - this.fixedDepositsService - .calculateFixedDepositInterest({ - principalAmount: this.principalAmount, - annualInterestRate: rate, - tenureInMonths, - tenureInDays, - interestCompoundingPeriodInMonths: compMonths, // 0 = daily, now supported by backend - interestPostingPeriodInMonths: postMonths, - submittedOnDate: submittedOnDateStr, - daysInYear: daysInYearVal - }) - .subscribe({ - next: (apiResult: any) => { - result.maturityAmount = apiResult.maturityAmount; - result.interestEarned = - apiResult.totalInterest ?? Math.round((apiResult.maturityAmount - this.principalAmount) * 100) / 100; - this.matchResult = { ...result }; - emitResult(); - }, - error: () => { - // Fallback if API fails: frontend formula - const r = rate / 100; - if (compMonths === 0) { - // Daily compounding fallback - result.maturityAmount = - Math.round(this.principalAmount * Math.pow(1 + r / daysInYearVal, enteredDays) * 100) / 100; - } else { - const n = 12 / compMonths; - const effectiveMonths = tenureInMonths ?? enteredDays / 30; - const rPerCompounding = rate / (n * 100); - // Whole compounding periods, then simple interest on the days left over. Flooring the - // periods and stopping there dropped the remainder entirely: a 200-day deposit - // compounding monthly quoted only six months of interest and lost the other twenty - // days, which is the depositor's money. - const wholePeriods = Math.floor(effectiveMonths / compMonths); - const compounded = this.principalAmount * Math.pow(1 + rPerCompounding, wholePeriods); - const leftoverDays = enteredDays - wholePeriods * compMonths * 30; - const leftoverInterest = - leftoverDays > 0 ? (compounded * rate * leftoverDays) / (100 * daysInYearVal) : 0; - result.maturityAmount = Math.round((compounded + leftoverInterest) * 100) / 100; - } - result.interestEarned = Math.round((result.maturityAmount - this.principalAmount) * 100) / 100; - this.matchResult = { ...result }; - emitResult(); - } - }); + // The whole quote - rate, maturity date, maturity amount and, for FD Special, the month-by-month payout + // schedule - comes from the backend, which builds the would-be account from the product exactly as Submit does + // (its posting periods, compounding, days in year, rate card, senior-citizen rate) and runs the account's own + // interest engine. Nothing is worked out here: on an error the quote is simply not shown, never estimated. + if (frequencyId !== null) { + this.loadAccountPreview(result, rate, submittedOnDateStr, frequencyId, emitResult); + } } else { this.matchResult = result; if (frequencyId !== null) { @@ -457,6 +422,96 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan } } + /** The backend's payout schedule in the same popup the account itself uses, in its quote (preview) mode. */ + openSpecialSchedule(): void { + if (!this.specialPreview) return; + this.dialog.open(InterestScheduleDialogComponent, { + width: '760px', + data: { + preview: true, + currencyCode: this.fixedDepositsAccountProductTemplate?.currency?.code ?? 'INR', + note: + 'Worked out by the account engine from the product, starting ' + + this.specialPreview.startDate + + ' (the day the account is opened) and ending the day before maturity on ' + + this.specialPreview.maturityDate + + '. The first and last periods are part-months; each pays for the days it covers.', + schedule: this.specialPreview.periods.map((p) => ({ + fromDate: p.fromDate, + toDate: p.toDate, + days: p.days, + interestAmount: Number(p.interestAmount), + paid: false + })) + } + }); + } + + /** + * Asks the backend for the quote (POST fixeddepositaccounts/account-preview) with the same terms Submit sends: the + * product's own interest terms, and the senior-citizen rate the same way Submit overrides it. The rate, maturity + * amount and interest shown are the backend's, so they match the account once it is created. + */ + private loadAccountPreview( + result: SlabMatchResult, + rate: number, + submittedOnDateStr: string, + frequencyId: number, + emitResult: () => void + ): void { + const pt = this.fixedDepositsAccountProductTemplate ?? {}; + const body: any = { + clientId: pt.clientId ?? this.fixedDepositsAccountTemplate?.clientId, + productId: pt.depositProductId ?? pt.productId ?? this.fixedDepositsAccountTemplate?.productId, + submittedOnDate: submittedOnDateStr, + dateFormat: 'yyyy-MM-dd', + locale: 'en', + depositAmount: this.principalAmount, + depositPeriod: this.tenureValue, + depositPeriodFrequencyId: frequencyId + }; + if (pt.nominalAnnualInterestRate != null) body.nominalAnnualInterestRate = pt.nominalAnnualInterestRate; + if (pt.interestCompoundingPeriodType?.id != null) + body.interestCompoundingPeriodType = pt.interestCompoundingPeriodType.id; + if (pt.interestPostingPeriodType?.id != null) body.interestPostingPeriodType = pt.interestPostingPeriodType.id; + if (pt.interestCalculationType?.id != null) body.interestCalculationType = pt.interestCalculationType.id; + if (pt.interestCalculationDaysInYearType?.id != null) + body.interestCalculationDaysInYearType = pt.interestCalculationDaysInYearType.id; + if (this.isSeniorCitizen && rate > 0) { + body.isSeniorCitizen = true; + body.seniorCitizenInterestRate = rate; + body.nominalAnnualInterestRate = rate; + } + + this.specialPreview = null; + this.specialPreviewError = null; + this.specialPreviewLoading = true; + this.fixedDepositsService.accountPreview(body).subscribe({ + next: (preview: SpecialPayoutPreview) => { + this.specialPreviewLoading = false; + this.specialPreview = preview; + result.appliedRate = Number(preview.annualInterestRate); + result.interestEarned = Number(preview.totalInterest); + result.estimatedMonthlyPayout = preview.fullMonthPayout != null ? Number(preview.fullMonthPayout) : undefined; + result.maturityAmount = Number(preview.maturityAmount); + this.matchResult = { ...result }; + emitResult(); + this.cdr.markForCheck(); + }, + error: (e: any) => { + this.specialPreviewLoading = false; + this.specialPreviewError = + e?.error?.errors?.[0]?.defaultUserMessage || + e?.error?.defaultUserMessage || + 'Could not work out the maturity. Check the deposit details and try again.'; + result.maturityAmount = undefined; + result.interestEarned = undefined; + this.matchResult = { ...result }; + this.cdr.markForCheck(); + } + }); + } + private resolveFrequencyId(unit: FdPeriodUnit): number | null { const options: any[] = this.fixedDepositsAccountProductTemplate?.periodFrequencyTypeOptions ?? []; const unitLower = unit.toLowerCase(); @@ -471,12 +526,16 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan this.tenureValue = null; this.principalAmount = null; this.matchResult = null; + this.specialPreview = null; + this.specialPreviewError = null; this.validationWarning = null; this.tenurePeriodUnit = this.allowedPeriodUnits[0]; } onPeriodUnitChange() { this.matchResult = null; + this.specialPreview = null; + this.specialPreviewError = null; this.validationWarning = null; } @@ -524,6 +583,8 @@ export class FixedDepositAccountInterestRateChartStepComponent implements OnChan this.calculateRate(); } else { this.matchResult = null; + this.specialPreview = null; + this.specialPreviewError = null; } } diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html index 98644bdb4..5b13cf1f4 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.html @@ -326,7 +326,7 @@
Inactive - @if (fixedDepositsAccountProductTemplate?.daysToInactive != null) { + @if ((fixedDepositsAccountProductTemplate?.daysToInactive ?? null) !== null) { After {{ fixedDepositsAccountProductTemplate.daysToInactive }} days of no transaction } @else { Not configured @@ -340,7 +340,7 @@
Dormant - @if (fixedDepositsAccountProductTemplate?.daysToDormancy != null) { + @if ((fixedDepositsAccountProductTemplate?.daysToDormancy ?? null) !== null) { {{ fixedDepositsAccountProductTemplate.daysToDormancy }} days after inactive } @else { Not configured @@ -354,7 +354,7 @@
Escheat - @if (fixedDepositsAccountProductTemplate?.daysToEscheat != null) { + @if ((fixedDepositsAccountProductTemplate?.daysToEscheat ?? null) !== null) { {{ fixedDepositsAccountProductTemplate.daysToEscheat }} days after dormant } @else { Not configured @@ -458,13 +458,15 @@ {{ maturitySummary.tenureLabel }}
- Estimated Interest Earned + {{ + isSpecial ? 'Total Interest (paid out monthly)' : 'Estimated Interest Earned' + }} + {{ maturitySummary.totalInterest | formatNumber }} {{ currencyName }}
- Projected Maturity Amount + {{ isSpecial ? 'Principal at Maturity' : 'Projected Maturity Amount' }} {{ maturitySummary.maturityAmount | formatNumber }} {{ currencyName }} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts index fc8c3130a..4f90460f5 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component.ts @@ -145,6 +145,11 @@ export class FixedDepositAccountPreviewStepComponent implements OnChanges { 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; } diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/interest-schedule-dialog/interest-schedule-dialog.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/interest-schedule-dialog/interest-schedule-dialog.component.html index de34802f2..5426c51bd 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/interest-schedule-dialog/interest-schedule-dialog.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/interest-schedule-dialog/interest-schedule-dialog.component.html @@ -11,15 +11,25 @@ {{ 'labels.heading.Monthly Interest Payout Schedule' | translate }} - {{ paidCount }} of {{ schedule.length }} periods paid so far + + @if (isPreview) { + {{ schedule.length }} monthly payouts + } @else { + {{ paidCount }} of {{ schedule.length }} periods paid so far + } +

- Computed by the actual interest engine for this account's full term -- the exact amount posted each month can vary - slightly with the number of days in that month. + @if (data.note) { + {{ data.note }} + } @else { + Computed by the actual interest engine for this account's full term -- the exact amount posted each month can + vary slightly with the number of days in that month. + }

@@ -28,8 +38,13 @@ {{ 'labels.inputs.From Date' | translate }} {{ 'labels.inputs.To Date' | translate }} + @if (isPreview) { + Days + } {{ 'labels.inputs.Interest Amount' | translate }} - {{ 'labels.inputs.Status' | translate }} + @if (!isPreview) { + {{ 'labels.inputs.Status' | translate }} + } @@ -37,28 +52,35 @@ {{ period.fromDate | dateFormat }} {{ period.toDate | dateFormat }} + @if (isPreview) { + {{ period.days }} + } {{ period.interestAmount | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} - - @if (period.paid) { - - Paid - - } @else { - Pending - } - + @if (!isPreview) { + + @if (period.paid) { + + Paid + + } @else { + Pending + } + + } } - {{ 'labels.inputs.Total Interest Payable' | translate }} + {{ 'labels.inputs.Total Interest Payable' | translate }} {{ totalInterest | currency: currencyCode : 'symbol-narrow' : '1.2-2' }} - + @if (!isPreview) { + + } diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/interest-schedule-dialog/interest-schedule-dialog.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/interest-schedule-dialog/interest-schedule-dialog.component.ts index 612cc78f3..39ec30f18 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/interest-schedule-dialog/interest-schedule-dialog.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/interest-schedule-dialog/interest-schedule-dialog.component.ts @@ -26,6 +26,9 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; * a 24+ month term makes for a long table that would otherwise push every other card on the tab down * the page. Each row also shows whether that period's interest has actually posted yet, not just what * the engine projects it to be. + * + * Also opened from the account-opening screen with `preview: true`: the backend's quote for an FD Special + * not opened yet, so there is no paid / pending status, and each period shows its number of days. */ @Component({ selector: 'mifosx-interest-schedule-dialog', @@ -46,14 +49,22 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; export class InterestScheduleDialogComponent { dialogRef = inject>(MatDialogRef); data: { - schedule: Array<{ fromDate: any; toDate: any; interestAmount: number; paid: boolean }>; + schedule: Array<{ fromDate: any; toDate: any; interestAmount: number; paid: boolean; days?: number }>; currencyCode: string; + /** Account-opening quote: no paid / pending status. */ + preview?: boolean; + /** Shown above the table instead of the account note. */ + note?: string; } = inject(MAT_DIALOG_DATA); - get schedule(): Array<{ fromDate: any; toDate: any; interestAmount: number; paid: boolean }> { + get schedule(): Array<{ fromDate: any; toDate: any; interestAmount: number; paid: boolean; days?: number }> { return this.data.schedule ?? []; } + get isPreview(): boolean { + return !!this.data.preview; + } + get currencyCode(): string { return this.data.currencyCode; } diff --git a/src/app/deposits/fixed-deposits/fixed-deposits.service.ts b/src/app/deposits/fixed-deposits/fixed-deposits.service.ts index e5af75631..fb2bb9c33 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposits.service.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposits.service.ts @@ -154,6 +154,16 @@ export class FixedDepositsService { return this.http.get(`/fixeddepositaccounts/calculate-fd-interest`, { params: httpParams }); } + /** + * FD quote while opening the account, for every FD type: the same body as create (clientId, productId, + * submittedOnDate, depositAmount, depositPeriod, depositPeriodFrequencyId, interest terms, senior citizen ...). The + * backend builds the would-be account from the product, without saving it, and returns the applied rate, maturity + * date and amount Submit would store, and the interest periods (FD Special: the monthly payout schedule). + */ + accountPreview(body: any): Observable { + return this.http.post(`/fixeddepositaccounts/account-preview`, body); + } + /** * FD Dashboard's "Renew" action on a matured FD whose product has a renewal grace period * configured: backdated to the original maturity date within the grace window, or from today diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.ts index d1abb4ad6..339a12d51 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.ts @@ -129,12 +129,24 @@ export class FixedDepositProductTermsStepComponent implements OnInit, OnChanges * (see DepositAccountAssembler#assembleFrom on the backend), so the product itself is locked to the * same values here to avoid an admin configuring a combination that the backend will silently override * at account-creation time. + * + * Its interest is paid out every month, so nothing compounds on the principal: the Interest Calculation Type is + * locked to Simple as well. The backend refuses an FD Special product with any other value for these three + * (DepositProductDataValidator), so a request from anywhere else cannot save them either. */ private applyFdTypeConstraints() { if (!this.fixedDepositProductTermsForm) return; const postingCtrl = this.fixedDepositProductTermsForm.get('interestPostingPeriodType'); const compoundingCtrl = this.fixedDepositProductTermsForm.get('interestCompoundingPeriodType'); + const calculationTypeCtrl = this.fixedDepositProductTermsForm.get('interestCalculateSelector'); if (this.fdType === 'SPECIAL') { + const simple = (this.interestCalculationTypeOptionData || []).find( + (o: any) => String(o.value ?? o.name ?? '').toLowerCase() === 'simple' + ); + if (calculationTypeCtrl && simple) { + calculationTypeCtrl.setValue(simple.id); + calculationTypeCtrl.disable(); + } // Monthly = id 4 (MONTHLY in Fineract's SavingsPostingInterestPeriodType/SavingsCompoundingInterestPeriodType enums) const findMonthly = (data: any) => data?.find((o: any) => o.value?.toLowerCase() === 'monthly' || o.id === 4); if (postingCtrl && this.interestPostingPeriodTypeData) { @@ -154,6 +166,7 @@ export class FixedDepositProductTermsStepComponent implements OnInit, OnChanges } else { postingCtrl?.enable(); compoundingCtrl?.enable(); + calculationTypeCtrl?.enable(); } }