This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit bf040561abdb8d4aa9df70d3506ab2695007b0a4 Author: Jose Alberto Hernandez <[email protected]> AuthorDate: Thu Jun 26 09:48:07 2025 -0500 FINERACT-2181: Not allow Reschedule loan with interest rate change from / to zero --- .../RescheduleLoansApiConstants.java | 1 + .../LoanRescheduleRequestDataValidatorImpl.java | 13 ++- ...gressiveLoanRescheduleRequestDataValidator.java | 3 +- ...PaymentAllocationLoanRepaymentScheduleTest.java | 109 +++++++++++++++++++++ 4 files changed, 122 insertions(+), 4 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java index eb0e410450..8973b74be6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java @@ -53,6 +53,7 @@ public final class RescheduleLoansApiConstants { public static final String rescheduleForMultiDisbursementNotSupportedErrorCode = "loan.reschedule.tranche.multidisbursement.error.code"; public static final String rescheduleMultipleOperationsNotSupportedErrorCode = "loan.reschedule.multioperations.error.code"; public static final String rescheduleSelectedOperationNotSupportedErrorCode = "loan.reschedule.selectedoperationnotsupported.error.code"; + public static final String rescheduleNotAllowedFromInterestRateZeroErrorCode = "loan.reschedule.not.allowed.from.current.interest.rate.zero"; public static final String allCommandParamName = "all"; public static final String approveCommandParamName = "approve"; public static final String pendingCommandParamName = "pending"; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java index 7bc58bd239..f65c3867cb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestDataValidatorImpl.java @@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; @@ -72,12 +73,17 @@ public class LoanRescheduleRequestDataValidatorImpl implements LoanRescheduleReq @Qualifier("progressiveLoanRescheduleRequestDataValidatorImpl") private final LoanRescheduleRequestDataValidator progressiveLoanRescheduleRequestDataValidatorDelegate; - public static BigDecimal validateInterestRate(FromJsonHelper fromJsonHelper, JsonElement jsonElement, - DataValidatorBuilder dataValidatorBuilder) { + public static BigDecimal validateInterestRate(final BigDecimal currentInterestRate, final FromJsonHelper fromJsonHelper, + final JsonElement jsonElement, DataValidatorBuilder dataValidatorBuilder) { final BigDecimal interestRate = fromJsonHelper .extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement); dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.newInterestRateParamName).value(interestRate).ignoreIfNull() .positiveAmount(); + if (interestRate != null && MathUtil.isZero(currentInterestRate) && !MathUtil.isZero(interestRate)) { + dataValidatorBuilder.reset().failWithCode(RescheduleLoansApiConstants.newInterestRateParamName, + RescheduleLoansApiConstants.rescheduleNotAllowedFromInterestRateZeroErrorCode, + "Loan rescheduling is not allowed from interest rate 0 (zero)"); + } return interestRate; } @@ -246,7 +252,8 @@ public class LoanRescheduleRequestDataValidatorImpl implements LoanRescheduleReq validateLoanIsActive(loan, dataValidatorBuilder); validateSubmittedOnDate(fromJsonHelper, loan, jsonElement, dataValidatorBuilder); final LocalDate rescheduleFromDate = validateAndRetrieveRescheduleFromDate(fromJsonHelper, jsonElement, dataValidatorBuilder); - validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder); + validateInterestRate(loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), fromJsonHelper, jsonElement, + dataValidatorBuilder); validateGraceOnPrincipal(fromJsonHelper, jsonElement, dataValidatorBuilder); validateGraceOnInterest(fromJsonHelper, jsonElement, dataValidatorBuilder); validateExtraTerms(fromJsonHelper, jsonElement, dataValidatorBuilder); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java index 17cd82f55e..b3804eb0c2 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java @@ -78,7 +78,8 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder); validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder); LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder); - BigDecimal interestRate = validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder); + BigDecimal interestRate = validateInterestRate(loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), fromJsonHelper, + jsonElement, dataValidatorBuilder); validateUnsupportedParams(jsonElement, dataValidatorBuilder); boolean hasInterestRateChange = interestRate != null; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 2bd83a92f1..c0e4278b4e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -6000,6 +6000,115 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan }); } + // uc156: Avoid Loan Reschedule to modify Interest Rate from X value to Zero + // 1. Create a Loan product + // 2. Submit, Approve and Disburse Loan with Nominal Interest equal to 4% + // 3. Apply a Loan repayment + // 4. Try to create Loan Reschedule with new Interest Rate equal to zero to get the exception + @Test + public void uc156() { + final String operationDate = "1 April 2025"; + AtomicLong createdLoanId = new AtomicLong(); + runAt("1 April 2025", () -> { + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = create4IProgressive().interestRatePerPeriod(4.0).numberOfRepayments(4)// + .installmentAmountInMultiplesOf(null)// + .multiDisburseLoan(false)// + .disallowExpectedDisbursements(null)// + .allowApprovedDisbursedAmountsOverApplied(false)// + .overAppliedCalculationType(null)// + .interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)// + .overAppliedNumber(null)// + ;// + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLP2ProgressiveLoanRequest(clientId, loanProductResponse.getResourceId(), + operationDate, 1000.0, 4.0, 4, null); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + createdLoanId.set(loanResponse.getLoanId()); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest() + .approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate) + .dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(BigDecimal.valueOf(1000.0))); + }); + + runAt("1 May 2025", () -> { + executeInlineCOB(createdLoanId.get()); + + loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest() + .transactionDate("1 May 2025").dateFormat("dd MMMM yyyy").locale("en").transactionAmount(250.00)); + }); + + runAt("6 May 2025", () -> { + executeInlineCOB(createdLoanId.get()); + + CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class, + () -> loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest() + .loanId(createdLoanId.get()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("6 May 2025") + .newInterestRate(BigDecimal.ZERO).rescheduleReasonId(1L).rescheduleFromDate("1 June 2025"))); + + Assertions.assertTrue( + callFailedRuntimeException.getMessage().contains("The parameter `newInterestRate` must be greater than 0.")); + }); + } + + // uc157: Avoid Loan Reschedule to modify Interest Rate from Zero to X value + // 1. Create a Loan product + // 2. Submit, Approve and Disburse Loan with Nominal Interest equal to 0 (zero) + // 3. Apply a Loan repayment + // 4. Try to create Loan Reschedule with new Interest Rate greater than zero to get the exception + @Test + public void uc157() { + final String operationDate = "1 April 2025"; + AtomicLong createdLoanId = new AtomicLong(); + runAt("1 April 2025", () -> { + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = create4IProgressive().interestRatePerPeriod(0.0).numberOfRepayments(4)// + .installmentAmountInMultiplesOf(null)// + .multiDisburseLoan(false)// + .disallowExpectedDisbursements(null)// + .allowApprovedDisbursedAmountsOverApplied(false)// + .overAppliedCalculationType(null)// + .interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)// + .overAppliedNumber(null)// + ;// + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLP2ProgressiveLoanRequest(clientId, loanProductResponse.getResourceId(), + operationDate, 1000.0, 0.0, 4, null); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + createdLoanId.set(loanResponse.getLoanId()); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest() + .approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate) + .dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(BigDecimal.valueOf(1000.0))); + }); + + runAt("1 May 2025", () -> { + executeInlineCOB(createdLoanId.get()); + + loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest() + .transactionDate("1 May 2025").dateFormat("dd MMMM yyyy").locale("en").transactionAmount(250.00)); + }); + + runAt("6 May 2025", () -> { + executeInlineCOB(createdLoanId.get()); + + CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class, + () -> loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest() + .loanId(createdLoanId.get()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("6 May 2025") + .newInterestRate(BigDecimal.valueOf(4.0)).rescheduleReasonId(1L).rescheduleFromDate("1 June 2025"))); + + LOG.info("ERROR: {}", callFailedRuntimeException.getMessage()); + Assertions.assertTrue( + callFailedRuntimeException.getMessage().contains("Loan rescheduling is not allowed from interest rate 0 (zero)")); + }); + } + private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId, Integer numberOfRepayments, String loanDisbursementDate, double amount) { LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
