Skip to content

Commit 79d9278

Browse files
committed
WEB-874: Prevent negative values in numeric fields - fix duplicate attribute error and add validators
1 parent eaae147 commit 79d9278

4 files changed

Lines changed: 152 additions & 52 deletions

File tree

src/app/loans/loans-account-stepper/loans-account-terms-step/loans-account-terms-step.component.html

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ <h4 class="mat-h4 flex-98">
5353
matInput
5454
required
5555
mifosxPositiveNumber
56-
min="0"
56+
min="1"
5757
formControlName="repaymentEvery"
5858
matTooltip="{{ 'tooltips.Fields are input to calculating the repayment schedule' | translate }}"
5959
/>
@@ -112,7 +112,7 @@ <h4 class="mat-h4 flex-98">
112112
@if (loanProductService.isLoanProduct) {
113113
<mat-form-field class="flex-fill flex-23">
114114
<mat-label>{{ 'labels.inputs.Loan Term' | translate }}</mat-label>
115-
<input type="number" matInput required formControlName="loanTermFrequency" />
115+
<input type="number" matInput required formControlName="loanTermFrequency" min="0" />
116116
@if (loansAccountTermsForm.controls.loanTermFrequency.hasError('required')) {
117117
<mat-error>
118118
{{ 'labels.inputs.Loan Term' | translate }} {{ 'labels.commons.is' | translate }}
@@ -157,7 +157,9 @@ <h4 class="mat-h4 flex-98">{{ 'labels.inputs.Repayments' | translate }}</h4>
157157
<input
158158
type="number"
159159
matInput
160+
required
160161
formControlName="numberOfRepayments"
162+
min="0"
161163
matTooltip="{{ 'tooltips.Enter the total count of repayments' | translate }}"
162164
/>
163165
@if (loansAccountTermsForm.controls.numberOfRepayments.hasError('required')) {
@@ -218,6 +220,7 @@ <h4 class="mat-h4 flex-98">
218220
matInput
219221
required
220222
formControlName="repaymentEvery"
223+
min="0"
221224
matTooltip="{{ 'tooltips.Fields are input to calculating the repayment schedule' | translate }}"
222225
/>
223226
@if (loansAccountTermsForm.controls.repaymentEvery.hasError('required')) {
@@ -287,7 +290,7 @@ <h4 class="mat-h4 flex-98">{{ 'labels.inputs.Nominal interest rate' | translate
287290
@if (!loansAccountTermsData?.isLoanProductLinkedToFloatingRate) {
288291
<mat-form-field class="flex-fill flex-23">
289292
<mat-label>{{ 'labels.inputs.Nominal interest rate' | translate }} %</mat-label>
290-
<input type="number" matInput formControlName="interestRatePerPeriod" />
293+
<input type="number" matInput formControlName="interestRatePerPeriod" min="0.01" step="0.01" />
291294
</mat-form-field>
292295
<mat-form-field class="flex-fill flex-23">
293296
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
@@ -468,6 +471,7 @@ <h4 class="mat-h4 flex-98">{{ 'labels.heading.Interest Calculations' | translate
468471
matInput
469472
type="number"
470473
formControlName="inArrearsTolerance"
474+
min="0"
471475
matTooltip="{{ 'tooltips.With Arrears tolerance' | translate }}"
472476
/>
473477
</mat-form-field>
@@ -476,7 +480,9 @@ <h4 class="mat-h4 flex-98">{{ 'labels.heading.Interest Calculations' | translate
476480
<mat-label>{{ 'labels.inputs.Interest free period' | translate }}</mat-label>
477481
<input
478482
matInput
483+
type="number"
479484
formControlName="graceOnInterestCharged"
485+
min="0"
480486
matTooltip="{{ 'tooltips.If the Interest Free Period' | translate }}"
481487
/>
482488
</mat-form-field>
@@ -488,17 +494,17 @@ <h4 class="mat-h4 flex-98">
488494

489495
<mat-form-field class="flex-fill flex-23">
490496
<mat-label>{{ 'labels.inputs.Grace on principal payment' | translate }}</mat-label>
491-
<input type="number" matInput formControlName="graceOnPrincipalPayment" />
497+
<input type="number" matInput formControlName="graceOnPrincipalPayment" min="0" />
492498
</mat-form-field>
493499

494500
<mat-form-field class="flex-fill flex-23">
495501
<mat-label>{{ 'labels.inputs.Grace on interest payment' | translate }}</mat-label>
496-
<input type="number" matInput formControlName="graceOnInterestPayment" />
502+
<input type="number" matInput formControlName="graceOnInterestPayment" min="0" />
497503
</mat-form-field>
498504

499505
<mat-form-field class="flex-48">
500506
<mat-label>{{ 'labels.inputs.On arrears ageing' | translate }}</mat-label>
501-
<input type="number" matInput formControlName="graceOnArrearsAgeing" />
507+
<input type="number" matInput formControlName="graceOnArrearsAgeing" min="0" />
502508
</mat-form-field>
503509

504510
@if (isDelinquencyEnabled()) {

src/app/loans/loans-account-stepper/loans-account-terms-step/loans-account-terms-step.component.ts

Lines changed: 93 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
105105
/** Is Multi Disburse Loan */
106106
multiDisburseLoan: any;
107107
// @Input() loansAccountFormValid: LoansAccountFormValid
108-
@Input() loansAccountFormValid: boolean;
108+
@Input() loansAccountFormValid: boolean = false;
109109
// @Input collateralOptions: Collateral Options
110110
@Input() collateralOptions: any;
111111
// @Input loanPrincipal: Loan Principle
@@ -116,7 +116,7 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
116116
/** Maximum date allowed. */
117117
maxDate = new Date(2100, 0, 1);
118118
/** Loans Account Terms Form */
119-
loansAccountTermsForm: UntypedFormGroup;
119+
loansAccountTermsForm: UntypedFormGroup = new UntypedFormGroup({});
120120
/** Term Frequency Type Data */
121121
termFrequencyTypeData: any;
122122
/** Repayment Frequency Nth Day Type Data */
@@ -259,7 +259,7 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
259259
'enableInstallmentLevelDelinquency',
260260
new UntypedFormControl(
261261
this.loansAccountTermsData.enableInstallmentLevelDelinquency ||
262-
this.loanProduct.enableInstallmentLevelDelinquency
262+
this.loanProduct?.enableInstallmentLevelDelinquency
263263
)
264264
);
265265
}
@@ -397,7 +397,7 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
397397
'enableInstallmentLevelDelinquency',
398398
new UntypedFormControl(
399399
this.loansAccountTermsData.enableInstallmentLevelDelinquency ||
400-
this.loanProduct.enableInstallmentLevelDelinquency
400+
this.loanProduct?.enableInstallmentLevelDelinquency
401401
)
402402
);
403403
}
@@ -437,6 +437,7 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
437437
this.setAdvancedPaymentStrategyControls();
438438
this.setCustomValidators();
439439
this.setLoanTermListener();
440+
this.setNumericFieldListeners();
440441

441442
if (this.allowAddDisbursementDetails()) {
442443
this.loansAccountTermsForm.removeControl('maxOutstandingLoanBalance');
@@ -487,34 +488,34 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
487488
const repaymentFrequencyNthDayType = this.loansAccountTermsForm.get('repaymentFrequencyNthDayType');
488489
const repaymentFrequencyDayOfWeekType = this.loansAccountTermsForm.get('repaymentFrequencyDayOfWeekType');
489490

490-
this.loansAccountTermsForm.get('repaymentFrequencyType').valueChanges.subscribe((repaymentFrequencyType) => {
491-
repaymentFrequencyNthDayType.setValidators(null);
492-
repaymentFrequencyDayOfWeekType.setValidators(null);
491+
this.loansAccountTermsForm.get('repaymentFrequencyType')?.valueChanges.subscribe((repaymentFrequencyType) => {
492+
repaymentFrequencyNthDayType?.setValidators(null);
493+
repaymentFrequencyDayOfWeekType?.setValidators(null);
493494

494495
setTimeout(() => {
495-
repaymentFrequencyNthDayType.updateValueAndValidity();
496-
repaymentFrequencyDayOfWeekType.updateValueAndValidity();
496+
repaymentFrequencyNthDayType?.updateValueAndValidity();
497+
repaymentFrequencyDayOfWeekType?.updateValueAndValidity();
497498
});
498499
});
499500
}
500501

501502
/** Custom Listeners for the form to calculate Loan Term */
502503
setLoanTermListener() {
503-
this.loansAccountTermsForm.get('numberOfRepayments').valueChanges.subscribe((numberOfRepayments) => {
504+
this.loansAccountTermsForm.get('numberOfRepayments')?.valueChanges.subscribe((numberOfRepayments) => {
504505
const repaymentEvery: number = this.loansAccountTermsForm.value.repaymentEvery;
505506
this.calculateLoanTerm(numberOfRepayments, repaymentEvery);
506507
});
507508

508-
this.loansAccountTermsForm.get('repaymentEvery').valueChanges.subscribe((repaymentEvery) => {
509+
this.loansAccountTermsForm.get('repaymentEvery')?.valueChanges.subscribe((repaymentEvery) => {
509510
const numberOfRepayments: number = this.loansAccountTermsForm.value.numberOfRepayments;
510511
this.calculateLoanTerm(numberOfRepayments, repaymentEvery);
511512
});
512513

513-
this.loansAccountTermsForm.get('loanTermFrequencyType').valueChanges.subscribe((loanTermFrequencyType) => {
514+
this.loansAccountTermsForm.get('loanTermFrequencyType')?.valueChanges.subscribe((loanTermFrequencyType) => {
514515
this.loansAccountTermsForm.patchValue({ repaymentFrequencyType: loanTermFrequencyType });
515516
});
516517

517-
this.loansAccountTermsForm.get('amortizationType').valueChanges.subscribe((amortizationType) => {
518+
this.loansAccountTermsForm.get('amortizationType')?.valueChanges.subscribe((amortizationType) => {
518519
if (amortizationType === 0) {
519520
// Equal Principal Payments
520521
this.loansAccountTermsForm.addControl('fixedPrincipalPercentagePerInstallment', new UntypedFormControl(''));
@@ -525,6 +526,38 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
525526
});
526527
}
527528

529+
/** Prevent negative values in numeric fields */
530+
setNumericFieldListeners() {
531+
const numericFieldsWithMinZero = [
532+
'graceOnPrincipalPayment',
533+
'graceOnInterestPayment',
534+
'graceOnArrearsAgeing',
535+
'inArrearsTolerance',
536+
'graceOnInterestCharged',
537+
'loanTermFrequency',
538+
'numberOfRepayments',
539+
'repaymentEvery'
540+
];
541+
numericFieldsWithMinZero.forEach((fieldName) => {
542+
const control = this.loansAccountTermsForm.get(fieldName);
543+
if (control) {
544+
control.valueChanges.subscribe((value) => {
545+
if (typeof value === 'number' && value < 0) {
546+
control.setValue(0, { emitEvent: false });
547+
}
548+
});
549+
}
550+
});
551+
const interestRateControl = this.loansAccountTermsForm.get('interestRatePerPeriod');
552+
if (interestRateControl) {
553+
interestRateControl.valueChanges.subscribe((value) => {
554+
if (typeof value === 'number' && value < 0.01) {
555+
interestRateControl.setValue(0.01, { emitEvent: false });
556+
}
557+
});
558+
}
559+
}
560+
528561
setAdvancedPaymentStrategyControls(): void {
529562
// Fixed Length validation
530563
if (this.loansAccountTermsData) {
@@ -533,7 +566,10 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
533566
if (this.loansAccountTermsData.product.fixedLength) {
534567
this.loansAccountTermsForm.addControl(
535568
'interestRatePerPeriod',
536-
new UntypedFormControl({ value: 0, disabled: true }, Validators.required)
569+
new UntypedFormControl({ value: 0, disabled: true }, [
570+
Validators.required,
571+
Validators.min(0.01)
572+
])
537573
);
538574
this.loansAccountTermsForm.addControl(
539575
'fixedLength',
@@ -542,7 +578,10 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
542578
} else {
543579
this.loansAccountTermsForm.addControl(
544580
'interestRatePerPeriod',
545-
new UntypedFormControl(this.loansAccountTermsData.interestRatePerPeriod, Validators.required)
581+
new UntypedFormControl(this.loansAccountTermsData.interestRatePerPeriod, [
582+
Validators.required,
583+
Validators.min(0.01)
584+
])
546585
);
547586
}
548587
}
@@ -569,19 +608,28 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
569608
],
570609
loanTermFrequency: [
571610
{ value: '', disabled: true },
572-
Validators.required
611+
[
612+
Validators.required,
613+
Validators.min(0)
614+
]
573615
],
574616
loanTermFrequencyType: [
575617
'',
576618
Validators.required
577619
],
578620
numberOfRepayments: [
579621
'',
580-
Validators.required
622+
[
623+
Validators.required,
624+
Validators.min(0)
625+
]
581626
],
582627
repaymentEvery: [
583628
'',
584-
Validators.required
629+
[
630+
Validators.required,
631+
Validators.min(0)
632+
]
585633
],
586634
repaymentFrequencyType: [
587635
{ value: '', disabled: true },
@@ -591,7 +639,10 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
591639
repaymentFrequencyDayOfWeekType: [''],
592640
repaymentsStartingFromDate: [''],
593641
interestChargedFromDate: [''],
594-
interestRatePerPeriod: [''],
642+
interestRatePerPeriod: [
643+
'',
644+
Validators.min(0.01)
645+
],
595646
interestType: [''],
596647
isFloatingInterestRate: [null],
597648
isEqualAmortization: [''],
@@ -601,11 +652,26 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
601652
],
602653
interestCalculationPeriodType: [''],
603654
allowPartialPeriodInterestCalculation: [''],
604-
inArrearsTolerance: [''],
605-
graceOnInterestCharged: [''],
606-
graceOnPrincipalPayment: [''],
607-
graceOnInterestPayment: [''],
608-
graceOnArrearsAgeing: [''],
655+
inArrearsTolerance: [
656+
'',
657+
Validators.min(0)
658+
],
659+
graceOnInterestCharged: [
660+
'',
661+
Validators.min(0)
662+
],
663+
graceOnPrincipalPayment: [
664+
'',
665+
Validators.min(0)
666+
],
667+
graceOnInterestPayment: [
668+
'',
669+
Validators.min(0)
670+
],
671+
graceOnArrearsAgeing: [
672+
'',
673+
Validators.min(0)
674+
],
609675
loanIdToClose: [''],
610676
fixedEmiAmount: [''],
611677
isTopup: [''],
@@ -689,7 +755,7 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
689755
* Adds the Disbursement Data entry form to given Disbursement Data entry.
690756
*/
691757
addDisbursementDataEntry() {
692-
const currentPrincipalAmount = this.loansAccountTermsForm.get('principalAmount').value;
758+
const currentPrincipalAmount = this.loansAccountTermsForm.get('principalAmount')?.value;
693759
const formfields: FormfieldBase[] = [
694760
new DatepickerBase({
695761
controlName: 'expectedDisbursementDate',
@@ -734,7 +800,7 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
734800
* @param {number} index Array index from where Disbursement Data entry form needs to be removed.
735801
*/
736802
removeDisbursementDataEntry(index: number) {
737-
const currentPrincipalAmount = this.loansAccountTermsForm.get('principalAmount').value;
803+
const currentPrincipalAmount = this.loansAccountTermsForm.get('principalAmount')?.value;
738804
const dialogRef = this.dialog.open(DeleteDialogComponent, {
739805
data: { deleteContext: `this` }
740806
});
@@ -814,7 +880,7 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
814880
this.clientActiveLoanData = this.loansAccountProductTemplate.clientActiveLoanOptions;
815881
this.loanScheduleType = this.loansAccountProductTemplate.loanScheduleType;
816882
this.transactionProcessingStrategyOptions = [];
817-
if (this.loanScheduleType.code === LoanProducts.LOAN_SCHEDULE_TYPE_CUMULATIVE) {
883+
if (this.loanScheduleType?.code === LoanProducts.LOAN_SCHEDULE_TYPE_CUMULATIVE) {
818884
// Filter Advanced Payment Allocation Strategy
819885
this.transactionProcessingStrategyOptions =
820886
this.loansAccountProductTemplate.transactionProcessingStrategyOptions.filter(

0 commit comments

Comments
 (0)