Skip to content

Commit bf29e21

Browse files
WEB-657: Working Capital product near breach configuration
1 parent 2cd4264 commit bf29e21

38 files changed

Lines changed: 1132 additions & 45 deletions

src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.html

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,27 @@ <h3 class="mat-h3 flex-fill">{{ 'labels.heading.Terms' | translate }}</h3>
111111
<span class="flex-40">{{ 'labels.inputs.Breach' | translate }}:</span>
112112
<span class="flex-60"><mifosx-breach-display [singleRow]="false" [breach]="loanProduct.breach" /></span>
113113
</div>
114+
<div class="flex-fill layout-row">
115+
<span class="flex-40">{{ 'labels.inputs.Enable Near Breach' | translate }}:</span>
116+
<span class="flex-60">{{ enableNearBreach() | yesNo }}</span>
117+
</div>
118+
@if (enableNearBreach()) {
119+
<div class="flex-fill layout-row">
120+
<span class="flex-40">{{ 'labels.inputs.Near Breach' | translate }}:</span>
121+
<span class="flex-60">{{ loanProduct.nearBreach.name }} </span>
122+
</div>
123+
<div class="flex-fill layout-row">
124+
<span class="flex-40">{{ 'labels.inputs.Near Breach Evaluation Frequency' | translate }}:</span>
125+
<span class="flex-60"
126+
>{{ loanProduct.nearBreach.frequency | formatNumber: '' : 0 }}
127+
{{ loanProduct.nearBreach.frequencyType?.code | translateKey: 'catalogs' }}
128+
</span>
129+
</div>
130+
<div class="flex-fill layout-row">
131+
<span class="flex-40">{{ 'labels.inputs.Near Breach Threshold' | translate }}:</span>
132+
<span class="flex-60">{{ loanProduct.nearBreach.threshold | formatNumber }} %</span>
133+
</div>
134+
}
114135
}
115136
@if (loanProductService.isLoanProduct) {
116137
<div class="flex-fill layout-row">

src/app/products/loan-products/common/loan-product-summary/loan-product-summary.component.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,16 @@ export class LoanProductSummaryComponent extends LoanProductBaseComponent implem
538538
};
539539
}
540540
}
541+
542+
if (this.loanProductService.isWorkingCapital) {
543+
/*
544+
let optionValue: OptionData = this.optionDataLookUp(
545+
this.loanProduct.nearBreach.frequencyType,
546+
this.loanProductsTemplate.periodFrequencyTypeOptions
547+
);
548+
this.loanProduct.nearBreachEvalFrequencyType = optionValue;
549+
*/
550+
}
541551
}
542552
}
543553

@@ -675,6 +685,10 @@ export class LoanProductSummaryComponent extends LoanProductBaseComponent implem
675685
);
676686
}
677687

688+
enableNearBreach(): boolean {
689+
return this.loanProductService.isWorkingCapital && 'nearBreach' in this.loanProduct;
690+
}
691+
678692
getAccountingRuleName(value: string): string {
679693
return this.loanProductService.isWorkingCapital ? '' : this.accounting.getAccountRuleName(value.toUpperCase());
680694
}

src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,65 @@
9393
</button>
9494
}
9595
</mat-form-field>
96+
97+
@if (loanProductSettingsForm.value.breachId) {
98+
<mat-form-field class="flex-48">
99+
<mat-label>{{ 'labels.inputs.Near Breach' | translate }}</mat-label>
100+
<mat-select formControlName="nearBreachId">
101+
@for (nearBreach of nearBreachOptions; track nearBreach) {
102+
<mat-option [value]="nearBreach.id">
103+
{{ nearBreach.name }}
104+
</mat-option>
105+
}
106+
</mat-select>
107+
@if (loanProductSettingsForm.controls.nearBreachId) {
108+
<button matSuffix mat-icon-button aria-label="Clear" (click)="clearProperty($event, 'nearBreachId')">
109+
<fa-icon icon="close" size="md"></fa-icon>
110+
</button>
111+
}
112+
</mat-form-field>
113+
}
114+
115+
@if (loanProductSettingsForm.value.enableNearBreach) {
116+
<mat-form-field class="flex-23">
117+
<mat-label>{{ 'labels.inputs.Near Breach Threshold' | translate }} %</mat-label>
118+
<input
119+
type="number"
120+
matInput
121+
required
122+
mifosxPositiveNumber
123+
formControlName="nearBreachThreshold"
124+
min="0.01"
125+
max="100.00"
126+
step="0.01"
127+
/>
128+
@if (loanProductSettingsForm.controls.nearBreachThreshold.hasError('required')) {
129+
<mat-error>
130+
{{ 'labels.inputs.Near Breach Threshold' | translate }} {{ 'labels.commons.is' | translate }}
131+
<strong>{{ 'labels.commons.required' | translate }}</strong>
132+
</mat-error>
133+
}
134+
</mat-form-field>
135+
136+
<mifosx-input-positive-integer
137+
class="flex-23"
138+
[inputFormControl]="loanProductSettingsForm.controls.nearBreachEvalFrequency"
139+
[inputLabel]="'Near Breach Evaluation Frequency'"
140+
[isRequired]="true"
141+
[minVal]="'1'"
142+
></mifosx-input-positive-integer>
143+
144+
<mat-form-field class="flex-23">
145+
<mat-label>{{ 'labels.inputs.Near Breach Evaluation Frequency Type' | translate }}</mat-label>
146+
<mat-select formControlName="nearBreachEvalFrequencyType" required>
147+
@for (nearBreachEvalFrequencyType of frequencyTypesOptions; track nearBreachEvalFrequencyType) {
148+
<mat-option [value]="nearBreachEvalFrequencyType.id">
149+
{{ nearBreachEvalFrequencyType.value | translateKey: 'catalogs' }}
150+
</mat-option>
151+
}
152+
</mat-select>
153+
</mat-form-field>
154+
}
96155
<mat-divider class="flex-98"></mat-divider>
97156
} @else if (loanProductService.isLoanProduct) {
98157
<mat-form-field class="flex-30">

src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.ts

Lines changed: 17 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
2121
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
2222
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
2323
import { LoanProductBaseComponent } from '../../common/loan-product-base.component';
24-
import { Breach } from '../../models/loan-product.model';
24+
import { Breach, NearBreach } from '../../models/loan-product.model';
2525
import { BreachDisplayComponent } from 'app/shared/loan/breach-display/breach-display.component';
2626
import { MatSelectTrigger } from '@angular/material/select';
27+
import { InputPositiveIntegerComponent } from 'app/shared/input-positive-integer/input-positive-integer.component';
2728

2829
@Component({
2930
selector: 'mifosx-loan-product-settings-step',
@@ -39,7 +40,8 @@ import { MatSelectTrigger } from '@angular/material/select';
3940
MatStepperPrevious,
4041
MatStepperNext,
4142
MatSelectTrigger,
42-
BreachDisplayComponent
43+
BreachDisplayComponent,
44+
InputPositiveIntegerComponent
4345
]
4446
})
4547
export class LoanProductSettingsStepComponent extends LoanProductBaseComponent implements OnInit {
@@ -87,6 +89,7 @@ export class LoanProductSettingsStepComponent extends LoanProductBaseComponent i
8789

8890
delinquencyStartTypeOptions: StringEnumOptionData[] = [];
8991
breachOptions: Breach[] = [];
92+
nearBreachOptions: NearBreach[] = [];
9093

9194
frequencyTypesOptions: StringEnumOptionData[] = [];
9295

@@ -178,6 +181,7 @@ export class LoanProductSettingsStepComponent extends LoanProductBaseComponent i
178181
if (this.loanProductService.isWorkingCapital) {
179182
this.frequencyTypesOptions = this.loanProductsTemplate.periodFrequencyTypeOptions ?? [];
180183
this.breachOptions = this.loanProductsTemplate.breachOptions ?? [];
184+
this.nearBreachOptions = this.loanProductsTemplate.nearBreachOptions ?? [];
181185
this.delinquencyStartTypeOptions = this.loanProductsTemplate.delinquencyStartTypeOptions;
182186
this.loanProductSettingsForm.patchValue({
183187
amortizationType: this.loanProductsTemplate.amortizationType
@@ -189,16 +193,8 @@ export class LoanProductSettingsStepComponent extends LoanProductBaseComponent i
189193
? this.loanProductsTemplate.delinquencyStartType.id
190194
: null,
191195
breachId: this.loanProductsTemplate.breach?.id ?? null,
192-
enableNearBreach: this.loanProductsTemplate.enableNearBreach || false
196+
nearBreachId: this.loanProductsTemplate.nearBreach?.id ?? null
193197
});
194-
195-
if (this.loanProductsTemplate.enableNearBreach) {
196-
this.loanProductSettingsForm.patchValue({
197-
nearBreachEvalFrequency: this.loanProductsTemplate.nearBreachEvalFrequency || '',
198-
nearBreachEvalFrequencyType: this.loanProductsTemplate.nearBreachEvalFrequencyType?.id || '',
199-
nearBreachThreshold: this.loanProductsTemplate.nearBreachThreshold || ''
200-
});
201-
}
202198
}
203199

204200
this.isAdvancedTransactionProcessingStrategy = LoanProducts.isAdvancedPaymentAllocationStrategy(
@@ -469,7 +465,7 @@ export class LoanProductSettingsStepComponent extends LoanProductBaseComponent i
469465
],
470466
delinquencyStartType: [''],
471467
breachId: [''],
472-
enableNearBreach: [false]
468+
nearBreachId: ['']
473469
});
474470
}
475471
}
@@ -870,31 +866,6 @@ export class LoanProductSettingsStepComponent extends LoanProductBaseComponent i
870866
});
871867
}
872868
});
873-
874-
this.loanProductSettingsForm.get('enableNearBreach').valueChanges.subscribe((enableNearBreach: any) => {
875-
if (enableNearBreach) {
876-
this.loanProductSettingsForm.addControl(
877-
'nearBreachEvalFrequency',
878-
new UntypedFormControl('', Validators.required)
879-
);
880-
this.loanProductSettingsForm.addControl(
881-
'nearBreachEvalFrequencyType',
882-
new UntypedFormControl('', Validators.required)
883-
);
884-
this.loanProductSettingsForm.addControl(
885-
'nearBreachThreshold',
886-
new UntypedFormControl('', [
887-
Validators.required,
888-
Validators.min(0.01),
889-
Validators.max(100.0)
890-
])
891-
);
892-
} else {
893-
this.loanProductSettingsForm.removeControl('nearBreachEvalFrequency');
894-
this.loanProductSettingsForm.removeControl('nearBreachEvalFrequencyType');
895-
this.loanProductSettingsForm.removeControl('nearBreachThreshold');
896-
}
897-
});
898869
}
899870
}
900871

@@ -956,6 +927,15 @@ export class LoanProductSettingsStepComponent extends LoanProductBaseComponent i
956927
delinquencyBucketId: ''
957928
});
958929
}
930+
} else if (propertyName === 'breachId') {
931+
this.loanProductSettingsForm.patchValue({
932+
breachId: ''
933+
});
934+
this.loanProductSettingsForm.removeControl('nearBreachId');
935+
} else if (propertyName === 'nearBreachId') {
936+
this.loanProductSettingsForm.patchValue({
937+
nearBreachId: ''
938+
});
959939
}
960940
this.loanProductSettingsForm.markAsDirty();
961941
$event.stopPropagation();

src/app/products/loan-products/models/loan-product.model.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,7 @@ export interface LoanProduct {
188188
writeOffReasonsToExpenseMappings?: ChargeOffReasonToExpenseAccountMapping[];
189189

190190
// Working Capital attributes
191-
enableNearBreach?: boolean;
192-
nearBreachEvalFrequency?: number;
193-
nearBreachEvalFrequencyType?: OptionData;
194-
nearBreachThreshold?: number;
191+
nearBreach?: NearBreach;
195192
}
196193

197194
export interface AllowAttributeOverrides {
@@ -251,3 +248,11 @@ export interface Breach {
251248
breachAmountCalculationType: StringEnumOptionData;
252249
breachAmount: number;
253250
}
251+
252+
export interface NearBreach {
253+
id: number;
254+
name: string;
255+
frequency: number;
256+
frequencyType: StringEnumOptionData;
257+
threshold: number;
258+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<!--
2+
Copyright since 2025 Mifos Initiative
3+
4+
This Source Code Form is subject to the terms of the Mozilla Public
5+
License, v. 2.0. If a copy of the MPL was not distributed with this
6+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
-->
8+
9+
<div class="container">
10+
<mat-card>
11+
<form [formGroup]="nearBreachForm" (ngSubmit)="submit()">
12+
<mat-card-content>
13+
<div class="layout-column">
14+
<mat-form-field class="flex-100">
15+
<mat-label>{{ 'labels.inputs.Name' | translate }}</mat-label>
16+
<input matInput required formControlName="nearBreachName" />
17+
@if (nearBreachForm.controls.nearBreachName.hasError('required')) {
18+
<mat-error>
19+
{{ 'labels.inputs.Name' | translate }} {{ 'labels.commons.is' | translate }}
20+
<strong>{{ 'labels.commons.required' | translate }}</strong>
21+
</mat-error>
22+
}
23+
</mat-form-field>
24+
<mifosx-input-positive-integer
25+
class="flex-48"
26+
[inputFormControl]="nearBreachForm.controls.nearBreachFrequency"
27+
[inputLabel]="'Frequency'"
28+
[isRequired]="true"
29+
[minVal]="'1'"
30+
></mifosx-input-positive-integer>
31+
<mat-form-field class="flex-48">
32+
<mat-label>{{ 'labels.inputs.Frequency Type' | translate }}</mat-label>
33+
<mat-select required formControlName="nearBreachFrequencyType">
34+
@for (frequencyType of frequencyTypeOptions; track frequencyType) {
35+
<mat-option [value]="frequencyType.id">
36+
{{ frequencyType.code | translateKey: 'catalogs' }}
37+
</mat-option>
38+
}
39+
</mat-select>
40+
@if (nearBreachForm.controls.nearBreachFrequencyType.hasError('required')) {
41+
<mat-error>
42+
{{ 'labels.inputs.Frequency Type' | translate }} {{ 'labels.commons.is' | translate }}
43+
<strong>{{ 'labels.commons.required' | translate }}</strong>
44+
</mat-error>
45+
}
46+
</mat-form-field>
47+
<mat-form-field class="flex-48">
48+
<mat-label>{{ 'labels.inputs.Threshold' | translate }}</mat-label>
49+
<input
50+
type="number"
51+
matInput
52+
required
53+
mifosxPositiveNumber
54+
formControlName="nearBreachThreshold"
55+
min="0.01"
56+
step="0.01"
57+
max="100.0"
58+
/>
59+
@if (nearBreachForm.controls.nearBreachThreshold.hasError('required')) {
60+
<mat-error>
61+
{{ 'labels.inputs.Threshold' | translate }} {{ 'labels.commons.is' | translate }}
62+
<strong>{{ 'labels.commons.required' | translate }}</strong>
63+
</mat-error>
64+
}
65+
</mat-form-field>
66+
</div>
67+
</mat-card-content>
68+
69+
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
70+
<button type="button" mat-raised-button [routerLink]="['../']">
71+
{{ 'labels.buttons.Cancel' | translate }}
72+
</button>
73+
<button
74+
mat-raised-button
75+
color="primary"
76+
[disabled]="!nearBreachForm.valid"
77+
*mifosxHasPermission="'CREATE_WORKINGCAPITALNEARBREACH'"
78+
>
79+
{{ 'labels.buttons.Submit' | translate }}
80+
</button>
81+
</mat-card-actions>
82+
</form>
83+
</mat-card>
84+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright since 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
.container {
10+
max-width: 37rem;
11+
12+
.content {
13+
div {
14+
margin: 1rem 0;
15+
overflow-wrap: break-word;
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)