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 
---------------------------------------");

Reply via email to