Skip to content

Commit 52d2595

Browse files
WEB-889: WC - Discount
1 parent 4acde0b commit 52d2595

20 files changed

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

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: 'UPDATEDISCOUNT_WORKINGCAPITALLOAN'
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
@@ -1801,6 +1801,7 @@
18011801
"Disbursement On": "Vyplácení zapnuto",
18021802
"Disbursement on": "Výplata zapnuta",
18031803
"Discount": "Sleva",
1804+
"Discount must be greater than or equal to 0": "Sleva musí být větší nebo rovna 0",
18041805
"Display Name": "Zobrazovaný název",
18051806
"Dividend Amount": "Částka dividendy",
18061807
"Dividend Period End Date": "Datum ukončení dividendového období",
@@ -2246,6 +2247,7 @@
22462247
"Not Available": "Není dostupný",
22472248
"Not Provided": "Není poskytnuto",
22482249
"Note": "Poznámka",
2250+
"Note is too long": "Poznámka je příliš dlouhá",
22492251
"Notes": "Poznámky",
22502252
"Notes/Comments": "Poznámky/Komentáře",
22512253
"Notification": "Oznámení",
@@ -2259,6 +2261,7 @@
22592261
"Number of Active Overdue Group Loans": "Počet aktivních skupinových půjček po splatnosti",
22602262
"Number of Centers": "Počet středisek",
22612263
"Number of Clients": "Počet klientů",
2264+
"Please enter a valid number": "Zadejte prosím platné číslo",
22622265
"Number of Collaterals": "Počet kolaterálů",
22632266
"Number of Days to Dormant sub-status": "Dílčí stav Počet dnů do klidového stavu",
22642267
"Number of Days to Escheat": "Počet dní do Escheat",
@@ -2979,6 +2982,7 @@
29792982
"Withdrawal": "Vybrání",
29802983
"Withdrawn by Client": "Staženo klientem",
29812984
"Write Off": "Odepsat",
2985+
"Update discount": "Aktualizovat slevu",
29822986
"Apply Additional Shares": "Použít další",
29832987
"Redeem Shares": "Uplatnit akcie",
29842988
"Approve Additional Shares": "Schválit další akcie",
@@ -4072,7 +4076,9 @@
40724076
"No Closed Share Accounts Found": "Žádné uzavřené účty podílů nalezeny",
40734077
"No Collateral Data Found": "Žádná data o kolaterálu nalezeny",
40744078
"No Identities Found": "Žádné identity nalezeny",
4075-
"validationSaved": "Data ověření byla úspěšně uložena."
4079+
"validationSaved": "Data ověření byla úspěšně uložena.",
4080+
"workingCapitalDiscountUpdated": "Sleva pro úvěr pracovního kapitálu byla úspěšně aktualizována.",
4081+
"unableToUpdateDiscount": "Slevu se nepodařilo aktualizovat."
40764082
},
40774083
"languages": {
40784084
"cs-CS": "Čeština",

src/assets/translations/de-DE.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1804,6 +1804,7 @@
18041804
"Disbursement On": "Auszahlung erfolgt",
18051805
"Disbursement on": "Auszahlung am",
18061806
"Discount": "Rabatt",
1807+
"Discount must be greater than or equal to 0": "Der Rabatt muss größer oder gleich 0 sein",
18071808
"Display Name": "Anzeigename",
18081809
"Dividend Amount": "Dividendenbetrag",
18091810
"Dividend Period End Date": "Enddatum des Dividendenzeitraums",
@@ -2248,6 +2249,7 @@
22482249
"Not Available": "Nicht gefunden",
22492250
"Not Provided": "Nicht bereitgestellt",
22502251
"Note": "Notiz",
2252+
"Note is too long": "Die Notiz ist zu lang",
22512253
"Notes": "Anmerkungen",
22522254
"Notes/Comments": "Notizen/Kommentare",
22532255
"Notification": "Benachrichtigung",
@@ -2261,6 +2263,7 @@
22612263
"Number of Active Overdue Group Loans": "Anzahl der aktiven überfälligen Gruppenkredite",
22622264
"Number of Centers": "Anzahl der Zentren",
22632265
"Number of Clients": "Anzahl der Kunden",
2266+
"Please enter a valid number": "Bitte geben Sie eine gültige Zahl ein",
22642267
"Number of Collaterals": "Anzahl der Sicherheiten",
22652268
"Number of Days to Dormant sub-status": "Unterstatus „Anzahl der Tage bis zum Ruhezustand“.",
22662269
"Number of Days to Escheat": "Anzahl der Tage bis zur Pfändung",
@@ -2981,6 +2984,7 @@
29812984
"Withdrawal": "Rückzug",
29822985
"Withdrawn by Client": "Vom Kunden zurückgezogen",
29832986
"Write Off": "Abschreiben",
2987+
"Update discount": "Rabatt aktualisieren",
29842988
"Apply Additional Shares": "Zusätzlich anwenden",
29852989
"Redeem Shares": "Anteile zurückgeben",
29862990
"Approve Additional Shares": "Genehmigen Sie zusätzliche Freigaben",
@@ -4073,7 +4077,9 @@
40734077
"No Closed Share Accounts Found": "Keine geschlossenen Aktienkonten gefunden",
40744078
"No Collateral Data Found": "Keine Sicherheiten gefunden",
40754079
"No Identities Found": "Keine Identitäten gefunden",
4076-
"validationSaved": "Validierungsdaten wurden erfolgreich gespeichert."
4080+
"validationSaved": "Validierungsdaten wurden erfolgreich gespeichert.",
4081+
"workingCapitalDiscountUpdated": "Der Rabatt für den Working-Capital-Kredit wurde erfolgreich aktualisiert.",
4082+
"unableToUpdateDiscount": "Rabatt konnte nicht aktualisiert werden."
40774083
},
40784084
"languages": {
40794085
"cs-CS": "Čeština (Tschechisch)",

0 commit comments

Comments
 (0)