-
Notifications
You must be signed in to change notification settings - Fork 904
WEB-889: WC - Discount #3436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WEB-889: WC - Discount #3436
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| <!-- | ||
| Copyright since 2025 Mifos Initiative | ||
|
|
||
| This Source Code Form is subject to the terms of the Mozilla Public | ||
| License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| --> | ||
|
|
||
| <div class="container mat-elevation-z8"> | ||
| <mat-card> | ||
| <form [formGroup]="updateDiscountForm" (ngSubmit)="submit()"> | ||
| <mat-card-content> | ||
| <div class="layout-column"> | ||
| <mat-form-field> | ||
| <mat-label>{{ 'labels.inputs.Discount' | translate }}</mat-label> | ||
| <input | ||
| matInput | ||
| type="number" | ||
| min="0" | ||
| step="0.01" | ||
| required | ||
| mifosxPositiveNumber | ||
| formControlName="discountAmount" | ||
| /> | ||
| @if (updateDiscountForm.controls.discountAmount.hasError('required')) { | ||
| <mat-error> | ||
| {{ 'labels.inputs.Discount' | translate }} {{ 'labels.commons.is' | translate }} | ||
| <strong>{{ 'labels.commons.required' | translate }}</strong> | ||
| </mat-error> | ||
| } | ||
| @if (updateDiscountForm.controls.discountAmount.hasError('min')) { | ||
| <mat-error>{{ 'labels.inputs.Discount must be greater than or equal to 0' | translate }}</mat-error> | ||
| } | ||
| @if (updateDiscountForm.controls.discountAmount.hasError('highAmountValue')) { | ||
| <mat-error>{{ 'labels.inputs.Please enter a valid number' | translate }}</mat-error> | ||
| } | ||
| </mat-form-field> | ||
|
|
||
| <mat-form-field> | ||
| <mat-label>{{ 'labels.inputs.Note' | translate }}</mat-label> | ||
| <textarea matInput formControlName="note" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea> | ||
| <mat-hint align="end"> | ||
| {{ updateDiscountForm.controls.note.value?.length || 0 }}/{{ maxNoteLength }} | ||
| </mat-hint> | ||
| @if (updateDiscountForm.controls.note.hasError('maxlength')) { | ||
| <mat-error>{{ 'labels.inputs.Note is too long' | translate }}</mat-error> | ||
| } | ||
| </mat-form-field> | ||
|
|
||
| @if (submitErrorMessage) { | ||
| <mat-error>{{ submitErrorMessage }}</mat-error> | ||
| } | ||
| </div> | ||
|
|
||
| <mat-card-actions class="layout-row align-center gap-5px responsive-column"> | ||
| <button type="button" mat-raised-button (click)="gotoLoanDefaultView()" [disabled]="isSubmitting"> | ||
| {{ 'labels.buttons.Cancel' | translate }} | ||
| </button> | ||
| <button | ||
| mat-raised-button | ||
| color="primary" | ||
| [disabled]="!updateDiscountForm.valid || isSubmitting" | ||
| *mifosxHasPermission="'UPDATEDISCOUNT_WORKINGCAPITALLOAN'" | ||
| > | ||
| @if (isSubmitting) { | ||
| <mat-spinner diameter="20"></mat-spinner> | ||
| } | ||
| @if (!isSubmitting) { | ||
| {{ 'labels.buttons.Submit' | translate }} | ||
| } | ||
| </button> | ||
| </mat-card-actions> | ||
| </mat-card-content> | ||
| </form> | ||
| </mat-card> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| /** | ||
| * Copyright since 2025 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| */ | ||
|
|
||
| /** Angular Imports */ | ||
| import { Component, OnInit, inject } from '@angular/core'; | ||
| import { HttpErrorResponse } from '@angular/common/http'; | ||
| import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; | ||
| import { TranslateService } from '@ngx-translate/core'; | ||
|
|
||
| /** Custom Services */ | ||
| import { AlertService } from 'app/core/alert/alert.service'; | ||
| import { amountValueValidator } from 'app/shared/validators/amount-value.validator'; | ||
| import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; | ||
| import { CdkTextareaAutosize } from '@angular/cdk/text-field'; | ||
| import { PositiveNumberDirective } from 'app/directives/positive-number.directive'; | ||
| import { LoanAccountActionsBaseComponent } from '../loan-account-actions-base.component'; | ||
| import { WorkingCapitalLoanDiscountUpdateRequest } from 'app/loans/loans.service'; | ||
|
|
||
| /** | ||
| * Update discount action for Working Capital Loan. | ||
| */ | ||
| @Component({ | ||
| selector: 'mifosx-update-discount', | ||
| standalone: true, | ||
| templateUrl: './update-discount.component.html', | ||
| imports: [ | ||
| ...STANDALONE_SHARED_IMPORTS, | ||
| CdkTextareaAutosize, | ||
| PositiveNumberDirective | ||
| ] | ||
| }) | ||
| export class UpdateDiscountComponent extends LoanAccountActionsBaseComponent implements OnInit { | ||
| private formBuilder = inject(UntypedFormBuilder); | ||
| private alertService = inject(AlertService); | ||
| private translateService = inject(TranslateService); | ||
|
|
||
| readonly maxNoteLength = 500; | ||
|
|
||
| updateDiscountForm: UntypedFormGroup; | ||
| isSubmitting = false; | ||
| submitErrorMessage = ''; | ||
| discountValue = 0; | ||
|
|
||
| ngOnInit(): void { | ||
| this.discountValue = this.dataObject?.discount ?? this.dataObject?.discountAmount ?? 0; | ||
| this.updateDiscountForm = this.formBuilder.group({ | ||
| discountAmount: [ | ||
| this.discountValue, | ||
| [ | ||
| Validators.required, | ||
| Validators.min(0), | ||
| amountValueValidator() | ||
| ] | ||
| ], | ||
| note: [ | ||
| '', | ||
| Validators.maxLength(this.maxNoteLength) | ||
| ] | ||
| }); | ||
| } | ||
|
|
||
| submit(): void { | ||
| if (!this.updateDiscountForm.valid || this.isSubmitting) { | ||
| return; | ||
| } | ||
|
|
||
| this.isSubmitting = true; | ||
| this.submitErrorMessage = ''; | ||
|
|
||
| const formValue = this.updateDiscountForm.value; | ||
| const payload: WorkingCapitalLoanDiscountUpdateRequest = { | ||
| discountAmount: Number(formValue.discountAmount), | ||
| note: formValue.note, | ||
| locale: this.settingsService.language.code, | ||
| dateFormat: this.settingsService.dateFormat | ||
| }; | ||
|
|
||
| this.loanService.updateWorkingCapitalLoanDiscount(this.loanId, payload).subscribe({ | ||
| next: () => { | ||
| this.alertService.alert({ | ||
| type: 'Success', | ||
| message: this.translateService.instant('labels.messages.workingCapitalDiscountUpdated') | ||
| }); | ||
| this.isSubmitting = false; | ||
| this.gotoLoanDefaultView(); | ||
| }, | ||
| error: (error: HttpErrorResponse) => { | ||
| this.submitErrorMessage = this.mapDiscountError(error); | ||
| this.isSubmitting = false; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| private mapDiscountError(error: HttpErrorResponse): string { | ||
| const backendError = error?.error?.errors?.[0]; | ||
| return ( | ||
| backendError?.defaultUserMessage || | ||
| error?.error?.defaultUserMessage || | ||
| this.translateService.instant('labels.messages.unableToUpdateDiscount') | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -257,6 +257,13 @@ export class LoansViewComponent extends LoanProductBaseComponent implements OnIn | |
| return; | ||
| } | ||
| this.buttonConfig = new LoansAccountButtonConfiguration(this.status, this.loanSubStatus); | ||
| if (this.canShowWorkingCapitalDiscountUpdate()) { | ||
| this.buttonConfig.addButton({ | ||
| name: 'Update discount', | ||
| icon: 'edit', | ||
| taskPermissionName: 'UPDATEDISCOUNT_WORKINGCAPITALLOAN' | ||
| }); | ||
| } | ||
|
Comment on lines
+260
to
+266
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Locate discount action wiring =="
rg -n --type ts -C3 "Update discount|canShowWorkingCapitalDiscountUpdate|workingCapital.*discount|discount.*update" src/app/loans
echo
echo "== Inspect resolver for action payload =="
fd 'loan-action-button.resolver.ts$' src/app --exec sed -n '1,260p' {}
echo
echo "== Inspect update-discount component for date/one-time/pre-existing checks =="
fd 'update-discount.component.ts$' src/app --exec sed -n '1,320p' {}
echo
echo "== Inspect loans service methods used by resolver/component =="
fd 'loans.service.ts$' src/app --exec rg -n -C4 "getWorkingCapital|updateWorkingCapital|discount|disbursement|businessDate|already" {}Repository: openMF/web-app Length of output: 19810 🏁 Script executed: # Get full implementation of canShowWorkingCapitalDiscountUpdate
sed -n '582,600p' src/app/loans/loans-view/loans-view.component.ts
echo "---"
# Check for any eligibility-related fields or conditions
rg -n "discount.*eligible|eligible.*discount|one.*time|disbursement.*date|discount.*created|discount.*approval" src/app/loans/loans-view --type ts
echo "---"
# Look at the loanDetailsData structure and what it contains
rg -n -A5 "loanDetailsData" src/app/loans/loans-view/loans-view.component.ts | head -60
echo "---"
# Check the update-discount template for any additional checks
fd 'update-discount.component.html$' src/app --exec cat {}Repository: openMF/web-app Length of output: 6507 Update discount action visibility is missing full eligibility checks. The guard at line 582 checks only working-capital product type and active status, but the PR rules require additional constraints: disbursement date validation, one-time-only enforcement, and blocking when already added during creation/approval. The update-discount component's submit() method (lines 42–65) does not validate these constraints either, exposing the action when it should be unavailable. Ensure all three eligibility criteria are enforced before showing the action and before allowing submission. 🤖 Prompt for AI Agents |
||
|
|
||
| if (this.status === 'Submitted and pending approval') { | ||
| this.buttonConfig.addOption({ | ||
|
|
@@ -571,4 +578,11 @@ export class LoansViewComponent extends LoanProductBaseComponent implements OnIn | |
| } | ||
| return substatus.code === 'loanSubStatus.loanSubStatusType.contractTermination'; | ||
| } | ||
|
|
||
| private canShowWorkingCapitalDiscountUpdate(): boolean { | ||
| if (!this.loanProductService.isWorkingCapital || !this.loanDetailsData) { | ||
| return false; | ||
| } | ||
| return this.loanDetailsData?.status?.active === true; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.