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:
+6
-6
@@ -18,7 +18,7 @@
|
|||||||
{{
|
{{
|
||||||
isDashboardRenewalMode
|
isDashboardRenewalMode
|
||||||
? 'Renew ' + (dashboardRenewalRow?.accountNo ?? 'this matured FD') + '.'
|
? '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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,10 +40,10 @@
|
|||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<form [formGroup]="paymentForm">
|
<form [formGroup]="paymentForm">
|
||||||
@if (isDashboardRenewalMode) {
|
@if (isMaturedRenewal) {
|
||||||
<p class="pmt__hint">
|
<p class="pmt__hint">
|
||||||
<fa-icon icon="info-circle" class="m-r-5"></fa-icon>
|
<fa-icon icon="info-circle" class="m-r-5"></fa-icon>
|
||||||
{{ dashboardRenewalTimingText }}
|
{{ maturedRenewalTimingText }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if (renewalPreviewLoading) {
|
@if (renewalPreviewLoading) {
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
<div class="pmt__stat">
|
<div class="pmt__stat">
|
||||||
<span class="pmt__stat-label">
|
<span class="pmt__stat-label">
|
||||||
{{
|
{{
|
||||||
isDashboardRenewalMode || !remainderIsApproximate
|
isMaturedRenewal || !remainderIsApproximate
|
||||||
? ('labels.inputs.Maturity Amount' | translate)
|
? ('labels.inputs.Maturity Amount' | translate)
|
||||||
: 'Amount available on the selected date'
|
: 'Amount available on the selected date'
|
||||||
}}
|
}}
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!isDashboardRenewalMode) {
|
@if (!isMaturedRenewal) {
|
||||||
<mat-form-field appearance="outline" class="pmt__date" (click)="closedOnDatePicker.open()">
|
<mat-form-field appearance="outline" class="pmt__date" (click)="closedOnDatePicker.open()">
|
||||||
<mat-label>{{ 'labels.inputs.Closed On Date' | translate }}</mat-label>
|
<mat-label>{{ 'labels.inputs.Closed On Date' | translate }}</mat-label>
|
||||||
<input
|
<input
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!isDashboardRenewalMode) {
|
@if (!isMaturedRenewal) {
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
|
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
|
||||||
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
|
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
|
||||||
|
|||||||
+38
-29
@@ -133,29 +133,32 @@ export class PaymentDialogComponent implements OnInit {
|
|||||||
note: ['']
|
note: ['']
|
||||||
});
|
});
|
||||||
|
|
||||||
const dashboardRow = this.dashboardRenewalRow;
|
// Fetched every time this dialog opens, not just from the FD Dashboard -- Payment is now only ever
|
||||||
if (dashboardRow) {
|
// offered once an FD has Matured (see FixedDepositsButtonsConfiguration), so a plain Payment on it
|
||||||
// Computed from the grace window, never picked -- the date field stays in the form (the generic
|
// is exactly the same "late renewal" scenario the Dashboard's Renew action handles, and needs the
|
||||||
// submit path and reinvestCap/processingOnOrAfterMaturity both key off it) but its picker is
|
// exact same date-locking/gap-interest treatment, or an FD closed this way instead of from the
|
||||||
// hidden in this mode (see the template), so it can't be moved off this value.
|
// Dashboard would silently skip crediting interest it genuinely earned.
|
||||||
this.paymentForm
|
this.renewalPreviewLoading = true;
|
||||||
.get('closedOnDate')
|
this.fixedDepositsService.getFdRenewalPreview(this.data.accountId).subscribe({
|
||||||
.setValue(dashboardRow.withinGraceWindow ? new Date(dashboardRow.maturityDate) : this.maxDate);
|
next: (preview: any) => {
|
||||||
this.gapInterestPending = !dashboardRow.withinGraceWindow;
|
this.renewalPreview = preview;
|
||||||
|
this.renewalPreviewLoading = false;
|
||||||
this.renewalPreviewLoading = true;
|
if (preview?.isMatured) {
|
||||||
this.fixedDepositsService.getFdRenewalPreview(this.data.accountId).subscribe({
|
// Computed from the grace window, never picked -- the date field stays in the form (the
|
||||||
next: (preview: any) => {
|
// generic submit path and reinvestCap/processingOnOrAfterMaturity both key off it) but its
|
||||||
this.renewalPreview = preview;
|
// picker is hidden once matured (see the template), so it can't be moved off this value.
|
||||||
this.renewalPreviewLoading = false;
|
this.paymentForm
|
||||||
},
|
.get('closedOnDate')
|
||||||
// Non-fatal -- the plain timing text above still explains what will happen even if the
|
.setValue(preview.withinGrace ? new Date(preview.maturityDate) : this.maxDate);
|
||||||
// numeric breakdown couldn't be fetched; only the numbers are missing, not the action itself.
|
this.gapInterestPending = !preview.withinGrace;
|
||||||
error: () => {
|
|
||||||
this.renewalPreviewLoading = false;
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}
|
// 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));
|
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
|
// 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;
|
return this.data.dashboardRenewal?.row ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What clicking Confirm is actually about to do -- shown above the (hidden-picker) date in this
|
/** True once the loaded preview confirms this FD has actually Matured -- which, since Payment is
|
||||||
* mode, since the date itself is no longer visible to explain it. */
|
* only ever offered on a Matured account, is every real open of this dialog now, from either entry
|
||||||
get dashboardRenewalTimingText(): string {
|
* point. Drives the date-locking and gap-interest preview/crediting identically either way. */
|
||||||
const row = this.dashboardRenewalRow;
|
get isMaturedRenewal(): boolean {
|
||||||
if (!row) return '';
|
return !!this.renewalPreview?.isMatured;
|
||||||
return row.withinGraceWindow
|
}
|
||||||
? `Backdated to its original maturity date (${row.maturityDate}), at the currently active interest rate.`
|
|
||||||
|
/** 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 ` +
|
: `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.`;
|
`credited at the linked account's rate first, then this renews from today.`;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -110,7 +110,18 @@ export class FixedDepositsButtonsConfiguration {
|
|||||||
setOptions(status: string) {
|
setOptions(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'Active':
|
case 'Active':
|
||||||
|
this.optionArray = [
|
||||||
|
{
|
||||||
|
name: 'Post Interest'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Add Charge'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
break;
|
||||||
case 'Matured':
|
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 = [
|
this.optionArray = [
|
||||||
{
|
{
|
||||||
name: 'Post Interest'
|
name: 'Post Interest'
|
||||||
|
|||||||
Reference in New Issue
Block a user