From 3428e33035643fa5ed58169318678502ad6c3a57 Mon Sep 17 00:00:00 2001 From: "mark.vituska" Date: Thu, 24 Jul 2025 13:50:30 +0200 Subject: [PATCH] FINERACT-2326: Add the ability to reschedule non-interest-bearing progressive loans to have interest --- .../stepdef/loan/LoanRescheduleStepDef.java | 3 +- .../src/test/resources/features/Loan.feature | 8 +- .../test/resources/features/LoanCBR.feature | 2 +- .../resources/features/LoanReschedule.feature | 209 +++++++----------- ...edPaymentScheduleTransactionProcessor.java | 28 ++- ...iveLoanRescheduleRequestDataValidator.java | 13 +- ...talizedIncomeWritePlatformServiceImpl.java | 2 +- .../calc/ProgressiveEMICalculator.java | 10 +- .../loanproduct/calc/data/InterestPeriod.java | 2 +- .../calc/data/RepaymentPeriod.java | 19 +- ...ymentScheduleTransactionProcessorTest.java | 1 - .../LoanTransactionProcessingServiceImpl.java | 10 +- .../ReprocessLoanTransactionsServiceImpl.java | 26 ++- ...ntAllocationLoanRepaymentScheduleTest.java | 109 --------- 14 files changed, 171 insertions(+), 271 deletions(-) diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRescheduleStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRescheduleStepDef.java index 2f9f9a1d5..dc575b12c 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRescheduleStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanRescheduleStepDef.java @@ -75,8 +75,7 @@ public class LoanRescheduleStepDef extends AbstractStepDef { : Integer.valueOf(rescheduleData.get(4)); Integer extraTerms = (rescheduleData.get(5) == null || "0".equals(rescheduleData.get(5))) ? null : Integer.valueOf(rescheduleData.get(5)); - BigDecimal newInterestRate = (rescheduleData.get(6) == null || "0".equals(rescheduleData.get(6))) ? null - : new BigDecimal(rescheduleData.get(6)); + BigDecimal newInterestRate = (rescheduleData.get(6) == null) ? null : new BigDecimal(rescheduleData.get(6)); PostCreateRescheduleLoansRequest request = new PostCreateRescheduleLoansRequest()// .loanId(loanId)// diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature index caf0dcf34..a5edee50d 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature @@ -6708,8 +6708,8 @@ Feature: Loan | 2 | 15 | 29 March 2024 | 24 March 2024 | 243.79 | 121.89 | 0.0 | 0.0 | 0.0 | 121.89 | 121.89 | 121.89 | 0.0 | 0.0 | | | | 01 April 2024 | | 312.69 | | | 0.0 | | 0.0 | 0.0 | | | | | 3 | 0 | 01 April 2024 | 01 April 2024 | 478.31 | 78.17 | 0.0 | 0.0 | 0.0 | 78.17 | 78.17 | 0.0 | 0.0 | 0.0 | - | 4 | 15 | 13 April 2024 | | 239.16 | 239.15 | 0.0 | 0.0 | 0.0 | 239.15 | 121.89 | 121.89 | 0.0 | 117.26 | - | 5 | 15 | 28 April 2024 | | 0.0 | 239.16 | 0.0 | 0.0 | 0.0 | 239.16 | 121.9 | 121.9 | 0.0 | 117.26 | + | 4 | 15 | 13 April 2024 | | 239.15 | 239.16 | 0.0 | 0.0 | 0.0 | 239.16 | 121.89 | 121.89 | 0.0 | 117.27 | + | 5 | 15 | 28 April 2024 | | 0.0 | 239.15 | 0.0 | 0.0 | 0.0 | 239.15 | 121.9 | 121.9 | 0.0 | 117.25 | Then Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 800.27 | 0.0 | 0.0 | 0.0 | 800.27 | 565.75 | 365.68 | 0.0 | 234.52 | @@ -6733,8 +6733,8 @@ Feature: Loan | 2 | 15 | 29 March 2024 | 24 March 2024 | 243.79 | 121.89 | 0.0 | 0.0 | 0.0 | 121.89 | 121.89 | 121.89 | 0.0 | 0.0 | | | | 01 April 2024 | | 312.69 | | | 0.0 | | 0.0 | 0.0 | | | | | 3 | 0 | 01 April 2024 | 01 April 2024 | 478.31 | 78.17 | 0.0 | 0.0 | 0.0 | 78.17 | 78.17 | 0.0 | 0.0 | 0.0 | - | 4 | 15 | 13 April 2024 | 10 April 2024 | 239.16 | 239.15 | 0.0 | 0.0 | 0.0 | 239.15 | 239.15 | 239.15 | 0.0 | 0.0 | - | 5 | 15 | 28 April 2024 | 10 April 2024 | 0.0 | 239.16 | 0.0 | 0.0 | 0.0 | 239.16 | 239.16 | 239.16 | 0.0 | 0.0 | + | 4 | 15 | 13 April 2024 | 10 April 2024 | 239.15 | 239.16 | 0.0 | 0.0 | 0.0 | 239.16 | 239.16 | 239.16 | 0.0 | 0.0 | + | 5 | 15 | 28 April 2024 | 10 April 2024 | 0.0 | 239.15 | 0.0 | 0.0 | 0.0 | 239.15 | 239.15 | 239.15 | 0.0 | 0.0 | Then Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 800.27 | 0.0 | 0.0 | 0.0 | 800.27 | 800.27 | 600.2 | 0.0 | 0.0 | diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanCBR.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanCBR.feature index e3f83fc1d..c6b3ac24f 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanCBR.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanCBR.feature @@ -1788,4 +1788,4 @@ Feature: Credit Balance Refund | 25 March 2025 | Credit Balance Refund | 122.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | | 01 April 2025 | Disbursement | 243.79 | 0.0 | 0.0 | 0.0 | 0.0 | 243.79 | false | false | | 01 April 2025 | Down Payment | 61.0 | 61.0 | 0.0 | 0.0 | 0.0 | 182.79 | false | false | - Then Loan status will be "ACTIVE" \ No newline at end of file + Then Loan status will be "ACTIVE" diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature index 5b60de3e9..d2b9bf285 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature @@ -917,141 +917,92 @@ Feature: LoanReschedule And Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | - -@TestRailId:C3809 - Scenario: Verify change interest rate with 0->4 reschedule scenario - UC1 - When Admin sets the business date to "01 April 2025" + + @TestRailId:C3882 + Scenario: Reschedule progressive loan to 0 percent interest then back to 10 percent + When Admin sets the business date to "1 January 2024" 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 | + | 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 | 1 January 2024 | 100 | 10 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "1 January 2024" with "100" amount and expected disbursement date on "1 January 2024" + When Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount + Then Loan Repayment schedule has 6 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 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | | 83.67 | 16.33 | 0.83 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 | + | 2 | 29 | 01 March 2024 | | 67.21 | 16.46 | 0.7 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 | + | 3 | 31 | 01 April 2024 | | 50.61 | 16.6 | 0.56 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 | + | 4 | 30 | 01 May 2024 | | 33.87 | 16.74 | 0.42 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 | + | 5 | 31 | 01 June 2024 | | 16.99 | 16.88 | 0.28 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.99 | 0.14 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 | 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 | + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100.0 | 2.93 | 0.0 | 0.0 | 102.93 | 0.0 | 0.0 | 0.0 | 102.93 | + When Admin creates and approves Loan reschedule with the following data: + | rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate | + | 02 January 2024 | 01 January 2024 | | | | | 0 | + Then Loan Repayment schedule has 6 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 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | | 83.33 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | + | 2 | 29 | 01 March 2024 | | 66.66 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | + | 3 | 31 | 01 April 2024 | | 49.99 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | + | 4 | 30 | 01 May 2024 | | 33.32 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | + | 5 | 31 | 01 June 2024 | | 16.65 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.65 | 0.0 | 0.0 | 0.0 | 16.65 | 0.0 | 0.0 | 0.0 | 16.65 | 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 - # --- Loan reschedule: Interest rate modification effective from next day --- - #When Admin attempt Loan reschedule with changing interest rate to zero following data validation error should occur - Then Loan reschedule with the following data results a 400 error and "LOAN_RESCHEDULE_NOT_ALLOWED_FROM_ZERO_TO_NEW_INTEREST_RATE" error message - | rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate | - | 06 May 2025 | 05 May 2025 | | | | | 4 | - When Admin sets the business date to "06 May 2025" - And Customer makes "AUTOPAY" repayment on "06 May 2025" with 100.0 EUR transaction amount - # TODO uncomment and update repayment schedule after fixing PS-2665 -# 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 | 100.0 | 100.0 | 0.0 | 150.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 | 350.0 | 100.0 | 0.0 | 650.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 | - | 06 May 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 650.0 | false | false | - Then Loan status will be "ACTIVE" + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | + When Admin creates and approves Loan reschedule with the following data: + | rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate | + | 03 January 2024 | 01 January 2024 | | | | | 10 | + Then Loan Repayment schedule has 6 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 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | | 83.66 | 16.34 | 0.81 | 0.0 | 0.0 | 17.15 | 0.0 | 0.0 | 0.0 | 17.15 | + | 2 | 29 | 01 March 2024 | | 67.21 | 16.45 | 0.7 | 0.0 | 0.0 | 17.15 | 0.0 | 0.0 | 0.0 | 17.15 | + | 3 | 31 | 01 April 2024 | | 50.62 | 16.59 | 0.56 | 0.0 | 0.0 | 17.15 | 0.0 | 0.0 | 0.0 | 17.15 | + | 4 | 30 | 01 May 2024 | | 33.89 | 16.73 | 0.42 | 0.0 | 0.0 | 17.15 | 0.0 | 0.0 | 0.0 | 17.15 | + | 5 | 31 | 01 June 2024 | | 17.02 | 16.87 | 0.28 | 0.0 | 0.0 | 17.15 | 0.0 | 0.0 | 0.0 | 17.15 | + | 6 | 30 | 01 July 2024 | | 0.0 | 17.02 | 0.14 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100.0 | 2.91 | 0.0 | 0.0 | 102.91 | 0.0 | 0.0 | 0.0 | 102.91 | - @TestRailId:C3810 - Scenario: Verify change interest rate with 4->0 reschedule scenario - UC2 - When Admin sets the business date to "01 April 2025" + @TestRailId:C3883 + Scenario: Create progressive loan with 0 percent interest then to 10 percent after first period + When Admin sets the business date to "1 January 2024" 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 | 4 | 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 | | 751.24 | 248.76 | 3.33 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 | - | 2 | 31 | 01 June 2025 | | 501.65 | 249.59 | 2.5 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 | - | 3 | 30 | 01 July 2025 | | 251.23 | 250.42 | 1.67 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 | - | 4 | 31 | 01 August 2025 | | 0.0 | 251.23 | 0.84 | 0.0 | 0.0 | 252.07 | 0.0 | 0.0 | 0.0 | 252.07 | + | 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 | 1 January 2024 | 100 | 0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "1 January 2024" with "100" amount and expected disbursement date on "1 January 2024" + When Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount + Then Loan Repayment schedule has 6 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 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | | 83.33 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | + | 2 | 29 | 01 March 2024 | | 66.66 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | + | 3 | 31 | 01 April 2024 | | 49.99 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | + | 4 | 30 | 01 May 2024 | | 33.32 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | + | 5 | 31 | 01 June 2024 | | 16.65 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.65 | 0.0 | 0.0 | 0.0 | 16.65 | 0.0 | 0.0 | 0.0 | 16.65 | Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 1000.0 | 8.34 | 0.0 | 0.0 | 1008.34 | 0.0 | 0.0 | 0.0 | 1008.34 | - 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 252.09 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 | 751.24 | 248.76 | 3.33 | 0.0 | 0.0 | 252.09 | 252.09 | 0.0 | 0.0 | 0.0 | - | 2 | 31 | 01 June 2025 | | 501.65 | 249.59 | 2.5 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 | - | 3 | 30 | 01 July 2025 | | 251.23 | 250.42 | 1.67 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 | - | 4 | 31 | 01 August 2025 | | 0.0 | 251.23 | 0.84 | 0.0 | 0.0 | 252.07 | 0.0 | 0.0 | 0.0 | 252.07 | + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | + When Admin creates and approves Loan reschedule with the following data: + | rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate | + | 02 February 2024 | 01 January 2024 | | | | | 10 | + Then Loan Repayment schedule has 6 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 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | | 83.33 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | + | 2 | 29 | 01 March 2024 | | 66.94 | 16.39 | 0.69 | 0.0 | 0.0 | 17.08 | 0.0 | 0.0 | 0.0 | 17.08 | + | 3 | 31 | 01 April 2024 | | 50.42 | 16.52 | 0.56 | 0.0 | 0.0 | 17.08 | 0.0 | 0.0 | 0.0 | 17.08 | + | 4 | 30 | 01 May 2024 | | 33.76 | 16.66 | 0.42 | 0.0 | 0.0 | 17.08 | 0.0 | 0.0 | 0.0 | 17.08 | + | 5 | 31 | 01 June 2024 | | 16.96 | 16.8 | 0.28 | 0.0 | 0.0 | 17.08 | 0.0 | 0.0 | 0.0 | 17.08 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.96 | 0.14 | 0.0 | 0.0 | 17.1 | 0.0 | 0.0 | 0.0 | 17.1 | Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 1000.0 | 8.34 | 0.0 | 0.0 | 1008.34 | 252.09 | 0.0 | 0.0 | 756.25 | - 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 | - | 30 April 2025 | Accrual | 3.22 | 0.0 | 3.22 | 0.0 | 0.0 | 0.0 | false | false | - | 01 May 2025 | Repayment | 252.09 | 248.76 | 3.33 | 0.0 | 0.0 | 751.24 | false | false | - When Admin sets the business date to "05 May 2025" - And Admin runs inline COB job for Loan - # --- Loan reschedule: Interest rate modification effective from next day --- - #When Admin attempt Loan reschedule with changing interest rate from zero to 4 following data validation error should occur - Then Loan reschedule with the following data results a 400 error and "LOAN_RESCHEDULE_NOT_ALLOWED_FROM_CURRENT_INTEREST_RATE_TO_ZERO" error message - | rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate | - | 06 May 2025 | 05 May 2025 | | | | | 0 | - When Admin sets the business date to "06 May 2025" - And Customer makes "AUTOPAY" repayment on "06 May 2025" with 100.0 EUR transaction amount - # TODO uncomment and update repayment schedule after fixing PS-2634 -# 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 | 751.24 | 248.76 | 3.33 | 0.0 | 0.0 | 252.09 | 252.09 | 0.0 | 0.0 | 0.0 | -# | 2 | 31 | 01 June 2025 | | 501.37 | 249.87 | 2.22 | 0.0 | 0.0 | 252.09 | 100.0 | 100.0 | 0.0 | 152.09 | -# | 3 | 30 | 01 July 2025 | | 250.95 | 250.42 | 1.67 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 | -# | 4 | 31 | 01 August 2025 | | 0.0 | 250.95 | 0.84 | 0.0 | 0.0 | 251.79 | 0.0 | 0.0 | 0.0 | 251.79 | -# Then Loan Repayment schedule has the following data in Total row: -# | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | -# | 1000.0 | 8.06 | 0.0 | 0.0 | 1008.06 | 352.09 | 100.0 | 0.0 | 655.97 | - 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 | - | 30 April 2025 | Accrual | 3.22 | 0.0 | 3.22 | 0.0 | 0.0 | 0.0 | false | false | - | 01 May 2025 | Repayment | 252.09 | 248.76 | 3.33 | 0.0 | 0.0 | 751.24 | false | false | - | 01 May 2025 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | - | 02 May 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false | - | 03 May 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false | - | 04 May 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false | - | 06 May 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 651.24 | false | false | - Then Loan status will be "ACTIVE" \ No newline at end of file + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100.0 | 2.09 | 0.0 | 0.0 | 102.09 | 0.0 | 0.0 | 0.0 | 102.09 | diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index f6e934adb..88011da22 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -507,7 +507,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep MoneyHolder overpaymentHolder = ctx.getOverpaymentHolder(); if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx - && loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) { + && loanTransaction.getLoan().isInterestRecalculationEnabled()) { var model = progressiveTransactionCtx.getModel(); // Copy and paste Logic from super.handleCreditBalanceRefund @@ -778,7 +778,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) { - if (loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) { + if (loanTransaction.getLoan().isInterestRecalculationEnabled()) { processCreditTransactionWithEmiCalculator(loanTransaction, (ProgressiveTransactionCtx) ctx); } else if (hasNoCustomCreditAllocationRule(loanTransaction)) { super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments()); @@ -1224,8 +1224,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { // TODO: Fix this and enhance EMICalculator to support reamortization and reaging - final Loan loan = disbursementTransaction.getLoan(); - if (loan.isInterestBearing()) { + if (shouldUseEmiCalculation(transactionCtx, disbursementTransaction.getTransactionDate())) { handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx); } else { handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx); @@ -1356,7 +1355,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } private void handleCapitalizedIncome(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) { - if (capitalizedIncomeTransaction.getLoan().isInterestBearing()) { + // TODO: Fix this and enhance EMICalculator to support reamortization and reaging + if (shouldUseEmiCalculation(transactionCtx, capitalizedIncomeTransaction.getTransactionDate())) { handleCapitalizedIncomeWithEMICalculator(capitalizedIncomeTransaction, transactionCtx); } else { handleCapitalizedIncomeWithoutEMICalculator(capitalizedIncomeTransaction, transactionCtx); @@ -1476,6 +1476,24 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } + private boolean shouldUseEmiCalculation(TransactionCtx transactionCtx, LocalDate transactionDate) { + if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx) { + boolean hasActiveReAmortization = progressiveTransactionCtx.getAlreadyProcessedTransactions().stream() + .anyMatch(t -> t.getTypeOf().isReAmortize() && t.isNotReversed()); + boolean hasActiveReAge = progressiveTransactionCtx.getAlreadyProcessedTransactions().stream() + .anyMatch(t -> t.getTypeOf().isReAge() && t.isNotReversed()); + if (hasActiveReAmortization) { + return false; + } else { + return !hasActiveReAge || !DateUtils.isAfter(transactionDate, progressiveTransactionCtx.getModel().getMaturityDate()); + } + } + // From now on we are defaulting to using the EMICalculator on all progressive loans. However currently the + // model is not aware of re-aging and re-amortization. So only these specific cases should ignore this + // requirement. This method can be removed once these operations are supported by the EMI model. + return true; + } + protected void handleWriteOff(final LoanTransaction transaction, TransactionCtx ctx) { super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments()); } 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 b3804eb0c..a110056f9 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 @@ -24,7 +24,6 @@ import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.Loan import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateAndRetrieveRescheduleFromDate; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateApprovalDate; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateForOverdueCharges; -import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateInterestRate; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateLoanIsActive; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonComment; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonId; @@ -78,8 +77,7 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder); validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder); LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder); - BigDecimal interestRate = validateInterestRate(loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), fromJsonHelper, - jsonElement, dataValidatorBuilder); + BigDecimal interestRate = validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder); validateUnsupportedParams(jsonElement, dataValidatorBuilder); boolean hasInterestRateChange = interestRate != null; @@ -197,4 +195,13 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche "Interest rate change for the provided date is already exists.", rescheduleFromDate); } } + + private BigDecimal validateInterestRate(final FromJsonHelper fromJsonHelper, final JsonElement jsonElement, + DataValidatorBuilder dataValidatorBuilder) { + final BigDecimal interestRate = fromJsonHelper + .extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement); + dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.newInterestRateParamName).value(interestRate).ignoreIfNull() + .zeroOrPositiveAmount(); + return interestRate; + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java index 840fd690a..6a7e6d484 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java @@ -182,7 +182,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc } private void recalculateLoanTransactions(Loan loan, LocalDate transactionDate, LoanTransaction transaction) { - if (loan.isInterestBearingAndInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) { + if (loan.isInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) { reprocessLoanTransactionsService.reprocessTransactions(loan); } else { reprocessLoanTransactionsService.processLatestTransaction(transaction, loan); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index bdd6b7e67..45c82e2c1 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -161,6 +161,10 @@ public final class ProgressiveEMICalculator implements EMICalculator { } private void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) { + scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate())) + .forEach(rp -> rp.setTotalCapitalizedIncomeAmount( + MathUtil.nullToZero(rp.getTotalCapitalizedIncomeAmount()).add(operation.getAmount().getAmount()))); + scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), scheduleModel.zero(), scheduleModel.zero(), operation.getAmount()).ifPresent((repaymentPeriod) -> { calculateEMIValueAndRateFactors( @@ -920,9 +924,11 @@ public final class ProgressiveEMICalculator implements EMICalculator { // already repaid principals should be subtracted from total disbursed amount to calculate correct EMI. BigDecimal alreadyRepaidPrincipals = first.getPrevious() - .map(rp -> rp.calculateTotalDisbursedAmountTillGivenPeriod(null).subtract(rp.getOutstandingLoanBalance().getAmount())) + .map(rp -> rp.calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(null) + .subtract(rp.getOutstandingLoanBalance().getAmount())) .orElse(BigDecimal.ZERO); - Money total = Money.of(currency, first.calculateTotalDisbursedAmountTillGivenPeriod(first.getLastInterestPeriod())) + Money total = Money + .of(currency, first.calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(first.getLastInterestPeriod())) .plus(sumOfInterest).minus(alreadyRepaidPrincipals); Money periodEmi = total.dividedBy(repaymentPeriods.size(), mc); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java index 4b1be251c..68989dece 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java @@ -150,7 +150,7 @@ public class InterestPeriod implements Comparable { return BigDecimal.ZERO; } BigDecimal baseAmount = switch (method) { - case FLAT -> MathUtil.nullToZero(getRepaymentPeriod().calculateTotalDisbursedAmountTillGivenPeriod(this)); + case FLAT -> MathUtil.nullToZero(getRepaymentPeriod().calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(this)); case DECLINING_BALANCE -> getOutstandingLoanBalance().getAmount(); default -> throw new UnsupportedOperationException("Method not implemented: " + method); }; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java index 0778e436e..623b4b6e7 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java @@ -84,6 +84,10 @@ public final class RepaymentPeriod { @Getter private BigDecimal totalDisbursedAmount; + @Setter + @Getter + private BigDecimal totalCapitalizedIncomeAmount; + @JsonExclude @Getter private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail; @@ -389,16 +393,21 @@ public final class RepaymentPeriod { /** * @param tillPeriod * can be null. if null it calculates total disbursement including last interest period. - * @return disbursed amount til interest period. + * @return disbursed and capitalized income amount till interest period. */ - public BigDecimal calculateTotalDisbursedAmountTillGivenPeriod(InterestPeriod tillPeriod) { - BigDecimal res = MathUtil.nullToZero(getTotalDisbursedAmount()); + public BigDecimal calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(InterestPeriod tillPeriod) { + BigDecimal res = MathUtil.nullToZero(getTotalDisbursedAmount()).add(MathUtil.nullToZero(getTotalCapitalizedIncomeAmount())); for (InterestPeriod interestPeriod : this.interestPeriods) { if (interestPeriod.equals(tillPeriod)) { break; } - if (!interestPeriod.getDueDate().equals(getFromDate()) && interestPeriod.getDisbursementAmount() != null) { - res = res.add(interestPeriod.getDisbursementAmount().getAmount()); + if (!interestPeriod.getDueDate().equals(getFromDate())) { + if (interestPeriod.getDisbursementAmount() != null) { + res = res.add(interestPeriod.getDisbursementAmount().getAmount()); + } + if (interestPeriod.getCapitalizedIncomePrincipal() != null) { + res = res.add(interestPeriod.getCapitalizedIncomePrincipal().getAmount()); + } } } return res; diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java index 744298b9d..534fbd4da 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java @@ -535,7 +535,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest { Loan loan = mock(Loan.class); when(loan.getLoanRepaymentScheduleDetail()).thenReturn(loanProductRelatedDetail); - when(loan.isInterestBearing()).thenReturn(true); LoanRepaymentScheduleInstallment installment1 = spy( new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(200.0), diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java index e7aa89385..af73ff978 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java @@ -95,16 +95,18 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc AdvancedPaymentScheduleTransactionProcessor advancedProcessor, Loan loan, LoanTransaction loanTransaction) { Optional savedModel = modelRepository.getSavedModel(loan, loanTransaction.getTransactionDate()); + ProgressiveLoanInterestScheduleModel model = savedModel + .orElseGet(() -> advancedProcessor.calculateInterestScheduleModel(loan.getId(), loanTransaction.getTransactionDate())); ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), - new ChangedTransactionDetail(), savedModel.orElse(null), getTotalRefundInterestAmount(loan)); + new ChangedTransactionDetail(), model, getTotalRefundInterestAmount(loan)); progressiveContext.getAlreadyProcessedTransactions().addAll(loanTransactionService.retrieveListOfTransactionsForReprocessing(loan)); progressiveContext.setChargedOff(loan.isChargedOff()); progressiveContext.setContractTerminated(loan.isContractTermination()); ChangedTransactionDetail result = advancedProcessor.processLatestTransaction(loanTransaction, progressiveContext); - if (savedModel.isPresent() && !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { - modelRepository.writeInterestScheduleModel(loan, savedModel.get()); + if (!TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + modelRepository.writeInterestScheduleModel(loan, model); } return result; } @@ -124,7 +126,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( transactionProcessingStrategyCode); if (loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor advancedProcessor - && loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) { + && loanTransaction.getLoan().isInterestRecalculationEnabled()) { return processLatestTransactionProgressiveInterestRecalculation(advancedProcessor, loanTransaction.getLoan(), loanTransaction); } return loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, ctx); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java index acf391aab..1043fc284 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java @@ -36,8 +36,11 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.springframework.stereotype.Service; @@ -145,10 +148,25 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa @Override public void processLatestTransaction(final LoanTransaction loanTransaction, final Loan loan) { - final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService.processLatestTransaction( - loan.getTransactionProcessingStrategyCode(), loanTransaction, - new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), - new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail())); + LoanRepaymentScheduleTransactionProcessor transactionProcessor = loanTransactionProcessingService + .getTransactionProcessor(loan.getTransactionProcessingStrategyCode()); + + TransactionCtx transactionCtx; + if (transactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor advancedProcessor) { + Optional savedModel = interestScheduleModelRepositoryWrapper.getSavedModel(loan, + loanTransaction.getTransactionDate()); + ProgressiveLoanInterestScheduleModel model = savedModel + .orElseGet(() -> advancedProcessor.calculateInterestScheduleModel(loan.getId(), loanTransaction.getTransactionDate())); + + transactionCtx = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), + loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), model); + } else { + transactionCtx = new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), + new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail()); + } + + final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService + .processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction, transactionCtx); final List newTransactions = changedTransactionDetail.getTransactionChanges().stream() .map(TransactionChangeData::getNewTransaction).peek(transaction -> transaction.updateLoan(loan)).toList(); loan.getLoanTransactions().addAll(newTransactions); 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 c0e4278b4..2bd83a92f 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,115 +6000,6 @@ 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 ---------------------------------------");