Skip to content

Commit 3eeb5d7

Browse files
author
Jose Alberto Hernandez
committed
FINERACT-2181: Not allow Reschedule loan with interest rate change from / to zero
1 parent 570f44a commit 3eeb5d7

File tree

4 files changed

+121
-4
lines changed

4 files changed

+121
-4
lines changed

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ private RescheduleLoansApiConstants() {
5353
public static final String rescheduleForMultiDisbursementNotSupportedErrorCode = "loan.reschedule.tranche.multidisbursement.error.code";
5454
public static final String rescheduleMultipleOperationsNotSupportedErrorCode = "loan.reschedule.multioperations.error.code";
5555
public static final String rescheduleSelectedOperationNotSupportedErrorCode = "loan.reschedule.selectedoperationnotsupported.error.code";
56+
public static final String rescheduleNotAllowedFromInterestRateZeroErrorCode = "loan.reschedule.not.allowed.from.current.interest.rate.zero";
5657
public static final String allCommandParamName = "all";
5758
public static final String approveCommandParamName = "approve";
5859
public static final String pendingCommandParamName = "pending";

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
4040
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
4141
import org.apache.fineract.infrastructure.core.service.DateUtils;
42+
import org.apache.fineract.infrastructure.core.service.MathUtil;
4243
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
4344
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
4445
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@@ -72,12 +73,17 @@ public class LoanRescheduleRequestDataValidatorImpl implements LoanRescheduleReq
7273
@Qualifier("progressiveLoanRescheduleRequestDataValidatorImpl")
7374
private final LoanRescheduleRequestDataValidator progressiveLoanRescheduleRequestDataValidatorDelegate;
7475

75-
public static BigDecimal validateInterestRate(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
76-
DataValidatorBuilder dataValidatorBuilder) {
76+
public static BigDecimal validateInterestRate(final BigDecimal currentInterestRate, final FromJsonHelper fromJsonHelper,
77+
final JsonElement jsonElement, DataValidatorBuilder dataValidatorBuilder) {
7778
final BigDecimal interestRate = fromJsonHelper
7879
.extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement);
7980
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.newInterestRateParamName).value(interestRate).ignoreIfNull()
8081
.positiveAmount();
82+
if (interestRate != null && MathUtil.isZero(currentInterestRate) && !MathUtil.isZero(interestRate)) {
83+
dataValidatorBuilder.reset().failWithCodeNoParameterAddedToErrorCode(
84+
RescheduleLoansApiConstants.rescheduleNotAllowedFromInterestRateZeroErrorCode,
85+
"Loan rescheduling is not allowed from interest rate 0 (zero)");
86+
}
8187
return interestRate;
8288
}
8389

@@ -246,7 +252,8 @@ public void validateForCreateAction(final JsonCommand jsonCommand, final Loan lo
246252
validateLoanIsActive(loan, dataValidatorBuilder);
247253
validateSubmittedOnDate(fromJsonHelper, loan, jsonElement, dataValidatorBuilder);
248254
final LocalDate rescheduleFromDate = validateAndRetrieveRescheduleFromDate(fromJsonHelper, jsonElement, dataValidatorBuilder);
249-
validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder);
255+
validateInterestRate(loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), fromJsonHelper, jsonElement,
256+
dataValidatorBuilder);
250257
validateGraceOnPrincipal(fromJsonHelper, jsonElement, dataValidatorBuilder);
251258
validateGraceOnInterest(fromJsonHelper, jsonElement, dataValidatorBuilder);
252259
validateExtraTerms(fromJsonHelper, jsonElement, dataValidatorBuilder);

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ public void validateForCreateAction(JsonCommand jsonCommand, Loan loan) {
7878
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
7979
validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder);
8080
LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder);
81-
BigDecimal interestRate = validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder);
81+
BigDecimal interestRate = validateInterestRate(loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), fromJsonHelper,
82+
jsonElement, dataValidatorBuilder);
8283
validateUnsupportedParams(jsonElement, dataValidatorBuilder);
8384

8485
boolean hasInterestRateChange = interestRate != null;

integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6000,6 +6000,114 @@ public void uc155() {
60006000
});
60016001
}
60026002

6003+
// uc156: Avoid Loan Reschedule to modify Interest Rate from X value to Zero
6004+
// 1. Create a Loan product
6005+
// 2. Submit, Approve and Disburse Loan with Nominal Interest equal to 4%
6006+
// 3. Apply a Loan repayment
6007+
// 4. Try to create Loan Reschedule with new Interest Rate equal to zero to get the exception
6008+
@Test
6009+
public void uc156() {
6010+
final String operationDate = "1 April 2025";
6011+
AtomicLong createdLoanId = new AtomicLong();
6012+
runAt("1 April 2025", () -> {
6013+
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
6014+
PostLoanProductsRequest product = create4IProgressive().interestRatePerPeriod(4.0).numberOfRepayments(4)//
6015+
.installmentAmountInMultiplesOf(null)//
6016+
.multiDisburseLoan(false)//
6017+
.disallowExpectedDisbursements(null)//
6018+
.allowApprovedDisbursedAmountsOverApplied(false)//
6019+
.overAppliedCalculationType(null)//
6020+
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
6021+
.overAppliedNumber(null)//
6022+
;//
6023+
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
6024+
PostLoansRequest applicationRequest = applyLP2ProgressiveLoanRequest(clientId, loanProductResponse.getResourceId(),
6025+
operationDate, 1000.0, 4.0, 4, null);
6026+
6027+
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
6028+
createdLoanId.set(loanResponse.getLoanId());
6029+
6030+
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
6031+
.approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
6032+
6033+
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
6034+
.dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(BigDecimal.valueOf(1000.0)));
6035+
});
6036+
6037+
runAt("1 May 2025", () -> {
6038+
executeInlineCOB(createdLoanId.get());
6039+
6040+
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
6041+
.transactionDate("1 May 2025").dateFormat("dd MMMM yyyy").locale("en").transactionAmount(250.00));
6042+
});
6043+
6044+
runAt("6 May 2025", () -> {
6045+
executeInlineCOB(createdLoanId.get());
6046+
6047+
CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class,
6048+
() -> loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()
6049+
.loanId(createdLoanId.get()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("6 May 2025")
6050+
.newInterestRate(BigDecimal.ZERO).rescheduleReasonId(1L).rescheduleFromDate("1 June 2025")));
6051+
6052+
Assertions.assertTrue(
6053+
callFailedRuntimeException.getMessage().contains("The parameter `newInterestRate` must be greater than 0."));
6054+
});
6055+
}
6056+
6057+
// uc157: Avoid Loan Reschedule to modify Interest Rate from Zero to X value
6058+
// 1. Create a Loan product
6059+
// 2. Submit, Approve and Disburse Loan with Nominal Interest equal to 0 (zero)
6060+
// 3. Apply a Loan repayment
6061+
// 4. Try to create Loan Reschedule with new Interest Rate greater than zero to get the exception
6062+
@Test
6063+
public void uc157() {
6064+
final String operationDate = "1 April 2025";
6065+
AtomicLong createdLoanId = new AtomicLong();
6066+
runAt("1 April 2025", () -> {
6067+
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
6068+
PostLoanProductsRequest product = create4IProgressive().interestRatePerPeriod(0.0).numberOfRepayments(4)//
6069+
.installmentAmountInMultiplesOf(null)//
6070+
.multiDisburseLoan(false)//
6071+
.disallowExpectedDisbursements(null)//
6072+
.allowApprovedDisbursedAmountsOverApplied(false)//
6073+
.overAppliedCalculationType(null)//
6074+
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
6075+
.overAppliedNumber(null)//
6076+
;//
6077+
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
6078+
PostLoansRequest applicationRequest = applyLP2ProgressiveLoanRequest(clientId, loanProductResponse.getResourceId(),
6079+
operationDate, 1000.0, 0.0, 4, null);
6080+
6081+
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
6082+
createdLoanId.set(loanResponse.getLoanId());
6083+
6084+
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
6085+
.approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
6086+
6087+
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
6088+
.dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(BigDecimal.valueOf(1000.0)));
6089+
});
6090+
6091+
runAt("1 May 2025", () -> {
6092+
executeInlineCOB(createdLoanId.get());
6093+
6094+
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
6095+
.transactionDate("1 May 2025").dateFormat("dd MMMM yyyy").locale("en").transactionAmount(250.00));
6096+
});
6097+
6098+
runAt("6 May 2025", () -> {
6099+
executeInlineCOB(createdLoanId.get());
6100+
6101+
CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class,
6102+
() -> loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()
6103+
.loanId(createdLoanId.get()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("6 May 2025")
6104+
.newInterestRate(BigDecimal.valueOf(4.0)).rescheduleReasonId(1L).rescheduleFromDate("1 June 2025")));
6105+
6106+
Assertions.assertTrue(
6107+
callFailedRuntimeException.getMessage().contains("Loan rescheduling is not allowed from interest rate 0 (zero)"));
6108+
});
6109+
}
6110+
60036111
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
60046112
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
60056113
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");

0 commit comments

Comments
 (0)