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 7285a958fed9eebd16f590e831280d5dcdaf5201
Author: Soma Sörös <[email protected]>
AuthorDate: Thu Jul 31 09:54:37 2025 +0200

    FINERACT-1981: Reschedule loan with interest rate change from zero breaks 
repayment schedule and loan status to OVERPAID
---
 .../test/data/LoanRescheduleErrorMessage.java      |   4 +-
 .../test/data/loanproduct/DefaultLoanProduct.java  |   1 +
 .../global/LoanProductGlobalInitializerStep.java   |  35 ++++++
 .../fineract/test/support/TestContextKey.java      |   1 +
 .../features/LoanInterestRateChange.feature        | 127 +++++++++++++++++++++
 .../portfolio/loanaccount/domain/Loan.java         |   6 +-
 ...gressiveLoanRescheduleRequestDataValidator.java |  18 ++-
 7 files changed, 186 insertions(+), 6 deletions(-)

diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanRescheduleErrorMessage.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanRescheduleErrorMessage.java
index 52119a8bbb..5e5be8aafb 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanRescheduleErrorMessage.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/LoanRescheduleErrorMessage.java
@@ -24,7 +24,9 @@ public enum LoanRescheduleErrorMessage {
     LOAN_RESCHEDULE_DATE_NOT_IN_FUTURE("Loan Reschedule From date (%s) for 
Loan: %s should be in the future."), //
     LOAN_LOCKED_BY_COB("Loan is locked by the COB job. Loan ID: %s"), //
     LOAN_RESCHEDULE_NOT_ALLOWED_FROM_ZERO_TO_NEW_INTEREST_RATE("Failed data 
validation due to: newInterestRate."), //
-    LOAN_RESCHEDULE_NOT_ALLOWED_FROM_CURRENT_INTEREST_RATE_TO_ZERO("The 
parameter `newInterestRate` must be greater than 0.");//
+    LOAN_RESCHEDULE_NOT_ALLOWED_FROM_CURRENT_INTEREST_RATE_TO_ZERO("The 
parameter `newInterestRate` must be greater than 0."), //
+    NEW_INTEREST_RATE_IS_1_BUT_MINIMUM_IS_3("The parameter `newInterestRate` 
value 1.0 must not be less than the minimum value 3.0"), //
+    NEW_INTEREST_RATE_IS_45_BUT_MAXIMUM_IS_20("The parameter `newInterestRate` 
value 45.0 must not be more than maximum value 20.0"); //
 
     private final String messageTemplate;
 
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
index 9f5c3ee529..d8e54c0ddd 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
@@ -157,6 +157,7 @@ public enum DefaultLoanProduct implements LoanProduct {
     
LP2_ADV_PYMNT_INTEREST_DECL_BAL_SARP_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_PARTIAL_PERIOD,
 //
     
LP2_ADV_PYMNT_INTEREST_DECL_BAL_SARP_EMI_360_30_NO_INT_RECALC_MULTIDISB_PARTIAL_PERIOD,
 //
     
LP2_ADV_PYMNT_INTEREST_DECL_BAL_SARP_EMI_360_30_NO_INT_RECALC_MULTIDISB_NO_PARTIAL_PERIOD,
 //
+    
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_MIN_INT_3_MAX_INT_20,
 //
     ;
 
     @Override
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
index 3eab591de1..9a415a046a 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
@@ -23,6 +23,7 @@ import static 
org.apache.fineract.test.data.TransactionProcessingStrategyCode.AD
 import static 
org.apache.fineract.test.factory.LoanProductsRequestFactory.INTEREST_CALCULATION_PERIOD_TYPE_SAME_AS_REPAYMENT;
 import static 
org.apache.fineract.test.factory.LoanProductsRequestFactory.INTEREST_RATE_FREQUENCY_TYPE_MONTH;
 import static 
org.apache.fineract.test.factory.LoanProductsRequestFactory.INTEREST_RATE_FREQUENCY_TYPE_WHOLE_TERM;
+import static 
org.apache.fineract.test.factory.LoanProductsRequestFactory.INTEREST_RATE_FREQUENCY_TYPE_YEAR;
 import static 
org.apache.fineract.test.factory.LoanProductsRequestFactory.LOAN_ACCOUNTING_RULE_NONE;
 import static 
org.apache.fineract.test.factory.LoanProductsRequestFactory.REPAYMENT_FREQUENCY_TYPE_MONTHS;
 import static 
org.apache.fineract.test.factory.LoanProductsRequestFactory.SHORT_NAME_PREFIX_EMI;
@@ -3570,6 +3571,40 @@ public class LoanProductGlobalInitializerStep implements 
FineractGlobalInitializ
         TestContext.INSTANCE.set(
                 
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES,
                 
responseLoanProductsRequestLP2ProgressiveAdvPymnt36030InterestRecalcMultidisbApprovedOverAppliedExpectTranches);
+
+        // LP2 with progressive loan schedule + horizontal + interest EMI + 
360/30
+        // + interest recalculation, preClosureInterestCalculationStrategy= 
till preclose,
+        // interestRecalculationCompoundingMethod = none
+        // Frequency for recalculate Outstanding Principal: Daily, Frequency 
Interval for recalculation: 1
+        // 
(LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE)
+        // min interest rate / year 3 and max interest rate / year is 20
+        String name135 = 
DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_MIN_INT_3_MAX_INT_20
+                .getName();
+        PostLoanProductsRequest 
loanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcDailyTillPrecloseMinInt3MaxInt20
 = loanProductsRequestFactory
+                .defaultLoanProductsRequestLP2Emi()//
+                .name(name135)//
+                .daysInYearType(DaysInYearType.DAYS360.value)//
+                .daysInMonthType(DaysInMonthType.DAYS30.value)//
+                .isInterestRecalculationEnabled(true)//
+                .preClosureInterestCalculationStrategy(1)//
+                .rescheduleStrategyMethod(4)//
+                .interestRecalculationCompoundingMethod(0)//
+                .recalculationRestFrequencyType(2)//
+                .recalculationRestFrequencyInterval(1)//
+                .minInterestRatePerPeriod(3D)//
+                .interestRatePerPeriod(12D) //
+                .maxInterestRatePerPeriod(20D)//
+                
.interestRateFrequencyType(INTEREST_RATE_FREQUENCY_TYPE_YEAR).paymentAllocation(List.of(//
+                        createPaymentAllocation("DEFAULT", 
"NEXT_INSTALLMENT"), //
+                        createPaymentAllocation("GOODWILL_CREDIT", 
"LAST_INSTALLMENT"), //
+                        createPaymentAllocation("MERCHANT_ISSUED_REFUND", 
"REAMORTIZATION"), //
+                        createPaymentAllocation("PAYOUT_REFUND", 
"NEXT_INSTALLMENT")));//
+        Response<PostLoanProductsResponse> 
responseLoanProductsRequestLP2AdvancedPaymentInterest36030InterestRecalcDailyTillPreCloseMinInt3MaxInt20
 = loanProductsApi
+                
.createLoanProduct(loanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcDailyTillPrecloseMinInt3MaxInt20)
+                .execute();
+        TestContext.INSTANCE.set(
+                
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_MIN_INT_3_MAX_INT_20,
+                
responseLoanProductsRequestLP2AdvancedPaymentInterest36030InterestRecalcDailyTillPreCloseMinInt3MaxInt20);
     }
 
     public static AdvancedPaymentData createPaymentAllocation(String 
transactionType, String futureInstallmentAllocationRule,
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
index 683b503fde..85d569fd83 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
@@ -259,4 +259,5 @@ public abstract class TestContextKey {
     public static final String 
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DECL_BAL_SARP_EMI_360_30_NO_INT_RECALC_MULTIDISB_PARTIAL_PERIOD
 = 
"loanProductCreateResponseLP2AdvPmtIntDeclSarpEmi3630NoIntRecalcMutiDisbPartial";
     public static final String 
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DECL_BAL_SARP_EMI_360_30_NO_INT_RECALC_MULTIDISB_NO_PARTIAL_PERIOD
 = 
"loanProductCreateResponseLP2AdvPmtIntDeclSarpEmi3630NoIntRecalcMutiDisbNoPartial";
     public static final String 
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES
 = 
"loanProductCreateResponseLP2ProgressiveAdvancedPaymentInterestRecalculationMultidisbursalApprovedOverAppliedAmountExpectedTransches";
+    public static final String 
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_MIN_INT_3_MAX_INT_20
 = 
"loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyTillPreCloseMinInt3MaxInt20";
 }
diff --git 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanInterestRateChange.feature
 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanInterestRateChange.feature
index 24480e0825..19295688c9 100644
--- 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanInterestRateChange.feature
+++ 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanInterestRateChange.feature
@@ -1190,3 +1190,130 @@ Feature: Loan interest rate change on repayment schedule
       | 01 April 2024    | Accrual            | 1.59   | 0.0       | 1.59     
| 0.0  | 0.0       | 0.0          | false    | false    |
       | 01 April 2024    | Accrual Adjustment | 0.43   | 0.0       | 0.43     
| 0.0  | 0.0       | 0.0          | false    | false    |
       | 01 April 2024    | Charge-off         | 97.49  | 83.57     | 0.92     
| 8.0  | 5.0       | 0.0          | false    | true     |
+  Scenario: Verify change interest rate with 0->4 reschedule scenario - UC1
+    When Admin sets the business date to "01 April 2025"
+    When Admin creates a client with random data
+    When Admin creates a fully customized loan with the following data:
+      | LoanProduct                                                            
            | submitted on date | with Principal | ANNUAL interest rate % | 
interest type     | interest calculation period | amortization type  | 
loanTermFrequency | loanTermFrequencyType | repaymentEvery | 
repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | 
graceOnInterestPayment | interest free period | Payment strategy            |
+      | 
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE
 | 01 April 2025     | 1000           | 0                      | 
DECLINING_BALANCE | DAILY                       | EQUAL_INSTALLMENTS | 4        
         | MONTHS                | 1              | MONTHS                 | 4  
                | 0                       | 0                      | 0          
          | ADVANCED_PAYMENT_ALLOCATION |
+    And Admin successfully approves the loan on "01 April 2025" with "1000" 
amount and expected disbursement date on "01 April 2025"
+    When Admin successfully disburse the loan on "01 April 2025" with "1000" 
EUR transaction amount
+    Then Loan Repayment schedule has 4 periods, with the following data for 
periods:
+      | Nr | Days | Date           | Paid date | Balance of loan | Principal 
due | Interest | Fees | Penalties | Due   | Paid | In advance | Late | 
Outstanding |
+      |    |      | 01 April 2025  |           | 1000.0          |             
  |          | 0.0  |           | 0.0   | 0.0  |            |      |            
 |
+      | 1  | 30   | 01 May 2025    |           | 750.0           | 250.0       
  | 0.0      | 0.0  | 0.0       | 250.0 | 0.0  | 0.0        | 0.0  | 250.0      
 |
+      | 2  | 31   | 01 June 2025   |           | 500.0           | 250.0       
  | 0.0      | 0.0  | 0.0       | 250.0 | 0.0  | 0.0        | 0.0  | 250.0      
 |
+      | 3  | 30   | 01 July 2025   |           | 250.0           | 250.0       
  | 0.0      | 0.0  | 0.0       | 250.0 | 0.0  | 0.0        | 0.0  | 250.0      
 |
+      | 4  | 31   | 01 August 2025 |           | 0.0             | 250.0       
  | 0.0      | 0.0  | 0.0       | 250.0 | 0.0  | 0.0        | 0.0  | 250.0      
 |
+    Then Loan Repayment schedule has the following data in Total row:
+      | Principal due | Interest | Fees | Penalties | Due    | Paid | In 
advance | Late | Outstanding |
+      | 1000.0        | 0.0      | 0.0  | 0.0       | 1000.0 | 0.0  | 0.0      
  | 0.0  | 1000.0      |
+    Then Loan Transactions tab has the following data:
+      | Transaction date | Transaction Type | Amount | Principal | Interest | 
Fees | Penalties | Loan Balance | Reverted | Replayed |
+      | 01 April 2025    | Disbursement     | 1000.0 | 0.0       | 0.0      | 
0.0  | 0.0       | 1000.0       | false    | false    |
+    When Admin sets the business date to "01 May 2025"
+    And Admin runs inline COB job for Loan
+    And Customer makes "AUTOPAY" repayment on "01 May 2025" with 250.0 EUR 
transaction amount
+    Then Loan Repayment schedule has 4 periods, with the following data for 
periods:
+      | Nr | Days | Date           | Paid date   | Balance of loan | Principal 
due | Interest | Fees | Penalties | Due   | Paid  | In advance | Late | 
Outstanding |
+      |    |      | 01 April 2025  |             | 1000.0          |           
    |          | 0.0  |           | 0.0   | 0.0   |            |      |         
    |
+      | 1  | 30   | 01 May 2025    | 01 May 2025 | 750.0           | 250.0     
    | 0.0      | 0.0  | 0.0       | 250.0 | 250.0 | 0.0        | 0.0  | 0.0     
    |
+      | 2  | 31   | 01 June 2025   |             | 500.0           | 250.0     
    | 0.0      | 0.0  | 0.0       | 250.0 | 0.0   | 0.0        | 0.0  | 250.0   
    |
+      | 3  | 30   | 01 July 2025   |             | 250.0           | 250.0     
    | 0.0      | 0.0  | 0.0       | 250.0 | 0.0   | 0.0        | 0.0  | 250.0   
    |
+      | 4  | 31   | 01 August 2025 |             | 0.0             | 250.0     
    | 0.0      | 0.0  | 0.0       | 250.0 | 0.0   | 0.0        | 0.0  | 250.0   
    |
+    Then Loan Repayment schedule has the following data in Total row:
+      | Principal due | Interest | Fees | Penalties | Due    | Paid  | In 
advance | Late | Outstanding |
+      | 1000.0        | 0.0      | 0.0  | 0.0       | 1000.0 | 250.0 | 0.0     
   | 0.0  | 750.0       |
+    Then Loan Transactions tab has the following data:
+      | Transaction date | Transaction Type | Amount | Principal | Interest | 
Fees | Penalties | Loan Balance | Reverted | Replayed |
+      | 01 April 2025    | Disbursement     | 1000.0 | 0.0       | 0.0      | 
0.0  | 0.0       | 1000.0       | false    | false    |
+      | 01 May 2025      | Repayment        | 250.0  | 250.0     | 0.0      | 
0.0  | 0.0       | 750.0        | false    | false    |
+    When Admin sets the business date to "05 May 2025"
+    And Admin runs inline COB job for Loan
+    When Admin creates and approves Loan reschedule with the following data:
+      | rescheduleFromDate | submittedOnDate | adjustedDueDate | 
graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
+      | 02 May 2025        | 02 May 2025     |                 |               
   |                 |            | 12              |
+    Then Loan Repayment schedule has 4 periods, with the following data for 
periods:
+      | Nr | Days | Date           | Paid date   | Balance of loan | Principal 
due | Interest | Fees | Penalties | Due    | Paid  | In advance | Late | 
Outstanding |
+      |    |      | 01 April 2025  |             | 1000.0          |           
    |          | 0.0  |           | 0.0    | 0.0   |            |      |        
     |
+      | 1  | 30   | 01 May 2025    | 01 May 2025 | 750.0           | 250.0     
    | 0.0      | 0.0  | 0.0       | 250.0  | 250.0 | 0.0        | 0.0  | 0.0    
     |
+      | 2  | 31   | 01 June 2025   |             | 502.49          | 247.51    
    | 7.5      | 0.0  | 0.0       | 255.01 | 0.0   | 0.0        | 0.0  | 255.01 
     |
+      | 3  | 30   | 01 July 2025   |             | 252.5           | 249.99    
    | 5.02     | 0.0  | 0.0       | 255.01 | 0.0   | 0.0        | 0.0  | 255.01 
     |
+      | 4  | 31   | 01 August 2025 |             | 0.0             | 252.5     
    | 2.52     | 0.0  | 0.0       | 255.02 | 0.0   | 0.0        | 0.0  | 255.02 
     |
+    Then Loan status will be "ACTIVE"
+    When Loan Pay-off is made on "05 May 2025"
+    Then Loan's all installments have obligations met
+
+  Scenario: Verify change interest rate with 0->4 reschedule scenario - UC2
+    When Admin sets the business date to "01 April 2025"
+    When Admin creates a client with random data
+    When Admin creates a fully customized loan with the following data:
+      | LoanProduct                             | submitted on date | with 
Principal | ANNUAL interest rate % | interest type     | interest calculation 
period | amortization type  | loanTermFrequency | loanTermFrequencyType | 
repaymentEvery | repaymentFrequencyType | numberOfRepayments | 
graceOnPrincipalPayment | graceOnInterestPayment | interest free period | 
Payment strategy            |
+      | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30 | 01 April 2025     | 1000     
      | 0                      | DECLINING_BALANCE | DAILY                      
 | EQUAL_INSTALLMENTS | 4                 | MONTHS                | 1           
   | MONTHS                 | 4                  | 0                       | 0  
                    | 0                    | ADVANCED_PAYMENT_ALLOCATION |
+    And Admin successfully approves the loan on "01 April 2025" with "1000" 
amount and expected disbursement date on "01 April 2025"
+    When Admin successfully disburse the loan on "01 April 2025" with "1000" 
EUR transaction amount
+    Then Loan Repayment schedule has 4 periods, with the following data for 
periods:
+      | Nr | Days | Date           | Paid date | Balance of loan | Principal 
due | Interest | Fees | Penalties | Due   | Paid | In advance | Late | 
Outstanding |
+      |    |      | 01 April 2025  |           | 1000.0          |             
  |          | 0.0  |           | 0.0   | 0.0  |            |      |            
 |
+      | 1  | 30   | 01 May 2025    |           | 750.0           | 250.0       
  | 0.0      | 0.0  | 0.0       | 250.0 | 0.0  | 0.0        | 0.0  | 250.0      
 |
+      | 2  | 31   | 01 June 2025   |           | 500.0           | 250.0       
  | 0.0      | 0.0  | 0.0       | 250.0 | 0.0  | 0.0        | 0.0  | 250.0      
 |
+      | 3  | 30   | 01 July 2025   |           | 250.0           | 250.0       
  | 0.0      | 0.0  | 0.0       | 250.0 | 0.0  | 0.0        | 0.0  | 250.0      
 |
+      | 4  | 31   | 01 August 2025 |           | 0.0             | 250.0       
  | 0.0      | 0.0  | 0.0       | 250.0 | 0.0  | 0.0        | 0.0  | 250.0      
 |
+    Then Loan Repayment schedule has the following data in Total row:
+      | Principal due | Interest | Fees | Penalties | Due    | Paid | In 
advance | Late | Outstanding |
+      | 1000.0        | 0.0      | 0.0  | 0.0       | 1000.0 | 0.0  | 0.0      
  | 0.0  | 1000.0      |
+    Then Loan Transactions tab has the following data:
+      | Transaction date | Transaction Type | Amount | Principal | Interest | 
Fees | Penalties | Loan Balance | Reverted | Replayed |
+      | 01 April 2025    | Disbursement     | 1000.0 | 0.0       | 0.0      | 
0.0  | 0.0       | 1000.0       | false    | false    |
+    When Admin sets the business date to "01 May 2025"
+    And Admin runs inline COB job for Loan
+    And Customer makes "AUTOPAY" repayment on "01 May 2025" with 250.0 EUR 
transaction amount
+    Then Loan Repayment schedule has 4 periods, with the following data for 
periods:
+      | Nr | Days | Date           | Paid date   | Balance of loan | Principal 
due | Interest | Fees | Penalties | Due   | Paid  | In advance | Late | 
Outstanding |
+      |    |      | 01 April 2025  |             | 1000.0          |           
    |          | 0.0  |           | 0.0   | 0.0   |            |      |         
    |
+      | 1  | 30   | 01 May 2025    | 01 May 2025 | 750.0           | 250.0     
    | 0.0      | 0.0  | 0.0       | 250.0 | 250.0 | 0.0        | 0.0  | 0.0     
    |
+      | 2  | 31   | 01 June 2025   |             | 500.0           | 250.0     
    | 0.0      | 0.0  | 0.0       | 250.0 | 0.0   | 0.0        | 0.0  | 250.0   
    |
+      | 3  | 30   | 01 July 2025   |             | 250.0           | 250.0     
    | 0.0      | 0.0  | 0.0       | 250.0 | 0.0   | 0.0        | 0.0  | 250.0   
    |
+      | 4  | 31   | 01 August 2025 |             | 0.0             | 250.0     
    | 0.0      | 0.0  | 0.0       | 250.0 | 0.0   | 0.0        | 0.0  | 250.0   
    |
+    Then Loan Repayment schedule has the following data in Total row:
+      | Principal due | Interest | Fees | Penalties | Due    | Paid  | In 
advance | Late | Outstanding |
+      | 1000.0        | 0.0      | 0.0  | 0.0       | 1000.0 | 250.0 | 0.0     
   | 0.0  | 750.0       |
+    Then Loan Transactions tab has the following data:
+      | Transaction date | Transaction Type | Amount | Principal | Interest | 
Fees | Penalties | Loan Balance | Reverted | Replayed |
+      | 01 April 2025    | Disbursement     | 1000.0 | 0.0       | 0.0      | 
0.0  | 0.0       | 1000.0       | false    | false    |
+      | 01 May 2025      | Repayment        | 250.0  | 250.0     | 0.0      | 
0.0  | 0.0       | 750.0        | false    | false    |
+    When Admin sets the business date to "05 May 2025"
+    And Admin runs inline COB job for Loan
+    When Admin creates and approves Loan reschedule with the following data:
+      | rescheduleFromDate | submittedOnDate | adjustedDueDate | 
graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
+      | 02 May 2025        | 02 May 2025     |                 |               
   |                 |            | 12              |
+    Then Loan Repayment schedule has 4 periods, with the following data for 
periods:
+      | Nr | Days | Date           | Paid date   | Balance of loan | Principal 
due | Interest | Fees | Penalties | Due    | Paid  | In advance | Late | 
Outstanding |
+      |    |      | 01 April 2025  |             | 1000.0          |           
    |          | 0.0  |           | 0.0    | 0.0   |            |      |        
     |
+      | 1  | 30   | 01 May 2025    | 01 May 2025 | 750.0           | 250.0     
    | 0.0      | 0.0  | 0.0       | 250.0  | 250.0 | 0.0        | 0.0  | 0.0    
     |
+      | 2  | 31   | 01 June 2025   |             | 502.49          | 247.51    
    | 7.5      | 0.0  | 0.0       | 255.01 | 0.0   | 0.0        | 0.0  | 255.01 
     |
+      | 3  | 30   | 01 July 2025   |             | 252.5           | 249.99    
    | 5.02     | 0.0  | 0.0       | 255.01 | 0.0   | 0.0        | 0.0  | 255.01 
     |
+      | 4  | 31   | 01 August 2025 |             | 0.0             | 252.5     
    | 2.52     | 0.0  | 0.0       | 255.02 | 0.0   | 0.0        | 0.0  | 255.02 
     |
+    Then Loan status will be "ACTIVE"
+    When Loan Pay-off is made on "05 May 2025"
+    Then Loan's all installments have obligations met
+
+  Scenario: Verify change interest rate fails on loan outside min and max  set 
on loan product reschedule scenario - UC3
+    When Admin sets the business date to "01 April 2025"
+    When Admin creates a client with random data
+    When Admin creates a fully customized loan with the following data:
+      | LoanProduct                                                            
                                 | submitted on date | with Principal | ANNUAL 
interest rate % | interest type     | interest calculation period | 
amortization type  | loanTermFrequency | loanTermFrequencyType | repaymentEvery 
| repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | 
graceOnInterestPayment | interest free period | Payment strategy            |
+      | 
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_MIN_INT_3_MAX_INT_20
 | 01 April 2025     | 1000           | 12                     | 
DECLINING_BALANCE | DAILY                       | EQUAL_INSTALLMENTS | 4        
         | MONTHS                | 1              | MONTHS                 | 4  
                | 0                       | 0                      | 0          
          | ADVANCED_PAYMENT_ALLOCATION |
+    And Admin successfully approves the loan on "01 April 2025" with "1000" 
amount and expected disbursement date on "01 April 2025"
+    When Admin successfully disburse the loan on "01 April 2025" with "1000" 
EUR transaction amount
+    Then Loan Transactions tab has the following data:
+      | Transaction date | Transaction Type | Amount | Principal | Interest | 
Fees | Penalties | Loan Balance | Reverted | Replayed |
+      | 01 April 2025    | Disbursement     | 1000.0 | 0.0       | 0.0      | 
0.0  | 0.0       | 1000.0       | false    | false    |
+    Then Loan reschedule with the following data results a 400 error and 
"NEW_INTEREST_RATE_IS_1_BUT_MINIMUM_IS_3" error message
+      | rescheduleFromDate | submittedOnDate | adjustedDueDate | 
graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
+      | 02 May 2025        | 02 May 2025     |                 |               
   |                 |            | 1               |
+    Then Loan reschedule with the following data results a 400 error and 
"NEW_INTEREST_RATE_IS_45_BUT_MAXIMUM_IS_20" error message
+      | rescheduleFromDate | submittedOnDate | adjustedDueDate | 
graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
+      | 02 May 2025        | 02 May 2025     |                 |               
   |                 |            | 45              |
+    When Loan Pay-off is made on "01 April 2025"
+    Then Loan's all installments have obligations met
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index 696b6c07b3..447a525776 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -61,6 +61,7 @@ import 
org.apache.fineract.infrastructure.codes.domain.CodeValue;
 import 
org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.MathUtil;
 import 
org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator;
 import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
 import org.apache.fineract.organisation.monetary.domain.Money;
@@ -1352,7 +1353,10 @@ public class Loan extends 
AbstractAuditableWithUTCDateTimeCustom<Long> {
     }
 
     public boolean isInterestBearing() {
-        return 
BigDecimal.ZERO.compareTo(getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate())
 < 0;
+        return 
BigDecimal.ZERO.compareTo(getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate())
 < 0
+                || (isProgressiveSchedule() && 
!getLoanTermVariations().isEmpty()
+                        && loanTermVariations.stream().anyMatch(ltv -> 
ltv.getTermType().isInterestRateFromInstallment()
+                                && ltv.getTermValue() != null && 
MathUtil.isGreaterThanZero(ltv.getTermValue())));
     }
 
     public boolean isInterestBearingAndInterestRecalculationEnabled() {
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 28fe8ae5d4..5d706d3c14 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,7 @@ 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(fromJsonHelper, 
jsonElement, dataValidatorBuilder, loan);
         validateUnsupportedParams(jsonElement, dataValidatorBuilder);
 
         boolean hasInterestRateChange = interestRate != null;
@@ -199,11 +199,21 @@ public class 
ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
     }
 
     private BigDecimal validateInterestRate(final FromJsonHelper 
fromJsonHelper, final JsonElement jsonElement,
-            DataValidatorBuilder dataValidatorBuilder) {
+            DataValidatorBuilder dataValidatorBuilder, Loan loan) {
         final BigDecimal interestRate = fromJsonHelper
                 
.extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName,
 jsonElement);
-        
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.newInterestRateParamName).value(interestRate).ignoreIfNull()
-                .zeroOrPositiveAmount();
+        DataValidatorBuilder interestRateDataValidatorBuilder = 
dataValidatorBuilder.reset()
+                
.parameter(RescheduleLoansApiConstants.newInterestRateParamName).value(interestRate).ignoreIfNull().zeroOrPositiveAmount();
+
+        BigDecimal minNominalInterestRatePerPeriod = 
loan.getLoanProduct().getMinNominalInterestRatePerPeriod();
+        if (minNominalInterestRatePerPeriod != null) {
+            
interestRateDataValidatorBuilder.notLessThanMin(minNominalInterestRatePerPeriod);
+        }
+
+        BigDecimal maxNominalInterestRatePerPeriod = 
loan.getLoanProduct().getMaxNominalInterestRatePerPeriod();
+        if (maxNominalInterestRatePerPeriod != null) {
+            
interestRateDataValidatorBuilder.notGreaterThanMax(maxNominalInterestRatePerPeriod);
+        }
         return interestRate;
     }
 }

Reply via email to