Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/directives/positive-number.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { Directive, ElementRef, HostListener, inject } from '@angular/core';

@Directive({ selector: '[mifosxPositiveNumber]' })
@Directive({ selector: '[mifosxPositiveNumber]', standalone: true })
export class PositiveNumberDirective {
private el = inject(ElementRef);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class LoanDelinquencyDataResolver extends LoanBaseResolver {
constructor() {
super();
}

/**
* Returns the Loans with Association data.
* @returns {Observable<any>}
Expand Down
6 changes: 5 additions & 1 deletion src/app/loans/common-resolvers/loan-originators.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { LoansService } from 'app/loans/loans.service';
import { LoanProductService } from 'app/products/loan-products/services/loan-product.service';
import { Observable, throwError } from 'rxjs';

@Injectable({
providedIn: 'root'
})
export class LoanOriginatorsResolver {
private loansService = inject(LoansService);
private loanProductService = inject(LoanProductService);

/**
* Returns the Loans data.
Expand All @@ -26,6 +28,8 @@ export class LoanOriginatorsResolver {
if (!loanId) {
return throwError(() => new Error('Missing loanId route param'));
}
return this.loansService.getLoanOriginators(loanId);
if (this.loanProductService.isLoanProduct) {
return this.loansService.getLoanOriginators(loanId);
}
Comment on lines +31 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
sed -n '24,36p' src/app/loans/common-resolvers/loan-originators.resolver.ts
sed -n '80,86p' src/app/loans/loans-view/loan-originators-tab/loan-originators-tab.component.ts
sed -n '280,289p' src/app/loans/loans-routing.module.ts

Repository: openMF/web-app

Length of output: 1054


🏁 Script executed:

rg -A 10 'getLoanOriginators' src/app/loans --type ts

Repository: openMF/web-app

Length of output: 1548


🏁 Script executed:

rg -B 5 -A 10 'loanOriginatorsData' src/app/loans --type ts

Repository: openMF/web-app

Length of output: 4964


🏁 Script executed:

grep -r 'isLoanProduct' src/app/loans --include='*.ts' | head -20

Repository: openMF/web-app

Length of output: 2458


Return an observable on the non-loan-product path to prevent undefined resolver data.

The resolve() method falls through without returning an observable when this.loanProductService.isLoanProduct is false. This violates the Observable<any> return contract and causes the component to fail when it attempts to dereference data.loanOriginatorsData.originators on an undefined value.

Return a fallback observable that matches the expected API response structure:

Suggested fix
-import { Observable, throwError } from 'rxjs';
+import { Observable, of, throwError } from 'rxjs';
...
     if (this.loanProductService.isLoanProduct) {
       return this.loansService.getLoanOriginators(loanId);
     }
+    return of({ originators: [] });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/loans/common-resolvers/loan-originators.resolver.ts` around lines 31
- 33, The resolve() method currently only returns when
this.loanProductService.isLoanProduct is true, causing undefined when false;
update resolve() to always return an Observable by returning the same-shaped
fallback observable when loanProductService.isLoanProduct is false (use rxjs
'of' to emit an object matching the API shape expected by the component, e.g. an
object with loanOriginatorsData and originators array), ensuring callers of
loansService.getLoanOriginators and consumers of
data.loanOriginatorsData.originators can always safely read the fields.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,35 @@ import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';

/** rxjs Imports */
import { Observable } from 'rxjs';
import { EMPTY, Observable } from 'rxjs';

/** Custom Services */
import { LoansService } from '../loans.service';
import { LoanProductService } from 'app/products/loan-products/services/loan-product.service';
import { LoanBaseResolver } from './loan-base.resolver';

/**
* Loan accounts template data resolver.
*/
@Injectable()
export class LoansAccountAndTemplateResolver {
export class LoansAccountAndTemplateResolver extends LoanBaseResolver {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This branch can select the wrong API after a previous navigation.

LoanBaseResolver keeps productType on the resolver instance, so the new initialize(route) call makes endpoint selection stateful. When a later navigation omits productType, a reused resolver can keep 'working-capital' and route a plain loan edit through the wrong service; !isNaN(+loanId) also still treats null as valid because +null === 0.

Suggested change
   resolve(route: ActivatedRouteSnapshot): Observable<any> {
-    this.initialize(route);
-    const loanId = route.paramMap.get('loanId') || route.parent.paramMap.get('loanId');
-    if (!isNaN(+loanId)) {
-      return this.isLoanProduct
+    const productType = route.queryParamMap.get('productType');
+    const isLoanProduct = productType !== 'working-capital';
+    const loanId = route.paramMap.get('loanId') ?? route.parent?.paramMap.get('loanId');
+    if (loanId !== null && loanId.trim() !== '' && !Number.isNaN(Number(loanId))) {
+      return isLoanProduct
         ? this.loansService.getLoansAccountAndTemplateResource(loanId)
         : this.loansService.getWorkingCapitalLoanDetails(loanId);
     }
     return EMPTY;
   }

Also applies to: 36-43

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/loans/common-resolvers/loans-account-and-template.resolver.ts` at
line 24, The resolver stores productType on the instance in LoanBaseResolver via
initialize(route), making endpoint selection stateful; change initialize so it
does not persist productType on the resolver instance but instead derives
productType from the incoming route each time (or explicitly reset/clear any
stored productType at start of initialize), and update any endpoint selection
logic in LoansAccountAndTemplateResolver to use the local productType from the
current route rather than an instance field; also tighten the loanId check by
replacing !isNaN(+loanId) with an explicit null/undefined guard plus numeric
test (e.g. loanId != null && !isNaN(Number(loanId))) so null is not treated as
0.

private loansService = inject(LoansService);
private loanProductService = inject(LoanProductService);

constructor() {
super();
}

/**
* Returns the loan account template data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
this.initialize(route);
const loanId = route.paramMap.get('loanId') || route.parent.paramMap.get('loanId');
return this.loanProductService.isLoanProduct
? this.loansService.getLoansAccountAndTemplateResource(loanId)
: this.loansService.getWorkingCapitalLoanDetails(loanId);
if (!isNaN(+loanId)) {
return this.isLoanProduct
? this.loansService.getLoansAccountAndTemplateResource(loanId)
: this.loansService.getWorkingCapitalLoanDetails(loanId);
}
return EMPTY;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ export class CreateLoansAccountComponent extends LoanProductBaseComponent implem
this.loansAccountProductTemplate.options = {
delinquencyBucketOptions: templateData.delinquencyBucketOptions,
fundOptions: templateData.fundOptions,
periodFrequencyTypeOptions: templateData.periodFrequencyTypeOptions
periodFrequencyTypeOptions: templateData.periodFrequencyTypeOptions,
delinquencyStartTypeOptions: templateData.delinquencyStartTypeOptions
};
}
this.currencyCode = this.loansAccountProductTemplate.currency.code;
Expand Down Expand Up @@ -290,6 +291,11 @@ export class CreateLoansAccountComponent extends LoanProductBaseComponent implem
}
}

// No Empty discount value to be sent
if (payload['discount'] == null || payload['discount'] === '') {
delete payload['discount'];
}

this.loansService
.createLoansAccount(this.loanProductService.loanAccountPath, payload)
.subscribe((response: any) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ export class EditLoansAccountComponent extends LoanProductBaseComponent {
this.loansAccountProductTemplate.options = {
delinquencyBucketOptions: templateData.delinquencyBucketOptions,
fundOptions: templateData.fundOptions,
periodFrequencyTypeOptions: templateData.periodFrequencyTypeOptions
periodFrequencyTypeOptions: templateData.periodFrequencyTypeOptions,
delinquencyStartTypeOptions: templateData.delinquencyStartTypeOptions
};
}
if (this.loansAccountProductTemplate.loanProductId) {
Expand Down Expand Up @@ -313,6 +314,11 @@ export class EditLoansAccountComponent extends LoanProductBaseComponent {
}
}

// No Empty discount value to be sent
if (payload['discount'] == null || payload['discount'] === '') {
delete payload['discount'];
}

this.loansService
.updateLoansAccount(this.loanProductService.loanAccountPath, this.loanId, payload)
.subscribe((response: any) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ <h3 class="mat-h3 margin-t flex-fill">{{ 'labels.heading.Terms' | translate }}</
</div>
}

@if (loansAccount.delinquencyGraceDays) {
<div class="flex-fill">
<span class="flex-30">{{ 'labels.inputs.Delinquency Grace Days' | translate }}:</span>
<span class="flex-70"> {{ loansAccount.delinquencyGraceDays | formatNumber }}</span>
</div>
}
Comment on lines +110 to +115
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Render 0 grace days in preview.

Line 110 uses a truthy check, so a valid value of 0 won’t render. Use a null/undefined guard instead.

🔧 Suggested fix
-    `@if` (loansAccount.delinquencyGraceDays) {
+    `@if` (loansAccount.delinquencyGraceDays !== null && loansAccount.delinquencyGraceDays !== undefined) {
       <div class="flex-fill">
         <span class="flex-30">{{ 'labels.inputs.Delinquency Grace Days' | translate }}:</span>
         <span class="flex-70"> {{ loansAccount.delinquencyGraceDays | formatNumber }}</span>
       </div>
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@if (loansAccount.delinquencyGraceDays) {
<div class="flex-fill">
<span class="flex-30">{{ 'labels.inputs.Delinquency Grace Days' | translate }}:</span>
<span class="flex-70"> {{ loansAccount.delinquencyGraceDays | formatNumber }}</span>
</div>
}
`@if` (loansAccount.delinquencyGraceDays !== null && loansAccount.delinquencyGraceDays !== undefined) {
<div class="flex-fill">
<span class="flex-30">{{ 'labels.inputs.Delinquency Grace Days' | translate }}:</span>
<span class="flex-70"> {{ loansAccount.delinquencyGraceDays | formatNumber }}</span>
</div>
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/loans/loans-account-stepper/loans-account-preview-step/loans-account-preview-step.component.html`
around lines 110 - 115, The preview currently uses a truthy check that hides a
valid 0 value for loansAccount.delinquencyGraceDays; update the ngIf to
explicitly check for null/undefined instead (e.g. use
loansAccount?.delinquencyGraceDays != null or loansAccount.delinquencyGraceDays
!== null && loansAccount.delinquencyGraceDays !== undefined) in
loans-account-preview-step.component.html so that 0 renders while null/undefined
still hide the element.


@if (loansAccount.delinquencyStartType) {
<div class="flex-fill">
<span class="flex-30">{{ 'labels.inputs.Delinquency Start Type' | translate }}:</span>
<span class="flex-60">{{ camalize(loansAccount.delinquencyStartType) | translateKey: 'catalogs' }}</span>
</div>
}

<div class="flex-fill">
<span class="flex-30">{{ 'labels.inputs.Period Payment Rate' | translate }}:</span>
<span class="flex-70"> {{ loansAccount.periodPaymentRate | formatNumber }} % </span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ <h4 class="mat-h4 flex-98">
}
</mat-form-field>

<mat-form-field class="flex-48">
<mat-form-field class="flex-23">
<mat-label>{{ 'labels.inputs.Discount' | translate }}</mat-label>
<input type="number" matInput mifosxPositiveNumber min="0" formControlName="discount" />
</mat-form-field>

<mat-form-field class="flex-48">
<mat-form-field class="flex-23">
<mat-label>{{ 'labels.inputs.Period Payment Rate' | translate }}</mat-label>
<input type="number" matInput mifosxPositiveNumber min="0" formControlName="periodPaymentRate" />
</mat-form-field>

<mat-form-field class="flex-24">
<mat-form-field class="flex-23">
<mat-label>{{ 'labels.inputs.Period Payment Frequency' | translate }}</mat-label>
<input
type="number"
Expand All @@ -65,7 +65,7 @@ <h4 class="mat-h4 flex-98">
}
</mat-form-field>

<mat-form-field class="flex-24">
<mat-form-field class="flex-23">
<mat-label>{{ 'labels.inputs.Period Payment Frequency Type' | translate }}</mat-label>
<mat-select formControlName="repaymentFrequencyType" required>
@for (repaymentFrequencyType of termFrequencyTypeData; track repaymentFrequencyType) {
Expand All @@ -75,6 +75,22 @@ <h4 class="mat-h4 flex-98">
}
</mat-select>
</mat-form-field>

<mat-form-field class="flex-23">
<mat-label>{{ 'labels.inputs.Delinquency Grace Days' | translate }}</mat-label>
<input type="number" min="0" matInput formControlName="delinquencyGraceDays" />
</mat-form-field>

<mat-form-field class="flex-23">
<mat-label>{{ 'labels.inputs.Delinquency Start Type' | translate }}</mat-label>
<mat-select formControlName="delinquencyStartType">
@for (delinquencyStartType of delinquencyStartTypeOptions; track delinquencyStartType) {
<mat-option [value]="delinquencyStartType.id">
{{ delinquencyStartType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
}

@if (loanProductService.isLoanProduct) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicke
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { Currency } from 'app/shared/models/general.model';
import { CodeName, OptionData } from 'app/shared/models/option-data.model';
import { CodeName, OptionData, StringEnumOptionData } from 'app/shared/models/option-data.model';
import { InputAmountComponent } from '../../../shared/input-amount/input-amount.component';
import { MatTooltip } from '@angular/material/tooltip';
import { MatCheckbox } from '@angular/material/checkbox';
Expand Down Expand Up @@ -174,6 +174,8 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp

allowAttributeOverrides: any | null = null;

delinquencyStartTypeOptions: StringEnumOptionData[] = [];

constructor() {
super();
this.createloansAccountTermsForm();
Expand Down Expand Up @@ -319,6 +321,7 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
this.loansAccountTermsData = this.loansAccountProductTemplate;
this.currency = this.loansAccountTermsData.currency;
this.termFrequencyTypeData = this.loansAccountTermsData.options?.periodFrequencyTypeOptions;
this.delinquencyStartTypeOptions = this.loansAccountTermsData.options?.delinquencyStartTypeOptions;
if (this.loanId != null && 'accountNo' in this.loansAccountTemplate) {
this.loansAccountTermsData = this.loansAccountTemplate;
this.loansAccountTermsForm.patchValue({
Expand All @@ -327,11 +330,16 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
periodPaymentRate: this.loansAccountTermsData.periodPaymentRate,
totalPayment: this.loansAccountTermsData.balance?.totalPayment,
repaymentEvery: this.loansAccountTermsData.repaymentEvery,
repaymentFrequencyType: this.loansAccountTermsData.repaymentFrequencyType?.id
repaymentFrequencyType: this.loansAccountTermsData.repaymentFrequencyType?.id,
delinquencyGraceDays: this.loansAccountTermsData.delinquencyGraceDays,
delinquencyStartType: this.loansAccountTermsData.delinquencyStartType?.code
});
} else {
this.loansAccountTermsForm.patchValue({
principalAmount: this.loansAccountTermsData.product.principal
discount: this.loansAccountTermsData.product.discount || '',
principalAmount: this.loansAccountTermsData.product.principal,
delinquencyGraceDays: this.loansAccountTermsData.product.delinquencyGraceDays || '',
delinquencyStartType: this.loansAccountTermsData.product.delinquencyStartType?.code || ''
});
}
this.allowAttributeOverrides = this.loansAccountProductTemplate.product.allowAttributeOverrides;
Expand Down Expand Up @@ -436,7 +444,12 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
principalAmount: this.loansAccountTermsData.principal || this.loansAccountTermsData.product.principal,
periodPaymentRate: this.loansAccountTermsData.periodPaymentRate,
repaymentEvery: this.loansAccountTermsData.repaymentEvery,
repaymentFrequencyType: this.loansAccountTermsData.repaymentFrequencyType.id
repaymentFrequencyType: this.loansAccountTermsData.repaymentFrequencyType.id,
delinquencyGraceDays:
this.loansAccountTermsData.delinquencyGraceDays || this.loansAccountTermsData.product.delinquencyGraceDays,
delinquencyStartType:
this.loansAccountTermsData.delinquencyStartType?.id ||
this.loansAccountTermsData.product.delinquencyStartType?.id
});
}
}
Expand Down Expand Up @@ -628,7 +641,14 @@ export class LoansAccountTermsStepComponent extends LoanProductBaseComponent imp
repaymentFrequencyType: [
'',
Validators.required
]
],
delinquencyGraceDays: [
'',
[
Validators.min(0)
]
],
delinquencyStartType: ['']
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ <h3>{{ 'labels.heading.Loan Details' | translate }}</h3>
<span class="flex-50">{{ loanDetails.discount | formatNumber }}</span>
</div>
}

<div class="flex-fill layout-row">
<span class="flex-50"> {{ 'labels.inputs.Delinquency Grace Days' | translate }} </span>
<span class="flex-50">{{ loanDetails.delinquencyGraceDays | formatNumber }}</span>
</div>

<div class="flex-fill layout-row">
<span class="flex-50"> {{ 'labels.inputs.Delinquency Start Type' | translate }} </span>
<span class="flex-50">{{ loanDetails.delinquencyStartType?.value | translateKey: 'catalogs' }}</span>
</div>
}

@if (loanProductService.isLoanProduct) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
<mat-label>{{ 'labels.inputs.Expected disbursement on' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[min]="approveLoanForm.value.approvedOnDate"
[max]="maxDate"
[matDatepicker]="disbursementDatePicker"
required
formControlName="expectedDisbursementDate"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class ApproveLoanComponent extends LoanAccountActionsBaseComponent implem
loanData: any = new Object();
/** Minimum Date allowed. */
minDate = new Date(2000, 0, 1);
maxDate = new Date();
currency: Currency;

/**
Expand All @@ -51,8 +52,8 @@ export class ApproveLoanComponent extends LoanAccountActionsBaseComponent implem
*/
constructor() {
super();
this.maxDate = this.settingsService.maxFutureDate;
this.route.data.subscribe((data: { actionButtonData: any }) => {
console.log(data.actionButtonData);
this.loanData = data.actionButtonData;
this.currency = data.actionButtonData.currency;
});
Expand All @@ -63,13 +64,15 @@ export class ApproveLoanComponent extends LoanAccountActionsBaseComponent implem
this.setApproveLoanForm();

// Get delinquency data for available disbursement amount with over applied
this.loanService.getLoanDelinquencyDataForTemplate(this.loanId).subscribe((delinquencyData: any) => {
// Check if the field is at root level
if (delinquencyData.availableDisbursementAmountWithOverApplied !== undefined) {
this.loanData.availableDisbursementAmountWithOverApplied =
delinquencyData.availableDisbursementAmountWithOverApplied;
}
});
if (this.isLoanProduct) {
this.loanService.getLoanDelinquencyDataForTemplate(this.loanId).subscribe((delinquencyData: any) => {
// Check if the field is at root level
if (delinquencyData.availableDisbursementAmountWithOverApplied !== undefined) {
this.loanData.availableDisbursementAmountWithOverApplied =
delinquencyData.availableDisbursementAmountWithOverApplied;
}
});
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,20 @@ <h3 class="mat-h3 flex-fill">{{ 'labels.heading.Moratorium' | translate }}</h3>
<span class="flex-60">{{ loanProduct.enableInstallmentLevelDelinquency | yesNo }}</span>
</div>
}
@if (loanProductService.isWorkingCapital) {
@if (loanProduct.delinquencyGraceDays) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Delinquency Grace Days' | translate }}:</span>
<span class="flex-60">{{ loanProduct.delinquencyGraceDays | formatNumber }}</span>
</div>
}
@if (loanProduct.delinquencyStartType) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Delinquency Start Type' | translate }}:</span>
<span class="flex-60">{{ loanProduct.delinquencyStartType.value | translateKey: 'catalogs' }}</span>
</div>
}
}
@if (loanProductService.isLoanProduct) {
@if (loanProduct.graceOnPrincipalPayment) {
<div class="flex-fill layout-row">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@
</mat-error>
}
</mat-form-field>

<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Delinquency Grace Days' | translate }}</mat-label>
<input type="number" min="0" matInput formControlName="delinquencyGraceDays" />
</mat-form-field>

<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Delinquency Start Type' | translate }}</mat-label>
<mat-select formControlName="delinquencyStartType">
@for (delinquencyStartType of delinquencyStartTypeOptions; track delinquencyStartType) {
<mat-option [value]="delinquencyStartType.id">
{{ delinquencyStartType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
} @else if (loanProductService.isLoanProduct) {
<mat-form-field class="flex-30">
<mat-label>{{ 'labels.inputs.products.loan.Amortization' | translate }}</mat-label>
Expand Down
Loading
Loading