feat(fd): restrict Payment to Matured accounts, unify its behavior with Dashboard Renew

Payment showed for both Active and Matured FDs, even though an Active FD
already has Premature Close for closing early. Now Matured-only.

That also meant a genuinely matured, past-grace FD closed via the account's
own Payment button (rather than the FD Dashboard's Renew action) silently
skipped crediting its gap-period interest -- the date-locking and
gap-interest preview/crediting were gated behind dashboard-only data, not
the account's actual matured state. Both now key off the same
renewal-preview response regardless of entry point, so Payment and Renew
are functionally identical once an FD has matured, as they should be since
they're already the same dialog component.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 13:03:47 +05:30
co-authored by Claude Sonnet 5
parent 8d0d8822f0
commit 3f6485f652
3 changed files with 55 additions and 35 deletions
@@ -18,7 +18,7 @@
{{
isDashboardRenewalMode
? 'Renew ' + (dashboardRenewalRow?.accountNo ?? 'this matured FD') + '.'
: "Process this Fixed Deposit now -- works whether it's Active or Matured."
: 'Process this matured Fixed Deposit now.'
}}
</p>
</div>
@@ -40,10 +40,10 @@
</div>
} @else {
<form [formGroup]="paymentForm">
@if (isDashboardRenewalMode) {
@if (isMaturedRenewal) {
<p class="pmt__hint">
<fa-icon icon="info-circle" class="m-r-5"></fa-icon>
{{ dashboardRenewalTimingText }}
{{ maturedRenewalTimingText }}
</p>
@if (renewalPreviewLoading) {
@@ -97,7 +97,7 @@
<div class="pmt__stat">
<span class="pmt__stat-label">
{{
isDashboardRenewalMode || !remainderIsApproximate
isMaturedRenewal || !remainderIsApproximate
? ('labels.inputs.Maturity Amount' | translate)
: 'Amount available on the selected date'
}}
@@ -106,7 +106,7 @@
</div>
}
@if (!isDashboardRenewalMode) {
@if (!isMaturedRenewal) {
<mat-form-field appearance="outline" class="pmt__date" (click)="closedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Closed On Date' | translate }}</mat-label>
<input
@@ -230,7 +230,7 @@
</mat-form-field>
}
@if (!isDashboardRenewalMode) {
@if (!isMaturedRenewal) {
<mat-form-field appearance="outline">
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
@@ -133,29 +133,32 @@ export class PaymentDialogComponent implements OnInit {
note: ['']
});
const dashboardRow = this.dashboardRenewalRow;
if (dashboardRow) {
// Computed from the grace window, never picked -- the date field stays in the form (the generic
// submit path and reinvestCap/processingOnOrAfterMaturity both key off it) but its picker is
// hidden in this mode (see the template), so it can't be moved off this value.
this.paymentForm
.get('closedOnDate')
.setValue(dashboardRow.withinGraceWindow ? new Date(dashboardRow.maturityDate) : this.maxDate);
this.gapInterestPending = !dashboardRow.withinGraceWindow;
this.renewalPreviewLoading = true;
this.fixedDepositsService.getFdRenewalPreview(this.data.accountId).subscribe({
next: (preview: any) => {
this.renewalPreview = preview;
this.renewalPreviewLoading = false;
},
// Non-fatal -- the plain timing text above still explains what will happen even if the
// numeric breakdown couldn't be fetched; only the numbers are missing, not the action itself.
error: () => {
this.renewalPreviewLoading = false;
// Fetched every time this dialog opens, not just from the FD Dashboard -- Payment is now only ever
// offered once an FD has Matured (see FixedDepositsButtonsConfiguration), so a plain Payment on it
// is exactly the same "late renewal" scenario the Dashboard's Renew action handles, and needs the
// exact same date-locking/gap-interest treatment, or an FD closed this way instead of from the
// Dashboard would silently skip crediting interest it genuinely earned.
this.renewalPreviewLoading = true;
this.fixedDepositsService.getFdRenewalPreview(this.data.accountId).subscribe({
next: (preview: any) => {
this.renewalPreview = preview;
this.renewalPreviewLoading = false;
if (preview?.isMatured) {
// Computed from the grace window, never picked -- the date field stays in the form (the
// generic submit path and reinvestCap/processingOnOrAfterMaturity both key off it) but its
// picker is hidden once matured (see the template), so it can't be moved off this value.
this.paymentForm
.get('closedOnDate')
.setValue(preview.withinGrace ? new Date(preview.maturityDate) : this.maxDate);
this.gapInterestPending = !preview.withinGrace;
}
});
}
},
// Non-fatal -- Confirm still works from here (backend re-derives the same numbers on submit),
// only the numeric preview and its date-locking are unavailable, not the action itself.
error: () => {
this.renewalPreviewLoading = false;
}
});
this.paymentForm.get('onAccountClosureId').valueChanges.subscribe((id: number) => this.onClosureTypeChange(id));
// The cap depends on whether the chosen date is on or after maturity, so a date change can make an
@@ -195,13 +198,19 @@ export class PaymentDialogComponent implements OnInit {
return this.data.dashboardRenewal?.row ?? null;
}
/** What clicking Confirm is actually about to do -- shown above the (hidden-picker) date in this
* mode, since the date itself is no longer visible to explain it. */
get dashboardRenewalTimingText(): string {
const row = this.dashboardRenewalRow;
if (!row) return '';
return row.withinGraceWindow
? `Backdated to its original maturity date (${row.maturityDate}), at the currently active interest rate.`
/** True once the loaded preview confirms this FD has actually Matured -- which, since Payment is
* only ever offered on a Matured account, is every real open of this dialog now, from either entry
* point. Drives the date-locking and gap-interest preview/crediting identically either way. */
get isMaturedRenewal(): boolean {
return !!this.renewalPreview?.isMatured;
}
/** What clicking Confirm is actually about to do -- shown above the (hidden-picker) date once
* matured, since the date itself is no longer visible to explain it. */
get maturedRenewalTimingText(): string {
if (!this.renewalPreview) return '';
return this.renewalPreview.withinGrace
? `Backdated to its original maturity date (${this.renewalPreview.maturityDate}), at the currently active interest rate.`
: `This FD's grace period has passed -- gap-period interest (for the days since maturity) will be ` +
`credited at the linked account's rate first, then this renews from today.`;
}
@@ -110,7 +110,18 @@ export class FixedDepositsButtonsConfiguration {
setOptions(status: string) {
switch (status) {
case 'Active':
this.optionArray = [
{
name: 'Post Interest'
},
{
name: 'Add Charge'
}
];
break;
case 'Matured':
// Payment only makes sense once the deposit has actually matured -- an Active FD that needs
// closing early already has Premature Close for that.
this.optionArray = [
{
name: 'Post Interest'