Files
cbsnexora/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
T
niteshandClaude Sonnet 5 395fb8ace8
Tests / testRigor (push) Canceled after 0s
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
feat(clients,deposits): auto-derive senior-citizen/customer-segment, fix disbursement display and reversed-transaction balances
Four independent, self-contained changes bundled from this session's work:

1. Auto-derive Senior Citizen / Minor / Customer Segment instead of manual entry

   Senior Citizen was a manual, staff-editable checkbox on FD/RD account
   creation with no validation against the client's actual age -- this let
   non-senior clients get the senior-citizen interest rate by mistake (found
   twice on live test accounts, where m_savings_account.is_senior_citizen
   was true despite the client being confirmed non-senior at the client
   record level). Likewise "minor" client classification was hardcoded to
   the age-18 threshold with no single source of truth, and FD/RD account
   creation's "Customer Segment" (Individual / Non-Individual) had no
   connection to the Individual/Non-Individual Legal Form already captured
   at client creation.

   - New src/app/clients/shared/client-age.util.ts centralises age/DOB
     derivation: MINOR_AGE_THRESHOLD (now 10, was 18), SENIOR_CITIZEN_AGE_
     THRESHOLD (60), AGE_OF_MAJORITY (18, kept separate from the minor
     threshold -- majority date math is unrelated to the minor/adult
     classification threshold), calculateAge/isMinorAge/isSeniorCitizenAge/
     isIndividualClient/deriveSeniorCitizenStatus.
   - FD/RD interest-rate-chart steps: Senior Citizen checkbox is now derived
     from the client's DOB (age >= 60, individual clients only) and
     disabled for manual editing; a hint explains why it is checked/
     unchecked.
   - Client general step and all four account-details-steps (savings,
     current, FD, RD): replaced hardcoded 18/60 literals with the shared
     helpers; "Must be below 18" hints now read the actual threshold.
   - FD/RD account-details-steps: Customer Segment auto-selects to match
     the client's Legal Form (exact match, not substring -- a substring
     match would misclassify "Individual" as matching "Non-Individual").
   - RD preview step: fixed a pre-existing display bug that always showed
     the product's default Customer Segment instead of what was actually
     selected on the account.

2. Fix wrong Account Name / Account Type on Disbursement Review screens

   Clerk disbursement review and "My Disbursement Requests" showed the
   account NUMBER instead of the holder's name, and always labelled the
   account type "SAVINGS" regardless of the real account type (FD/RD/
   Share/Loan), because transaction-entry.component.ts's account-type
   mapping only checked for CA/CB and its account-name fallback chain
   never tried clientName. Added creditEntryAccountType() covering
   GL/CA/CB/FD/RD/SHR/LA, and accountTypeLabel() display helpers on both
   review screens.

3. Stop reversed transactions from distorting displayed running balances

   Passbook and Transactions tabs were including reversed transactions in
   their client-side running-balance recalculation, so a reversed posting
   (visible struck-through) still shifted the balance shown for every
   transaction after it. Both now filter out txn.reversed before computing
   running balances; Transactions tab also defaults "Hide Reversed" to on.

4. New Interest Schedule dialog on the FD account view

   Added custom-dialogs/interest-schedule-dialog, opened from the FD
   General tab, showing the account's period-by-period interest schedule.

Excludes src/environments/.env.ts (auto-generated build version/hash
stamp, regenerated on every build -- not meaningful to hand-commit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 18:58:20 +05:30

546 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
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/.
-->
<div class="fdrc-wrapper">
<!-- ══ Chart Header Card ════════════════════════════════════════════════ -->
@if (interestRateChartData.length > 0) {
<div class="fdrc-header-card">
<div class="fdrc-header-card__left">
<div class="fdrc-header-card__icon">
<i class="fas fa-chart-line"></i>
</div>
<div>
<div class="fdrc-header-card__title">{{ chartName || 'Interest Rate Chart' }}</div>
@if (chartDescription) {
<div class="fdrc-header-card__desc">{{ chartDescription }}</div>
}
</div>
</div>
<div class="fdrc-header-card__meta">
<span class="fdrc-pill" [class.fdrc-pill--active]="isActive" [class.fdrc-pill--inactive]="!isActive">
<i class="fas" [class.fa-check-circle]="isActive" [class.fa-times-circle]="!isActive"></i>
{{ isActive ? 'Active' : 'Inactive' }}
</span>
@if (fdType) {
<span
class="fdrc-pill fdrc-pill--type"
[class.fdrc-pill--single]="fdType === 'SINGLE'"
[class.fdrc-pill--double]="fdType === 'DOUBLE'"
[class.fdrc-pill--special]="fdType === 'SPECIAL'"
>
<i class="fas fa-tag"></i>
{{ fdTypeLabel }}
</span>
}
<div class="fdrc-header-card__dates">
<span class="fdrc-meta-item">
<i class="fas fa-calendar-check"></i>
From: <strong>{{ chartFromDate | dateFormat }}</strong>
</span>
@if (chartEndDate) {
<span class="fdrc-meta-item">
<i class="fas fa-calendar-times"></i>
To: <strong>{{ chartEndDate | dateFormat }}</strong>
</span>
}
</div>
@if (isPrimaryGroupingByAmount) {
<span class="fdrc-pill fdrc-pill--amount"> <i class="fas fa-layer-group"></i> Grouped by Amount </span>
}
</div>
</div>
}
<!-- ══ FD Type Rule Banner ══════════════════════════════════════════════ -->
@if (fdType) {
<div
class="fdrc-rule-banner"
[class.fdrc-rule-banner--single]="fdType === 'SINGLE'"
[class.fdrc-rule-banner--double]="fdType === 'DOUBLE'"
[class.fdrc-rule-banner--special]="fdType === 'SPECIAL'"
>
<div class="fdrc-rule-banner__icon">
@if (fdType === 'SINGLE') {
<i class="fas fa-hourglass-start"></i>
}
@if (fdType === 'DOUBLE') {
<i class="fas fa-hourglass-end"></i>
}
@if (fdType === 'SPECIAL') {
<i class="fas fa-star"></i>
}
</div>
<div class="fdrc-rule-banner__content">
@if (fdType === 'SINGLE') {
<strong>FD Single — Short Term</strong>
<span>Tenure must be less than 1 year (1 to 364 days). Enter tenure in Days only.</span>
}
@if (fdType === 'DOUBLE') {
<strong>FD Double — Long Term</strong>
<span>Tenure must be 1 year or more. Enter tenure in Months or Years (minimum 12 months).</span>
}
@if (fdType === 'SPECIAL') {
<strong>FD Special — Monthly Interest Payout</strong>
<span
>Interest is paid out monthly. Enter your principal to estimate monthly income. Any tenure is allowed.</span
>
}
</div>
</div>
}
<!-- ══ Tenure Calculator ════════════════════════════════════════════════ -->
<div class="fdrc-calc-card">
<div class="fdrc-calc-card__header">
<div class="fdrc-calc-card__header-left">
<div class="fdrc-calc-icon">
<i class="fas fa-calculator"></i>
</div>
<div>
<span class="fdrc-calc-card__title">Rate Calculator</span>
<span class="fdrc-calc-card__sub">Enter your deposit tenure to find the applicable interest rate</span>
</div>
</div>
</div>
<div class="fdrc-calc-card__body">
<div class="fdrc-calc-inputs">
<!-- Tenure Value -->
<div class="fdrc-calc-field">
<label class="fdrc-calc-label">Deposit Tenure</label>
<div class="fdrc-tenure-row">
<input
type="number"
class="fdrc-tenure-input"
[(ngModel)]="tenureValue"
[min]="1"
placeholder="Enter value"
(keyup.enter)="calculateRate()"
/>
<!-- Period Unit Selector -->
@if (allowedPeriodUnits.length === 1) {
<span class="fdrc-period-badge">{{ allowedPeriodUnits[0] }}</span>
} @else {
<div class="fdrc-period-tabs">
@for (unit of allowedPeriodUnits; track unit) {
<button
type="button"
class="fdrc-period-tab"
[class.fdrc-period-tab--active]="tenurePeriodUnit === unit"
(click)="tenurePeriodUnit = unit; onPeriodUnitChange()"
>
{{ unit }}
</button>
}
</div>
}
</div>
<!-- FD type constraint hint -->
@if (fdType === 'SINGLE') {
<span class="fdrc-calc-hint"><i class="fas fa-info-circle"></i> Max 364 days allowed</span>
}
@if (fdType === 'DOUBLE') {
<span class="fdrc-calc-hint"><i class="fas fa-info-circle"></i> Minimum 12 months required</span>
}
</div>
<!-- Principal Amount (all FD types) -->
<div class="fdrc-calc-field">
<label class="fdrc-calc-label">
Deposit Amount
<span class="fdrc-calc-optional">(optional — to calculate maturity amount)</span>
</label>
<div class="fdrc-amount-row">
<span class="fdrc-amount-prefix">₹</span>
<input
type="number"
class="fdrc-tenure-input fdrc-tenure-input--amount"
[(ngModel)]="principalAmount"
[min]="1"
placeholder="e.g. 100000"
/>
</div>
</div>
<!-- Senior Citizen -->
<div class="fdrc-calc-field fdrc-calc-field--senior">
<label class="fdrc-senior-label">
<input type="checkbox" [ngModel]="isSeniorCitizen" [disabled]="true" />
<span class="fdrc-senior-text">
<i class="fas fa-user-shield"></i>
Senior Citizen
</span>
<span class="fdrc-senior-hint">{{ seniorCitizenHint }}</span>
</label>
</div>
<div class="fdrc-calc-actions">
<button type="button" class="fdrc-calc-btn" (click)="calculateRate()">
<i class="fas fa-search"></i> Find Rate
</button>
@if (tenureValue || matchResult) {
<button type="button" class="fdrc-calc-reset" (click)="resetCalculator()">
<i class="fas fa-times"></i> Reset
</button>
}
</div>
</div>
<!-- Validation Warning -->
@if (validationWarning) {
<div class="fdrc-alert fdrc-alert--warn">
<i class="fas fa-exclamation-triangle"></i>
<span>{{ validationWarning }}</span>
</div>
}
<!-- Match Result -->
@if (matchResult) {
<div class="fdrc-result-card">
<div class="fdrc-result-card__body">
<!-- Tenure Banner -->
<div class="fdrc-tenure-banner">
<i class="fas fa-calendar-check"></i>
<div class="fdrc-tenure-banner__content">
<span class="fdrc-tenure-banner__label">Your Selected FD Tenure</span>
<span class="fdrc-tenure-banner__value">{{ tenureValue }} {{ tenurePeriodUnit }}</span>
</div>
<span class="fdrc-tenure-banner__saved"><i class="fas fa-save"></i> Will be saved</span>
</div>
<!-- Senior Citizen Status — always shown when checked -->
@if (isSeniorCitizen) {
@if ((matchResult.slab.seniorCitizenRate ?? 0) > 0) {
<div class="fdrc-sc-status fdrc-sc-status--applied">
<i class="fas fa-user-shield fdrc-sc-status__icon"></i>
<span class="fdrc-sc-status__text">Senior Citizen Rate Applied</span>
<span class="fdrc-sc-status__bonus">
+{{
(matchResult.slab.seniorCitizenRate ?? 0) - (matchResult.slab.annualInterestRate ?? 0)
| number: '1.2-2'
}}% bonus
</span>
</div>
} @else {
<div class="fdrc-sc-status fdrc-sc-status--notice">
<i class="fas fa-info-circle fdrc-sc-status__icon"></i>
<span class="fdrc-sc-status__text"
>Senior Citizen selected — no additional rate configured for this slab, normal rate applied</span
>
</div>
}
}
<!-- Rate Display -->
@if (isSeniorCitizen && (matchResult.slab.seniorCitizenRate ?? 0) > 0) {
<!-- Before / After comparison -->
<div class="fdrc-rate-compare">
<div class="fdrc-rate-compare__block">
<span class="fdrc-rate-compare__tag">Normal Rate</span>
<span class="fdrc-rate-compare__val fdrc-rate-compare__val--strike"
>{{ matchResult.slab.annualInterestRate | number: '1.2-2' }}%</span
>
<span class="fdrc-rate-compare__sub">p.a.</span>
</div>
<div class="fdrc-rate-compare__arrow"><i class="fas fa-long-arrow-alt-right"></i></div>
<div class="fdrc-rate-compare__block fdrc-rate-compare__block--sc">
<span class="fdrc-rate-compare__tag fdrc-rate-compare__tag--sc"
><i class="fas fa-user-shield"></i> Sr. Citizen Rate</span
>
<span class="fdrc-rate-compare__val fdrc-rate-compare__val--sc"
>{{ matchResult.appliedRate | number: '1.2-2' }}%</span
>
<span class="fdrc-rate-compare__sub">p.a.</span>
</div>
</div>
} @else {
<div class="fdrc-result-rate">
<span class="fdrc-result-rate__value">{{ matchResult.appliedRate | number: '1.2-2' }}%</span>
<span class="fdrc-result-rate__label">Annual Interest Rate p.a.</span>
</div>
}
<!-- Slab Info -->
<div class="fdrc-result-meta">
<div class="fdrc-result-meta__item">
<i class="fas fa-layer-group"></i>
<span
>Applicable Slab:
<strong
>{{ matchResult.slab.fromPeriod }}–{{ matchResult.slab.toPeriod ?? '∞' }}
{{ matchResult.slab.periodType?.value }}</strong
></span
>
</div>
@if (matchResult.slab.description) {
<div class="fdrc-result-meta__item">
<i class="fas fa-tag"></i>
<span>{{ matchResult.slab.description }}</span>
</div>
}
@if (matchResult.slab.amountRangeFrom || matchResult.slab.amountRangeTo) {
<div class="fdrc-result-meta__item">
<i class="fas fa-coins"></i>
<span
>Amount Range:
<strong
>{{ matchResult.slab.amountRangeFrom | number }}–{{
matchResult.slab.amountRangeTo | number
}}</strong
></span
>
</div>
}
</div>
<!-- Maturity Summary -->
@if (matchResult.maturityAmount) {
<div class="fdrc-maturity-card">
<div class="fdrc-maturity-card__header">
<i class="fas fa-coins"></i>
<span>Maturity Summary</span>
@if (isSeniorCitizen && (matchResult.slab.seniorCitizenRate ?? 0) > 0) {
<span class="fdrc-sc-tag"><i class="fas fa-user-shield"></i> Sr. Citizen Rate</span>
}
</div>
<div class="fdrc-maturity-card__body">
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Deposit Amount</span>
<span class="fdrc-maturity-row__value">₹ {{ principalAmount | number: '1.2-2' }}</span>
</div>
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Tenure</span>
<span class="fdrc-maturity-row__value">{{ tenureValue }} {{ tenurePeriodUnit }}</span>
</div>
<div
class="fdrc-maturity-row"
[class.fdrc-maturity-row--sc]="isSeniorCitizen && (matchResult.slab.seniorCitizenRate ?? 0) > 0"
>
<span class="fdrc-maturity-row__label">
@if (isSeniorCitizen && (matchResult.slab.seniorCitizenRate ?? 0) > 0) {
<i class="fas fa-user-shield"></i> Sr. Citizen Rate
} @else {
Interest Rate
}
</span>
<span class="fdrc-maturity-row__value">{{ matchResult.appliedRate | number: '1.2-2' }}% p.a.</span>
</div>
<div class="fdrc-maturity-row fdrc-maturity-row--interest">
<span class="fdrc-maturity-row__label">Interest Earned</span>
<span class="fdrc-maturity-row__value">₹ {{ matchResult.interestEarned | number: '1.2-2' }}</span>
</div>
@if (isSpecial && matchResult.estimatedMonthlyPayout) {
<div class="fdrc-maturity-row">
<span class="fdrc-maturity-row__label">Monthly Payout</span>
<span class="fdrc-maturity-row__value fdrc-maturity-row__value--highlight">
₹ {{ matchResult.estimatedMonthlyPayout | number: '1.2-2' }} / month
</span>
</div>
}
<div class="fdrc-maturity-row fdrc-maturity-row--total">
<span class="fdrc-maturity-row__label">
@if (isSpecial) {
Principal at Maturity
} @else {
Maturity Amount
}
</span>
<span class="fdrc-maturity-row__value fdrc-maturity-row__value--big">
₹ {{ (isSpecial ? principalAmount : matchResult.maturityAmount) | number: '1.2-2' }}
</span>
</div>
</div>
</div>
}
@if (!matchResult.maturityAmount) {
<div class="fdrc-payout-hint">
<i class="fas fa-lightbulb"></i>
Enter a deposit amount above to see the maturity calculation.
</div>
}
</div>
</div>
}
</div>
</div>
<!-- ══ Rate Chart Table ══════════════════════════════════════════════════ -->
<div class="fdrc-table-card">
<div class="fdrc-table-card__header">
<div class="fdrc-table-card__title-row">
<i class="fas fa-table"></i>
<span class="fdrc-table-card__title">Interest Rate Slabs</span>
@if (interestRateChartData.length) {
<span class="fdrc-slab-count"
>{{ interestRateChartData.length }} slab{{ interestRateChartData.length !== 1 ? 's' : '' }}</span
>
}
</div>
@if (matchResult) {
<span class="fdrc-highlight-note"> <i class="fas fa-highlighter"></i> Matched slab is highlighted </span>
}
</div>
<div class="fdrc-table-wrap">
<table mat-table [dataSource]="interestRateChartData" #chartsTable multiTemplateDataRows class="fdrc-table">
<!-- Period Column -->
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Period</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td fdrc-td--period">
<span class="fdrc-period-range">
{{ slab.fromPeriod }}
@if (slab.toPeriod !== null) {
– {{ slab.toPeriod }}
} @else {
+
}
</span>
<span class="fdrc-period-type">{{ slab.periodType?.value }}</span>
</td>
</ng-container>
<!-- Amount Range Column -->
<ng-container matColumnDef="amountRange">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Amount Range</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td">
@if (slab.amountRangeFrom || slab.amountRangeTo) {
<span>{{ slab.amountRangeFrom | number }} – {{ slab.amountRangeTo | number }}</span>
} @else {
<span class="fdrc-td--na">—</span>
}
</td>
</ng-container>
<!-- Interest Column -->
<ng-container matColumnDef="interest">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Interest Rate</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td">
<span class="fdrc-rate-badge">{{ slab.annualInterestRate | number: '1.2-2' }}%</span>
</td>
</ng-container>
<!-- Senior Citizen Rate Column -->
<ng-container matColumnDef="seniorCitizenRate">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">
<i class="fas fa-user-shield" style="margin-right: 4px; color: #6366f1"></i>Sr. Citizen Rate
</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td">
@if (slab.seniorCitizenRate > 0) {
<span class="fdrc-rate-badge fdrc-rate-badge--senior"
>{{ slab.seniorCitizenRate | number: '1.2-2' }}%</span
>
} @else {
<span class="fdrc-td--na">—</span>
}
</td>
</ng-container>
<!-- Description Column -->
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Description</th>
<td mat-cell *matCellDef="let slab" class="fdrc-td">
{{ slab.description || '—' }}
</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="fdrc-th">Incentives</th>
<td mat-cell *matCellDef="let slab; let i = dataIndex" class="fdrc-td">
@if (slab.incentives?.length) {
<button
type="button"
class="fdrc-incentive-btn"
(click)="expandChartSlabIndex = expandChartSlabIndex === i ? null : i"
>
<i
class="fas"
[class.fa-eye]="expandChartSlabIndex !== i"
[class.fa-eye-slash]="expandChartSlabIndex === i"
></i>
{{ expandChartSlabIndex === i ? 'Hide' : 'View' }} ({{ slab.incentives.length }})
</button>
} @else {
<span class="fdrc-td--na">None</span>
}
</td>
</ng-container>
<!-- Incentives Expanded Row -->
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let slab; let i = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns.length"
class="fdrc-incentives-cell"
>
<div
class="fdrc-incentives-wrap"
[@expandChartSlab]="i === expandChartSlabIndex ? 'expanded' : 'collapsed'"
>
@if (slab.incentives?.length) {
<div class="fdrc-incentives-inner">
<div class="fdrc-incentives-title"><i class="fas fa-gift"></i> Incentives for this slab</div>
<table mat-table [dataSource]="slab.incentives" class="fdrc-incentives-table">
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>Entity Type</th>
<td mat-cell *matCellDef="let inc">{{ inc.entityType?.value }}</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>Attribute</th>
<td mat-cell *matCellDef="let inc">{{ inc.attributeName?.value }}</td>
</ng-container>
<ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>Condition</th>
<td mat-cell *matCellDef="let inc">{{ inc.conditionType?.value | titlecase }}</td>
</ng-container>
<ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>Value</th>
<td mat-cell *matCellDef="let inc">{{ inc.attributeValueDesc }}</td>
</ng-container>
<ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>Type</th>
<td mat-cell *matCellDef="let inc">{{ inc.incentiveType?.value }}</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>Amount</th>
<td mat-cell *matCellDef="let inc">{{ inc.amount }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
</table>
</div>
}
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns" class="fdrc-header-row"></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsDisplayedColumns; let i = dataIndex"
class="fdrc-data-row"
[class.fdrc-data-row--matched]="isMatchedSlab(i)"
></tr>
<tr
mat-row
*matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns"
class="fdrc-incentives-row"
></tr>
</table>
@if (!interestRateChartData.length) {
<div class="fdrc-empty">
<i class="fas fa-chart-bar"></i>
<span>No interest rate slabs defined for this product.</span>
</div>
}
</div>
</div>
</div>