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>
This commit is contained in:
+6
@@ -116,6 +116,12 @@
|
||||
|
||||
<div class="fdpd-field fdpd-field--full fdpd-checkbox-row">
|
||||
<mat-checkbox formControlName="shareLinkingEnabled">Deposit Account Link</mat-checkbox>
|
||||
@if (fixedDepositProductDetailsForm.get('fdType')?.value === 'SPECIAL') {
|
||||
<span class="fdpd-hint">
|
||||
Required for FD Special -- every account will need a linked savings account so its monthly interest, and its
|
||||
principal at maturity, can be paid out automatically.
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+11
-1
@@ -76,6 +76,16 @@ $gray-900: #202124;
|
||||
|
||||
.fdpd-checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.fdpd-hint {
|
||||
font-size: 12px;
|
||||
color: $gray-900;
|
||||
opacity: 0.65;
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
+20
-1
@@ -43,6 +43,24 @@ export class FixedDepositProductDetailsStepComponent implements OnInit, OnChange
|
||||
|
||||
constructor() {
|
||||
this.createFixedDepositProductDetailsForm();
|
||||
// FD Special is a non-cumulative, monthly-interest-payout product: the backend requires a linked
|
||||
// savings account for every SPECIAL-type FD account (it's where the monthly interest and, at
|
||||
// maturity, the principal both get transferred to automatically), so force this on here rather than
|
||||
// let an admin publish a SPECIAL product that later fails every account creation for missing config.
|
||||
this.fixedDepositProductDetailsForm
|
||||
.get('fdType')
|
||||
?.valueChanges.subscribe((fdType: string) => this.applyFdTypeConstraints(fdType));
|
||||
}
|
||||
|
||||
private applyFdTypeConstraints(fdType: string | null) {
|
||||
const linkCtrl = this.fixedDepositProductDetailsForm.get('shareLinkingEnabled');
|
||||
if (!linkCtrl) return;
|
||||
if (fdType === 'SPECIAL') {
|
||||
linkCtrl.setValue(true);
|
||||
linkCtrl.disable();
|
||||
} else {
|
||||
linkCtrl.enable();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -124,10 +142,11 @@ export class FixedDepositProductDetailsStepComponent implements OnInit, OnChange
|
||||
endDate: this.parseDate(template.endDate),
|
||||
shareLinkingEnabled: !!template.shareLinkingEnabled
|
||||
});
|
||||
this.applyFdTypeConstraints(this.fixedDepositProductDetailsForm.get('fdType')?.value ?? null);
|
||||
}
|
||||
|
||||
get fixedDepositProductDetails() {
|
||||
const formValue = { ...this.fixedDepositProductDetailsForm.value };
|
||||
const formValue = { ...this.fixedDepositProductDetailsForm.getRawValue() };
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
if (formValue.effectiveTo instanceof Date) {
|
||||
formValue.effectiveTo = this.dateUtils.formatDate(formValue.effectiveTo, dateFormat) || '';
|
||||
|
||||
+26
-10
@@ -114,21 +114,37 @@ export class FixedDepositProductTermsStepComponent implements OnInit, OnChanges
|
||||
this.applyFdTypeConstraints();
|
||||
}
|
||||
|
||||
/**
|
||||
* FD Special is a non-cumulative, monthly-interest-payout product -- the backend forces both the
|
||||
* posting AND compounding period to Monthly for any account opened under a SPECIAL-type FD product
|
||||
* (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.
|
||||
*/
|
||||
private applyFdTypeConstraints() {
|
||||
if (!this.fixedDepositProductTermsForm || !this.interestPostingPeriodTypeData) return;
|
||||
if (!this.fixedDepositProductTermsForm) return;
|
||||
const postingCtrl = this.fixedDepositProductTermsForm.get('interestPostingPeriodType');
|
||||
if (!postingCtrl) return;
|
||||
const compoundingCtrl = this.fixedDepositProductTermsForm.get('interestCompoundingPeriodType');
|
||||
if (this.fdType === 'SPECIAL') {
|
||||
// Monthly posting = id 4 (MONTHLY in Fineract enum)
|
||||
const monthlyOption = this.interestPostingPeriodTypeData?.find(
|
||||
(o: any) => o.value?.toLowerCase() === 'monthly' || o.id === 4
|
||||
);
|
||||
if (monthlyOption) {
|
||||
postingCtrl.setValue(monthlyOption.id);
|
||||
postingCtrl.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) {
|
||||
const monthlyPosting = findMonthly(this.interestPostingPeriodTypeData);
|
||||
if (monthlyPosting) {
|
||||
postingCtrl.setValue(monthlyPosting.id);
|
||||
postingCtrl.disable();
|
||||
}
|
||||
}
|
||||
if (compoundingCtrl && this.interestCompoundingPeriodTypeData) {
|
||||
const monthlyCompounding = findMonthly(this.interestCompoundingPeriodTypeData);
|
||||
if (monthlyCompounding) {
|
||||
compoundingCtrl.setValue(monthlyCompounding.id);
|
||||
compoundingCtrl.disable();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
postingCtrl.enable();
|
||||
postingCtrl?.enable();
|
||||
compoundingCtrl?.enable();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user