From 5649aa3dedd1fd6523a4d133737e549bb1e75d1c Mon Sep 17 00:00:00 2001 From: "mark.vituska" Date: Thu, 5 Jun 2025 11:43:24 +0200 Subject: [PATCH] FINERACT-2281: fix unrecognized interest handling when first period is unpaid --- .../features/LoanAccrualActivity.feature | 240 +++++++++++++++++- .../calc/ProgressiveEMICalculator.java | 151 ++++++++--- .../loanproduct/calc/data/InterestPeriod.java | 2 +- .../calc/data/RepaymentPeriod.java | 53 ++-- 4 files changed, 373 insertions(+), 73 deletions(-) diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature index d766312ac..f5a0af563 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature @@ -6195,7 +6195,7 @@ Feature: LoanAccrualActivity Then Loan Repayment schedule has 12 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 | | | | 21 March 2025 | | 242.46 | | | 0.0 | | 0.0 | 0.0 | | | | - | 1 | 31 | 21 April 2025 | | 218.82 | 123.64 | 0.77 | 0.0 | 0.0 | 124.41 | 83.64 | 83.64 | 0.0 | 40.77 | + | 1 | 31 | 21 April 2025 | | 218.82 | 123.64 | 0.84 | 0.0 | 0.0 | 124.48 | 83.64 | 83.64 | 0.0 | 40.84 | | 2 | 30 | 21 May 2025 | 21 March 2025 | 195.18 | 23.64 | 0.0 | 0.0 | 0.0 | 23.64 | 23.64 | 23.64 | 0.0 | 0.0 | | 3 | 31 | 21 June 2025 | 21 March 2025 | 171.54 | 23.64 | 0.0 | 0.0 | 0.0 | 23.64 | 23.64 | 23.64 | 0.0 | 0.0 | | 4 | 30 | 21 July 2025 | 21 March 2025 | 147.9 | 23.64 | 0.0 | 0.0 | 0.0 | 23.64 | 23.64 | 23.64 | 0.0 | 0.0 | @@ -6209,7 +6209,7 @@ Feature: LoanAccrualActivity | 12 | 28 | 21 March 2026 | 21 March 2025 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | And Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 342.46 | 0.77 | 0.0 | 0.0 | 343.23 | 302.46 | 302.46 | 0.0 | 40.77 | + | 342.46 | 0.84 | 0.0 | 0.0 | 343.3 | 302.46 | 302.46 | 0.0 | 40.84 | And Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 21 March 2025 | Disbursement | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | 242.46 | false | false | @@ -6759,11 +6759,11 @@ Feature: LoanAccrualActivity When Admin sets the business date to "29 April 2025" And Admin runs inline COB job for Loan And Customer makes "AUTOPAY" repayment on "29 April 2025" with 100 EUR transaction amount - Then Loan has 1.93 outstanding amount + Then Loan has 2.6 outstanding amount Then Loan Repayment schedule has 12 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 | | | | 21 March 2025 | | 242.46 | | | 0.0 | | 0.0 | 0.0 | | | | - | 1 | 31 | 21 April 2025 | | 222.26 | 120.2 | 1.93 | 0.0 | 0.0 | 122.13 |120.2 | 20.2 | 100.0| 1.93 | + | 1 | 31 | 21 April 2025 | | 222.26 | 120.2 | 2.6 | 0.0 | 0.0 | 122.8 |120.2 | 20.2 | 100.0| 2.6 | | 2 | 30 | 21 May 2025 | 21 March 2025 | 202.06 | 20.2 | 0.0 | 0.0 | 0.0 | 20.2 | 20.2 | 20.2 | 0.0 | 0.0 | | 3 | 31 | 21 June 2025 | 21 March 2025 | 181.86 | 20.2 | 0.0 | 0.0 | 0.0 | 20.2 | 20.2 | 20.2 | 0.0 | 0.0 | | 4 | 30 | 21 July 2025 | 21 March 2025 | 161.66 | 20.2 | 0.0 | 0.0 | 0.0 | 20.2 | 20.2 | 20.2 | 0.0 | 0.0 | @@ -6777,7 +6777,7 @@ Feature: LoanAccrualActivity | 12 | 28 | 21 March 2026 | 21 March 2025 | 0.0 | 20.26 | 0.0 | 0.0 | 0.0 | 20.26 | 20.26 | 20.26 | 0.0 | 0.0 | And Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 342.46 | 1.93 | 0.0 | 0.0 | 344.39 | 342.46 | 242.46 | 100.0 | 1.93 | + | 342.46 | 2.6 | 0.0 | 0.0 | 345.06 | 342.46 | 242.46 | 100.0 | 2.6 | And Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 21 March 2025 | Disbursement | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | 242.46 | false | false | @@ -6798,13 +6798,13 @@ Feature: LoanAccrualActivity Then Credit Balance Refund transaction on active loan "29 April 2025" with 100 EUR transaction amount will result an error When Admin sets the business date to "06 May 2025" And Admin runs inline COB job for Loan - And Customer makes "AUTOPAY" repayment on "06 May 2025" with 1.93 EUR transaction amount + And Customer makes "AUTOPAY" repayment on "06 May 2025" with 2.6 EUR transaction amount Then Loan status will be "CLOSED_OBLIGATIONS_MET" Then Loan has 0 outstanding amount Then Loan Repayment schedule has 12 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 | | | | 21 March 2025 | | 242.46 | | | 0.0 | | 0.0 | 0.0 | | | | - | 1 | 31 | 21 April 2025 | 06 May 2025 | 222.26 | 120.2 | 1.93 | 0.0 | 0.0 | 122.13 |122.13 | 20.2 | 101.93 | 0.0 | + | 1 | 31 | 21 April 2025 | 06 May 2025 | 222.26 | 120.2 | 2.6 | 0.0 | 0.0 | 122.8 |122.8 | 20.2 | 102.6 | 0.0 | | 2 | 30 | 21 May 2025 | 21 March 2025 | 202.06 | 20.2 | 0.0 | 0.0 | 0.0 | 20.2 | 20.2 | 20.2 | 0.0 | 0.0 | | 3 | 31 | 21 June 2025 | 21 March 2025 | 181.86 | 20.2 | 0.0 | 0.0 | 0.0 | 20.2 | 20.2 | 20.2 | 0.0 | 0.0 | | 4 | 30 | 21 July 2025 | 21 March 2025 | 161.66 | 20.2 | 0.0 | 0.0 | 0.0 | 20.2 | 20.2 | 20.2 | 0.0 | 0.0 | @@ -6818,7 +6818,7 @@ Feature: LoanAccrualActivity | 12 | 28 | 21 March 2026 | 21 March 2025 | 0.0 | 20.26 | 0.0 | 0.0 | 0.0 | 20.26 | 20.26 | 20.26 | 0.0 | 0.0 | And Loan Repayment schedule has the following data in Total row: | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 342.46 | 1.93 | 0.0 | 0.0 | 344.39 | 344.39 | 242.46 | 101.93 | 0.0 | + | 342.46 | 2.6 | 0.0 | 0.0 | 345.06 | 345.06 | 242.46 | 102.6 | 0.0 | And Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 21 March 2025 | Disbursement | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | 242.46 | false | false | @@ -6836,8 +6836,7 @@ Feature: LoanAccrualActivity | 28 April 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false | | 29 April 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | | 29 April 2025 | Accrual | 0.09 | 0.0 | 0.09 | 0.0 | 0.0 | 0.0 | false | false | - | 06 May 2025 | Repayment | 1.93 | 0.0 | 1.93 | 0.0 | 0.0 | 0.0 | false | false | - | 06 May 2025 | Accrual Adjustment | 0.67 | 0.0 | 0.67 | 0.0 | 0.0 | 0.0 | false | false | + | 06 May 2025 | Repayment | 2.6 | 0.0 | 2.6 | 0.0 | 0.0 | 0.0 | false | false | # - CBR on closed loan is forbidden - # Then Credit Balance Refund transaction on active loan "06 May 2025" with 100 EUR transaction amount will result an error @@ -7157,4 +7156,223 @@ Feature: LoanAccrualActivity | 07 May 2025 | Accrual | 0.05 | 0.0 | 0.05 | 0.0 | 0.0 | 0.0 | false | false | | 07 May 2025 | Accrual Activity | 1.18 | 0.0 | 1.18 | 0.0 | 0.0 | 0.0 | false | false | # - CBR on closed loan is forbidden - # - Then Credit Balance Refund transaction on active loan "07 May 2025" with 72.35 EUR transaction amount will result an error \ No newline at end of file + Then Credit Balance Refund transaction on active loan "07 May 2025" with 72.35 EUR transaction amount will result an error + + @TestRailId:C3736 + Scenario: Verify that interest is calculated after last unpaid period in case of reversed repayment made before MIR and CBR for progressive loan with downpayment + When Admin sets the business date to "21 March 2025" + And Admin creates a client with random data + And 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_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE_DOWNPAYMENT | 21 March 2025 | 242.46 | 29.99 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 12 | MONTHS | 1 | MONTHS | 12 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "21 March 2025" with "242.46" amount and expected disbursement date on "21 March 2025" + And Admin successfully disburse the loan on "21 March 2025" with "242.46" EUR transaction amount + Then Loan Repayment schedule has 13 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 | + | | | 21 March 2025 | | 242.46 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 0 | 21 March 2025 | | 181.84 | 60.62 | 0.0 | 0.0 | 0.0 | 60.62 | 0.0 | 0.0 | 0.0 | 60.62 | + | 2 | 31 | 21 April 2025 | | 168.65 | 13.19 | 4.54 | 0.0 | 0.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | + | 3 | 30 | 21 May 2025 | | 155.13 | 13.52 | 4.21 | 0.0 | 0.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | + | 4 | 31 | 21 June 2025 | | 141.28 | 13.85 | 3.88 | 0.0 | 0.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | + | 5 | 30 | 21 July 2025 | | 127.08 | 14.2 | 3.53 | 0.0 | 0.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | + | 6 | 31 | 21 August 2025 | | 112.53 | 14.55 | 3.18 | 0.0 | 0.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | + | 7 | 31 | 21 September 2025 | | 97.61 | 14.92 | 2.81 | 0.0 | 0.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | + | 8 | 30 | 21 October 2025 | | 82.32 | 15.29 | 2.44 | 0.0 | 0.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | + | 9 | 31 | 21 November 2025 | | 66.65 | 15.67 | 2.06 | 0.0 | 0.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | + | 10 | 30 | 21 December 2025 | | 50.59 | 16.06 | 1.67 | 0.0 | 0.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | + | 11 | 31 | 21 January 2026 | | 34.12 | 16.47 | 1.26 | 0.0 | 0.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | + | 12 | 31 | 21 February 2026 | | 17.24 | 16.88 | 0.85 | 0.0 | 0.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | + | 13 | 28 | 21 March 2026 | | 0.0 | 17.24 | 0.43 | 0.0 | 0.0 | 17.67 | 0.0 | 0.0 | 0.0 | 17.67 | + And Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 242.46 | 30.86 | 0.0 | 0.0 | 273.32 | 0.0 | 0.0 | 0.0 | 273.32 | + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 21 March 2025 | Disbursement | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | 242.46 | false | false | + And Customer makes "AUTOPAY" repayment on "21 March 2025" with 100 EUR transaction amount + And Customer makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "21 March 2025" with 242.46 EUR transaction amount and system-generated Idempotency key + Then Loan status will be "OVERPAID" + Then Loan has 100 overpaid amount + Then Loan Repayment schedule has 13 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 | + | | | 21 March 2025 | | 242.46 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 0 | 21 March 2025 | 21 March 2025 | 181.84 | 60.62 | 0.0 | 0.0 | 0.0 | 60.62 | 60.62 | 0.0 | 0.0 | 0.0 | + | 2 | 31 | 21 April 2025 | 21 March 2025 | 164.11 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 3 | 30 | 21 May 2025 | 21 March 2025 | 146.38 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 4 | 31 | 21 June 2025 | 21 March 2025 | 128.65 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 5 | 30 | 21 July 2025 | 21 March 2025 | 110.92 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 6 | 31 | 21 August 2025 | 21 March 2025 | 93.19 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 7 | 31 | 21 September 2025 | 21 March 2025 | 75.46 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 8 | 30 | 21 October 2025 | 21 March 2025 | 57.73 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 9 | 31 | 21 November 2025 | 21 March 2025 | 40.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 10 | 30 | 21 December 2025 | 21 March 2025 | 22.27 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 11 | 31 | 21 January 2026 | 21 March 2025 | 4.54 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 12 | 31 | 21 February 2026 | 21 March 2025 | 0.0 | 4.54 | 0.0 | 0.0 | 0.0 | 4.54 | 4.54 | 4.54 | 0.0 | 0.0 | + | 13 | 28 | 21 March 2026 | 21 March 2025 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + And Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 242.46 | 0.0 | 0.0 | 0.0 | 242.46 | 242.46 | 181.84 | 0.0 | 0.0 | + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 21 March 2025 | Disbursement | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | 242.46 | false | false | + | 21 March 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 142.46 | false | false | + | 21 March 2025 | Merchant Issued Refund | 242.46 | 142.46 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + When Admin sets the business date to "28 March 2025" + And Admin runs inline COB job for Loan + When Admin makes Credit Balance Refund transaction on "28 March 2025" with 100 EUR transaction amount + Then Loan status will be "CLOSED_OBLIGATIONS_MET" + Then Loan has 0 outstanding amount + Then Loan Repayment schedule has 13 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 | + | | | 21 March 2025 | | 242.46 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 0 | 21 March 2025 | 21 March 2025 | 181.84 | 60.62 | 0.0 | 0.0 | 0.0 | 60.62 | 60.62 | 0.0 | 0.0 | 0.0 | + | 2 | 31 | 21 April 2025 | 21 March 2025 | 164.11 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 3 | 30 | 21 May 2025 | 21 March 2025 | 146.38 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 4 | 31 | 21 June 2025 | 21 March 2025 | 128.65 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 5 | 30 | 21 July 2025 | 21 March 2025 | 110.92 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 6 | 31 | 21 August 2025 | 21 March 2025 | 93.19 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 7 | 31 | 21 September 2025 | 21 March 2025 | 75.46 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 8 | 30 | 21 October 2025 | 21 March 2025 | 57.73 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 9 | 31 | 21 November 2025 | 21 March 2025 | 40.0 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 10 | 30 | 21 December 2025 | 21 March 2025 | 22.27 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 11 | 31 | 21 January 2026 | 21 March 2025 | 4.54 | 17.73 | 0.0 | 0.0 | 0.0 | 17.73 | 17.73 | 17.73 | 0.0 | 0.0 | + | 12 | 31 | 21 February 2026 | 21 March 2025 | 0.0 | 4.54 | 0.0 | 0.0 | 0.0 | 4.54 | 4.54 | 4.54 | 0.0 | 0.0 | + | 13 | 28 | 21 March 2026 | 21 March 2025 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | + And Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 242.46 | 0.0 | 0.0 | 0.0 | 242.46 | 242.46 | 181.84 | 0.0 | 0.0 | + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 21 March 2025 | Disbursement | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | 242.46 | false | false | + | 21 March 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 142.46 | false | false | + | 21 March 2025 | Merchant Issued Refund | 242.46 | 142.46 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 28 March 2025 | Credit Balance Refund | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + When Admin sets the business date to "02 April 2025" + And Admin runs inline COB job for Loan + When Customer undo "1"th "Repayment" transaction made on "21 March 2025" + Then Loan Repayment schedule has 13 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 | + | | | 21 March 2025 | | 242.46 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 0 | 21 March 2025 | 21 March 2025 | 181.84 | 60.62 | 0.0 | 0.0 | 0.0 | 60.62 | 60.62 | 0.0 | 0.0 | 0.0 | + | 2 | 31 | 21 April 2025 | | 166.69 | 115.15 | 1.93 | 0.0 | 0.0 | 117.08 | 15.15 | 15.15 | 0.0 | 101.93 | + | 3 | 30 | 21 May 2025 | 21 March 2025 | 151.54 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 4 | 31 | 21 June 2025 | 21 March 2025 | 136.39 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 5 | 30 | 21 July 2025 | 21 March 2025 | 121.24 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 6 | 31 | 21 August 2025 | 21 March 2025 | 106.09 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 7 | 31 | 21 September 2025 | 21 March 2025 | 90.94 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 8 | 30 | 21 October 2025 | 21 March 2025 | 75.79 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 9 | 31 | 21 November 2025 | 21 March 2025 | 60.64 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 10 | 30 | 21 December 2025 | 21 March 2025 | 45.49 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 11 | 31 | 21 January 2026 | 21 March 2025 | 30.34 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 12 | 31 | 21 February 2026 | 21 March 2025 | 15.19 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 13 | 28 | 21 March 2026 | 21 March 2025 | 0.0 | 15.19 | 0.0 | 0.0 | 0.0 | 15.19 | 15.19 | 15.19 | 0.0 | 0.0 | + And Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 342.46 | 1.93 | 0.0 | 0.0 | 344.39 | 242.46 | 181.84 | 0.0 | 101.93 | + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 21 March 2025 | Disbursement | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | 242.46 | false | false | + | 21 March 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 142.46 | true | false | + | 21 March 2025 | Merchant Issued Refund | 242.46 | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | false | true | + | 28 March 2025 | Credit Balance Refund | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | true | + When Admin sets the business date to "21 April 2025" + And Admin runs inline COB job for Loan + Then Loan Repayment schedule has 13 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 | + | | | 21 March 2025 | | 242.46 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 0 | 21 March 2025 | 21 March 2025 | 181.84 | 60.62 | 0.0 | 0.0 | 0.0 | 60.62 | 60.62 | 0.0 | 0.0 | 0.0 | + | 2 | 31 | 21 April 2025 | | 166.69 | 115.15 | 1.93 | 0.0 | 0.0 | 117.08 | 15.15 | 15.15 | 0.0 | 101.93 | + | 3 | 30 | 21 May 2025 | 21 March 2025 | 151.54 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 4 | 31 | 21 June 2025 | 21 March 2025 | 136.39 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 5 | 30 | 21 July 2025 | 21 March 2025 | 121.24 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 6 | 31 | 21 August 2025 | 21 March 2025 | 106.09 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 7 | 31 | 21 September 2025 | 21 March 2025 | 90.94 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 8 | 30 | 21 October 2025 | 21 March 2025 | 75.79 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 9 | 31 | 21 November 2025 | 21 March 2025 | 60.64 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 10 | 30 | 21 December 2025 | 21 March 2025 | 45.49 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 11 | 31 | 21 January 2026 | 21 March 2025 | 30.34 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 12 | 31 | 21 February 2026 | 21 March 2025 | 15.19 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 13 | 28 | 21 March 2026 | 21 March 2025 | 0.0 | 15.19 | 0.0 | 0.0 | 0.0 | 15.19 | 15.19 | 15.19 | 0.0 | 0.0 | + And Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 342.46 | 1.93 | 0.0 | 0.0 | 344.39 | 242.46 | 181.84 | 0.0 | 101.93 | + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 21 March 2025 | Disbursement | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | 242.46 | false | false | + | 21 March 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 142.46 | true | false | + | 21 March 2025 | Merchant Issued Refund | 242.46 | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | false | true | + | 28 March 2025 | Credit Balance Refund | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | true | + | 20 April 2025 | Accrual | 1.85 | 0.0 | 1.85 | 0.0 | 0.0 | 0.0 | false | false | + When Admin sets the business date to "22 April 2025" + And Admin runs inline COB job for Loan + Then Loan Repayment schedule has 13 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 | + | | | 21 March 2025 | | 242.46 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 0 | 21 March 2025 | 21 March 2025 | 181.84 | 60.62 | 0.0 | 0.0 | 0.0 | 60.62 | 60.62 | 0.0 | 0.0 | 0.0 | + | 2 | 31 | 21 April 2025 | | 166.69 | 115.15 | 2.01 | 0.0 | 0.0 | 117.16 | 15.15 | 15.15 | 0.0 | 102.01 | + | 3 | 30 | 21 May 2025 | 21 March 2025 | 151.54 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 4 | 31 | 21 June 2025 | 21 March 2025 | 136.39 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 5 | 30 | 21 July 2025 | 21 March 2025 | 121.24 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 6 | 31 | 21 August 2025 | 21 March 2025 | 106.09 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 7 | 31 | 21 September 2025 | 21 March 2025 | 90.94 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 8 | 30 | 21 October 2025 | 21 March 2025 | 75.79 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 9 | 31 | 21 November 2025 | 21 March 2025 | 60.64 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 10 | 30 | 21 December 2025 | 21 March 2025 | 45.49 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 11 | 31 | 21 January 2026 | 21 March 2025 | 30.34 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 12 | 31 | 21 February 2026 | 21 March 2025 | 15.19 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 13 | 28 | 21 March 2026 | 21 March 2025 | 0.0 | 15.19 | 0.0 | 0.0 | 0.0 | 15.19 | 15.19 | 15.19 | 0.0 | 0.0 | + And Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 342.46 | 2.01 | 0.0 | 0.0 | 344.47 | 242.46 | 181.84 | 0.0 | 102.01 | + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 21 March 2025 | Disbursement | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | 242.46 | false | false | + | 21 March 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 142.46 | true | false | + | 21 March 2025 | Merchant Issued Refund | 242.46 | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | false | true | + | 28 March 2025 | Credit Balance Refund | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | true | + | 20 April 2025 | Accrual | 1.85 | 0.0 | 1.85 | 0.0 | 0.0 | 0.0 | false | false | + | 21 April 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false | + When Admin sets the business date to "23 April 2025" + And Admin runs inline COB job for Loan + Then Loan Repayment schedule has 13 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 | + | | | 21 March 2025 | | 242.46 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 0 | 21 March 2025 | 21 March 2025 | 181.84 | 60.62 | 0.0 | 0.0 | 0.0 | 60.62 | 60.62 | 0.0 | 0.0 | 0.0 | + | 2 | 31 | 21 April 2025 | | 166.69 | 115.15 | 2.1 | 0.0 | 0.0 | 117.25 | 15.15 | 15.15 | 0.0 | 102.1 | + | 3 | 30 | 21 May 2025 | 21 March 2025 | 151.54 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 4 | 31 | 21 June 2025 | 21 March 2025 | 136.39 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 5 | 30 | 21 July 2025 | 21 March 2025 | 121.24 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 6 | 31 | 21 August 2025 | 21 March 2025 | 106.09 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 7 | 31 | 21 September 2025 | 21 March 2025 | 90.94 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 8 | 30 | 21 October 2025 | 21 March 2025 | 75.79 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 9 | 31 | 21 November 2025 | 21 March 2025 | 60.64 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 10 | 30 | 21 December 2025 | 21 March 2025 | 45.49 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 11 | 31 | 21 January 2026 | 21 March 2025 | 30.34 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 12 | 31 | 21 February 2026 | 21 March 2025 | 15.19 | 15.15 | 0.0 | 0.0 | 0.0 | 15.15 | 15.15 | 15.15 | 0.0 | 0.0 | + | 13 | 28 | 21 March 2026 | 21 March 2025 | 0.0 | 15.19 | 0.0 | 0.0 | 0.0 | 15.19 | 15.19 | 15.19 | 0.0 | 0.0 | + And Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 342.46 | 2.1 | 0.0 | 0.0 | 344.56 | 242.46 | 181.84 | 0.0 | 102.1 | + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 21 March 2025 | Disbursement | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | 242.46 | false | false | + | 21 March 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 142.46 | true | false | + | 21 March 2025 | Merchant Issued Refund | 242.46 | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | false | true | + | 28 March 2025 | Credit Balance Refund | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | true | + | 20 April 2025 | Accrual | 1.85 | 0.0 | 1.85 | 0.0 | 0.0 | 0.0 | false | false | + | 21 April 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false | + | 22 April 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false | + And Customer makes "AUTOPAY" repayment on "23 April 2025" with 102.1 EUR transaction amount + Then Loan status will be "CLOSED_OBLIGATIONS_MET" + Then Loan has 0 outstanding amount + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 21 March 2025 | Disbursement | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | 242.46 | false | false | + | 21 March 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 142.46 | true | false | + | 21 March 2025 | Merchant Issued Refund | 242.46 | 242.46 | 0.0 | 0.0 | 0.0 | 0.0 | false | true | + | 28 March 2025 | Credit Balance Refund | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | true | + | 20 April 2025 | Accrual | 1.85 | 0.0 | 1.85 | 0.0 | 0.0 | 0.0 | false | false | + | 21 April 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false | + | 22 April 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false | + | 23 April 2025 | Repayment | 102.1 | 100.0 | 2.1 | 0.0 | 0.0 | 0.0 | false | false | + | 23 April 2025 | Accrual | 0.09 | 0.0 | 0.09 | 0.0 | 0.0 | 0.0 | false | false | 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 cd12648ec..f4f84359c 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 @@ -53,6 +53,7 @@ import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanIntere import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; @Component @RequiredArgsConstructor @@ -182,16 +183,25 @@ public final class ProgressiveEMICalculator implements EMICalculator { scheduleModel.zero()).ifPresent(repaymentPeriod -> { calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel); calculateOutstandingBalance(scheduleModel); - calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, balanceCorrectionDate); }); } @Override public void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate, Money interestAmount) { - findRepaymentPeriod(scheduleModel, repaymentPeriodDueDate).ifPresent(rp -> rp.addPaidInterestAmount(interestAmount)); + Optional repaymentPeriod = findRepaymentPeriod(scheduleModel, repaymentPeriodDueDate); + + Optional latestNotLastOpenRepaymentPeriodBeforeDate = getLatestNotLastOpenRepaymentPeriodBeforeDate(scheduleModel, + transactionDate); + if (latestNotLastOpenRepaymentPeriodBeforeDate.isPresent() && repaymentPeriod.equals(latestNotLastOpenRepaymentPeriodBeforeDate)) { + calculateUnrecognizedInterestTillDateOnScheduleModelCopyAndDefer(scheduleModel, + latestNotLastOpenRepaymentPeriodBeforeDate.get(), transactionDate); + } + + repaymentPeriod.ifPresent(rp -> rp.addPaidInterestAmount(interestAmount)); calculateOutstandingBalance(scheduleModel); - calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, transactionDate); } @Override @@ -211,17 +221,38 @@ public final class ProgressiveEMICalculator implements EMICalculator { repaymentPeriod.ifPresent(rp -> { // If any period total paid > calculated EMI, then set EMI to total paid -> effectively it is marked as // fully paid - if (transactionDateIsBefore && rp.getTotalPaidAmount().isGreaterThan(rp.getEmiPlusCreditedAmounts())) { + if (transactionDateIsBefore + && rp.getTotalPaidAmount().isGreaterThan(rp.getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest())) { rp.setEmi(rp.getTotalPaidAmount().minus(rp.getTotalCreditedAmount())); } else if (transactionDateIsBefore && rp.getTotalPaidAmount().isEqualTo(rp.getOriginalEmi().add(rp.getTotalCreditedAmount()))) { rp.setEmi(rp.getTotalPaidAmount().minus(rp.getTotalCreditedAmount())); } - calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, balanceCorrectionDate); }); } } + private Optional getLatestNotLastOpenRepaymentPeriodBeforeDate(ProgressiveLoanInterestScheduleModel scheduleModel, + LocalDate transactionDate) { + List unpaidRepaymentPeriods = scheduleModel.repaymentPeriods() // + .stream() // + .filter(rp -> !rp.isFullyPaid()) // + .toList(); // + + if (CollectionUtils.isEmpty(unpaidRepaymentPeriods) + || unpaidRepaymentPeriods.getLast().equals(scheduleModel.repaymentPeriods().getLast())) { + return Optional.empty(); + } + + RepaymentPeriod latestNotLastOpenRepaymentPeriod = unpaidRepaymentPeriods.getLast(); + if (DateUtils.isBefore(transactionDate, latestNotLastOpenRepaymentPeriod.getDueDate())) { + return Optional.empty(); + } + + return Optional.of(latestNotLastOpenRepaymentPeriod); + } + private void addCreditedAmountsToInterestPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedPrincipalAmount, Money creditedInterestAmount) { scheduleModel.repaymentPeriods().stream().filter(checkRepaymentPeriodIsInCreditRange(scheduleModel, transactionDate)).findFirst() @@ -260,7 +291,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { addCreditedAmountsToInterestPeriod(scheduleModel, transactionDate, creditedPrincipalAmount, creditedInterestAmount); calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel); calculateOutstandingBalance(scheduleModel); - calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, transactionDate); }); } @@ -334,22 +365,9 @@ public final class ProgressiveEMICalculator implements EMICalculator { public OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate targetDate) { MathContext mc = scheduleModel.mc(); ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc); - // TODO use findInterestPeriod - scheduleModelCopy.repaymentPeriods().stream()// - .filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()// - .flatMap(rp -> rp.getInterestPeriods().stream()// - .filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) // - .reduce((one, two) -> two)) - .ifPresent(ip -> ip.setDueDate(targetDate)); // - - calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy); - scheduleModelCopy.repaymentPeriods() - .forEach(rp -> rp.getInterestPeriods().stream().filter(ip -> targetDate.isBefore(ip.getDueDate())).forEach(ip -> { - ip.setRateFactor(BigDecimal.ZERO); - ip.setRateFactorTillPeriodDueDate(BigDecimal.ZERO); - })); + calculateRateFactorForScheduleTillDateInclusive(scheduleModelCopy, targetDate); calculateOutstandingBalance(scheduleModelCopy); - calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy, targetDate); Money totalOutstandingPrincipal = MathUtil .negativeToZero(scheduleModelCopy.getTotalDuePrincipal().minus(scheduleModelCopy.getTotalPaidPrincipal())); @@ -421,7 +439,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { }); calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy); calculateOutstandingBalance(scheduleModelCopy); - calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy, targetDate); } return scheduleModelCopy; } @@ -443,37 +461,63 @@ public final class ProgressiveEMICalculator implements EMICalculator { calculateEMIOnNewModelAndMerge(relatedRepaymentPeriods, scheduleModel, operation); } calculateOutstandingBalance(scheduleModel); - calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, calculateFromRepaymentPeriodDueDate); if (onlyOnActualModelShouldApply && (scheduleModel.loanTermVariations() == null || scheduleModel.loanTermVariations().get(LoanTermVariationType.DUE_DATE) == null)) { checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, relatedRepaymentPeriods); } } - private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestScheduleModel scheduleModel) { - MathContext mc = scheduleModel.mc(); - Money totalDueInterest = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getDueInterest).reduce(scheduleModel.zero(), - (m1, m2) -> m1.plus(m2, mc)); // 1.46 - Money totalEMI = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getEmiPlusCreditedAmounts) - .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 101.48 - Money totalDisbursedAmount = scheduleModel.repaymentPeriods().stream() - .flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount)) - .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100 - Money totalCapitalizedIncome = scheduleModel.repaymentPeriods().stream() - .flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCapitalizedIncomePrincipal)) - .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100 - - Money diff = totalDisbursedAmount.plus(totalCapitalizedIncome, mc).plus(scheduleModel.getTotalCreditedPrincipal(), mc) - .plus(totalDueInterest, mc).minus(totalEMI, mc); + private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate tillDate) { Optional findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()) .reduce((first, second) -> second); + findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> { + repaymentPeriod.setFutureUnrecognizedInterest(scheduleModel.zero()); + scheduleModel.repaymentPeriods().forEach(rp -> { + rp.setInterestMoved(false); + }); + + MathContext mc = scheduleModel.mc(); + Money totalDueInterest = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getDueInterest) + .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 1.46 + Money totalEMI = scheduleModel.repaymentPeriods().stream() + .map(RepaymentPeriod::getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest) + .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 101.48 + Money totalDisbursedAmount = scheduleModel.repaymentPeriods().stream() + .flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount)) + .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100 + Money totalCapitalizedIncome = scheduleModel.repaymentPeriods().stream() + .flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCapitalizedIncomePrincipal)) + .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100 + + Money diff = totalDisbursedAmount.plus(totalCapitalizedIncome, mc).plus(scheduleModel.getTotalCreditedPrincipal(), mc) + .plus(totalDueInterest, mc).minus(totalEMI, mc); + repaymentPeriod.setEmi(repaymentPeriod.getEmi().add(diff, mc)); if (repaymentPeriod.getEmi() .isLessThan(repaymentPeriod.getTotalPaidAmount().minus(repaymentPeriod.getTotalCreditedAmount(), mc))) { repaymentPeriod.setEmi(repaymentPeriod.getTotalPaidAmount().minus(repaymentPeriod.getTotalCreditedAmount(), mc)); - calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, tillDate); } + + calculateUnrecognizedInterestTillDateOnScheduleModelCopyAndDefer(scheduleModel, repaymentPeriod, tillDate); + }); + } + + private void calculateUnrecognizedInterestTillDateOnScheduleModelCopyAndDefer(ProgressiveLoanInterestScheduleModel scheduleModel, + RepaymentPeriod repaymentPeriod, LocalDate tillDate) { + MathContext mc = scheduleModel.mc(); + ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc); + calculateRateFactorForScheduleTillDateInclusive(scheduleModelCopy, tillDate); + Optional futureUnrecognizedInterestPeriod = getPeriodWithUnrecognizedInterest(repaymentPeriod, scheduleModelCopy); + + futureUnrecognizedInterestPeriod.ifPresent(period -> { + repaymentPeriod.setFutureUnrecognizedInterest(period.getUnrecognizedInterest()); + scheduleModel.repaymentPeriods().stream().filter(rp -> rp.getDueDate().isAfter(repaymentPeriod.getDueDate())) // + .forEach(rp -> { + rp.setInterestMoved(true); + }); }); } @@ -509,7 +553,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { } }); calculateOutstandingBalance(newScheduleModel); - calculateLastUnpaidRepaymentPeriodEMI(newScheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(newScheduleModel, relatedPeriodsFirstDueDate); if (!getEmiAdjustment(newScheduleModel.repaymentPeriods()).hasLessEmiDifference(emiAdjustment)) { break; } @@ -861,6 +905,31 @@ public final class ProgressiveEMICalculator implements EMICalculator { return new EmiAdjustment(repaymentPeriods.get(0).getEmi(), repaymentPeriods.get(0).getEmi().copy(0.0), repaymentPeriods, 0); } + private void calculateRateFactorForScheduleTillDateInclusive(ProgressiveLoanInterestScheduleModel scheduleModelCopy, + LocalDate targetDate) { + scheduleModelCopy.findRepaymentPeriod(targetDate).flatMap(rp -> rp.findInterestPeriod(targetDate)) + .ifPresent(ip -> ip.setDueDate(targetDate)); + + calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy); + + scheduleModelCopy.repaymentPeriods() + .forEach(rp -> rp.getInterestPeriods().stream().filter(ip -> targetDate.isBefore(ip.getDueDate())).forEach(ip -> { + ip.setRateFactor(BigDecimal.ZERO); + ip.setRateFactorTillPeriodDueDate(BigDecimal.ZERO); + })); + } + + private Optional getPeriodWithUnrecognizedInterest(RepaymentPeriod lastUnpaidRepaymentPeriod, + ProgressiveLoanInterestScheduleModel scheduleModelCopy) { + for (RepaymentPeriod period : scheduleModelCopy.repaymentPeriods().reversed()) { + if (MathUtil.isGreaterThanZero(period.getUnrecognizedInterest()) + && period.getDueDate().isAfter(lastUnpaidRepaymentPeriod.getDueDate())) { + return Optional.of(period); + } + } + return Optional.empty(); + } + /** * Calculate Rate Factor Product from rate factors */ @@ -1082,7 +1151,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { final List relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(startDate); calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel); calculateOutstandingBalance(scheduleModel); - calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, startDate); } private long getUncountablePeriods(final List relatedRepaymentPeriods, final Money originalEmi) { 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 c6497e8d2..d38280f28 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 @@ -145,7 +145,7 @@ public class InterestPeriod implements Comparable { .multiply(getRateFactorTillPeriodDueDate(), mc) // .divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) // .multiply(BigDecimal.valueOf(getLength()), mc); // - return MathUtil.negativeToZero(MathUtil.add(creditedInterest.getAmount(), interestDueTillRepaymentDueDate, mc)); + return MathUtil.negativeToZero(MathUtil.add(mc, creditedInterest.getAmount(), interestDueTillRepaymentDueDate)); } public long getLength() { 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 26e9e2a9d..33b94d852 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 @@ -60,6 +60,9 @@ public final class RepaymentPeriod { private Money paidPrincipal; @Getter private Money paidInterest; + @Setter + @Getter + private Money futureUnrecognizedInterest; @JsonExclude private final MathContext mc; @@ -72,9 +75,12 @@ public final class RepaymentPeriod { private Memo dueInterestCalculation; @JsonExclude private Memo outstandingBalanceCalculation; + @Getter + @Setter + private boolean isInterestMoved = false; private RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List interestPeriods, - Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, MathContext mc) { + Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, Money futureUnrecognizedInterest, MathContext mc) { this.previous = previous; this.fromDate = fromDate; this.dueDate = dueDate; @@ -83,17 +89,18 @@ public final class RepaymentPeriod { this.originalEmi = originalEmi; this.paidPrincipal = paidPrincipal; this.paidInterest = paidInterest; + this.futureUnrecognizedInterest = futureUnrecognizedInterest; this.mc = mc; } public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc) { - return new RepaymentPeriod(previous, null, null, new ArrayList<>(), null, null, null, null, mc); + return new RepaymentPeriod(previous, null, null, new ArrayList<>(), null, null, null, null, null, mc); } public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc) { final Money zero = emi.zero(); final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, zero, - mc); + zero, mc); // There is always at least 1 interest period, by default with same from-due date as repayment period newRepaymentPeriod.interestPeriods.add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate)); return newRepaymentPeriod; @@ -102,7 +109,7 @@ public final class RepaymentPeriod { public static RepaymentPeriod copy(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) { final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate, new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, repaymentPeriod.paidPrincipal, - repaymentPeriod.paidInterest, mc); + repaymentPeriod.paidInterest, repaymentPeriod.futureUnrecognizedInterest, mc); // There is always at least 1 interest period, by default with same from-due date as repayment period for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) { newRepaymentPeriod.interestPeriods.add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc)); @@ -113,7 +120,7 @@ public final class RepaymentPeriod { public static RepaymentPeriod copyWithoutPaidAmounts(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) { final Money zero = repaymentPeriod.emi.zero(); final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate, - new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, zero, zero, mc); + new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, zero, zero, zero, mc); // There is always at least 1 interest period, by default with same from-due date as repayment period for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) { var interestPeriodCopy = InterestPeriod.copy(newRepaymentPeriod, interestPeriod); @@ -154,14 +161,19 @@ public final class RepaymentPeriod { public Money getCalculatedDueInterest() { if (calculatedDueInterestCalculation == null) { calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, - () -> new Object[] { this.previous, this.interestPeriods }); + () -> new Object[] { this.previous, this.interestPeriods, this.futureUnrecognizedInterest, this.isInterestMoved }); } return calculatedDueInterestCalculation.get(); } private Money calculateCalculatedDueInterest() { - Money calculatedDueInterest = Money.of(emi.getCurrencyData(), - getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(BigDecimal.ZERO, BigDecimal::add), mc); + Money calculatedDueInterest = getZero(mc); + if (!isInterestMoved) { + calculatedDueInterest = Money.of(emi.getCurrencyData(), + getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(BigDecimal.ZERO, BigDecimal::add), + mc); + } + calculatedDueInterest = calculatedDueInterest.add(getFutureUnrecognizedInterest(), mc); if (getPrevious().isPresent()) { calculatedDueInterest = calculatedDueInterest.add(getPrevious().get().getUnrecognizedInterest(), mc); } @@ -176,22 +188,22 @@ public final class RepaymentPeriod { public Money getDueInterest() { if (dueInterestCalculation == null) { // Due interest might be the maximum paid if there is pay-off or early repayment - dueInterestCalculation = Memo - .of(() -> MathUtil.max( - getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? getPaidInterest() - : MathUtil.min(getCalculatedDueInterest(), getEmiPlusCreditedAmounts(), false), - getPaidInterest(), false), () -> new Object[] { paidPrincipal, paidInterest, interestPeriods }); + dueInterestCalculation = Memo.of( + () -> MathUtil.max(getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? getPaidInterest() + : MathUtil.min(getCalculatedDueInterest(), getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest(), false), + getPaidInterest(), false), + () -> new Object[] { paidPrincipal, paidInterest, interestPeriods, futureUnrecognizedInterest }); } return dueInterestCalculation.get(); } /** - * Gives back an EMI amount which includes credited amounts as well + * Gives back an EMI amount which includes credited amounts and future unrecognized interest as well * * @return */ - public Money getEmiPlusCreditedAmounts() { - return getEmi().plus(getTotalCreditedAmount(), mc); // + public Money getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest() { + return getEmi().plus(getTotalCreditedAmount(), mc).plus(getFutureUnrecognizedInterest(), mc); // } /** @@ -200,7 +212,7 @@ public final class RepaymentPeriod { * @return */ public Money getCalculatedDuePrincipal() { - return MathUtil.negativeToZero(getEmiPlusCreditedAmounts().minus(getCalculatedDueInterest(), mc), mc); + return MathUtil.negativeToZero(getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest().minus(getCalculatedDueInterest(), mc), mc); } /** @@ -243,8 +255,9 @@ public final class RepaymentPeriod { */ public Money getDuePrincipal() { // Due principal might be the maximum paid if there is pay-off or early repayment - return MathUtil.max(MathUtil.negativeToZero(getEmiPlusCreditedAmounts().minus(getDueInterest(), mc), mc), getPaidPrincipal(), - false); + return MathUtil.max( + MathUtil.negativeToZero(getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest().minus(getDueInterest(), mc), mc), + getPaidPrincipal(), false); } /** @@ -266,7 +279,7 @@ public final class RepaymentPeriod { } public boolean isFullyPaid() { - return getEmiPlusCreditedAmounts().isEqualTo(getTotalPaidAmount()); + return getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest().isEqualTo(getTotalPaidAmount()); } /**