Skip to content

Commit 2e8f717

Browse files
WEB-657: Working Capital product near breach configuration
1 parent 33b7290 commit 2e8f717

49 files changed

Lines changed: 2675 additions & 94 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app/core/http/error-handler.interceptor.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ const log = new Logger('ErrorHandlerInterceptor');
3232
export class ErrorHandlerInterceptor implements HttpInterceptor {
3333
private alertService = inject(AlertService);
3434
private translate = inject(TranslateService);
35+
private databaseErrorCodes: string[] = [
36+
'error.msg.data.integrity.issue.entity.duplicated',
37+
'error.msg.data.integrity.issue'
38+
];
3539

3640
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
3741
return next.handle(request).pipe(catchError((error) => this.handleError(error, request)));
@@ -63,9 +67,16 @@ export class ErrorHandlerInterceptor implements HttpInterceptor {
6367
let parameterName: string | null = null;
6468
if (response.error.errors) {
6569
if (response.error.errors[0]) {
66-
errorMessage =
67-
response.error.errors[0].defaultUserMessage.replace(/\\./g, ' ') ||
68-
response.error.errors[0].developerMessage.replace(/\\./g, ' ');
70+
if (
71+
response.error.errors[0].userMessageGlobalisationCode &&
72+
this.databaseErrorCodes.indexOf(response.error.errors[0].userMessageGlobalisationCode) > -1
73+
) {
74+
errorMessage = this.translate.instant('errors.error.msg.data.integrity.issue');
75+
} else {
76+
errorMessage =
77+
response.error.errors[0].defaultUserMessage.replace(/\\./g, ' ') ||
78+
response.error.errors[0].developerMessage.replace(/\\./g, ' ');
79+
}
6980
}
7081
if ('parameterName' in response.error.errors[0]) {
7182
parameterName = response.error.errors[0].parameterName;

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,18 @@ <h3 class="mat-h3 flex-fill">{{ 'labels.heading.Terms' | translate }}</h3>
109109
</div>
110110
<div class="flex-fill layout-row">
111111
<span class="flex-40">{{ 'labels.inputs.Breach' | translate }}:</span>
112-
<span class="flex-60"><mifosx-breach-display [singleRow]="false" [breach]="loanProduct.breach" /></span>
112+
<span class="flex-60"><mifosx-breach-display [singleRow]="false" [breach]="getBreach()" /></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"><mifosx-breach-display [singleRow]="false" [nearBreach]="getNearBreach()" /></span>
122+
</div>
123+
}
114124
}
115125
@if (loanProductService.isLoanProduct) {
116126
<div class="flex-fill layout-row">

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { Component, Input, OnChanges, OnInit, SimpleChanges, inject } from '@angular/core';
10-
import { DelinquencyBucket, LoanProduct } from '../../models/loan-product.model';
10+
import { Breach, DelinquencyBucket, LoanProduct, NearBreach } from '../../models/loan-product.model';
1111
import {
1212
AccountingMapping,
1313
Charge,
@@ -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,11 +685,37 @@ export class LoanProductSummaryComponent extends LoanProductBaseComponent implem
675685
);
676686
}
677687

688+
enableNearBreach(): boolean {
689+
return this.loanProductService.isWorkingCapital && this.getNearBreach() !== null;
690+
}
691+
678692
getAccountingRuleName(value: string): string {
679693
return this.loanProductService.isWorkingCapital ? '' : this.accounting.getAccountRuleName(value.toUpperCase());
680694
}
681695

682696
mapHumanReadableValueStringEnumOptionDataList(incomingParameter: StringEnumOptionData[]): string[] {
683697
return incomingParameter.map((v) => v.value);
684698
}
699+
700+
getBreach(): Breach | null {
701+
if (this.loanProduct.breach) {
702+
return this.loanProduct.breach;
703+
}
704+
if (this.loanProduct.breachId === null || this.loanProduct.breachId === undefined) {
705+
return null;
706+
}
707+
return this.loanProductsTemplate.breachOptions?.find((b: Breach) => b.id === this.loanProduct.breachId) || null;
708+
}
709+
710+
getNearBreach(): NearBreach | null {
711+
if (this.loanProduct.nearBreach) {
712+
return this.loanProduct.nearBreach;
713+
}
714+
if (this.loanProduct.nearBreachId === null || this.loanProduct.nearBreachId === undefined) {
715+
return null;
716+
}
717+
return (
718+
this.loanProductsTemplate.nearBreachOptions?.find((b: Breach) => b.id === this.loanProduct.nearBreachId) || null
719+
);
720+
}
685721
}

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

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

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

Lines changed: 24 additions & 4 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
@@ -188,7 +192,8 @@ export class LoanProductSettingsStepComponent extends LoanProductBaseComponent i
188192
delinquencyStartType: this.loanProductsTemplate.delinquencyStartType
189193
? this.loanProductsTemplate.delinquencyStartType.id
190194
: null,
191-
breachId: this.loanProductsTemplate.breach?.id ?? null
195+
breachId: this.loanProductsTemplate.breach?.id ?? null,
196+
nearBreachId: this.loanProductsTemplate.nearBreach?.id ?? null
192197
});
193198
}
194199

@@ -459,7 +464,8 @@ export class LoanProductSettingsStepComponent extends LoanProductBaseComponent i
459464
]
460465
],
461466
delinquencyStartType: [''],
462-
breachId: ['']
467+
breachId: [''],
468+
nearBreachId: ['']
463469
});
464470
}
465471
}
@@ -921,6 +927,15 @@ export class LoanProductSettingsStepComponent extends LoanProductBaseComponent i
921927
delinquencyBucketId: ''
922928
});
923929
}
930+
} else if (propertyName === 'breachId') {
931+
this.loanProductSettingsForm.patchValue({
932+
breachId: '',
933+
nearBreachId: ''
934+
});
935+
} else if (propertyName === 'nearBreachId') {
936+
this.loanProductSettingsForm.patchValue({
937+
nearBreachId: ''
938+
});
924939
}
925940
this.loanProductSettingsForm.markAsDirty();
926941
$event.stopPropagation();
@@ -931,6 +946,11 @@ export class LoanProductSettingsStepComponent extends LoanProductBaseComponent i
931946
return this.breachOptions ? this.breachOptions.find((b) => b.id === id) : undefined;
932947
}
933948

949+
get selectedNearBreach(): NearBreach | undefined {
950+
const id = this.loanProductSettingsForm.get('nearBreachId')?.value;
951+
return id ? (this.nearBreachOptions ? this.nearBreachOptions.find((b) => b.id === id) : undefined) : undefined;
952+
}
953+
934954
get loanProductSettings() {
935955
const productSettings = this.loanProductSettingsForm.value;
936956
if (this.loanProductSettingsForm.value.useDueForRepaymentsConfigurations) {

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@ export interface LoanProduct {
186186
buydownFeeClassificationToIncomeAccountMappings?: ClassificationToIncomeAccountMapping[];
187187
capitalizedIncomeClassificationToIncomeAccountMappings?: ClassificationToIncomeAccountMapping[];
188188
writeOffReasonsToExpenseMappings?: ChargeOffReasonToExpenseAccountMapping[];
189+
190+
// Working Capital attributes
191+
breach?: Breach;
192+
breachId?: number;
193+
nearBreach?: NearBreach;
194+
nearBreachId?: number;
189195
}
190196

191197
export interface AllowAttributeOverrides {
@@ -240,8 +246,17 @@ export interface AccountingMappingDTO {
240246

241247
export interface Breach {
242248
id: number;
249+
name: string;
243250
breachFrequency: number;
244251
breachFrequencyType: StringEnumOptionData;
245252
breachAmountCalculationType: StringEnumOptionData;
246253
breachAmount: number;
247254
}
255+
256+
export interface NearBreach {
257+
id: number;
258+
name: string;
259+
frequency: number;
260+
frequencyType: StringEnumOptionData;
261+
threshold: number;
262+
}

src/app/products/loan-products/working-capital/breach-configuration/breach-configuration.component.html

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,17 @@
3333
<td mat-cell *matCellDef="let breach">{{ breach.id }}</td>
3434
</ng-container>
3535

36-
<ng-container matColumnDef="breachFrequency">
37-
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Frequency' | translate }}</th>
38-
<td mat-cell *matCellDef="let breach">{{ breach.breachFrequency | formatNumber: '' : 0 }}</td>
36+
<ng-container matColumnDef="name">
37+
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Name' | translate }}</th>
38+
<td mat-cell *matCellDef="let breach">{{ breach.name }}</td>
3939
</ng-container>
4040

41-
<ng-container matColumnDef="breachFrequencyType">
42-
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Frequency Type' | translate }}</th>
43-
<td mat-cell *matCellDef="let breach">{{ breach.breachFrequencyType.code | translateKey: 'catalogs' }}</td>
41+
<ng-container matColumnDef="breachFrequency">
42+
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Frequency' | translate }}</th>
43+
<td mat-cell *matCellDef="let breach">
44+
{{ breach.breachFrequency | formatNumber: '' : 0 }}
45+
{{ breach.breachFrequencyType.code | translateKey: 'catalogs' }}
46+
</td>
4447
</ng-container>
4548

4649
<ng-container matColumnDef="breachAmountCalculationType">

src/app/products/loan-products/working-capital/breach-configuration/breach-configuration.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ export class BreachConfigurationComponent implements OnInit {
6464
/** Columns to be displayed in breaches table. */
6565
displayedColumns: string[] = [
6666
'id',
67+
'name',
6768
'breachFrequency',
68-
'breachFrequencyType',
6969
'breachAmountCalculationType',
7070
'breachAmount',
7171
'actions'
@@ -88,6 +88,7 @@ export class BreachConfigurationComponent implements OnInit {
8888
this.dataSource.filterPredicate = (data: Breach, filter: string) =>
8989
[
9090
data.id,
91+
data.name,
9192
data.breachFrequency,
9293
data.breachFrequencyType?.code,
9394
data.breachAmountCalculationType?.code,

src/app/products/loan-products/working-capital/breach-configuration/create-breach-configuration/create-breach-configuration.component.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111
<form [formGroup]="breachForm" (ngSubmit)="submit()">
1212
<mat-card-content>
1313
<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="name" />
17+
@if (breachForm.controls.name.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>
1424
<mifosx-input-positive-integer
1525
class="flex-48"
1626
[inputFormControl]="breachForm.controls.breachFrequency"

src/app/products/loan-products/working-capital/breach-configuration/create-breach-configuration/create-breach-configuration.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ export class CreateBreachConfigurationComponent implements OnInit {
4949

5050
ngOnInit(): void {
5151
this.breachForm = this.formBuilder.group({
52+
name: [
53+
'',
54+
[
55+
Validators.required
56+
]
57+
],
5258
breachFrequency: [
5359
'',
5460
[

0 commit comments

Comments
 (0)