Fixed Length configuration in Loan Products

This commit is contained in:
Jose Alberto Hernandez
2024-03-19 09:21:21 +01:00
committed by Adam Saghy
parent 1e2027c37b
commit e4274c55bb
9 changed files with 157 additions and 53 deletions
@@ -290,6 +290,11 @@
<span fxFlex="47%">Repay Every:</span>
<span fxFlex="53%">{{ loanProduct.repaymentEvery + ' ' + loanProduct.repaymentFrequencyType.value }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.fixedLength">
<span fxFlex="47%">Fixed Length:</span>
<span fxFlex="53%">{{ loanProduct.fixedLength + ' ' + loanProduct.repaymentFrequencyType.value }}</span>
</div>
<div fxFlexFill *ngIf="loanProduct.minimumDaysBetweenDisbursalAndFirstRepayment">
<span fxFlex="47%">Minimum days between disbursal and first repayment date:</span>
@@ -154,6 +154,11 @@ export class LoanProductSummaryComponent implements OnInit, OnChanges {
optionValue = this.optionDataLookUp(this.loanProduct.interestCalculationPeriodType, this.loanProductsTemplate.interestCalculationPeriodTypeOptions);
this.loanProduct.interestCalculationPeriodType = optionValue;
if (!this.loanProduct.repaymentFrequencyType || !this.loanProduct.repaymentFrequencyType.value) {
optionValue = this.optionDataLookUp(this.loanProduct.repaymentFrequencyType, this.loanProductsTemplate.repaymentFrequencyTypeOptions);
this.loanProduct.repaymentFrequencyType = optionValue;
}
optionValue = this.optionDataLookUp(this.loanProduct.daysInMonthType, this.loanProductsTemplate.daysInMonthTypeOptions);
this.loanProduct.daysInMonthType = optionValue;
optionValue = this.optionDataLookUp(this.loanProduct.daysInYearType, this.loanProductsTemplate.daysInYearTypeOptions);
@@ -38,14 +38,6 @@
</mat-step>
<mat-step [stepControl]="loanProductTermsForm">
<ng-template matStepLabel>TERMS</ng-template>
<mifosx-loan-product-terms-step [loanProductsTemplate]="loanProductsTemplate"></mifosx-loan-product-terms-step>
</mat-step>
<mat-step [stepControl]="loanProductSettingsForm">
<ng-template matStepLabel>SETTINGS</ng-template>
@@ -79,6 +71,14 @@
</mat-step>
<mat-step [stepControl]="loanProductTermsForm">
<ng-template matStepLabel>TERMS</ng-template>
<mifosx-loan-product-terms-step [loanProductsTemplate]="loanProductsTemplate"></mifosx-loan-product-terms-step>
</mat-step>
<mat-step>
<ng-template matStepLabel>CHARGES</ng-template>
@@ -38,14 +38,6 @@
</mat-step>
<mat-step [stepControl]="loanProductTermsForm" completed>
<ng-template matStepLabel>TERMS</ng-template>
<mifosx-loan-product-terms-step [loanProductsTemplate]="loanProductAndTemplate"></mifosx-loan-product-terms-step>
</mat-step>
<mat-step [stepControl]="loanProductSettingsForm" completed>
<ng-template matStepLabel>SETTINGS</ng-template>
@@ -79,6 +71,14 @@
</mat-step>
<mat-step [stepControl]="loanProductTermsForm" completed>
<ng-template matStepLabel>TERMS</ng-template>
<mifosx-loan-product-terms-step [loanProductsTemplate]="loanProductAndTemplate"></mifosx-loan-product-terms-step>
</mat-step>
<mat-step completed>
<ng-template matStepLabel>CHARGES</ng-template>
@@ -4,6 +4,7 @@ import { LoanProducts } from '../../loan-products';
import { rangeValidator } from 'app/shared/validators/percentage.validator';
import { GlobalConfiguration } from 'app/system/configurations/global-configurations-tab/configuration.model';
import { CodeName, OptionData } from 'app/shared/models/option-data.model';
import { ProcessingStrategyService } from '../../services/processing-strategy.service';
@Component({
@@ -46,7 +47,8 @@ export class LoanProductSettingsStepComponent implements OnInit {
/** Values to Days for Repayments */
defaultConfigValues: GlobalConfiguration[] = [];
constructor(private formBuilder: UntypedFormBuilder) {
constructor(private formBuilder: UntypedFormBuilder,
private processingStrategyService: ProcessingStrategyService) {
this.createLoanProductSettingsForm();
this.setConditionalControls();
}
@@ -117,11 +119,8 @@ export class LoanProductSettingsStepComponent implements OnInit {
});
this.isAdvancedTransactionProcessingStrategy = LoanProducts.isAdvancedPaymentAllocationStrategy(transactionProcessingStrategyCode);
if (this.isAdvancedTransactionProcessingStrategy) {
this.loanProductSettingsForm.addControl('loanScheduleProcessingType', new UntypedFormControl(
this.loanProductsTemplate.loanScheduleProcessingType.code || LoanProducts.LOAN_SCHEDULE_PROCESSING_TYPE_HORIZONTAL,
[Validators.required ]));
}
this.processingStrategyService.initialize(this.isAdvancedTransactionProcessingStrategy);
this.validateAdvancedPaymentStrategyControls();
if (this.loanProductsTemplate.dueDaysForRepaymentEvent != null &&
this.loanProductsTemplate.overDueDaysForRepaymentEvent != null) {
@@ -405,12 +404,8 @@ export class LoanProductSettingsStepComponent implements OnInit {
.subscribe((transactionProcessingStrategyCode: string) => {
this.advancePaymentStrategy.emit(transactionProcessingStrategyCode);
this.isAdvancedTransactionProcessingStrategy = LoanProducts.isAdvancedPaymentAllocationStrategy(transactionProcessingStrategyCode);
if (this.isAdvancedTransactionProcessingStrategy) {
this.loanProductSettingsForm.addControl('loanScheduleProcessingType', new UntypedFormControl(
this.loanProductsTemplate.loanScheduleProcessingType.code || LoanProducts.LOAN_SCHEDULE_PROCESSING_TYPE_HORIZONTAL, [Validators.required]));
} else {
this.loanProductSettingsForm.removeControl('loanScheduleProcessingType');
}
this.processingStrategyService.initialize(this.isAdvancedTransactionProcessingStrategy);
this.validateAdvancedPaymentStrategyControls();
});
this.loanProductSettingsForm.get('allowAttributeConfiguration').valueChanges
@@ -484,6 +479,7 @@ export class LoanProductSettingsStepComponent implements OnInit {
});
this.isAdvancedTransactionProcessingStrategy = true;
}
this.processingStrategyService.initialize(this.isAdvancedTransactionProcessingStrategy);
});
}
@@ -519,4 +515,12 @@ export class LoanProductSettingsStepComponent implements OnInit {
return productSettings;
}
private validateAdvancedPaymentStrategyControls(): void {
if (this.isAdvancedTransactionProcessingStrategy) {
this.loanProductSettingsForm.addControl('loanScheduleProcessingType', new UntypedFormControl(
this.loanProductsTemplate.loanScheduleProcessingType.code || LoanProducts.LOAN_SCHEDULE_PROCESSING_TYPE_HORIZONTAL, [Validators.required]));
} else {
this.loanProductSettingsForm.removeControl('loanScheduleProcessingType');
}
}
}
@@ -87,7 +87,11 @@
<h3 fxFlex="23%" class="mat-h3">Interest Rates</h3>
<mat-checkbox fxFlex="73%" labelPosition="before" formControlName="isLinkedToFloatingInterestRates">
<mat-checkbox [formControl]="zeroInterest" fxFlex="23%" labelPosition="before" *ngIf="!loanProductTermsForm.value.isLinkedToFloatingInterestRates">
Is Zero Interest?
</mat-checkbox>
<mat-checkbox fxFlex="32%" labelPosition="before" formControlName="isLinkedToFloatingInterestRates" *ngIf="!isZeroInterest()">
Is Linked to floating interest rates?
</mat-checkbox>
@@ -97,7 +101,7 @@
<mat-form-field fxFlex="23%">
<mat-label>Minimum</mat-label>
<input type="number" matInput formControlName="minInterestRatePerPeriod" [min]="0">
<input type="number" matInput formControlName="minInterestRatePerPeriod" [min]="0" [attr.disabled]="isZeroInterest()">
<mat-error>
Minimum Value must be <strong>greater than 0</strong>
</mat-error>
@@ -105,7 +109,7 @@
<mat-form-field fxFlex="23%">
<mat-label>Default</mat-label>
<input type="number" matInput formControlName="interestRatePerPeriod" required>
<input type="number" matInput formControlName="interestRatePerPeriod" required [attr.disabled]="isZeroInterest()">
<mat-error>
Default nominal interest rate is <strong>required</strong>
</mat-error>
@@ -113,7 +117,7 @@
<mat-form-field fxFlex="23%">
<mat-label>Maximum</mat-label>
<input type="number" matInput formControlName="maxInterestRatePerPeriod" [min]="0">
<input type="number" matInput formControlName="maxInterestRatePerPeriod" [min]="0" [attr.disabled]="isZeroInterest()">
<mat-error>
Maximum Value must be <strong>greater equal to than 0</strong> and must be greater than <strong>Minimum Principal</strong>
</mat-error>
@@ -121,7 +125,7 @@
<mat-form-field fxFlex="23%">
<mat-label>Frequency</mat-label>
<mat-select formControlName="interestRateFrequencyType" required>
<mat-select formControlName="interestRateFrequencyType" required [disabled]="isZeroInterest()">
<mat-option *ngFor="let interestRateFrequencyType of interestRateFrequencyTypeData" [value]="interestRateFrequencyType.id">
{{ interestRateFrequencyType.value }}
</mat-option>
@@ -133,7 +137,7 @@
</div>
<div *ngIf="loanProductTermsForm.value.isLinkedToFloatingInterestRates" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column" fxLayoutAlign.gt-sm="start center">
<div *ngIf="loanProductTermsForm.value.isLinkedToFloatingInterestRates && !isZeroInterest()" fxFlexFill fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column" fxLayoutAlign.gt-sm="start center">
<h4 fxFlex="98%" class="mat-h4">Floating interest rate <i class="fas fa-question" matTooltip="These fields are used to define the minimum, default, maximum, and period for the floating interest rate allowed for the loan product. The minimum, default, and maximum floating interest rates are expressed as percentages."></i></h4>
@@ -390,9 +394,9 @@
<mat-divider fxFlex="98%"></mat-divider>
<h4 fxFlex="98%" class="mat-h4">Repaid every <i class="fas fa-question" matTooltip="These fields are input to calculating the repayment schedule for a loan account and are used to determine when payments are due."></i></h4>
<h4 fxFlex="33%" class="mat-h4">Repaid every <i class="fas fa-question" matTooltip="These fields are input to calculating the repayment schedule for a loan account and are used to determine when payments are due."></i></h4>
<mat-form-field fxFlex="48%">
<mat-form-field fxFlex="30%">
<mat-label>Frequency</mat-label>
<input type="number" matInput formControlName="repaymentEvery" required>
<mat-error>
@@ -400,7 +404,7 @@
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="48%">
<mat-form-field fxFlex="30%">
<mat-label>Type</mat-label>
<mat-select formControlName="repaymentFrequencyType" required>
<mat-option *ngFor="let repaymentFrequencyType of repaymentFrequencyTypeData" [value]="repaymentFrequencyType.id">
@@ -412,6 +416,15 @@
</mat-error>
</mat-form-field>
<h4 fxFlex="33%" class="mat-h4" *ngIf="allowFixedLength()">Fixed Length <i class="fas fa-question"
matTooltip="Due to regulatory requirements in some countries, the loans cannot exceed X days, the Fineract SOR needs to be extended to support defining that a loan has a maximum length and the maximum length of that loan defines the length of the last installment which will be a shorter period than the first installments."></i></h4>
<mat-form-field fxFlex="30%" *ngIf="allowFixedLength()">
<mat-label>Fixed Length</mat-label>
<input type="number" matInput formControlName="fixedLength">
</mat-form-field>
<span fxFlex="30%" *ngIf="allowFixedLength()" class="label-field">{{ loanProductTermsForm.value.repaymentFrequencyType | find:repaymentFrequencyTypeData:'id':'value' }}</span>
<mat-form-field fxFlex="48%">
<mat-label>Minimum days between disbursal and first repayment date</mat-label>
<input type="number" matInput formControlName="minimumDaysBetweenDisbursalAndFirstRepayment">
@@ -21,3 +21,7 @@ mat-divider {
.margin-t {
margin-top: 1em;
}
.label-field {
margin-top: 32px;
}
@@ -1,4 +1,4 @@
import { Component, OnInit, Input } from '@angular/core';
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormArray, UntypedFormControl } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { TooltipPosition } from '@angular/material/tooltip';
@@ -9,18 +9,23 @@ import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.co
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base';
import { LoanProducts } from '../../loan-products';
import { ProcessingStrategyService } from '../../services/processing-strategy.service';
@Component({
selector: 'mifosx-loan-product-terms-step',
templateUrl: './loan-product-terms-step.component.html',
styleUrls: ['./loan-product-terms-step.component.scss']
})
export class LoanProductTermsStepComponent implements OnInit {
export class LoanProductTermsStepComponent implements OnInit, OnChanges {
@Input() loanProductsTemplate: any;
loanProductTermsForm: UntypedFormGroup;
/** Zero Interest control. */
zeroInterest = new UntypedFormControl(false);
valueConditionTypeData: any;
floatingRateData: any;
interestRateFrequencyTypeData: any;
@@ -29,9 +34,11 @@ export class LoanProductTermsStepComponent implements OnInit {
repaymentStartDateTypeOptions: any;
displayedColumns: string[] = ['valueConditionType', 'borrowerCycleNumber', 'minValue', 'defaultValue', 'maxValue', 'actions'];
isAdvancedTransactionProcessingStrategy = false;
constructor(private formBuilder: UntypedFormBuilder,
public dialog: MatDialog) {
private processingStrategyService: ProcessingStrategyService,
private dialog: MatDialog) {
this.createLoanProductTermsForm();
this.setConditionalControls();
}
@@ -83,6 +90,13 @@ export class LoanProductTermsStepComponent implements OnInit {
this.formBuilder.array(this.loanProductsTemplate.numberOfRepaymentVariationsForBorrowerCycle.map((variation: any) => ({ ...variation, valueConditionType: variation.valueConditionType.id }))));
this.loanProductTermsForm.setControl('interestRateVariationsForBorrowerCycle',
this.formBuilder.array(this.loanProductsTemplate.interestRateVariationsForBorrowerCycle.map((variation: any) => ({ ...variation, valueConditionType: variation.valueConditionType.id }))));
this.zeroInterest.patchValue((this.loanProductsTemplate.minInterestRatePerPeriod === 0 && this.loanProductsTemplate.interestRatePerPeriod === 0 && this.loanProductsTemplate.maxInterestRatePerPeriod === 0));
this.processingStrategyService.advancedTransactionProcessingStrategy.subscribe((value: boolean) => {
this.isAdvancedTransactionProcessingStrategy = value;
});
this.validateAdvancedPaymentStrategyControls();
}
createLoanProductTermsForm() {
@@ -103,10 +117,15 @@ export class LoanProductTermsStepComponent implements OnInit {
'repaymentEvery': ['', Validators.required],
'repaymentFrequencyType': ['', Validators.required],
'minimumDaysBetweenDisbursalAndFirstRepayment': [''],
'repaymentStartDateType': [1]
'repaymentStartDateType': [1],
'fixedLength': [null]
});
}
ngOnChanges(changes: SimpleChanges): void {
this.validateAdvancedPaymentStrategyControls();
}
setConditionalControls() {
this.loanProductTermsForm.get('allowApprovedDisbursedAmountsOverApplied').valueChanges
.subscribe(allowApprovedDisbursedAmountsOverApplied => {
@@ -149,18 +168,37 @@ export class LoanProductTermsStepComponent implements OnInit {
}
});
this.loanProductTermsForm.get('useBorrowerCycle').valueChanges
.subscribe(useBorrowerCycle => {
if (useBorrowerCycle) {
this.loanProductTermsForm.addControl('principalVariationsForBorrowerCycle', this.formBuilder.array([]));
this.loanProductTermsForm.addControl('numberOfRepaymentVariationsForBorrowerCycle', this.formBuilder.array([]));
this.loanProductTermsForm.addControl('interestRateVariationsForBorrowerCycle', this.formBuilder.array([]));
} else {
this.loanProductTermsForm.removeControl('principalVariationsForBorrowerCycle');
this.loanProductTermsForm.removeControl('numberOfRepaymentVariationsForBorrowerCycle');
this.loanProductTermsForm.removeControl('interestRateVariationsForBorrowerCycle');
}
});
this.loanProductTermsForm.get('useBorrowerCycle').valueChanges
.subscribe(useBorrowerCycle => {
if (useBorrowerCycle) {
this.loanProductTermsForm.addControl('principalVariationsForBorrowerCycle', this.formBuilder.array([]));
this.loanProductTermsForm.addControl('numberOfRepaymentVariationsForBorrowerCycle', this.formBuilder.array([]));
this.loanProductTermsForm.addControl('interestRateVariationsForBorrowerCycle', this.formBuilder.array([]));
} else {
this.loanProductTermsForm.removeControl('principalVariationsForBorrowerCycle');
this.loanProductTermsForm.removeControl('numberOfRepaymentVariationsForBorrowerCycle');
this.loanProductTermsForm.removeControl('interestRateVariationsForBorrowerCycle');
}
});
this.zeroInterest.valueChanges.subscribe(zeroInterest => {
if (zeroInterest) {
this.loanProductTermsForm.get('minInterestRatePerPeriod').patchValue(0);
this.loanProductTermsForm.get('minInterestRatePerPeriod').disable();
this.loanProductTermsForm.get('interestRatePerPeriod').patchValue(0);
this.loanProductTermsForm.get('interestRatePerPeriod').disable();
this.loanProductTermsForm.get('maxInterestRatePerPeriod').patchValue(0);
this.loanProductTermsForm.get('maxInterestRatePerPeriod').disable();
} else {
this.loanProductTermsForm.get('minInterestRatePerPeriod').patchValue(this.loanProductsTemplate.minInterestRatePerPeriod);
this.loanProductTermsForm.get('minInterestRatePerPeriod').enable();
this.loanProductTermsForm.get('interestRatePerPeriod').patchValue(this.loanProductsTemplate.interestRatePerPeriod);
this.loanProductTermsForm.get('interestRatePerPeriod').enable();
this.loanProductTermsForm.get('maxInterestRatePerPeriod').patchValue(this.loanProductsTemplate.maxInterestRatePerPeriod);
this.loanProductTermsForm.get('maxInterestRatePerPeriod').enable();
}
this.validateAdvancedPaymentStrategyControls();
});
}
get principalVariationsForBorrowerCycle(): UntypedFormArray {
@@ -271,4 +309,20 @@ export class LoanProductTermsStepComponent implements OnInit {
return this.loanProductTermsForm.value;
}
isZeroInterest(): boolean {
return this.zeroInterest.value;
}
allowFixedLength(): boolean {
return this.isAdvancedTransactionProcessingStrategy && this.isZeroInterest();
}
private validateAdvancedPaymentStrategyControls(): void {
if (this.allowFixedLength()) {
this.loanProductTermsForm.get('fixedLength').patchValue(this.loanProductsTemplate.fixedLength || null);
} else {
this.loanProductTermsForm.get('fixedLength').patchValue(null);
}
}
}
@@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProcessingStrategyService {
advancedTransactionProcessingStrategy = new BehaviorSubject<boolean>(false);
constructor() { }
initialize(value: boolean) {
this.advancedTransactionProcessingStrategy.next(value);
}
get isAdvancedTransactionProcessingStrategy(): boolean { return this.advancedTransactionProcessingStrategy.value; }
}