Maturity Instructions can now be left unselected when opening a SINGLE/DOUBLE/SPECIAL FD -- a new "-- Not selected (decide later via FD Dashboard) --" option, plus a clear button next to the field, since a maker had no way to go back to blank once something was picked. An account left this way is never touched by the nightly maturity job; what happens to it at maturity is decided later, manually, from the FD Dashboard's Renew action (see the backend/dashboard commits) -- an explicit choice here still auto-processes on its own exactly as before. Also fixes a real, separate pre-existing bug found in the same step: editing an existing account never read back its own already-saved Maturity Instructions/transfer-target choice -- the Edit screen always opened onto a blank picker even for an account that genuinely had one set, because it was being read from the wrong template object (the blank per-product default template refetched from the Product dropdown, not the actual account+template response that carries the account's own saved values). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
156 lines
7.1 KiB
TypeScript
156 lines
7.1 KiB
TypeScript
/**
|
|
* 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/.
|
|
*/
|
|
|
|
/** Angular Imports */
|
|
import { Component, Input, OnChanges, SimpleChanges, inject } from '@angular/core';
|
|
import { UntypedFormGroup, UntypedFormBuilder, UntypedFormControl, Validators } from '@angular/forms';
|
|
import { MatCheckbox } from '@angular/material/checkbox';
|
|
import { MatIcon } from '@angular/material/icon';
|
|
import { MatIconButton } from '@angular/material/button';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
|
|
/**
|
|
* Fixed Deposit Account Maturity Terms Step -- what happens to this account at maturity
|
|
* (Withdraw / Transfer to Savings / Reinvest) and the pre-mature-closure penalty, decided
|
|
* per account rather than silently inherited from the product.
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-fixed-deposit-account-maturity-terms-step',
|
|
templateUrl: './fixed-deposit-account-maturity-terms-step.component.html',
|
|
styleUrls: ['./fixed-deposit-account-maturity-terms-step.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
MatCheckbox,
|
|
MatIcon,
|
|
MatIconButton
|
|
]
|
|
})
|
|
export class FixedDepositAccountMaturityTermsStepComponent implements OnChanges {
|
|
private formBuilder = inject(UntypedFormBuilder);
|
|
|
|
@Input() fixedDepositsAccountTemplate: any;
|
|
@Input() fixedDepositsAccountProductTemplate: any;
|
|
/** Account already linked elsewhere in the wizard (the Details step's Deposit Account Link) --
|
|
* pre-fills Transfer to Savings so the officer doesn't have to pick the same account twice. */
|
|
@Input() linkedSavingsAccountId: number | null = null;
|
|
|
|
closureSettingsForm: UntypedFormGroup;
|
|
maturityInstructionOptions: any[] = [];
|
|
savingsAccountsData: any[] = [];
|
|
preClosurePenalInterestOnTypeData: any[] = [];
|
|
|
|
constructor() {
|
|
this.closureSettingsForm = this.formBuilder.group({
|
|
maturityInstructionId: [''],
|
|
preClosurePenalApplicable: [false],
|
|
preClosurePenalInterest: [
|
|
'',
|
|
Validators.min(0)
|
|
],
|
|
preClosurePenalInterestOnTypeId: ['']
|
|
});
|
|
this.closureSettingsForm.get('maturityInstructionId').valueChanges.subscribe((value: number) => {
|
|
// 200 = Transfer to Savings -- the whole matured balance goes here. 400 = Reinvest Principal
|
|
// Only -- the principal rolls into a new FD, but the leftover interest still needs the same
|
|
// kind of target so it isn't left stranded on the closed account (a real gap the account used
|
|
// to silently hit before this field also asked for it here). Reinvest Principal + Interest
|
|
// (300) rolls the whole balance into the same account, so it never needs a target at all.
|
|
if (value === 200 || value === 400) {
|
|
this.closureSettingsForm.addControl(
|
|
'transferToSavingsId',
|
|
new UntypedFormControl(this.linkedSavingsAccountId ?? '', Validators.required)
|
|
);
|
|
} else {
|
|
this.closureSettingsForm.removeControl('transferToSavingsId');
|
|
}
|
|
});
|
|
}
|
|
|
|
ngOnChanges(changes: SimpleChanges) {
|
|
if (changes.linkedSavingsAccountId) {
|
|
const control = this.closureSettingsForm.get('transferToSavingsId');
|
|
if (control && !control.value && this.linkedSavingsAccountId) {
|
|
control.setValue(this.linkedSavingsAccountId);
|
|
}
|
|
}
|
|
if (this.fixedDepositsAccountProductTemplate) {
|
|
// Withdraw Deposit (id 100) books the maturity proceeds out of the bank's cash/vault the moment
|
|
// the account matures, before the customer has actually collected it -- disabled everywhere.
|
|
this.maturityInstructionOptions = (
|
|
this.fixedDepositsAccountProductTemplate.maturityInstructionOptions ?? []
|
|
).filter((option: any) => option.id !== 100);
|
|
this.savingsAccountsData = this.fixedDepositsAccountProductTemplate.savingsAccounts ?? [];
|
|
this.preClosurePenalInterestOnTypeData =
|
|
this.fixedDepositsAccountProductTemplate.preClosurePenalInterestOnTypeOptions ?? [];
|
|
if (!this.closureSettingsForm.get('preClosurePenalApplicable').dirty) {
|
|
this.closureSettingsForm.patchValue({
|
|
preClosurePenalApplicable: this.fixedDepositsAccountProductTemplate.preClosurePenalApplicable ?? false,
|
|
preClosurePenalInterest: this.fixedDepositsAccountProductTemplate.preClosurePenalInterest ?? '',
|
|
preClosurePenalInterestOnTypeId:
|
|
this.fixedDepositsAccountProductTemplate.preClosurePenalInterestOnType?.id ?? ''
|
|
});
|
|
}
|
|
}
|
|
// Editing an existing account: its own already-saved Maturity Instructions choice lives on
|
|
// `fixedDepositsAccountTemplate` (the real account+template GET, FixedDepositAccountData's
|
|
// `onAccountClosure`) -- NOT on `fixedDepositsAccountProductTemplate`, which is only the blank
|
|
// per-product default template refetched from the Product dropdown's own productId (via
|
|
// getFixedDepositsAccountTemplate(clientId, productId)) and carries no account-specific data at
|
|
// all. This was never read from the right place before, so Edit always opened onto a blank
|
|
// picker even for an account that really does have an instruction saved. `.dirty` guards it the
|
|
// same way the penal-interest fields above do, so a maker's own in-progress edit here is never
|
|
// silently overwritten if this input fires again mid-edit.
|
|
if (this.fixedDepositsAccountTemplate && !this.closureSettingsForm.get('maturityInstructionId').dirty) {
|
|
this.closureSettingsForm
|
|
.get('maturityInstructionId')
|
|
?.setValue(this.fixedDepositsAccountTemplate.onAccountClosure?.id ?? '');
|
|
const transferControl = this.closureSettingsForm.get('transferToSavingsId');
|
|
if (transferControl) {
|
|
transferControl.setValue(
|
|
this.fixedDepositsAccountTemplate.transferToSavingsId ?? this.linkedSavingsAccountId ?? ''
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** The linked-savings account for the transfer-to-savings / reinvest-principal-only-interest
|
|
* maturity instruction, once picked. */
|
|
get selectedTransferAccount(): any {
|
|
const id = this.closureSettingsForm.value.transferToSavingsId;
|
|
if (!id) return null;
|
|
return this.savingsAccountsData.find((account: any) => account.id === id) ?? null;
|
|
}
|
|
|
|
get transferAccountCardTitle(): string {
|
|
return this.closureSettingsForm.value.maturityInstructionId === 400
|
|
? 'Interest payout account'
|
|
: 'Transfer to savings account';
|
|
}
|
|
|
|
get transferAccountCardSubtitle(): string {
|
|
return this.closureSettingsForm.value.maturityInstructionId === 400
|
|
? 'The principal renews into a new FD; interest earned is paid out to this account.'
|
|
: 'Maturity proceeds will be credited to this account.';
|
|
}
|
|
|
|
clearTransferAccount(): void {
|
|
this.closureSettingsForm.get('transferToSavingsId')?.setValue('');
|
|
}
|
|
|
|
/** Chosen maturity/closure settings, ready to merge into the submit payload. */
|
|
get closureSettings() {
|
|
const settings = this.closureSettingsForm.getRawValue();
|
|
for (const key in settings) {
|
|
if (settings[key] === '') {
|
|
delete settings[key];
|
|
}
|
|
}
|
|
return settings;
|
|
}
|
|
}
|