FINERACT-2281: fix unrecognized interest handling when first period is unpaid

This commit is contained in:
mark.vituska
2025-06-18 10:53:30 +02:00
committed by Adam Saghy
parent 4d56a4f230
commit 5649aa3ded
4 changed files with 373 additions and 73 deletions
@@ -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
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 |
@@ -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> repaymentPeriod = findRepaymentPeriod(scheduleModel, repaymentPeriodDueDate);
Optional<RepaymentPeriod> 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<RepaymentPeriod> getLatestNotLastOpenRepaymentPeriodBeforeDate(ProgressiveLoanInterestScheduleModel scheduleModel,
LocalDate transactionDate) {
List<RepaymentPeriod> 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<RepaymentPeriod> 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<RepaymentPeriod> 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<RepaymentPeriod> 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<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(startDate);
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, startDate);
}
private long getUncountablePeriods(final List<RepaymentPeriod> relatedRepaymentPeriods, final Money originalEmi) {
@@ -145,7 +145,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
.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() {
@@ -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<Money> dueInterestCalculation;
@JsonExclude
private Memo<Money> outstandingBalanceCalculation;
@Getter
@Setter
private boolean isInterestMoved = false;
private RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> 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());
}
/**