Skip to content

Commit ed72c07

Browse files
WEB-889: WC - Discount
1 parent 35b428f commit ed72c07

20 files changed

Lines changed: 302 additions & 15 deletions

src/app/loans/common-resolvers/loan-action-button.resolver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ export class LoanActionButtonResolver {
9999
return this.loansService.getLoanActionTemplate(loanId, 'reAmortization');
100100
} else if (loanActionButton === 'Attach Loan Originator') {
101101
return this.organizationService.getLoanOriginators();
102+
} else if (loanActionButton === 'Update discount') {
103+
return this.loansService.getWorkingCapitalLoannDetails(loanId);
102104
} else {
103105
return undefined;
104106
}

src/app/loans/loans-view/loan-account-actions/loan-account-actions.component.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,6 @@
117117
@if (actions['Attach Loan Originator']) {
118118
<mifosx-attach-originator [dataObject]="actionButtonData"></mifosx-attach-originator>
119119
}
120+
@if (actions['Update discount']) {
121+
<mifosx-update-discount [dataObject]="actionButtonData"></mifosx-update-discount>
122+
}

src/app/loans/loans-view/loan-account-actions/loan-account-actions.component.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { UndoWriteOffComponent } from './undo-write-off/undo-write-off.component
4242
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
4343
import { AttachOriginatorComponent } from './attach-originator/attach-originator.component';
4444
import { LoanProductBaseComponent } from 'app/products/loan-products/common/loan-product-base.component';
45+
import { UpdateDiscountComponent } from './update-discount/update-discount.component';
4546

4647
/**
4748
* Loan Account Actions component.
@@ -82,7 +83,8 @@ import { LoanProductBaseComponent } from 'app/products/loan-products/common/loan
8283
LoanReamortizeComponent,
8384
AddInterestPauseComponent,
8485
UndoWriteOffComponent,
85-
AttachOriginatorComponent
86+
AttachOriginatorComponent,
87+
UpdateDiscountComponent
8688
]
8789
})
8890
export class LoanAccountActionsComponent {
@@ -134,6 +136,7 @@ export class LoanAccountActionsComponent {
134136
'Buy Down Fee': boolean;
135137
'Undo Write-off': boolean;
136138
'Attach Loan Originator': boolean;
139+
'Update discount': boolean;
137140
} = {
138141
Close: false,
139142
'Undo Approval': false,
@@ -174,7 +177,8 @@ export class LoanAccountActionsComponent {
174177
'Contract Termination': false,
175178
'Buy Down Fee': false,
176179
'Undo Write-off': false,
177-
'Attach Loan Originator': false
180+
'Attach Loan Originator': false,
181+
'Update discount': false
178182
};
179183

180184
actionButtonData: any;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 mat-elevation-z8">
10+
<mat-card>
11+
<form [formGroup]="updateDiscountForm" (ngSubmit)="submit()">
12+
<mat-card-content>
13+
<div class="layout-column">
14+
<mat-form-field>
15+
<mat-label>{{ 'labels.inputs.Discount' | translate }}</mat-label>
16+
<input matInput type="number" min="0" step="0.01" formControlName="discountAmount" />
17+
@if (updateDiscountForm.controls.discountAmount.hasError('required')) {
18+
<mat-error>
19+
{{ 'labels.inputs.Discount' | translate }} {{ 'labels.commons.is' | translate }}
20+
<strong>{{ 'labels.commons.required' | translate }}</strong>
21+
</mat-error>
22+
}
23+
@if (updateDiscountForm.controls.discountAmount.hasError('min')) {
24+
<mat-error>{{ 'labels.inputs.Discount must be greater than or equal to 0' | translate }}</mat-error>
25+
}
26+
@if (updateDiscountForm.controls.discountAmount.hasError('pattern')) {
27+
<mat-error>{{ 'labels.inputs.Please enter a valid number' | translate }}</mat-error>
28+
}
29+
</mat-form-field>
30+
31+
<mat-form-field>
32+
<mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label>
33+
<textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
34+
<mat-hint align="end">
35+
{{ updateDiscountForm.controls.note.value?.length || 0 }}/{{ maxNoteLength }}
36+
</mat-hint>
37+
@if (updateDiscountForm.controls.note.hasError('maxlength')) {
38+
<mat-error>{{ 'labels.inputs.Note is too long' | translate }}</mat-error>
39+
}
40+
</mat-form-field>
41+
42+
@if (submitErrorMessage) {
43+
<mat-error>{{ submitErrorMessage }}</mat-error>
44+
}
45+
</div>
46+
47+
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
48+
<button type="button" mat-raised-button (click)="gotoLoanDefaultView()" [disabled]="isSubmitting">
49+
{{ 'labels.buttons.Cancel' | translate }}
50+
</button>
51+
<button
52+
mat-raised-button
53+
color="primary"
54+
[disabled]="!updateDiscountForm.valid || isSubmitting"
55+
*mifosxHasPermission="'UPDATEDISCOUNT_WORKINGCAPITALLOAN'"
56+
>
57+
@if (isSubmitting) {
58+
<mat-spinner diameter="20"></mat-spinner>
59+
}
60+
@if (!isSubmitting) {
61+
{{ 'labels.buttons.Submit' | translate }}
62+
}
63+
</button>
64+
</mat-card-actions>
65+
</mat-card-content>
66+
</form>
67+
</mat-card>
68+
</div>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
/** Angular Imports */
10+
import { Component, OnInit, inject } from '@angular/core';
11+
import { HttpErrorResponse } from '@angular/common/http';
12+
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
13+
import { TranslateService } from '@ngx-translate/core';
14+
15+
/** Custom Services */
16+
import { AlertService } from 'app/core/alert/alert.service';
17+
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
18+
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
19+
import { LoanAccountActionsBaseComponent } from '../loan-account-actions-base.component';
20+
import { WorkingCapitalLoanDiscountUpdateRequest } from 'app/loans/loans.service';
21+
22+
/**
23+
* Update discount action for Working Capital Loan.
24+
*/
25+
@Component({
26+
selector: 'mifosx-update-discount',
27+
standalone: true,
28+
templateUrl: './update-discount.component.html',
29+
imports: [
30+
...STANDALONE_SHARED_IMPORTS,
31+
CdkTextareaAutosize
32+
]
33+
})
34+
export class UpdateDiscountComponent extends LoanAccountActionsBaseComponent implements OnInit {
35+
private formBuilder = inject(UntypedFormBuilder);
36+
private alertService = inject(AlertService);
37+
private translateService = inject(TranslateService);
38+
39+
readonly maxNoteLength = 500;
40+
41+
updateDiscountForm: UntypedFormGroup;
42+
isSubmitting = false;
43+
submitErrorMessage = '';
44+
discountValue = 0;
45+
46+
ngOnInit(): void {
47+
this.discountValue = this.dataObject?.discount ?? this.dataObject?.discountAmount ?? 0;
48+
this.updateDiscountForm = this.formBuilder.group({
49+
discountAmount: [
50+
this.discountValue,
51+
[
52+
Validators.required,
53+
Validators.min(0),
54+
Validators.pattern(/^\d+(\.\d+)?$/)
55+
]
56+
],
57+
note: [
58+
'',
59+
Validators.maxLength(this.maxNoteLength)
60+
]
61+
});
62+
}
63+
64+
submit(): void {
65+
if (!this.updateDiscountForm.valid || !this.isWorkingCapital || this.isSubmitting) {
66+
return;
67+
}
68+
69+
this.isSubmitting = true;
70+
this.submitErrorMessage = '';
71+
72+
const formValue = this.updateDiscountForm.value;
73+
const payload: WorkingCapitalLoanDiscountUpdateRequest = {
74+
discountAmount: Number(formValue.discountAmount),
75+
note: formValue.note,
76+
locale: this.settingsService.language.code,
77+
dateFormat: this.settingsService.dateFormat
78+
};
79+
80+
this.loanService.updateWorkingCapitalLoanDiscount(this.loanId, payload).subscribe({
81+
next: () => {
82+
this.alertService.alert({
83+
type: 'Success',
84+
message: this.translateService.instant('labels.messages.workingCapitalDiscountUpdated')
85+
});
86+
this.isSubmitting = false;
87+
this.gotoLoanDefaultView();
88+
},
89+
error: (error: HttpErrorResponse) => {
90+
this.submitErrorMessage = this.mapDiscountError(error);
91+
this.isSubmitting = false;
92+
}
93+
});
94+
}
95+
96+
private mapDiscountError(error: HttpErrorResponse): string {
97+
const backendError = error?.error?.errors?.[0];
98+
return (
99+
backendError?.defaultUserMessage ||
100+
error?.error?.defaultUserMessage ||
101+
this.translateService.instant('labels.messages.unableToUpdateDiscount')
102+
);
103+
}
104+
}

src/app/loans/loans-view/loans-view.component.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,13 @@ export class LoansViewComponent extends LoanProductBaseComponent implements OnIn
257257
return;
258258
}
259259
this.buttonConfig = new LoansAccountButtonConfiguration(this.status, this.loanSubStatus);
260+
if (this.canShowWorkingCapitalDiscountUpdate()) {
261+
this.buttonConfig.addButton({
262+
name: 'Update discount',
263+
icon: 'edit',
264+
taskPermissionName: 'DISBURSE_LOAN'
265+
});
266+
}
260267

261268
if (this.status === 'Submitted and pending approval') {
262269
this.buttonConfig.addOption({
@@ -571,4 +578,11 @@ export class LoansViewComponent extends LoanProductBaseComponent implements OnIn
571578
}
572579
return substatus.code === 'loanSubStatus.loanSubStatusType.contractTermination';
573580
}
581+
582+
private canShowWorkingCapitalDiscountUpdate(): boolean {
583+
if (!this.loanProductService.isWorkingCapital || !this.loanDetailsData) {
584+
return false;
585+
}
586+
return this.loanDetailsData?.status?.active === true;
587+
}
574588
}

src/app/loans/loans.service.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ import { Dates } from 'app/core/utils/dates';
1616
import { SettingsService } from 'app/settings/settings.service';
1717
import { DisbursementData } from './models/loan-account.model';
1818

19+
export interface WorkingCapitalLoanDiscountUpdateRequest {
20+
discountAmount: number;
21+
note?: string;
22+
locale: string;
23+
dateFormat: string;
24+
}
25+
1926
/**
2027
* Loans service.
2128
*/
@@ -560,6 +567,13 @@ export class LoansService {
560567
return this.http.get(`/working-capital-loans/${loanId}/template`, { params: httpParams });
561568
}
562569

570+
updateWorkingCapitalLoanDiscount(
571+
loanId: string | number,
572+
data: WorkingCapitalLoanDiscountUpdateRequest
573+
): Observable<any> {
574+
return this.http.put(`/working-capital-loans/${loanId}/discount`, data);
575+
}
576+
563577
guarantorAccountResource(loanId: string, clientId: any): Observable<any> {
564578
const httpParams = new HttpParams().set('clientId', clientId);
565579
return this.http.get(`/loans/${loanId}/guarantors/accounts/template`, { params: httpParams });

src/assets/translations/cs-CS.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,7 @@
17961796
"Disbursement On": "Vyplácení zapnuto",
17971797
"Disbursement on": "Výplata zapnuta",
17981798
"Discount": "Sleva",
1799+
"Discount must be greater than or equal to 0": "Sleva musí být větší nebo rovna 0",
17991800
"Display Name": "Zobrazovaný název",
18001801
"Dividend Amount": "Částka dividendy",
18011802
"Dividend Period End Date": "Datum ukončení dividendového období",
@@ -2241,6 +2242,7 @@
22412242
"Not Available": "Není dostupný",
22422243
"Not Provided": "Není poskytnuto",
22432244
"Note": "Poznámka",
2245+
"Note is too long": "Poznámka je příliš dlouhá",
22442246
"Notes": "Poznámky",
22452247
"Notes/Comments": "Poznámky/Komentáře",
22462248
"Notification": "Oznámení",
@@ -2254,6 +2256,7 @@
22542256
"Number of Active Overdue Group Loans": "Počet aktivních skupinových půjček po splatnosti",
22552257
"Number of Centers": "Počet středisek",
22562258
"Number of Clients": "Počet klientů",
2259+
"Please enter a valid number": "Zadejte prosím platné číslo",
22572260
"Number of Collaterals": "Počet kolaterálů",
22582261
"Number of Days to Dormant sub-status": "Dílčí stav Počet dnů do klidového stavu",
22592262
"Number of Days to Escheat": "Počet dní do Escheat",
@@ -2974,6 +2977,7 @@
29742977
"Withdrawal": "Vybrání",
29752978
"Withdrawn by Client": "Staženo klientem",
29762979
"Write Off": "Odepsat",
2980+
"Update discount": "Aktualizovat slevu",
29772981
"Apply Additional Shares": "Použít další",
29782982
"Redeem Shares": "Uplatnit akcie",
29792983
"Approve Additional Shares": "Schválit další akcie",
@@ -4067,7 +4071,9 @@
40674071
"No Closed Share Accounts Found": "Žádné uzavřené účty podílů nalezeny",
40684072
"No Collateral Data Found": "Žádná data o kolaterálu nalezeny",
40694073
"No Identities Found": "Žádné identity nalezeny",
4070-
"validationSaved": "Data ověření byla úspěšně uložena."
4074+
"validationSaved": "Data ověření byla úspěšně uložena.",
4075+
"workingCapitalDiscountUpdated": "Sleva pro úvěr pracovního kapitálu byla úspěšně aktualizována.",
4076+
"unableToUpdateDiscount": "Slevu se nepodařilo aktualizovat."
40714077
},
40724078
"languages": {
40734079
"cs-CS": "Čeština",

src/assets/translations/de-DE.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1799,6 +1799,7 @@
17991799
"Disbursement On": "Auszahlung erfolgt",
18001800
"Disbursement on": "Auszahlung am",
18011801
"Discount": "Rabatt",
1802+
"Discount must be greater than or equal to 0": "Der Rabatt muss größer oder gleich 0 sein",
18021803
"Display Name": "Anzeigename",
18031804
"Dividend Amount": "Dividendenbetrag",
18041805
"Dividend Period End Date": "Enddatum des Dividendenzeitraums",
@@ -2243,6 +2244,7 @@
22432244
"Not Available": "Nicht gefunden",
22442245
"Not Provided": "Nicht bereitgestellt",
22452246
"Note": "Notiz",
2247+
"Note is too long": "Die Notiz ist zu lang",
22462248
"Notes": "Anmerkungen",
22472249
"Notes/Comments": "Notizen/Kommentare",
22482250
"Notification": "Benachrichtigung",
@@ -2256,6 +2258,7 @@
22562258
"Number of Active Overdue Group Loans": "Anzahl der aktiven überfälligen Gruppenkredite",
22572259
"Number of Centers": "Anzahl der Zentren",
22582260
"Number of Clients": "Anzahl der Kunden",
2261+
"Please enter a valid number": "Bitte geben Sie eine gültige Zahl ein",
22592262
"Number of Collaterals": "Anzahl der Sicherheiten",
22602263
"Number of Days to Dormant sub-status": "Unterstatus „Anzahl der Tage bis zum Ruhezustand“.",
22612264
"Number of Days to Escheat": "Anzahl der Tage bis zur Pfändung",
@@ -2976,6 +2979,7 @@
29762979
"Withdrawal": "Rückzug",
29772980
"Withdrawn by Client": "Vom Kunden zurückgezogen",
29782981
"Write Off": "Abschreiben",
2982+
"Update discount": "Rabatt aktualisieren",
29792983
"Apply Additional Shares": "Zusätzlich anwenden",
29802984
"Redeem Shares": "Anteile zurückgeben",
29812985
"Approve Additional Shares": "Genehmigen Sie zusätzliche Freigaben",
@@ -4068,7 +4072,9 @@
40684072
"No Closed Share Accounts Found": "Keine geschlossenen Aktienkonten gefunden",
40694073
"No Collateral Data Found": "Keine Sicherheiten gefunden",
40704074
"No Identities Found": "Keine Identitäten gefunden",
4071-
"validationSaved": "Validierungsdaten wurden erfolgreich gespeichert."
4075+
"validationSaved": "Validierungsdaten wurden erfolgreich gespeichert.",
4076+
"workingCapitalDiscountUpdated": "Der Rabatt für den Working-Capital-Kredit wurde erfolgreich aktualisiert.",
4077+
"unableToUpdateDiscount": "Rabatt konnte nicht aktualisiert werden."
40724078
},
40734079
"languages": {
40744080
"cs-CS": "Čeština (Tschechisch)",

0 commit comments

Comments
 (0)