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; } }
