From 4f02f24aaeefc7ebc50f32adf20830277bbf1491 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Thu, 1 Oct 2026 12:19:37 +0530 Subject: [PATCH] feat(deposits): RBI premature closure compliance UI, renewal-history status fix Premature Close dialog: adds the Closure Reason dropdown, the explicit Waive Penalty checkbox, a tenure/date preview panel, and an "Effective Rate for this closure" display so the maker sees exactly what rate and penalty treatment is about to apply. Renewal History tab: the latest term's card kept showing an ACTIVE badge even after the account was prematurely closed, since the archived renewal-status column only ever updates on an actual renewal rollover. For the last card only, status now cross-checks the account's own live status instead. "Projected Maturity" now reads "Closed At" once that term has prematurely closed, rather than implying it's still a projection. Co-Authored-By: Claude Sonnet 5 --- .../premature-close-dialog.component.html | 80 +++++++- .../premature-close-dialog.component.scss | 80 ++++++++ .../premature-close-dialog.component.ts | 176 +++++++++++++++++- .../fixed-deposit-account-view.component.ts | 28 ++- .../renewal-history-tab.component.html | 18 +- .../renewal-history-tab.component.ts | 45 +++++ 6 files changed, 412 insertions(+), 15 deletions(-) diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.html index 018b95331..00f425f6e 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.html @@ -51,12 +51,53 @@ } + + Closure Reason + + @for (reason of closureReasonOptions; track reason.id) { + {{ reason.label }} + } + + + @if (loadingAmount) {
Calculating premature-closure amount...
} @else { + @if (data.activatedOnDate || data.maturityDate || data.currentBalance !== undefined) { +
+ @if (data.activatedOnDate) { +
+ Current Term Started + {{ activatedOnDateDisplay }} +
+
+ Tenure Completed (this term) + {{ tenureElapsedDisplay }} ({{ daysElapsed }} days) +
+ } + @if (data.maturityDate) { +
+ Scheduled Maturity Date + {{ maturityDateDisplay }} + ({{ daysRemainingToMaturity }} days left) +
+ } + @if (data.currentBalance !== undefined && data.currentBalance !== null) { +
+ Current Balance (as of today) + {{ data.currentBalance | formatNumber }} +
+ } +
+ } + @if (maturityAmount !== null && maturityAmount !== undefined) {
@if (data.depositAmount) { @@ -71,12 +112,27 @@ {{ data.nominalAnnualInterestRate }}%
} + @if (effectiveInterestRate !== null && effectiveInterestRate !== undefined) { +
+ Effective Rate for this closure + {{ effectiveInterestRate }}% +
+ } @if (data.preClosurePenalApplicable) { -
+
Pre-Closure Penalty (on {{ data.preClosurePenalInterestOnType?.value }}) - − {{ data.preClosurePenalInterest }}% + + @if (prematureCloseForm.get('waivePreClosurePenalty').value) { + Waived + } @else { + − {{ data.preClosurePenalInterest }}% + } +
} @if (interestEarned !== null) { @@ -94,6 +150,26 @@
} + @if (data.preClosurePenalApplicable) { + @if (isAutoWaivedReason) { +
+ + + Penalty automatically waived -- {{ selectedReasonLabel }} always waives the + pre-closure penalty, regardless of this checkbox. + +
+ } @else { + + Waive the pre-closure penalty for this closure + + Interest recalculates at the full contracted rate instead of the penalty rate above. This does not + change the product's own penalty setting -- it applies to this one closure only. + + + } + } +
@for (closureOption of onAccountClosureOptions; track closureOption) { diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.scss b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.scss index 289c1380e..ad41732d2 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.scss +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.scss @@ -204,6 +204,86 @@ font-weight: 700; color: #0f172a; } + + &-sub { + font-family: 'DM Sans', sans-serif; + font-size: 11px; + font-weight: 500; + color: #94a3b8; + margin-left: 4px; + } +} + +.pcd__preview--tenure { + margin-bottom: 14px; +} + +.pcd__preview-row--waived { + .pcd__preview-label { + color: #16a34a; + } +} + +.pcd__penalty-waived-tag { + font-family: 'DM Sans', sans-serif; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #15803d; + background: #dcfce7; + border: 1px solid #86efac; + border-radius: 999px; + padding: 3px 10px; +} + +.pcd__waive-checkbox { + display: flex; + align-items: flex-start; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 14px 16px; + margin: 0 0 18px; + font-size: 13px; + font-weight: 600; + color: #0f172a; + + ::ng-deep .mdc-form-field { + align-items: flex-start; + } + + ::ng-deep .mdc-checkbox { + margin-top: -9px; + } +} + +.pcd__waive-locked { + display: flex; + align-items: flex-start; + gap: 10px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-left: 4px solid #16a34a; + border-radius: 10px; + color: #15803d; + font-size: 13px; + line-height: 1.5; + padding: 12px 16px; + margin: 0 0 18px; + + strong { + color: #166534; + } +} + +.pcd__waive-hint { + display: block; + font-size: 12px; + font-weight: 500; + color: #64748b; + margin-top: 3px; + line-height: 1.4; } .pcd__section-label { diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.ts index 2122653e5..fbb1dab00 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/custom-dialogs/premature-close-dialog/premature-close-dialog.component.ts @@ -67,6 +67,29 @@ export class PrematureCloseDialogComponent implements OnInit { [PrematureCloseDialogComponent.TRANSFER_TO_SAVINGS_ID]: 'Move the full balance to another savings account' }; + /** + * Mirrors the backend's PrematureClosureReasonType values exactly -- see that enum for why these + * four (everything but Normal) always waive the penalty by RBI policy rather than at the maker's + * discretion. `gatedBy` is the product-facility flag (from `data`) that must be true for the option + * to even be offered; `null` means always offered. Deceased Settlement / Nominee-Legal Heir Closure + * are gated because they are the product's own opt-in facilities (see SavingsProduct + * deceasedSettlement/noPenaltyNomineeHeirClosure) -- a product that never enabled one shouldn't let + * a maker pick it to bypass a penalty that product's terms don't actually exempt. + */ + private static readonly NORMAL_REASON_ID = 100; + private static readonly ALL_CLOSURE_REASONS: { + id: number; + label: string; + autoWaived: boolean; + gatedBy: 'deceasedSettlement' | 'noPenaltyNomineeHeirClosure' | null; + }[] = [ + { id: 100, label: 'Normal Premature Closure', autoWaived: false, gatedBy: null }, + { id: 200, label: 'Premature Extension / Renewal', autoWaived: true, gatedBy: null }, + { id: 300, label: 'Conversion to Another Term Deposit', autoWaived: true, gatedBy: null }, + { id: 400, label: 'Deceased Settlement', autoWaived: true, gatedBy: 'deceasedSettlement' }, + { id: 500, label: 'Nominee / Legal Heir Closure', autoWaived: true, gatedBy: 'noPenaltyNomineeHeirClosure' } + ]; + dialogRef = inject>(MatDialogRef); data: { accountId: number | string; @@ -76,6 +99,11 @@ export class PrematureCloseDialogComponent implements OnInit { preClosurePenalApplicable?: boolean; preClosurePenalInterest?: number; preClosurePenalInterestOnType?: { value: string }; + deceasedSettlement?: boolean; + noPenaltyNomineeHeirClosure?: boolean; + activatedOnDate?: any; + maturityDate?: any; + currentBalance?: number; } = inject(MAT_DIALOG_DATA); private formBuilder = inject(UntypedFormBuilder); private fixedDepositsService = inject(FixedDepositsService); @@ -86,6 +114,10 @@ export class PrematureCloseDialogComponent implements OnInit { onAccountClosureOptions: any[] = []; savingsAccountsData: any[] = []; maturityAmount: number = null; + /** The rate actually resolved for THIS preview by the backend (actual-period vs contracted, + * penalty-deducted or not) -- distinct from data.nominalAnnualInterestRate, which is always the + * account's unmodified contracted rate. Read fresh on every recalculation. */ + effectiveInterestRate: number = null; loadingAmount = true; loadFailed = false; submitting = false; @@ -99,17 +131,66 @@ export class PrematureCloseDialogComponent implements OnInit { closedOnDate: [ this.maxDate, Validators.required - ] + ], + // Both always present (not added conditionally like onAccountClosureId below) so submit() + // always carries a value, even for a product with no penalty configured at all -- the backend + // ignores them there anyway, since there is nothing to waive. + prematureClosureReasonId: [PrematureCloseDialogComponent.NORMAL_REASON_ID], + waivePreClosurePenalty: [false] }); this.prematureCloseForm .get('closedOnDate') .valueChanges.subscribe((date: Date) => this.calculatePrematureAmount(date)); + // Waiving/un-waiving recalculates the preview the same way changing the date does -- the maker + // should see the actual amount they're about to confirm, not the penalty-applied one, the moment + // they tick the box. + this.prematureCloseForm + .get('waivePreClosurePenalty') + .valueChanges.subscribe(() => this.calculatePrematureAmount(this.prematureCloseForm.get('closedOnDate').value)); + // Picking an RBI-mandated waiver reason forces the checkbox on (see onClosureReasonChange) and, + // like it, recalculates the preview so the amount shown always matches the reason selected. + this.prematureCloseForm + .get('prematureClosureReasonId') + .valueChanges.subscribe((id: number) => this.onClosureReasonChange(id)); // Same date the form already starts with -- without this the amount/options never load unless the // maker happens to touch the date picker, which the full-page form this replaces required too. this.calculatePrematureAmount(this.maxDate); } + /** The reason list this account's product actually allows -- see ALL_CLOSURE_REASONS' `gatedBy`. */ + get closureReasonOptions(): { id: number; label: string; autoWaived: boolean }[] { + return PrematureCloseDialogComponent.ALL_CLOSURE_REASONS.filter( + (reason) => !reason.gatedBy || this.data[reason.gatedBy] + ); + } + + /** True when the selected reason waives the penalty by policy, not maker choice -- the checkbox is + * hidden in favour of a plain statement of that fact when this is true (see the template). */ + get isAutoWaivedReason(): boolean { + return !!this.selectedReason?.autoWaived; + } + + get selectedReasonLabel(): string { + return this.selectedReason?.label ?? ''; + } + + private get selectedReason(): { id: number; label: string; autoWaived: boolean } | undefined { + const id = this.prematureCloseForm?.get('prematureClosureReasonId')?.value; + return PrematureCloseDialogComponent.ALL_CLOSURE_REASONS.find((reason) => reason.id === id); + } + + private onClosureReasonChange(reasonId: number): void { + const reason = PrematureCloseDialogComponent.ALL_CLOSURE_REASONS.find((r) => r.id === reasonId); + if (reason?.autoWaived) { + // emitEvent: false -- the explicit calculatePrematureAmount call below already covers this + // change; without it, this setValue would also fire waivePreClosurePenalty's own subscription + // and the amount would be calculated twice for one reason change. + this.prematureCloseForm.get('waivePreClosurePenalty').setValue(true, { emitEvent: false }); + } + this.calculatePrematureAmount(this.prematureCloseForm.get('closedOnDate').value); + } + private calculatePrematureAmount(date: Date): void { this.loadingAmount = true; this.loadFailed = false; @@ -118,7 +199,9 @@ export class PrematureCloseDialogComponent implements OnInit { const data = { closedOnDate: this.dateUtils.formatDate(date, dateFormat), dateFormat, - locale + locale, + prematureClosureReasonId: this.prematureCloseForm.get('prematureClosureReasonId')?.value, + waivePreClosurePenalty: !!this.prematureCloseForm.get('waivePreClosurePenalty')?.value }; this.fixedDepositsService .executeFixedDepositsAccountCommand(this.data.accountId as string, 'calculatePrematureAmount', data) @@ -127,6 +210,7 @@ export class PrematureCloseDialogComponent implements OnInit { this.savingsAccountsData = response.savingsAccounts ?? []; this.onAccountClosureOptions = response.onAccountClosureOptions ?? []; this.maturityAmount = response.maturityAmount; + this.effectiveInterestRate = response.nominalAnnualInterestRate ?? null; this.loadingAmount = false; if (!this.prematureCloseForm.contains('onAccountClosureId')) { this.prematureCloseForm.addControl('onAccountClosureId', new UntypedFormControl('', Validators.required)); @@ -166,6 +250,94 @@ export class PrematureCloseDialogComponent implements OnInit { return Number(this.maturityAmount) - Number(this.data.depositAmount); } + get activatedOnDateDisplay(): string { + return this.formatFullDate(this.data.activatedOnDate); + } + + get maturityDateDisplay(): string { + return this.formatFullDate(this.data.maturityDate); + } + + /** + * The date every "as of" figure in this dialog is measured against -- the maker's own chosen Premature + * Close Date, falling back to Fineract's configured Business Date. Deliberately NOT the real-world + * clock (`new Date()`): Fineract's business date can lag behind the calendar (a bank's own admin + * advances it), and the backend's interest calculation is measured from THIS date, not today's actual + * date. Using the real clock here previously showed a "days elapsed" figure that disagreed with the + * interest the backend actually computed for the same closure -- both must read off the same clock. + */ + private get referenceDate(): Date { + const picked = this.prematureCloseForm?.get('closedOnDate')?.value; + return picked instanceof Date ? picked : this.maxDate; + } + + /** Days from the current term's start to the chosen closure date -- 0 if the date is missing or the + * closure date is somehow before it, rather than a negative. */ + get daysElapsed(): number | null { + const start = this.data.activatedOnDate ? this.dateUtils.parseDate(this.data.activatedOnDate) : null; + if (!start) { + return null; + } + return Math.max(0, this.dateUtils.calculateDiff(start, this.referenceDate)); + } + + /** Days from the chosen closure date to the scheduled maturity date -- 0 once matured/overdue rather + * than negative, since this dialog is for a still-Active account closing early, not a matured one. */ + get daysRemainingToMaturity(): number | null { + const maturity = this.data.maturityDate ? this.dateUtils.parseDate(this.data.maturityDate) : null; + if (!maturity) { + return null; + } + return Math.max(0, this.dateUtils.calculateDiff(this.referenceDate, maturity)); + } + + /** Elapsed tenure as Years/Months/Days, the way a real passbook states it, rather than a raw day + * count that is hard to read for a multi-year FD. */ + get tenureElapsedDisplay(): string { + const start = this.data.activatedOnDate ? this.dateUtils.parseDate(this.data.activatedOnDate) : null; + if (!start) { + return '—'; + } + const { years, months, days } = PrematureCloseDialogComponent.yearsMonthsDaysBetween(start, this.referenceDate); + const parts: string[] = []; + if (years > 0) { + parts.push(`${years} yr${years === 1 ? '' : 's'}`); + } + if (months > 0) { + parts.push(`${months} mo${months === 1 ? '' : 's'}`); + } + if (days > 0 || parts.length === 0) { + parts.push(`${days} day${days === 1 ? '' : 's'}`); + } + return parts.join(', '); + } + + private formatFullDate(value: any): string { + if (!value) { + return '—'; + } + const parsed = this.dateUtils.parseDate(value); + // "28 September 2026" -- day/month-name/year, so this reads unambiguously regardless of the + // account's own configured short date format elsewhere in the app. + return this.dateUtils.formatDate(parsed, 'd MMMM y'); + } + + private static yearsMonthsDaysBetween(start: Date, end: Date): { years: number; months: number; days: number } { + let years = end.getFullYear() - start.getFullYear(); + let months = end.getMonth() - start.getMonth(); + let days = end.getDate() - start.getDate(); + if (days < 0) { + months -= 1; + const daysInPreviousMonth = new Date(end.getFullYear(), end.getMonth(), 0).getDate(); + days += daysInPreviousMonth; + } + if (months < 0) { + years -= 1; + months += 12; + } + return { years: Math.max(0, years), months: Math.max(0, months), days: Math.max(0, days) }; + } + /** True when the chosen action needs a destination account but the customer has none to pick. */ get noDestinationAccounts(): boolean { return ( diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts index a07dde825..9ebae159f 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/fixed-deposit-account-view.component.ts @@ -298,6 +298,18 @@ export class FixedDepositAccountViewComponent implements OnInit { }); } + /** + * The start date of the term currently running -- the last entry in renewalHistory, which is ordered + * oldest-first (see RenewalHistoryTabComponent). Falls back to the account's own activation date for + * an FD that has never been renewed (or whose renewalHistory hasn't loaded), which is the same date + * in that case anyway. + */ + private get currentTermStartDate(): any { + const history = this.fixedDepositsAccountData?.renewalHistory; + const latestTerm = Array.isArray(history) && history.length > 0 ? history[history.length - 1] : null; + return latestTerm?.activatedondate ?? this.fixedDepositsAccountData?.timeline?.activatedOnDate; + } + /** * Opens the Premature Close action -- Active-only, no reinvest option, since the deposit hasn't * matured yet. @@ -316,7 +328,21 @@ export class FixedDepositAccountViewComponent implements OnInit { nominalAnnualInterestRate: this.fixedDepositsAccountData.nominalAnnualInterestRate, preClosurePenalApplicable: this.fixedDepositsAccountData.preClosurePenalApplicable, preClosurePenalInterest: this.fixedDepositsAccountData.preClosurePenalInterest, - preClosurePenalInterestOnType: this.fixedDepositsAccountData.preClosurePenalInterestOnType + preClosurePenalInterestOnType: this.fixedDepositsAccountData.preClosurePenalInterestOnType, + // Gate which RBI-mandated waiver reasons the dialog offers -- these two are the product's own + // facility toggles (Deceased Settlement / No Penalty Nominee-Heir Closure), off by default, so + // a product that never enabled them shouldn't offer a reason implying it did. + deceasedSettlement: this.fixedDepositsAccountData.deceasedSettlement, + noPenaltyNomineeHeirClosure: this.fixedDepositsAccountData.noPenaltyNomineeHeirClosure, + // Tenure context (current term's start date, maturity date, current balance) so the dialog can + // show how far into THIS term the account is and how much is actually sitting in it today, not + // just the penalty-adjusted amount calculatePrematureAmount returns. Deliberately the current + // term's own start (see currentTermStartDate), not the account's original activation -- a + // renewed FD's interest/tenure runs from the renewal, and showing the original activation date + // here would make an account renewed days ago look like it has been running for months. + activatedOnDate: this.currentTermStartDate, + maturityDate: this.fixedDepositsAccountData.maturityDate, + currentBalance: this.fixedDepositsAccountData.summary?.accountBalance } }); prematureCloseDialogRef.afterClosed().subscribe((response: any) => { diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/renewal-history-tab/renewal-history-tab.component.html b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/renewal-history-tab/renewal-history-tab.component.html index 3648e2ee1..885983e11 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/renewal-history-tab/renewal-history-tab.component.html +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/renewal-history-tab/renewal-history-tab.component.html @@ -41,8 +41,8 @@ ) {
- - @if (term.renewalstatus === 'ACTIVE') { + + @if (isTermActive(term, last)) { } @@ -51,15 +51,15 @@ }
-
+

{{ term.renewalcycleno === 0 ? 'Original Term' : 'Renewal ' + term.renewalcycleno }}

- - - {{ term.renewalstatus === 'ACTIVE' ? 'Active' : 'Matured' }} + + + {{ termStatusLabel(term, last) }} @if (renewalTypeLabel(term)) { @@ -95,7 +95,7 @@
{{ - term.renewalstatus === 'ACTIVE' ? 'Interest So Far' : 'Interest Earned' + isTermActive(term, last) ? 'Interest So Far' : 'Interest Earned' }} {{ @@ -106,9 +106,7 @@
- {{ - term.maturedamount === null || term.maturedamount === undefined ? 'Projected Maturity' : 'Matured At' - }} + {{ maturityLabel(term, last) }} {{ maturityFor(term) | currency: currency.code : 'symbol-narrow' }} diff --git a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/renewal-history-tab/renewal-history-tab.component.ts b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/renewal-history-tab/renewal-history-tab.component.ts index 34bd1bb2b..b58372aa5 100644 --- a/src/app/deposits/fixed-deposits/fixed-deposit-account-view/renewal-history-tab/renewal-history-tab.component.ts +++ b/src/app/deposits/fixed-deposits/fixed-deposit-account-view/renewal-history-tab/renewal-history-tab.component.ts @@ -183,6 +183,51 @@ export class RenewalHistoryTabComponent implements OnInit { return term?.renewaltype === 'MANUAL' ? 'user-cog' : 'robot'; } + /** + * Whether one term's card should show as "still running". `term.renewalstatus` only ever flips from + * ACTIVE to MATURED when a term rolls into its successor (see FixedDepositRenewalHistory#markMatured + * on the backend) -- a premature closure never touches that row, so the latest term's archived status + * would read ACTIVE forever even after the whole account has closed. For the LAST term only, this + * cross-checks the account's own live status instead of trusting the archived flag; every earlier + * term is reliably matured already (something renewed past it), so its own renewalstatus is + * trustworthy as-is and doesn't need the live account status at all. + */ + isTermActive(term: any, isLast: boolean): boolean { + if (isLast && this.fixedDepositsAccountData?.status) { + return !!this.fixedDepositsAccountData.status.active; + } + return term?.renewalstatus === 'ACTIVE'; + } + + /** Badge text for one term's card -- see {@link isTermActive} for why the last term needs its own check. */ + termStatusLabel(term: any, isLast: boolean): string { + if (this.isTermActive(term, isLast)) { + return 'Active'; + } + if (isLast && this.fixedDepositsAccountData?.status?.prematureClosed) { + return 'Premature Closed'; + } + return 'Matured'; + } + + /** + * Label for the maturity/value figure -- "Projected Maturity" only makes sense while the term is + * still genuinely running towards that projection. `term.maturedamount` (the archived settled value) + * is only ever written when a term rolls into its successor, same gap as {@link isTermActive}: a + * premature closure settles the account without ever touching that column, so the last term would + * keep reading as a live projection forever even though the figure showing is already the final, + * settled one (see {@link maturityFor}). + */ + maturityLabel(term: any, isLast: boolean): string { + if (term?.maturedamount != null) { + return 'Matured At'; + } + if (isLast && !this.isTermActive(term, isLast)) { + return 'Closed At'; + } + return 'Projected Maturity'; + } + /** * The rate-by-tenure chart for one term of this FD's history, normalised to one shape regardless of source. *