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 5a1c6edbefc1760021ce13c2cfa1837fdd7c72df Author: mariiaKraievska <[email protected]> AuthorDate: Tue Oct 21 10:49:30 2025 +0300 FINERACT-2354: Third step - backdated re-age edge case for Interest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change --- .../test/stepdef/loan/LoanReAgingStepDef.java | 7 +- .../test/resources/features/LoanReAging.feature | 242 ++++++++++++++++++++- ...dvancedPaymentScheduleTransactionProcessor.java | 134 ++---------- .../portfolio/loanproduct/calc/EMICalculator.java | 11 +- .../loanproduct/calc/ProgressiveEMICalculator.java | 183 ++++++++++++++-- .../loanproduct/calc/data/InterestPeriod.java | 1 + .../service/reaging/LoanReAgingValidator.java | 1 + 7 files changed, 438 insertions(+), 141 deletions(-) diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java index 0a59b36672..45b783f575 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java @@ -233,8 +233,9 @@ public class LoanReAgingStepDef extends AbstractStepDef { eventAssertion.assertEventRaised(LoanReAgeEvent.class, loanId); } - @When("Admin fails to create a Loan re-aging transaction with error {string} and with the following data:") - public void adminFailsToCreateReAgingTransactionWithError(final String expectedError, final DataTable table) throws IOException { + @When("Admin fails to create a Loan re-aging transaction with status code {int} error {string} and with the following data:") + public void adminFailsToCreateReAgingTransactionWithError(final int statusCode, final String expectedError, final DataTable table) + throws IOException { final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); final long loanId = loanResponse.body().getLoanId(); @@ -259,7 +260,7 @@ public class LoanReAgingStepDef extends AbstractStepDef { assertThat(errorBody.string()).contains(expectedError); } - ErrorHelper.checkFailedApiCall(response, 403); + ErrorHelper.checkFailedApiCall(response, statusCode); } @Then("Admin fails to create a Loan re-aging transaction with the following data because loan was charged-off:") diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature index 2025154a77..1242c0feb9 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature @@ -3473,7 +3473,7 @@ Feature: LoanReAging | 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | | 01 January 2024 | Down Payment | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 750.0 | false | | 20 February 2024 | Repayment | 750.0 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | - When Admin fails to create a Loan re-aging transaction with error "error.msg.loan.reage.no.outstanding.balance.to.reage" and with the following data: + When Admin fails to create a Loan re-aging transaction with status code 403 error "error.msg.loan.reage.no.outstanding.balance.to.reage" and with the following data: | frequencyNumber | frequencyType | startDate | numberOfInstallments | | 1 | MONTHS | 01 March 2024 | 3 | Then Loan Repayment schedule has 5 periods, with the following data for periods: @@ -5354,6 +5354,242 @@ Feature: LoanReAging | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | | 15 March 2024 | Re-age | 84.28 | 83.57 | 0.71 | 0.0 | 0.0 | 0.0 | true | true | | 16 March 2024 | Repayment | 14.3 | 13.81 | 0.49 | 0.0 | 0.0 | 69.76 | false | true | - When Loan Pay-off is made on "01 April 2024" - Then Loan is closed with zero outstanding balance and it's all installments have obligations met \ No newline at end of file + Then Loan is closed with zero outstanding balance and it's all installments have obligations met + + @TestRailId:C4110 @AdvancedPaymentAllocation + Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - backdated re-aging transaction - UC16 + When Admin sets the business date to "01 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_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 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.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + 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.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + When Admin sets the business date to "01 February 2024" + And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | + When Admin sets the business date to "01 March 2024" + And Customer makes "AUTOPAY" repayment on "01 March 2024" with 17.01 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 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + 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.05 | 0.0 | 0.0 | 102.05 | 34.02 | 0.0 | 0.0 | 68.03 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | + | 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | + When Admin sets the business date to "01 June 2024" + # Backdated re-aging - created in June but effective from April 01 + When Admin creates a Loan re-aging transaction with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | MONTHS | 01 April 2024 | 6 | + Then Loan Repayment schedule has 8 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 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 3 | 31 | 01 April 2024 | | 56.0 | 11.05 | 0.39 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 | + | 4 | 30 | 01 May 2024 | | 44.95 | 11.05 | 0.39 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 | + | 5 | 31 | 01 June 2024 | | 33.9 | 11.05 | 0.39 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 | + | 6 | 30 | 01 July 2024 | | 22.66 | 11.24 | 0.2 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 | + | 7 | 31 | 01 August 2024 | | 11.35 | 11.31 | 0.13 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 | + | 8 | 31 | 01 September 2024| | 0.0 | 11.35 | 0.07 | 0.0 | 0.0 | 11.42 | 0.0 | 0.0 | 0.0 | 11.42 | + 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.64 | 0.0 | 0.0 | 102.64 | 34.02 | 0.0 | 0.0 | 68.62 | + Then 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 | + | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | + | 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false | + | 01 April 2024 | Re-age | 67.44 | 67.05 | 0.39 | 0.0 | 0.0 | 0.0 | false | true | + + @TestRailId:C4154 @AdvancedPaymentAllocation + Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - re-aging on same day as disbursement - UC16.1 + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + When Admin set "LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule + 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_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 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.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + 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.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + When Admin creates a Loan re-aging transaction with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | MONTHS | 01 January 2024 | 6 | + 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.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + 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.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + | 01 January 2024 | Re-age | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | + + @TestRailId:C4154 @AdvancedPaymentAllocation + Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - validation that reAgeStartDate must be after disbursement date - UC16.2 + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + When Admin set "LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule + 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_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 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.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + 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.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + When Admin sets the business date to "15 February 2024" + When Admin fails to create a Loan re-aging transaction with status code 400 error "validation.msg.validation.errors.exist" and with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | MONTHS | 31 December 2023 | 6 | + + @TestRailId:C4156 @AdvancedPaymentAllocation + Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - Verify backdated re-aging on the day of repayment with interest recalculation enabled - UC16.3 + When Admin sets the business date to "01 January 2024" + When Admin creates a client with random data + When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule + 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_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024" + When Admin successfully disburse the loan on "01 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.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + 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.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + When Admin sets the business date to "01 February 2024" + And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 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 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + 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.05 | 0.0 | 0.0 | 102.05 | 17.01 | 0.0 | 0.0 | 85.04 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | + When Admin sets the business date to "01 March 2024" + And Customer makes "AUTOPAY" repayment on "01 March 2024" with 17.01 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 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + 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.05 | 0.0 | 0.0 | 102.05 | 34.02 | 0.0 | 0.0 | 68.03 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | + | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | + | 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | + When Admin sets the business date to "01 June 2024" + When Admin creates a Loan re-aging transaction with the following data: + | frequencyNumber | frequencyType | startDate | numberOfInstallments | + | 1 | MONTHS | 01 February 2024 | 6 | + #Internal Server Error 500 is seen here, needs fixing + Then Loan Repayment schedule has 7 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 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 3 | 31 | 01 April 2024 | | 56.02 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | + | 4 | 30 | 01 May 2024 | | 44.99 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | + | 5 | 31 | 01 June 2024 | | 33.96 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | + | 6 | 30 | 01 July 2024 | | 22.93 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | + | 7 | 31 | 01 August 2024 | | 0.0 | 22.93 | 0.0 | 0.0 | 0.0 | 22.93 | 0.0 | 0.0 | 0.0 | 22.93 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100.0 | 1.07 | 0.0 | 0.0 | 101.07 | 34.02 | 0.0 | 0.0 | 67.05 | + Then 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 | + | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false | + | 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false | + | 01 March 2024 | Re-age | 67.05 | 67.05 | 0.0 | 0.0 | 0.0 | 0.0 | false | true | 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 bb12a9191e..4d38193361 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 @@ -1578,49 +1578,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments()); } - private List<RepaymentPeriod> findPossiblyOverdueRepaymentPeriods(LocalDate targetDate, ProgressiveLoanInterestScheduleModel model) { - return model.repaymentPeriods().stream() // - .filter(repaymentPeriod -> DateUtils.isAfter(targetDate, repaymentPeriod.getDueDate())).toList(); - } - - public boolean rework(LocalDate targetDate, ProgressiveTransactionCtx ctx) { - boolean hasChange = false; - ProgressiveLoanInterestScheduleModel model = ctx.getModel(); - List<RepaymentPeriod> overdueInstallmentsSortedByInstallmentNumber = findPossiblyOverdueRepaymentPeriods(targetDate, model); - if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) { - RepaymentPeriod lastPeriod = model.getLastRepaymentPeriod(); - RepaymentPeriod currentPeriod = model.findRepaymentPeriod(targetDate).orElse(lastPeriod); - MonetaryCurrency currency = model.zero().getCurrency(); - Money overDuePrincipal = Money.zero(currency); - Money aggregatedOverDuePrincipal = Money.zero(currency); - for (RepaymentPeriod processingPeriod : overdueInstallmentsSortedByInstallmentNumber) { - // add and subtract outstanding principal - if (!overDuePrincipal.isZero()) { - boolean currentChanges = adjustOverduePrincipal(targetDate, processingPeriod, overDuePrincipal, - aggregatedOverDuePrincipal, ctx); - - hasChange = hasChange || currentChanges; - } - - overDuePrincipal = processingPeriod.getOutstandingPrincipal(); - aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal); - } - - if (!currentPeriod.equals(lastPeriod) || !targetDate.isAfter(lastPeriod.getDueDate())) { - boolean currentChanges = adjustOverduePrincipal(targetDate, currentPeriod, overDuePrincipal, aggregatedOverDuePrincipal, - ctx); - hasChange = hasChange || currentChanges; - - } - if (aggregatedOverDuePrincipal.isGreaterThanZero() - && (model.lastOverdueBalanceChange() == null || model.lastOverdueBalanceChange().isBefore(targetDate))) { - model.lastOverdueBalanceChange(targetDate); - } - } - - return hasChange; - } - public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) { recalculateInterestForDate(targetDate, ctx, true); } @@ -1631,7 +1588,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep if (isInterestRecalculationSupported(ctx, loan) && !loan.isNpa() && !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) { - boolean modelHasUpdates = rework(targetDate, ctx); + boolean modelHasUpdates = emiCalculator.recalculateModelOverdueAmountsTillDate(ctx, targetDate); if (modelHasUpdates && updateInstallments) { updateInstallmentsPrincipalAndInterestByModel(ctx); } @@ -1639,38 +1596,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } - private boolean adjustOverduePrincipal(LocalDate currentDate, RepaymentPeriod currentInstallment, Money overduePrincipal, - Money aggregatedOverDuePrincipal, ProgressiveTransactionCtx ctx) { - - LocalDate fromDate = currentInstallment.getFromDate(); - LocalDate toDate = currentInstallment.getDueDate(); - ProgressiveLoanInterestScheduleModel model = ctx.getModel(); - boolean prepayAttempt = ctx.isPrepayAttempt(); - LoanInterestRecalculationDetails loanInterestRecalculationDetails = ctx.getInstallments().getFirst().getLoan() - .getLoanInterestRecalculationDetails(); - - if (!currentDate.equals(model.lastOverdueBalanceChange())) { - if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) { - emiCalculator.addBalanceCorrection(model, fromDate, overduePrincipal); - } else { - emiCalculator.addBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal); - } - - if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) { - LocalDate lastOverdueBalanceChange; - if (shouldRecalculateTillInstallmentDueDate(loanInterestRecalculationDetails, prepayAttempt)) { - lastOverdueBalanceChange = toDate; - } else { - lastOverdueBalanceChange = currentDate; - } - emiCalculator.addBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated()); - model.lastOverdueBalanceChange(lastOverdueBalanceChange); - } - return true; - } - return false; - } - private void updateInstallmentsPrincipalAndInterestByModel(ProgressiveTransactionCtx ctx) { ctx.getModel().repaymentPeriods().forEach(repayment -> { LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream() @@ -2991,9 +2916,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } } - private void cleanupInstallmentsAndRepaymentPeriodsAfterReAging(final ProgressiveTransactionCtx ctx) { + private void cleanupInstallmentsAfterReAging(final ProgressiveTransactionCtx ctx) { final List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments(); - final List<RepaymentPeriod> repaymentPeriods = ctx.getModel().repaymentPeriods(); // Find the last re-aged installment number final OptionalInt lastReAgedInstallmentNumberOpt = installments.stream().filter(LoanRepaymentScheduleInstallment::isReAged) @@ -3001,17 +2925,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep if (lastReAgedInstallmentNumberOpt.isPresent()) { final int lastReAgedInstallmentNumber = lastReAgedInstallmentNumberOpt.getAsInt(); - final LoanRepaymentScheduleInstallment lastReAgedInstallment = installments.stream() - .filter(i -> i.getInstallmentNumber().equals(lastReAgedInstallmentNumber)).findFirst().orElse(null); // Remove installments with numbers greater than the last re-aged installment final List<LoanRepaymentScheduleInstallment> installmentsToRemove = installments.stream().filter(i -> i != null && !i.isAdditional() && i.getInstallmentNumber() != null && i.getInstallmentNumber() > lastReAgedInstallmentNumber) .toList(); - if (lastReAgedInstallment != null) { - final List<RepaymentPeriod> repaymentPeriodsToRemove = repaymentPeriods.stream() - .filter(rp -> !rp.getFromDate().isBefore(lastReAgedInstallment.getDueDate())).toList(); - repaymentPeriodsToRemove.forEach(repaymentPeriods::remove); - } installmentsToRemove.forEach(installments::remove); } } @@ -3050,7 +2967,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } }); - cleanupInstallmentsAndRepaymentPeriodsAfterReAging(ctx); + cleanupInstallmentsAfterReAging(ctx); } private void reprocessInstallments(final List<LoanRepaymentScheduleInstallment> installments) { @@ -3307,47 +3224,29 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep final MonetaryCurrency currency = ctx.getCurrency(); final Loan loan = loanTransaction.getLoan(); final MathContext mc = MoneyHelper.getMathContext(); - final LocalDate transactionDate = loanTransaction.getTransactionDate(); final List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments(); - final List<RepaymentPeriod> repaymentPeriods = ctx.getModel().repaymentPeriods(); final LocalDate reAgingStartDate = loanTransaction.getLoanReAgeParameter().getStartDate(); - final List<RepaymentPeriod> periodsBeforeReAging = repaymentPeriods.stream() - .filter(rp -> rp.getFromDate().isBefore(reAgingStartDate) && !rp.isFullyPaid()).toList(); + final List<LoanRepaymentScheduleInstallment> installmentsBeforeReAging = installments.stream() + .filter(rp -> rp.getFromDate().isBefore(reAgingStartDate)).toList(); - final RepaymentPeriod lastPeriod = periodsBeforeReAging.getLast(); + // Define the date, which must match reAgingStartDate + final LocalDate expectedReAgingDate = installmentsBeforeReAging.isEmpty() ? installments.getFirst().getFromDate() + : installmentsBeforeReAging.getLast().getDueDate(); - if (!lastPeriod.getDueDate().isEqual(reAgingStartDate)) { + if (!expectedReAgingDate.isEqual(reAgingStartDate)) { // TODO: implement logic when re-aging changes the due dates throw new NotImplementedException("Logic when re-aging changes the due dates not implemented"); } - final BigDecimal interestBeforeReAging = emiCalculator - .getPeriodInterestTillDate(ctx.getModel(), lastPeriod.getDueDate(), transactionDate, false).getAmount(); - - final AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency)); - installments.forEach(i -> { - final Money principalOutstanding = i.getPrincipalOutstanding(currency); - if (principalOutstanding.isGreaterThanZero()) { - outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding)); - } - }); - - final AtomicReference<BigDecimal> interestFromZeroedInstallments = new AtomicReference<>(interestBeforeReAging); - - installments.stream() - .filter(installment -> !installment.isObligationsMet() && !installment.getDueDate().isAfter(lastPeriod.getFromDate())) - .forEach(installment -> { - final BigDecimal currentInterest = interestFromZeroedInstallments.get(); - final BigDecimal additionalInterest = MathUtil.nullToZero(installment.getInterestOutstanding(currency).getAmount() - .add(MathUtil.nullToZero(installment.getCreditedInterest()).negate())); - interestFromZeroedInstallments.set(currentInterest.add(additionalInterest)); - }); + final Money interestFromZeroedInstallments = emiCalculator.getOutstandingInterestTillDate(ctx.getModel(), + loanTransaction.getTransactionDate()); final BigDecimal interestRate = loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(); + final Money totalOutstandingPrincipal = ctx.getModel().getTotalOutstandingPrincipal(); final LoanApplicationTerms loanApplicationTerms = new LoanApplicationTerms.Builder().currency(currency.getCurrencyData()) - .repaymentsStartingFromDate(reAgingStartDate).principal(outstandingPrincipalBalance.get()) + .repaymentsStartingFromDate(reAgingStartDate).principal(totalOutstandingPrincipal) .loanTermFrequency(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()) .loanTermPeriodFrequencyType(loanTransaction.getLoanReAgeParameter().getFrequencyType()) .numberOfRepayments(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()) @@ -3366,11 +3265,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep .mc(mc).build(); // Update the existing model with re-aged periods - emiCalculator.updateModelRepaymentPeriodsDuringReAge(ctx.getModel(), loanTransaction, loanApplicationTerms, mc); + emiCalculator.updateModelRepaymentPeriodsDuringReAge(ctx, loanTransaction, loanApplicationTerms, mc); + updateInstallmentsByModelForReAging(loanTransaction, ctx); - loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(), - Money.of(currency, interestFromZeroedInstallments.get()), Money.zero(currency), Money.zero(currency)); + loanTransaction.updateComponentsAndTotal(totalOutstandingPrincipal, interestFromZeroedInstallments, Money.zero(currency), + Money.zero(currency)); reprocessInstallments(installments); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java index ed091679e9..de654f7c0b 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java @@ -28,6 +28,7 @@ import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails; @@ -143,6 +144,14 @@ public interface EMICalculator { */ void applyInterestPause(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate fromDate, LocalDate endDate); - void updateModelRepaymentPeriodsDuringReAge(ProgressiveLoanInterestScheduleModel scheduleModel, LoanTransaction loanTransaction, + void updateModelRepaymentPeriodsDuringReAge(ProgressiveTransactionCtx ctx, LoanTransaction loanTransaction, LoanApplicationTerms loanApplicationTerms, MathContext mc); + + boolean recalculateModelOverdueAmountsTillDate(ProgressiveTransactionCtx ctx, LocalDate targetDate); + + /** + * Gives back the sum of the outstanding interest from the whole model till the provided date. + */ + @NotNull + Money getOutstandingInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate tillDate); } 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 559b2d950a..ec558fc802 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 @@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; @@ -46,10 +47,13 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; @@ -571,20 +575,126 @@ public final class ProgressiveEMICalculator implements EMICalculator { } @Override - public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveLoanInterestScheduleModel scheduleModel, - final LoanTransaction loanTransaction, final LoanApplicationTerms loanApplicationTerms, final MathContext mc) { - final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter(); - final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate(); - final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods(); + public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveTransactionCtx ctx, final LoanTransaction loanTransaction, + final LoanApplicationTerms loanApplicationTerms, final MathContext mc) { + final List<RepaymentPeriod> existingRepaymentPeriods = ctx.getModel().repaymentPeriods(); - moveOutstandingAmountsFromPeriodsBeforeReAging(existingRepaymentPeriods, reAgingStartDate); + moveOutstandingAmountsFromPeriodsBeforeReAging(existingRepaymentPeriods, loanTransaction.getLoanReAgeParameter().getStartDate()); - final LocalDate periodStartDate = calculateFirstReAgedPeriodStartDate(loanReAgeParameter); + final LocalDate periodStartDate = calculateFirstReAgedPeriodStartDate(loanTransaction); final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryReAgedScheduleModel(loanApplicationTerms, mc, periodStartDate); - mergeNewInterestScheduleModelWithExistingOne(scheduleModel, temporaryReAgedScheduleModel, loanTransaction); + mergeNewInterestScheduleModelWithExistingOne(ctx, temporaryReAgedScheduleModel, loanTransaction); + } + + @Override + public boolean recalculateModelOverdueAmountsTillDate(final ProgressiveTransactionCtx ctx, final LocalDate targetDate) { + boolean hasChange = false; + final ProgressiveLoanInterestScheduleModel model = ctx.getModel(); + final List<RepaymentPeriod> overdueInstallmentsSortedByInstallmentNumber = findPossiblyOverdueRepaymentPeriods(targetDate, model); + if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) { + final RepaymentPeriod lastPeriod = model.getLastRepaymentPeriod(); + final RepaymentPeriod currentPeriod = model.findRepaymentPeriod(targetDate).orElse(lastPeriod); + final MonetaryCurrency currency = model.zero().getCurrency(); + Money overDuePrincipal = Money.zero(currency); + Money aggregatedOverDuePrincipal = Money.zero(currency); + for (RepaymentPeriod processingPeriod : overdueInstallmentsSortedByInstallmentNumber) { + // add and subtract outstanding principal + if (!overDuePrincipal.isZero()) { + final boolean currentChanges = adjustOverduePrincipal(targetDate, processingPeriod, overDuePrincipal, + aggregatedOverDuePrincipal, ctx); + + hasChange = hasChange || currentChanges; + } + + overDuePrincipal = processingPeriod.getOutstandingPrincipal(); + aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal); + } + + if (!currentPeriod.equals(lastPeriod) || !targetDate.isAfter(lastPeriod.getDueDate())) { + final boolean currentChanges = adjustOverduePrincipal(targetDate, currentPeriod, overDuePrincipal, + aggregatedOverDuePrincipal, ctx); + hasChange = hasChange || currentChanges; + + } + if (aggregatedOverDuePrincipal.isGreaterThanZero() + && (model.lastOverdueBalanceChange() == null || model.lastOverdueBalanceChange().isBefore(targetDate))) { + model.lastOverdueBalanceChange(targetDate); + } + } + + return hasChange; + } + + @Override + public Money getOutstandingInterestTillDate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate tillDate) { + final ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel, + tillDate); + return recalculatedScheduleModelTillDate.repaymentPeriods().stream() + .filter(repaymentPeriod -> repaymentPeriod.getFromDate().isBefore(tillDate)).map(RepaymentPeriod::getOutstandingInterest) + .reduce(scheduleModel.zero(), Money::add); + } + + private List<RepaymentPeriod> findPossiblyOverdueRepaymentPeriods(final LocalDate targetDate, + final ProgressiveLoanInterestScheduleModel model) { + return model.repaymentPeriods().stream() // + .filter(repaymentPeriod -> DateUtils.isAfter(targetDate, repaymentPeriod.getDueDate())).toList(); + } + + private boolean adjustOverduePrincipal(final LocalDate currentDate, final RepaymentPeriod currentInstallment, + final Money overduePrincipal, final Money aggregatedOverDuePrincipal, final ProgressiveTransactionCtx ctx) { + final LocalDate fromDate = currentInstallment.getFromDate(); + final LocalDate toDate = currentInstallment.getDueDate(); + final ProgressiveLoanInterestScheduleModel model = ctx.getModel(); + final boolean prepayAttempt = ctx.isPrepayAttempt(); + final LoanInterestRecalculationDetails loanInterestRecalculationDetails = ctx.getInstallments().getFirst().getLoan() + .getLoanInterestRecalculationDetails(); + + if (!currentDate.equals(model.lastOverdueBalanceChange())) { + if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) { + addBalanceCorrection(model, fromDate, overduePrincipal); + } else { + addBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal); + } + + if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) { + LocalDate lastOverdueBalanceChange; + if (shouldRecalculateTillInstallmentDueDate(loanInterestRecalculationDetails, prepayAttempt)) { + lastOverdueBalanceChange = toDate; + } else { + lastOverdueBalanceChange = currentDate; + } + addBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated()); + model.lastOverdueBalanceChange(lastOverdueBalanceChange); + } + return true; + } + return false; + } + + private boolean shouldRecalculateTillInstallmentDueDate(final LoanInterestRecalculationDetails recalculationDetails, + final boolean isPrepayAttempt) { + // Rest frequency type and pre close interest calculation strategy can be controversial + // if restFrequencyType == DAILY and preCloseInterestCalculationStrategy == TILL_PRE_CLOSURE_DATE + // no problem. Calculate till transaction date + // if restFrequencyType == SAME_AS_REPAYMENT_PERIOD and preCloseInterestCalculationStrategy == + // TILL_REST_FREQUENCY_DATE + // again, no problem. Calculate till due date of current installment + // if restFrequencyType == DAILY and preCloseInterestCalculationStrategy == TILL_REST_FREQUENCY_DATE + // or restFrequencyType == SAME_AS_REPAYMENT_PERIOD and preCloseInterestCalculationStrategy == + // TILL_PRE_CLOSURE_DATE + // we cannot harmonize the two configs. Behaviour should mimic prepay api. + return switch (recalculationDetails.getRestFrequencyType()) { + case DAILY -> + isPrepayAttempt && recalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled(); + case SAME_AS_REPAYMENT_PERIOD -> + recalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled(); + case WEEKLY -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WEEKLY"); + case MONTHLY -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: MONTHLY"); + case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID"); + }; } /** @@ -592,18 +702,23 @@ public final class ProgressiveEMICalculator implements EMICalculator { * the balances of the updated model and also recalculate the EMI if the EMI of the last repayment period differs * significantly from other periods. */ - private void mergeNewInterestScheduleModelWithExistingOne(final ProgressiveLoanInterestScheduleModel scheduleModel, + private void mergeNewInterestScheduleModelWithExistingOne(final ProgressiveTransactionCtx ctx, final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel, final LoanTransaction loanTransaction) { final List<RepaymentPeriod> newPeriods = temporaryReAgedScheduleModel.repaymentPeriods(); + final ProgressiveLoanInterestScheduleModel scheduleModel = ctx.getModel(); + final Loan loan = loanTransaction.getLoan(); if (newPeriods.isEmpty()) { return; } final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods(); + final RepaymentPeriod firstRepaymentPeriod = existingRepaymentPeriods.getFirst(); + final Money disbursedAmount = firstRepaymentPeriod.getTotalDisbursedAmount(); + final LocalDate reAgingStartDate = loanTransaction.getLoanReAgeParameter().getStartDate(); - final Optional<RepaymentPeriod> firstExistingRepaymentPeriodOpt = existingRepaymentPeriods.stream() + final Optional<RepaymentPeriod> firstExistingReAgedRepaymentPeriodOpt = existingRepaymentPeriods.stream() .filter(period -> period.getDueDate().equals(reAgingStartDate)).findFirst(); for (final RepaymentPeriod newPeriod : newPeriods) { @@ -611,8 +726,8 @@ public final class ProgressiveEMICalculator implements EMICalculator { period -> period.getFromDate().equals(newPeriod.getFromDate()) && period.getDueDate().equals(newPeriod.getDueDate())) .findFirst(); Optional<RepaymentPeriod> previousExistingRepaymentPeriodOpt = Optional.empty(); - if (existingRepaymentPeriodOpt.isPresent() && firstExistingRepaymentPeriodOpt.isPresent() - && existingRepaymentPeriodOpt.get().equals(firstExistingRepaymentPeriodOpt.get())) { + if (existingRepaymentPeriodOpt.isPresent() && firstExistingReAgedRepaymentPeriodOpt.isPresent() + && existingRepaymentPeriodOpt.get().equals(firstExistingReAgedRepaymentPeriodOpt.get())) { previousExistingRepaymentPeriodOpt = existingRepaymentPeriodOpt.get().getPrevious(); } @@ -623,25 +738,53 @@ public final class ProgressiveEMICalculator implements EMICalculator { previousExistingRepaymentPeriodOpt.orElseGet(existingRepaymentPeriods::getLast), newPeriod.getFromDate(), newPeriod.getDueDate(), newPrincipal.add(newInterest), MoneyHelper.getMathContext(), loanTransaction.getLoan().getLoanProductRelatedDetail()); - rp.setTotalDisbursedAmount(scheduleModel.repaymentPeriods().getFirst().getTotalDisbursedAmount()); + rp.setTotalDisbursedAmount(disbursedAmount); + + if (existingRepaymentPeriodOpt.isPresent()) { + // Add an interest period on the disbursement date for the very first replaced period + if (existingRepaymentPeriodOpt.get().equals(firstRepaymentPeriod)) { + final LocalDate disbursementDate = loanTransaction.getLoan().getDisbursementDate(); + rp.getInterestPeriods().getFirst().setDisbursementAmount(disbursedAmount); + rp.getInterestPeriods().getFirst().setFromDate(disbursementDate); + rp.getInterestPeriods().getFirst().setDueDate(disbursementDate); + rp.getInterestPeriods().add(InterestPeriod.withEmptyAmounts(rp, rp.getFromDate(), rp.getDueDate())); + } + existingRepaymentPeriods.remove(existingRepaymentPeriodOpt.get()); + } - existingRepaymentPeriodOpt.ifPresent(existingRepaymentPeriods::remove); existingRepaymentPeriods.add(rp); calculateRateFactorForRepaymentPeriod(rp, scheduleModel); } - final RepaymentPeriod lastReAgedInstallment = newPeriods.getLast(); + if (reAgingStartDate.isBefore(DateUtils.getBusinessLocalDate()) && isInterestRecalculationIsAllowed(ctx, loan)) { + scheduleModel.lastOverdueBalanceChange(null); + recalculateModelOverdueAmountsTillDate(ctx, loanTransaction.getSubmittedOnDate()); + } + final List<RepaymentPeriod> reAgedRepaymentPeriods = existingRepaymentPeriods.stream() .filter(repaymentPeriod -> (!repaymentPeriod.getFromDate().isBefore(reAgingStartDate) || repaymentPeriod.getDueDate().isEqual(reAgingStartDate)) - && !repaymentPeriod.getDueDate().isAfter(lastReAgedInstallment.getDueDate())) + && !repaymentPeriod.getDueDate().isAfter(newPeriods.getLast().getDueDate())) .toList(); + // cleanup repayment periods after re-aging + if (reAgedRepaymentPeriods.getLast() != null) { + final List<RepaymentPeriod> repaymentPeriodsToRemove = existingRepaymentPeriods.stream() + .filter(rp -> !rp.getFromDate().isBefore(reAgedRepaymentPeriods.getLast().getDueDate())).toList(); + repaymentPeriodsToRemove.forEach(existingRepaymentPeriods::remove); + } + calculateOutstandingBalance(scheduleModel); calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, loanTransaction.getTransactionDate()); checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, reAgedRepaymentPeriods); } + private boolean isInterestRecalculationIsAllowed(final ProgressiveTransactionCtx ctx, final Loan loan) { + return loan.isInterestBearingAndInterestRecalculationEnabled() && !ctx.isChargedOff() && !ctx.isWrittenOff() + && !ctx.isContractTerminated() && !loan.isNpa() + && !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue(); + } + /** * * Generates temporary interestScheduleModel with re-aged repayment periods */ @@ -663,8 +806,14 @@ public final class ProgressiveEMICalculator implements EMICalculator { * is used to generate re-aged repayment periods */ @NotNull - private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanReAgeParameter loanReAgeParameter) { + private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanTransaction loanTransaction) { + final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter(); final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate(); + + if (reAgingStartDate.isEqual(loanTransaction.getLoan().getDisbursementDate())) { + return reAgingStartDate; + } + return switch (loanReAgeParameter.getFrequencyType()) { case DAYS -> reAgingStartDate.minusDays(loanReAgeParameter.getFrequencyNumber()); case WEEKS -> reAgingStartDate.minusWeeks(loanReAgeParameter.getFrequencyNumber()); 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 740b2752b7..e143cc4b40 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 @@ -60,6 +60,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> { private Money creditedPrincipal; /** Stores credited interest. Related transaction: Chargeback */ private Money creditedInterest; + @Setter private Money disbursementAmount; private Money balanceCorrectionAmount; private Money outstandingLoanBalance; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java index a2ce20da23..7f57f8b5b4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java @@ -76,6 +76,7 @@ public class LoanReAgingValidator { LocalDate startDate = command.localDateValueOfParameterNamed(LoanReAgingApiConstants.startDate); if (loan.isProgressiveSchedule()) { + // validate re-age transaction occurs after or on the disbursement date baseDataValidator.reset().parameter(LoanReAgingApiConstants.startDate).value(startDate).notNull() .validateDateAfterOrEqual(loan.getDisbursementDate()); } else {
