FINERACT-2162: Chargeback implementation for progressive interest recalculation enabled loan.

This commit is contained in:
Janos Meszaros
2025-02-14 15:16:59 +01:00
committed by Adam Saghy
parent 22f7972f60
commit cdbf0df571
23 changed files with 2303 additions and 176 deletions
@@ -4496,6 +4496,281 @@ Feature: LoanChargeback
| 01 March 2024 | Chargeback | 15.0 | 15.0 | 0.0 | 0.0 | 0.0 | 82.05 | false | false |
| 01 March 2024 | Chargeback | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 98.57 | false | false |
@TestRailId:C3427
Scenario: Verify full chargeback before maturity date - no chargeback rule - interest recalculation enabled
When Admin sets the business date to "01 January 2024"
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_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
And Admin sets the business date to "01 February 2024"
And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount
And Admin sets the business date to "01 March 2024"
And Customer makes "AUTOPAY" repayment on "01 March 2024" with 17.01 EUR transaction amount
And Admin makes "REPAYMENT_ADJUSTMENT_CHARGEBACK" chargeback with 17.01 EUR transaction amount for Payment nr. 2
Then Loan Repayment schedule has 6 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | | 50.53 | 33.53 | 0.49 | 0.0 | 0.0 | 34.02 | 0.0 | 0.0 | 0.0 | 34.02 |
| 4 | 30 | 01 May 2024 | | 33.81 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 17.0 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 17.0 | 0.1 | 0.0 | 0.0 | 17.1 | 0.0 | 0.0 | 0.0 | 17.1 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 117.01 | 2.15 | 0.0 | 0.0 | 119.16 | 34.02 | 0.0 | 0.0 | 85.14 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false |
| 01 March 2024 | Chargeback | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | 84.06 | false | false |
@TestRailId:C3428
Scenario: Verify partial chargeback before maturity date-1 - no chargeback rule - interest recalculation enabled
When Admin sets the business date to "01 January 2024"
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_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
When Admin sets the business date to "01 February 2024"
And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 March 2024"
And Customer makes "AUTOPAY" repayment on "01 March 2024" with 17.01 EUR transaction amount
And Admin makes "REPAYMENT_ADJUSTMENT_CHARGEBACK" chargeback with 15 EUR transaction amount for Payment nr. 2
Then Loan Repayment schedule has 6 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | | 50.52 | 31.53 | 0.48 | 0.0 | 0.0 | 32.01 | 0.0 | 0.0 | 0.0 | 32.01 |
| 4 | 30 | 01 May 2024 | | 33.8 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.99 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.99 | 0.1 | 0.0 | 0.0 | 17.09 | 0.0 | 0.0 | 0.0 | 17.09 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 115.0 | 2.14 | 0.0 | 0.0 | 117.14 | 34.02 | 0.0 | 0.0 | 83.12 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false |
| 01 March 2024 | Chargeback | 15.0 | 15.0 | 0.0 | 0.0 | 0.0 | 82.05 | false | false |
@TestRailId:C3429
Scenario: Verify two chargebacks before maturity - no chargeback rule - interest recalculation enabled
When Admin sets the business date to "01 January 2024"
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_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
When Admin sets the business date to "01 February 2024"
And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 March 2024"
And Customer makes "AUTOPAY" repayment on "01 March 2024" with 17.01 EUR transaction amount
And Admin makes "REPAYMENT_ADJUSTMENT_CHARGEBACK" chargeback with 15 EUR transaction amount for Payment nr. 1
Then Loan Repayment schedule has 6 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | | 50.52 | 31.53 | 0.48 | 0.0 | 0.0 | 32.01 | 0.0 | 0.0 | 0.0 | 32.01 |
| 4 | 30 | 01 May 2024 | | 33.8 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.99 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.99 | 0.1 | 0.0 | 0.0 | 17.09 | 0.0 | 0.0 | 0.0 | 17.09 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 115.0 | 2.14 | 0.0 | 0.0 | 117.14 | 34.02 | 0.0 | 0.0 | 83.12 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | 0.0 |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | 0.0 |
| 01 March 2024 | Chargeback | 15.0 | 15.0 | 0.0 | 0.0 | 0.0 | 82.05 | 0.0 |
And Admin makes "REPAYMENT_ADJUSTMENT_CHARGEBACK" chargeback with 17.01 EUR transaction amount for Payment nr. 2
Then Loan Repayment schedule has 6 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | | 50.62 | 48.44 | 0.58 | 0.0 | 0.0 | 49.02 | 0.0 | 0.0 | 0.0 | 49.02 |
| 4 | 30 | 01 May 2024 | | 33.91 | 16.71 | 0.3 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 17.1 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 17.1 | 0.1 | 0.0 | 0.0 | 17.2 | 0.0 | 0.0 | 0.0 | 17.2 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 132.01 | 2.25 | 0.0 | 0.0 | 134.26 | 34.02 | 0.0 | 0.0 | 100.24 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false |
| 01 March 2024 | Chargeback | 15.0 | 15.0 | 0.0 | 0.0 | 0.0 | 82.05 | false | false |
| 01 March 2024 | Chargeback | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | 99.06 | false | false |
@TestRailId:C3430
Scenario: Verify full chargeback before maturity date on different business date - no chargeback rule - interest recalculation enabled
When Admin sets the business date to "01 January 2024"
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_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
When Admin sets the business date to "01 February 2024"
And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 March 2024"
And Customer makes "AUTOPAY" repayment on "01 March 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "15 March 2024"
And Admin makes "REPAYMENT_ADJUSTMENT_CHARGEBACK" chargeback with 17.01 EUR transaction amount for Payment nr. 2
Then Loan Repayment schedule has 6 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | | 50.49 | 33.57 | 0.45 | 0.0 | 0.0 | 34.02 | 0.0 | 0.0 | 0.0 | 34.02 |
| 4 | 30 | 01 May 2024 | | 33.77 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.96 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.96 | 0.1 | 0.0 | 0.0 | 17.06 | 0.0 | 0.0 | 0.0 | 17.06 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 117.01 | 2.11 | 0.0 | 0.0 | 119.12 | 34.02 | 0.0 | 0.0 | 85.1 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false |
| 15 March 2024 | Chargeback | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | 84.06 | false | false |
@TestRailId:C3431
Scenario: Verify full chargeback after maturity date - no chargeback rule - interest recalculation enabled
When Admin sets the business date to "01 January 2024"
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_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
When Admin sets the business date to "01 February 2024"
And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 March 2024"
And Customer makes "AUTOPAY" repayment on "01 March 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 April 2024"
And Customer makes "AUTOPAY" repayment on "01 April 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 May 2024"
And Customer makes "AUTOPAY" repayment on "01 May 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 June 2024"
And Customer makes "AUTOPAY" repayment on "01 June 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 July 2024"
And Customer makes "AUTOPAY" repayment on "01 July 2024" with 17.00 EUR transaction amount
When Admin sets the business date to "15 July 2024"
And Admin makes "REPAYMENT_ADJUSTMENT_CHARGEBACK" chargeback with 17.01 EUR transaction amount for Payment nr. 5
Then Loan Repayment schedule has 7 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | 01 April 2024 | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 4 | 30 | 01 May 2024 | 01 May 2024 | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 5 | 31 | 01 June 2024 | 01 June 2024 | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 6 | 30 | 01 July 2024 | 01 July 2024 | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 17.0 | 0.0 | 0.0 | 0.0 |
| 7 | 14 | 15 July 2024 | | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 117.01 | 2.05 | 0.0 | 0.0 | 119.06 | 102.05 | 0.0 | 0.0 | 17.01 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false |
| 01 April 2024 | Repayment | 17.01 | 16.62 | 0.39 | 0.0 | 0.0 | 50.43 | false | false |
| 01 May 2024 | Repayment | 17.01 | 16.72 | 0.29 | 0.0 | 0.0 | 33.71 | false | false |
| 01 June 2024 | Repayment | 17.01 | 16.81 | 0.2 | 0.0 | 0.0 | 16.9 | false | false |
| 01 July 2024 | Repayment | 17.0 | 16.9 | 0.1 | 0.0 | 0.0 | 0.0 | false | false |
| 01 July 2024 | Accrual | 2.05 | 0.0 | 2.05 | 0.0 | 0.0 | 0.0 | false | false |
| 15 July 2024 | Chargeback | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | false | false |
@TestRailId:C3432
Scenario: Verify two chargebacks after maturity date - no chargeback rule - interest recalculation enabled
When Admin sets the business date to "01 January 2024"
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_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
When Admin sets the business date to "01 February 2024"
And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 March 2024"
And Customer makes "AUTOPAY" repayment on "01 March 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 April 2024"
And Customer makes "AUTOPAY" repayment on "01 April 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 May 2024"
And Customer makes "AUTOPAY" repayment on "01 May 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 June 2024"
And Customer makes "AUTOPAY" repayment on "01 June 2024" with 17.01 EUR transaction amount
When Admin sets the business date to "01 July 2024"
And Customer makes "AUTOPAY" repayment on "01 July 2024" with 17.00 EUR transaction amount
When Admin sets the business date to "15 July 2024"
And Admin makes "REPAYMENT_ADJUSTMENT_CHARGEBACK" chargeback with 17.01 EUR transaction amount for Payment nr. 5
Then Loan Repayment schedule has 7 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | 01 April 2024 | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 4 | 30 | 01 May 2024 | 01 May 2024 | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 5 | 31 | 01 June 2024 | 01 June 2024 | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 6 | 30 | 01 July 2024 | 01 July 2024 | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 17.0 | 0.0 | 0.0 | 0.0 |
| 7 | 14 | 15 July 2024 | | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 117.01 | 2.05 | 0.0 | 0.0 | 119.06 | 102.05 | 0.0 | 0.0 | 17.01 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false |
| 01 April 2024 | Repayment | 17.01 | 16.62 | 0.39 | 0.0 | 0.0 | 50.43 | false | false |
| 01 May 2024 | Repayment | 17.01 | 16.72 | 0.29 | 0.0 | 0.0 | 33.71 | false | false |
| 01 June 2024 | Repayment | 17.01 | 16.81 | 0.2 | 0.0 | 0.0 | 16.9 | false | false |
| 01 July 2024 | Repayment | 17.0 | 16.9 | 0.1 | 0.0 | 0.0 | 0.0 | false | false |
| 01 July 2024 | Accrual | 2.05 | 0.0 | 2.05 | 0.0 | 0.0 | 0.0 | false | false |
| 15 July 2024 | Chargeback | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | false | false |
When Admin sets the business date to "15 July 2024"
And Admin makes "REPAYMENT_ADJUSTMENT_CHARGEBACK" chargeback with 17 EUR transaction amount for Payment nr. 6
Then Loan Repayment schedule has 7 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | 01 April 2024 | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 4 | 30 | 01 May 2024 | 01 May 2024 | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 5 | 31 | 01 June 2024 | 01 June 2024 | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 6 | 30 | 01 July 2024 | 01 July 2024 | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 17.0 | 0.0 | 0.0 | 0.0 |
| 7 | 14 | 15 July 2024 | | 0.0 | 34.01 | 0.0 | 0.0 | 0.0 | 34.01 | 0.0 | 0.0 | 0.0 | 34.01 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 134.01 | 2.05 | 0.0 | 0.0 | 136.06 | 102.05 | 0.0 | 0.0 | 34.01 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false |
| 01 April 2024 | Repayment | 17.01 | 16.62 | 0.39 | 0.0 | 0.0 | 50.43 | false | false |
| 01 May 2024 | Repayment | 17.01 | 16.72 | 0.29 | 0.0 | 0.0 | 33.71 | false | false |
| 01 June 2024 | Repayment | 17.01 | 16.81 | 0.2 | 0.0 | 0.0 | 16.9 | false | false |
| 01 July 2024 | Repayment | 17.0 | 16.9 | 0.1 | 0.0 | 0.0 | 0.0 | false | false |
| 01 July 2024 | Accrual | 2.05 | 0.0 | 2.05 | 0.0 | 0.0 | 0.0 | false | false |
| 15 July 2024 | Chargeback | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | false | false |
| 15 July 2024 | Chargeback | 17.0 | 17.0 | 0.0 | 0.0 | 0.0 | 34.01 | false | false |
@TestRailId:C3443
Scenario: Verify that Accrual transactions are not created for the charged back interest - interest recalculation false + chargeback allocation rule is set
When Admin sets the business date to "01 January 2024"
@@ -32,6 +32,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
@@ -137,9 +138,11 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
// TODO: At some point in time this database column needs to be renamed to credited_principal using the following
// approach
// https://blog.thepete.net/blog/2023/12/05/expand/contract-making-a-breaking-change-without-a-big-bang/
@Setter
@Column(name = "credits_amount", scale = 6, precision = 19, nullable = true)
private BigDecimal creditedPrincipal;
@Setter
@Column(name = "credited_interest", scale = 6, precision = 19, nullable = true)
private BigDecimal creditedInterest;
@@ -853,14 +856,6 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
checkIfRepaymentPeriodObligationsAreMet(transactionDate, transactionAmount.getCurrency());
}
public void addToCreditedPrincipal(final BigDecimal amount) {
if (this.creditedPrincipal == null) {
this.creditedPrincipal = amount;
} else {
this.creditedPrincipal = this.creditedPrincipal.add(amount);
}
}
public void addToCreditedInterest(final BigDecimal amount) {
if (this.creditedInterest == null) {
this.creditedInterest = amount;
@@ -869,6 +864,14 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
}
}
public void addToCreditedPrincipal(final BigDecimal amount) {
if (this.creditedPrincipal == null) {
this.creditedPrincipal = amount;
} else {
this.creditedPrincipal = this.creditedPrincipal.add(amount);
}
}
public void addToCreditedFee(final BigDecimal amount) {
if (this.creditedFee == null) {
this.creditedFee = amount;
@@ -66,17 +66,12 @@ public class LoanScheduleService {
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
/*
* LocalDate recalculateFrom = null; List<LoanTransaction> loanTransactions =
* this.retrieveListOfTransactionsPostDisbursementExcludeAccruals(); for (LoanTransaction loanTransaction :
* loanTransactions) { if (recalculateFrom == null ||
* loanTransaction.getTransactionDate().isAfter(recalculateFrom)) { recalculateFrom =
* loanTransaction.getTransactionDate(); } } generatorDTO.setRecalculateFrom(recalculateFrom);
*/
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isChargedOff()) {
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
} else {
regenerateRepaymentSchedule(loan, generatorDTO);
if (!loan.isProgressiveSchedule()) {
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isChargedOff()) {
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
} else {
regenerateRepaymentSchedule(loan, generatorDTO);
}
}
return loan.reprocessTransactions();
}
@@ -86,12 +86,12 @@ import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParamet
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.DueType;
@@ -405,7 +405,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Long toId = chargebackId;
Collection<LoanTransaction> updatedTransactions = changedTransactionDetail.getNewTransactionMappings().values();
Optional<LoanTransaction> fromTransaction = updatedTransactions.stream()
.filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(hasMatchingToLoanTransaction(toId, CHARGEBACK)))
.filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(hasMatchingToLoanTransaction(toId, CHARGEBACK))
|| tr.getLoanTransactionRelations().stream()
.anyMatch(this.hasMatchingToLoanTransaction(chargebackTransaction, CHARGEBACK)))
.findFirst();
if (fromTransaction.isPresent()) {
return fromTransaction.get();
@@ -414,7 +416,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Long toId = chargebackId;
// if the original transaction is not in the ctx, then it means that it has not changed during reverse replay
Optional<LoanTransaction> fromTransaction = chargebackTransaction.getLoan().getLoanTransactions().stream()
.filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(this.hasMatchingToLoanTransaction(toId, CHARGEBACK)))
.filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(this.hasMatchingToLoanTransaction(toId, CHARGEBACK))
|| tr.getLoanTransactionRelations().stream()
.anyMatch(this.hasMatchingToLoanTransaction(chargebackTransaction, CHARGEBACK)))
.findFirst();
if (fromTransaction.isEmpty()) {
throw new RuntimeException("Chargeback transaction must have an original transaction");
@@ -422,8 +426,141 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return fromTransaction.get();
}
protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
private Map<AllocationType, Money> calculateChargebackAllocationMapPrincipalOnly(Money transactionAmount, MonetaryCurrency currency) {
Map<AllocationType, Money> chargebackAllocation = new HashMap<>();
chargebackAllocation.put(PRINCIPAL, transactionAmount);
chargebackAllocation.put(INTEREST, Money.zero(currency));
chargebackAllocation.put(PENALTY, Money.zero(currency));
chargebackAllocation.put(FEE, Money.zero(currency));
return chargebackAllocation;
}
/**
* Finds and returns installment from the provided context which has the given due date.
*
* @param ctx
* Progressive transaction context
* @param dueDate
* installment's due date
* @return found installment or null if there is no matching installment.
*/
private LoanRepaymentScheduleInstallment getInstallmentWithDueDate(@NotNull final ProgressiveTransactionCtx ctx,
@NotNull final LocalDate dueDate) {
return ctx.getInstallments().stream().filter(i -> i.getDueDate().isEqual(dueDate)).findAny().orElse(null);
}
/**
* Finds and returns installment from the provided context which has from date <= given date < due date.
*
* @param ctx
* Progressive transaction context
* @param date
* the given date
* @return found installment or null if there is no matching installment.
*/
private LoanRepaymentScheduleInstallment getRelatedInstallmentByFromDateInclusiveAndToDateExclusive(
@NotNull final ProgressiveTransactionCtx ctx, @NotNull final LocalDate date) {
if (ctx.getInstallments() == null) {
return null;
}
return ctx.getInstallments().stream().filter(i -> !date.isBefore(i.getFromDate()) && date.isBefore(i.getDueDate())).findAny()
.orElse(null);
}
protected void processCreditTransactionWithEmiCalculator(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx) {
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
MonetaryCurrency currency = ctx.getCurrency();
loanTransaction.resetDerivedComponents();
Money transactionAmount = loanTransaction.getAmount(currency);
Money totalOverpaid = ctx.getOverpaymentHolder().getMoneyObject();
loanTransaction.setOverPayments(totalOverpaid);
if (!transactionAmount.isGreaterThanZero()) {
return;
}
if (!loanTransaction.isChargeback()) {
throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name());
}
Map<AllocationType, Money> chargebackAllocation;
if (hasNoCustomCreditAllocationRule(loanTransaction)) {
// whole amount should allocate as principal no need to check previous chargebacks.
chargebackAllocation = calculateChargebackAllocationMapPrincipalOnly(transactionAmount, currency);
} else {
chargebackAllocation = calculateChargebackAllocationMapByCreditAllocationRule(loanTransaction, ctx);
}
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
LocalDate lastInstallmentDueDate = model.getMaturityDate();
if (!loanTransaction.getTransactionDate().isAfter(lastInstallmentDueDate)) {
// handle charge back before or on last installments due date
if (chargebackAllocation.get(PRINCIPAL).isGreaterThanZero()) {
emiCalculator.chargebackPrincipal(model, loanTransaction.getTransactionDate(), chargebackAllocation.get(PRINCIPAL));
}
if (chargebackAllocation.get(INTEREST).isGreaterThanZero()) {
emiCalculator.chargebackInterest(model, loanTransaction.getTransactionDate(), chargebackAllocation.get(INTEREST));
}
// update repayment periods until maturity date, for principal and interest portions
updateRepaymentPeriods(loanTransaction, ctx);
// search last instalment if transaction is posted on last installment's due date
// otherwise search installment where transaction date is in from date (inclusive) to due date (exclusive)
// interval
LoanRepaymentScheduleInstallment instalment = lastInstallmentDueDate.isEqual(loanTransaction.getTransactionDate())
? getInstallmentWithDueDate(ctx, loanTransaction.getTransactionDate())
: getRelatedInstallmentByFromDateInclusiveAndToDateExclusive(ctx, loanTransaction.getTransactionDate());
// recognize fees and penalties only because principal and interest portions are already updated.
recognizeFeePenaltiesAmountsAfterChargeback(ctx, instalment, chargebackAllocation);
} else {
// (N+1)th installment case for post maturity date charge back
LoanRepaymentScheduleInstallment instalment = ctx.getInstallments().stream()
.filter(LoanRepaymentScheduleInstallment::isAdditional).findAny()
.orElseGet(() -> createAdditionalInstalment(loanTransaction, ctx));
// recognize principal, interest, fees and penalties portions
recognizeAmountsAfterChargeback(ctx, loanTransaction.getTransactionDate(), instalment, chargebackAllocation);
if (instalment.getDueDate().isBefore(loanTransaction.getTransactionDate())) {
instalment.updateDueDate(loanTransaction.getTransactionDate());
}
}
allocateOverpayment(loanTransaction, ctx);
}
private LoanRepaymentScheduleInstallment createAdditionalInstalment(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx) {
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loanTransaction.getLoan(),
(ctx.getInstallments().size() + 1), ctx.getModel().getMaturityDate(), loanTransaction.getTransactionDate(), ZERO, ZERO,
ZERO, ZERO, false, null);
installment.markAsAdditional();
loanTransaction.getLoan().addLoanRepaymentScheduleInstallment(installment);
return installment;
}
private Map<AllocationType, Money> calculateChargebackAllocationMapByCreditAllocationRule(LoanTransaction loanTransaction,
TransactionCtx ctx) {
MonetaryCurrency currency = ctx.getCurrency();
LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx);
// get the original allocation from the original transaction
Map<AllocationType, Money> originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction, currency);
LoanCreditAllocationRule chargebackAllocationRule = getChargebackAllocationRules(loanTransaction);
// if there were earlier chargebacks then let's calculate the remaining amounts for each portion
Map<AllocationType, Money> originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction,
originalAllocationNotAdjusted, loanTransaction, ctx, chargebackAllocationRule);
// calculate the current chargeback allocation
return calculateChargebackAllocationMap(originalAllocation, loanTransaction.getAmount(currency).getAmount(),
chargebackAllocationRule.getAllocationTypes(), currency);
}
protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
if (loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
processCreditTransactionWithEmiCalculator(loanTransaction, (ProgressiveTransactionCtx) ctx);
} else if (hasNoCustomCreditAllocationRule(loanTransaction)) {
super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments());
} else {
loanTransaction.resetDerivedComponents();
@@ -444,17 +581,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx);
// get the original allocation from the opriginal transaction
// get the original allocation from the original transaction
Map<AllocationType, Money> originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction, currency);
LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction);
LoanCreditAllocationRule chargebackAllocationRule = getChargebackAllocationRules(loanTransaction);
// if there were earlier chargebacks then let's calculate the remaining amounts for each portion
Map<AllocationType, Money> originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction,
originalAllocationNotAdjusted, loanTransaction, ctx, chargeBackAllocationRule);
originalAllocationNotAdjusted, loanTransaction, ctx, chargebackAllocationRule);
// calculate the current chargeback allocation
Map<AllocationType, Money> chargebackAllocation = calculateChargebackAllocationMap(originalAllocation,
transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), currency);
transactionAmount.getAmount(), chargebackAllocationRule.getAllocationTypes(), currency);
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
@@ -501,23 +638,22 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
private Map<AllocationType, Money> adjustOriginalAllocationWithFormerChargebacks(LoanTransaction originalTransaction,
Map<AllocationType, Money> originalAllocation, LoanTransaction chargeBackTransaction, TransactionCtx ctx,
LoanCreditAllocationRule chargeBackAllocationRule) {
Map<AllocationType, Money> originalAllocation, LoanTransaction chargebackTransaction, TransactionCtx ctx,
LoanCreditAllocationRule chargebackAllocationRule) {
// these are the list of existing transactions
List<LoanTransaction> allTransactions = new ArrayList<>(chargeBackTransaction.getLoan().getLoanTransactions());
List<LoanTransaction> allTransactions = new ArrayList<>(chargebackTransaction.getLoan().getLoanTransactions());
// Remove the current chargeback from the list
if (chargeBackTransaction.getId() != null) {
allTransactions.remove(chargeBackTransaction);
} else {
Long oldId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(chargeBackTransaction);
allTransactions.remove(allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), oldId)).findFirst().get());
allTransactions.remove(chargebackTransaction);
if (ctx.getChangedTransactionDetail() != null) {
Long oldId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(chargebackTransaction);
allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), oldId)).findFirst().ifPresent(allTransactions::remove);
}
// Add the replayed transactions and remove their old version before the replay
if (ctx.getChangedTransactionDetail() != null && ctx.getChangedTransactionDetail().getNewTransactionMappings() != null) {
for (Long id : ctx.getChangedTransactionDetail().getNewTransactionMappings().keySet()) {
allTransactions.remove(allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), id)).findFirst().get());
allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), id)).findFirst().ifPresent(allTransactions::remove);
allTransactions.add(ctx.getChangedTransactionDetail().getNewTransactionMappings().get(id));
}
}
@@ -529,18 +665,36 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Comparator<LoanTransaction> comparator = LoanTransactionComparator.INSTANCE;
List<LoanTransaction> chargebacksForTheSameOriginal = chargebacks.stream()
.filter(tr -> findChargebackOriginalTransaction(tr, ctx) == originalTransaction
&& comparator.compare(tr, chargeBackTransaction) < 0)
&& comparator.compare(tr, chargebackTransaction) < 0)
.sorted(comparator).toList();
Map<AllocationType, Money> allocation = new HashMap<>(originalAllocation);
for (LoanTransaction loanTransaction : chargebacksForTheSameOriginal) {
Map<AllocationType, Money> temp = calculateChargebackAllocationMap(allocation, loanTransaction.getAmount(),
chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency());
chargebackAllocationRule.getAllocationTypes(), ctx.getCurrency());
allocation.keySet().forEach(k -> allocation.put(k, allocation.get(k).minus(temp.get(k))));
}
return allocation;
}
private void recognizeFeePenaltiesAmountsAfterChargeback(TransactionCtx ctx, LoanRepaymentScheduleInstallment installment,
Map<AllocationType, Money> chargebackAllocation) {
MonetaryCurrency currency = ctx.getCurrency();
Money fee = chargebackAllocation.get(FEE);
if (fee.isGreaterThanZero()) {
installment.addToCreditedFee(fee.getAmount());
installment.addToChargePortion(fee, Money.zero(currency), Money.zero(currency), Money.zero(currency), Money.zero(currency),
Money.zero(currency));
}
Money penalty = chargebackAllocation.get(PENALTY);
if (penalty.isGreaterThanZero()) {
installment.addToCreditedPenalty(penalty.getAmount());
installment.addToChargePortion(Money.zero(currency), Money.zero(currency), Money.zero(currency), penalty, Money.zero(currency),
Money.zero(currency));
}
}
private void recognizeAmountsAfterChargeback(final TransactionCtx ctx, final LocalDate transactionDate,
final LoanRepaymentScheduleInstallment installment, final Map<AllocationType, Money> chargebackAllocation) {
final Money principal = chargebackAllocation.get(PRINCIPAL);
@@ -554,21 +708,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
installment.addToCreditedInterest(interest.getAmount());
installment.addToInterest(transactionDate, interest);
}
final MonetaryCurrency currency = ctx.getCurrency();
final Money fee = chargebackAllocation.get(FEE);
if (fee != null && fee.isGreaterThanZero()) {
installment.addToCreditedFee(fee.getAmount());
installment.addToChargePortion(fee, Money.zero(currency), Money.zero(currency), Money.zero(currency), Money.zero(currency),
Money.zero(currency));
}
final Money penalty = chargebackAllocation.get(PENALTY);
if (penalty != null && penalty.isGreaterThanZero()) {
installment.addToCreditedPenalty(penalty.getAmount());
installment.addToChargePortion(Money.zero(currency), Money.zero(currency), Money.zero(currency), penalty, Money.zero(currency),
Money.zero(currency));
}
recognizeFeePenaltiesAmountsAfterChargeback(ctx, installment, chargebackAllocation);
}
@NotNull
@@ -613,6 +753,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return relation -> relation.getRelationType().equals(typeEnum) && Objects.equals(relation.getToTransaction().getId(), id);
}
private Predicate<LoanTransactionRelation> hasMatchingToLoanTransaction(LoanTransaction loanTransaction,
LoanTransactionRelationTypeEnum typeEnum) {
return relation -> relation.getRelationType().equals(typeEnum) && relation.getToTransaction() == loanTransaction;
}
protected void handleRefund(LoanTransaction loanTransaction, TransactionCtx ctx) {
MonetaryCurrency currency = ctx.getCurrency();
Money zero = Money.zero(currency);
@@ -1228,9 +1373,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (!installments.isEmpty() && transactionDate.isBefore(loan.getMaturityDate())) {
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
final BigDecimal newInterest = emiCalculator
.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(), currentInstallment.getDueDate(), transactionDate)
.getAmount();
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(),
currentInstallment.getDueDate(), transactionDate, true).getAmount();
currentInstallment.updateInterestCharged(newInterest);
} else {
final BigDecimal totalInterest = currentInstallment.getInterestOutstanding(transactionCtx.getCurrency()).getAmount();
@@ -1315,7 +1459,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
installments.stream().filter(installment -> !installment.getFromDate().isAfter(transactionDate)
&& installment.getDueDate().isAfter(transactionDate)).forEach(installment -> {
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(),
installment.getDueDate(), transactionDate).getAmount();
installment.getDueDate(), transactionDate, true).getAmount();
final BigDecimal interestRemoved = installment.getInterestCharged().subtract(newInterest);
installment.updatePrincipal(MathUtil.nullToZero(installment.getPrincipal()).add(interestRemoved));
installment.updateInterestCharged(newInterest);
@@ -1826,6 +1970,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (installment != null) {
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
installment.updateInterestCharged(rm.getDueInterest().getAmount());
installment.setCreditedInterest(rm.getChargebackInterest().getAmount());
installment.setCreditedPrincipal(rm.getChargebackPrincipal().getAmount());
installment.updateObligationsMet(ctx.getCurrency(), loanTransaction.getTransactionDate());
}
});
@@ -2099,7 +2245,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
BigDecimal::add);
final BigDecimal newInterest = emiCalculator
.getPeriodInterestTillDate(transactionCtx.getModel(), lastPeriod.getDueDate(), chargeOffDate).getAmount();
.getPeriodInterestTillDate(transactionCtx.getModel(), lastPeriod.getDueDate(), chargeOffDate, false).getAmount();
lastPeriod.setEmi(lastPeriod.getDuePrincipal().add(totalPrincipal).add(newInterest));
@@ -32,7 +32,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleIns
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@Getter
public class ProgressiveTransactionCtx extends TransactionCtx {
@@ -48,10 +48,10 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleD
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.springframework.stereotype.Component;
@@ -182,9 +182,15 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
.principal(outstandingAmounts.getOutstandingPrincipal()) //
.interest(outstandingAmounts.getOutstandingInterest());//
installments.forEach(installment -> result //
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)));
installments.forEach(installment -> {
if (installment.isAdditional()) {
result.plusPrincipal(installment.getPrincipalOutstanding(currency))
.plusInterest(installment.getInterestOutstanding(currency));
}
result //
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency));
});
return result;
}
@@ -200,7 +206,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
return Money.zero(loan.getCurrency());
}
ProgressiveLoanInterestScheduleModel model = processor.calculateInterestScheduleModel(loan.getId(), targetDate);
return emiCalculator.getPeriodInterestTillDate(model, installment.getDueDate(), targetDate);
return emiCalculator.getPeriodInterestTillDate(model, installment.getDueDate(), targetDate, false);
}
// Private, internal methods
@@ -27,49 +27,97 @@ import java.util.Optional;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
public interface EMICalculator {
/**
* This method creates an Interest model with repayment periods from the schedule periods which generated by
* schedule generator.
*/
@NotNull
ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
@NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
List<LoanTermVariationsData> loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc);
/**
* This method creates an Interest model with repayment periods from the installments which retrieved from the
* database.
*/
@NotNull
ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel(
@NotNull List<LoanRepaymentScheduleInstallment> installments,
@NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
List<LoanTermVariationsData> loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc);
/**
* Find repayment period based on Due Date.
*/
Optional<RepaymentPeriod> findRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate dueDate);
/**
* Applies the disbursement on the interest model. This method recalculates the EMI amounts from the action date.
*/
void addDisbursement(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate disbursementDueDate, Money disbursedAmount);
/**
* Applies the interest rate change on the interest model. This method recalculates the EMI amounts from the action
* date.
*/
void changeInterestRate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestSubmittedOnDate,
BigDecimal newInterestRate);
/**
* This method applies outstanding balance correction on the interest model. Negative amount decreases the
* outstanding balance while positive amounts are increasing that. Typically used for late repayment or to count
* repayments.
*/
void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
Money balanceCorrectionAmount);
/**
* This method used for pay interest portion during the repayment transaction.
*/
void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
Money interestAmount);
/**
* This method used for pay principal portion during the repayment transaction.
*/
void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
Money principalAmount);
/**
* This method used for charge back principal portion. This method increases the outstanding balance. This method
* creates a calculated "virtual" EMI for the applied period.
*/
void chargebackPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate,
Money chargebackPrincipalAmount);
/**
* This method used for charge back interest portion. This method adds extra interest due. This method creates a
* calculated "virtual" EMI for the applied period.
*/
void chargebackInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money chargebackInterestAmount);
/**
* This method gives back the maximum of the due principal and maximum of the due interest for a requested day.
*/
@NotNull
PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
@NotNull LocalDate targetDate);
/**
* Gives back the sum of the interest from the whole model on the given date.
*/
@NotNull
Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
@NotNull LocalDate targetDate);
@NotNull LocalDate targetDate, boolean includeChargebackInterest);
Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
LocalDate targetDate);
@@ -80,5 +128,9 @@ public interface EMICalculator {
Money getSumOfDueInterestsOnDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate subjectDate);
/**
* This method stops the interest counting for the given range. Chargeback interest counts even if the normal
* interest paused.
*/
void applyInterestPause(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate fromDate, LocalDate endDate);
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanproduct.calc;
import jakarta.annotation.Nonnull;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
@@ -30,6 +31,7 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
@@ -40,13 +42,14 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.EmiAdjustment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiAdjustment;
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiChangeOperation;
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.springframework.stereotype.Component;
@@ -188,13 +191,60 @@ 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 (rp.getTotalPaidAmount().compareTo(rp.getEmi()) > 0) {
rp.setEmi(rp.getTotalPaidAmount());
if (rp.getTotalPaidAmount().isGreaterThan(rp.getEmiPlusChargeback())) {
rp.setEmi(rp.getTotalPaidAmount().minus(rp.getTotalChargebackAmount()));
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
}
});
}
private void addChargebackAmountsToInterestPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate,
Money chargebackPrincipalAmount, Money chargeBackInterestAmount) {
scheduleModel.repaymentPeriods().stream().filter(checkRepaymentPeriodIsInChargebackRange(scheduleModel, transactionDate))
.findFirst()
.flatMap(repaymentPeriod -> repaymentPeriod.getInterestPeriods().stream()
.filter(interestPeriod -> interestPeriod.getFromDate().equals(transactionDate)).reduce((v1, v2) -> v2))
.ifPresent(interestPeriod -> {
interestPeriod.addChargebackPrincipalAmount(chargebackPrincipalAmount);
interestPeriod.addChargebackInterestAmount(chargeBackInterestAmount);
});
}
@Nonnull
private static Predicate<RepaymentPeriod> checkRepaymentPeriodIsInChargebackRange(ProgressiveLoanInterestScheduleModel scheduleModel,
LocalDate transactionDate) {
return repaymentPeriod -> scheduleModel.isLastRepaymentPeriod(repaymentPeriod)
? !transactionDate.isBefore(repaymentPeriod.getFromDate()) && !transactionDate.isAfter(repaymentPeriod.getDueDate())
: !transactionDate.isBefore(repaymentPeriod.getFromDate()) && transactionDate.isBefore(repaymentPeriod.getDueDate());
}
@Override
public void chargebackPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate,
Money chargebackPrincipalAmount) {
addChargeback(scheduleModel, transactionDate, chargebackPrincipalAmount, scheduleModel.zero());
}
@Override
public void chargebackInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate,
Money chargebackInterestAmount) {
addChargeback(scheduleModel, transactionDate, scheduleModel.zero(), chargebackInterestAmount);
}
private void addChargeback(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate,
Money chargebackPrincipalAmount, Money chargebackInterestAmount) {
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), chargebackPrincipalAmount)
.ifPresent(repaymentPeriod -> {
addChargebackAmountsToInterestPeriod(scheduleModel, transactionDate, chargebackPrincipalAmount,
chargebackInterestAmount);
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
});
}
/**
* This method gives back the maximum of the due principal and maximum of the due interest for a requested day.
*/
@Override
@NotNull
public PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
@@ -216,11 +266,13 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
@NotNull
public Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
@NotNull LocalDate targetDate) {
@NotNull LocalDate targetDate, boolean includeChargebackInterest) {
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
periodDueDate, targetDate);
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
return repaymentPeriod.getCalculatedDueInterest();
return includeChargebackInterest ? repaymentPeriod.getCalculatedDueInterest()
: repaymentPeriod.getCalculatedDueInterest().minus(repaymentPeriod.getChargebackInterest(),
recalculatedScheduleModelTillDate.mc());
}
@Override
@@ -288,20 +340,25 @@ public final class ProgressiveEMICalculator implements EMICalculator {
interestPeriod = repaymentPeriod.getLastInterestPeriod();
adjustedTargetDate = repaymentPeriod.getDueDate();
} else {
// TODO use findInterestPeriod
interestPeriod = repaymentPeriod.getInterestPeriods().stream()
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())).findFirst().orElseThrow();
interestPeriod = repaymentPeriod.findInterestPeriod(targetDate).orElseThrow();
}
// 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)); //
scheduleModelCopy.findRepaymentPeriod(targetDate)//
.flatMap(rp -> rp.findInterestPeriod(targetDate)).ifPresent(ip -> ip.setDueDate(targetDate)); //
interestPeriod.setDueDate(adjustedTargetDate);
int index = repaymentPeriod.getInterestPeriods().indexOf(interestPeriod);
repaymentPeriod.getInterestPeriods().subList(index + 1, repaymentPeriod.getInterestPeriods().size()).clear();
int nextIdx = index + 1;
boolean thereIsInterestPeriodFromDateOnTargetDate = repaymentPeriod.getInterestPeriods().size() > nextIdx
&& repaymentPeriod.getInterestPeriods().get(nextIdx).getFromDate().isEqual(targetDate);
if (thereIsInterestPeriodFromDateOnTargetDate) {
// NOTE: If there is a next interest period with fromDate on the target date
// then the related chargeback amounts comes from the next interest period too.
InterestPeriod nextInterestPeriod = repaymentPeriod.getInterestPeriods().get(nextIdx);
interestPeriod.addChargebackPrincipalAmount(nextInterestPeriod.getChargebackPrincipal());
interestPeriod.addChargebackInterestAmount(nextInterestPeriod.getChargebackInterest());
}
repaymentPeriod.getInterestPeriods().subList(nextIdx, repaymentPeriod.getInterestPeriods().size()).clear();
scheduleModelCopy.repaymentPeriods().forEach(rp -> rp.getInterestPeriods().removeIf(ip -> ip.getDueDate().isAfter(targetDate)));
calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy);
calculateOutstandingBalance(scheduleModelCopy);
@@ -338,19 +395,21 @@ public final class ProgressiveEMICalculator implements EMICalculator {
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::getEmi).reduce(scheduleModel.zero(),
Money totalEMI = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getEmiPlusChargeback).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
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)) // 100
.plus(scheduleModel.getTotalChargebackPrincipal(), mc); //
Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc);
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
.reduce((first, second) -> second);
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> {
repaymentPeriod.setEmi(repaymentPeriod.getEmi().add(diff, mc));
if (repaymentPeriod.getEmi().isLessThan(repaymentPeriod.getTotalPaidAmount())) {
repaymentPeriod.setEmi(repaymentPeriod.getTotalPaidAmount());
if (repaymentPeriod.getEmi()
.isLessThan(repaymentPeriod.getTotalPaidAmount().minus(repaymentPeriod.getTotalChargebackAmount(), mc))) {
repaymentPeriod.setEmi(repaymentPeriod.getTotalPaidAmount().minus(repaymentPeriod.getTotalChargebackAmount(), mc));
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
}
});
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
package org.apache.fineract.portfolio.loanproduct.calc.data;
import java.util.List;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanproduct.calc;
package org.apache.fineract.portfolio.loanproduct.calc.data;
import java.math.BigDecimal;
import java.time.LocalDate;
@@ -16,13 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
package org.apache.fineract.portfolio.loanproduct.calc.data;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.Optional;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@@ -35,11 +36,13 @@ import org.apache.fineract.organisation.monetary.domain.Money;
@Getter
@ToString(exclude = { "repaymentPeriod" })
@EqualsAndHashCode(exclude = { "repaymentPeriod" })
@AllArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class InterestPeriod implements Comparable<InterestPeriod> {
private final RepaymentPeriod repaymentPeriod;
private final LocalDate fromDate;
@Setter
@NotNull
private LocalDate fromDate;
@Setter
@NotNull
private LocalDate dueDate;
@@ -47,17 +50,36 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
private BigDecimal rateFactor;
@Setter
private BigDecimal rateFactorTillPeriodDueDate;
private Money chargebackPrincipal;
private Money chargebackInterest;
private Money disbursementAmount;
private Money balanceCorrectionAmount;
private Money outstandingLoanBalance;
private final MathContext mc;
private final boolean isPaused;
public InterestPeriod(RepaymentPeriod repaymentPeriod, InterestPeriod interestPeriod) {
this(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(), interestPeriod.getRateFactor(),
interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getDisbursementAmount(),
interestPeriod.getBalanceCorrectionAmount(), interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(),
interestPeriod.isPaused());
public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod) {
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getChargebackPrincipal(),
interestPeriod.getChargebackInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), interestPeriod.isPaused());
}
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
LocalDate dueDate) {
final Money zero = repaymentPeriod.getEmi().zero();
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
zero.getMc(), false);
}
public static InterestPeriod withPausedAndEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
LocalDate dueDate) {
final Money zero = repaymentPeriod.getEmi().zero();
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
zero.getMc(), true);
}
@Override
@@ -73,19 +95,28 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
this.disbursementAmount = MathUtil.plus(this.disbursementAmount, disbursementAmount, mc);
}
public void addChargebackPrincipalAmount(final Money chargebackPrincipal) {
this.chargebackPrincipal = MathUtil.plus(this.chargebackPrincipal, chargebackPrincipal, mc);
}
public void addChargebackInterestAmount(final Money chargebackInterest) {
this.chargebackInterest = MathUtil.plus(this.chargebackInterest, chargebackInterest, mc);
}
public Money getCalculatedDueInterest() {
if (isPaused) {
return Money.zero(outstandingLoanBalance.getCurrencyData(), mc);
return chargebackInterest;
}
long lengthTillPeriodDueDate = getLengthTillPeriodDueDate();
final BigDecimal interestDueTillRepaymentDueDate = lengthTillPeriodDueDate == 0 //
final Money interestDueTillRepaymentDueDate = Money.of(outstandingLoanBalance.getCurrencyData(), lengthTillPeriodDueDate == 0 //
? BigDecimal.ZERO //
: getOutstandingLoanBalance().getAmount() //
.multiply(getRateFactorTillPeriodDueDate(), mc) //
.divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) //
.multiply(BigDecimal.valueOf(getLength()), mc); //
return Money.of(outstandingLoanBalance.getCurrencyData(), interestDueTillRepaymentDueDate, mc);
.multiply(BigDecimal.valueOf(getLength()), mc),
mc); //
return MathUtil.plus(chargebackInterest, interestDueTillRepaymentDueDate, mc);
}
public long getLength() {
@@ -116,6 +147,13 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
}
}
/**
* Include principal like amounts (all disbursement amount + chargeback principal)
*/
public Money getCreditedAmounts() {
return getDisbursementAmount().plus(getChargebackPrincipal(), mc);
}
public boolean isFirstInterestPeriod() {
return this.equals(getRepaymentPeriod().getFirstInterestPeriod());
}
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
package org.apache.fineract.portfolio.loanproduct.calc.data;
import java.math.BigDecimal;
import java.time.LocalDate;
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
package org.apache.fineract.portfolio.loanproduct.calc.data;
import lombok.Data;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
package org.apache.fineract.portfolio.loanproduct.calc.data;
import lombok.Data;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
package org.apache.fineract.portfolio.loanproduct.calc.data;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod;
@@ -147,7 +147,7 @@ public class ProgressiveLoanInterestScheduleModel {
return 0;
}
final RepaymentPeriod firstPeriod = repaymentPeriods.get(0);
final RepaymentPeriod lastPeriod = repaymentPeriods.size() > 1 ? repaymentPeriods.get(repaymentPeriods.size() - 1) : firstPeriod;
final RepaymentPeriod lastPeriod = repaymentPeriods.size() > 1 ? getLastRepaymentPeriod() : firstPeriod;
return DateUtils.getExactDifferenceInDays(firstPeriod.getFromDate(), lastPeriod.getDueDate());
}
@@ -156,7 +156,7 @@ public class ProgressiveLoanInterestScheduleModel {
}
public LocalDate getMaturityDate() {
return !repaymentPeriods.isEmpty() ? repaymentPeriods.get(repaymentPeriods.size() - 1).getDueDate() : null;
return !repaymentPeriods.isEmpty() ? getLastRepaymentPeriod().getDueDate() : null;
}
public Optional<RepaymentPeriod> changeOutstandingBalanceAndUpdateInterestPeriods(final LocalDate balanceChangeDate,
@@ -201,7 +201,10 @@ public class ProgressiveLoanInterestScheduleModel {
private Consumer<RepaymentPeriod> updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount,
final Money correctionAmount) {
return repaymentPeriod -> {
final Optional<InterestPeriod> interestPeriodOptional = findInterestPeriodForBalanceChange(repaymentPeriod, balanceChangeDate);
final boolean isChangeOnMaturityDate = isLastRepaymentPeriod(repaymentPeriod)
&& balanceChangeDate.isEqual(repaymentPeriod.getDueDate());
final Optional<InterestPeriod> interestPeriodOptional = findInterestPeriodForBalanceChange(repaymentPeriod, balanceChangeDate,
isChangeOnMaturityDate);
if (interestPeriodOptional.isPresent()) {
interestPeriodOptional.get().addDisbursementAmount(disbursedAmount);
interestPeriodOptional.get().addBalanceCorrectionAmount(correctionAmount);
@@ -212,10 +215,16 @@ public class ProgressiveLoanInterestScheduleModel {
}
private Optional<InterestPeriod> findInterestPeriodForBalanceChange(final RepaymentPeriod repaymentPeriod,
final LocalDate balanceChangeDate) {
final LocalDate balanceChangeDate, final boolean isChangeOnMaturityDate) {
if (repaymentPeriod == null || balanceChangeDate == null) {
return Optional.empty();
}
// We want to create a 0 length interest period (if not existed yet) for any credit activity occurs on maturity
// date
if (isChangeOnMaturityDate) {
var lastInterestPeriod = repaymentPeriod.getLastInterestPeriod();
return lastInterestPeriod.getLength() == 0 ? Optional.of(lastInterestPeriod) : Optional.empty();
}
return repaymentPeriod.getInterestPeriods().stream()//
.filter(interestPeriod -> balanceChangeDate.isEqual(interestPeriod.getDueDate()))//
.findFirst();
@@ -231,8 +240,7 @@ public class ProgressiveLoanInterestScheduleModel {
previousInterestPeriod.addDisbursementAmount(disbursedAmount);
previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount);
final InterestPeriod interestPeriod = new InterestPeriod(repaymentPeriod, newDueDate, originalDueDate, BigDecimal.ZERO,
BigDecimal.ZERO, zero, zero, zero, mc, false);
final InterestPeriod interestPeriod = InterestPeriod.withEmptyAmounts(repaymentPeriod, newDueDate, originalDueDate);
repaymentPeriod.getInterestPeriods().add(interestPeriod);
}
@@ -248,24 +256,20 @@ public class ProgressiveLoanInterestScheduleModel {
newInterestPeriods.add(interestPeriod);
} else {
if (interestPeriod.getFromDate().isBefore(finalPauseStart)) {
final InterestPeriod leftSlice = new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), finalPauseStart,
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(),
interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), false);
final InterestPeriod leftSlice = InterestPeriod.copy(repaymentPeriod, interestPeriod);
leftSlice.setDueDate(finalPauseStart);
newInterestPeriods.add(leftSlice);
}
if (interestPeriod.getDueDate().isAfter(finalPauseEnd)) {
final InterestPeriod rightSlice = new InterestPeriod(repaymentPeriod, finalPauseEnd, interestPeriod.getDueDate(),
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(),
interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), false);
final InterestPeriod rightSlice = InterestPeriod.copy(repaymentPeriod, interestPeriod);
rightSlice.setFromDate(finalPauseEnd);
newInterestPeriods.add(rightSlice);
}
}
}
final InterestPeriod pausedSlice = new InterestPeriod(repaymentPeriod, finalPauseStart, finalPauseEnd, BigDecimal.ZERO,
BigDecimal.ZERO, zero, zero, zero, mc, true);
final InterestPeriod pausedSlice = InterestPeriod.withPausedAndEmptyAmounts(repaymentPeriod, finalPauseStart, finalPauseEnd);
newInterestPeriods.add(pausedSlice);
newInterestPeriods.sort(Comparator.comparing(InterestPeriod::getFromDate));
@@ -275,7 +279,7 @@ public class ProgressiveLoanInterestScheduleModel {
private InterestPeriod findPreviousInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate date) {
if (date.isAfter(repaymentPeriod.getFromDate())) {
return repaymentPeriod.getInterestPeriods().get(repaymentPeriod.getInterestPeriods().size() - 1);
return repaymentPeriod.getLastInterestPeriod();
} else {
return repaymentPeriod.getInterestPeriods().stream()
.filter(ip -> date.isAfter(ip.getFromDate()) && !date.isAfter(ip.getDueDate())).reduce((first, second) -> second)
@@ -283,30 +287,65 @@ public class ProgressiveLoanInterestScheduleModel {
}
}
/**
* Gives back the total due interest amount in the whole repayment schedule. Also includes chargeback interest
* amount.
*
* @return
*/
public Money getTotalDueInterest() {
return repaymentPeriods().stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest))
.reduce(zero(), Money::plus);
}
/**
* Gives back the total due principal amount in the whole repayment schedule based on disbursements. Do not contain
* chargeback principal amount.
*
* @return
*/
public Money getTotalDuePrincipal() {
return repaymentPeriods.stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
.reduce(zero(), Money::plus);
return repaymentPeriods.stream().map(RepaymentPeriod::getCreditedAmounts).reduce(zero(), Money::plus);
}
/**
* Gives back the total paid interest amount in the whole repayment schedule.
*
* @return
*/
public Money getTotalPaidInterest() {
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidInterest).reduce(zero, Money::plus);
}
/**
* Gives back the total paid principal amount in the whole repayment schedule.
*
* @return
*/
public Money getTotalPaidPrincipal() {
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidPrincipal).reduce(zero, Money::plus);
}
/**
* Gives back the total chargeback principal amount in the whole repayment schedule.
*
* @return
*/
public Money getTotalChargebackPrincipal() {
return repaymentPeriods().stream().map(RepaymentPeriod::getChargebackPrincipal).reduce(zero, Money::plus);
}
public Optional<RepaymentPeriod> findRepaymentPeriod(@NotNull LocalDate transactionDate) {
return repaymentPeriods.stream() //
.filter(period -> isInPeriod(transactionDate, period.getFromDate(), period.getDueDate(), period.isFirstRepaymentPeriod()))//
.findFirst();
}
/**
* Check if there is a disbursement in the model.
*
* @return
*/
public boolean isEmpty() {
return repaymentPeriods.stream() //
.filter(rp -> !rp.getEmi().isZero()) //
@@ -314,6 +353,15 @@ public class ProgressiveLoanInterestScheduleModel {
.isEmpty(); //
}
@NotNull
public RepaymentPeriod getLastRepaymentPeriod() {
return repaymentPeriods.get(repaymentPeriods.size() - 1);
}
public boolean isLastRepaymentPeriod(@NotNull RepaymentPeriod repaymentPeriod) {
return getLastRepaymentPeriod().equals(repaymentPeriod);
}
/**
* This method gives you repayment pairs to copy attributes.
*
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
package org.apache.fineract.portfolio.loanproduct.calc.data;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod;
@@ -74,8 +74,7 @@ public class RepaymentPeriod {
this.mc = mc;
this.interestPeriods = new ArrayList<>();
// There is always at least 1 interest period, by default with same from-due date as repayment period
getInterestPeriods().add(new InterestPeriod(this, getFromDate(), getDueDate(), BigDecimal.ZERO, BigDecimal.ZERO, getZero(mc),
getZero(mc), getZero(mc), mc, false));
getInterestPeriods().add(InterestPeriod.withEmptyAmounts(this, getFromDate(), getDueDate()));
this.paidInterest = getZero(mc);
this.paidPrincipal = getZero(mc);
}
@@ -92,7 +91,7 @@ public class RepaymentPeriod {
this.mc = mc;
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
interestPeriods.add(new InterestPeriod(this, interestPeriod));
interestPeriods.add(InterestPeriod.copy(this, interestPeriod));
}
}
@@ -100,6 +99,11 @@ public class RepaymentPeriod {
return Optional.ofNullable(previous);
}
/**
* This method gives back sum of (Rate Factor +1) from the interest periods
*
* @return
*/
public BigDecimal getRateFactorPlus1() {
if (rateFactorPlus1Calculation == null) {
rateFactorPlus1Calculation = Memo.of(this::calculateRateFactorPlus1, () -> this.interestPeriods);
@@ -111,6 +115,11 @@ public class RepaymentPeriod {
return interestPeriods.stream().map(InterestPeriod::getRateFactor).reduce(BigDecimal.ONE, BigDecimal::add);
}
/**
* Gives back calculated due interest + chargeback interest
*
* @return
*/
@NotNull
public Money getCalculatedDueInterest() {
if (calculatedDueInterestCalculation == null) {
@@ -129,6 +138,11 @@ public class RepaymentPeriod {
return calculatedDueInterest;
}
/**
* Gives back due interest + chargeback interest OR paid interest
*
* @return
*/
public Money getDueInterest() {
if (dueInterestCalculation == null) {
// Due interest might be the maximum paid if there is pay-off or early repayment
@@ -139,15 +153,70 @@ public class RepaymentPeriod {
return dueInterestCalculation.get();
}
public Money getCalculatedDuePrincipal() {
return getEmi().minus(getCalculatedDueInterest(), mc);
/**
* Gives back an EMI amount which includes chargeback amounts as well
*
* @return
*/
public Money getEmiPlusChargeback() {
return getEmi().plus(getTotalChargebackAmount(), mc); //
}
/**
* Gives back principal due + charge back principal based on (EMI - Calculated Due Interest)
*
* @return
*/
public Money getCalculatedDuePrincipal() {
return getEmiPlusChargeback().minus(getCalculatedDueInterest(), mc);
}
/**
* Sum of chargeback principals
*
* @return
*/
public Money getChargebackPrincipal() {
return interestPeriods.stream() //
.map(InterestPeriod::getChargebackPrincipal) //
.reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)); //
}
/**
* Sum of chargeback interests
*
* @return
*/
public Money getChargebackInterest() {
return interestPeriods.stream() //
.map(InterestPeriod::getChargebackInterest) //
.reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)); //
}
/**
* Gives back due principal + chargeback principal or paid principal
*
* @return
*/
public Money getDuePrincipal() {
// Due principal might be the maximum paid if there is pay-off or early repayment
return MathUtil.max(getEmi().minus(getDueInterest(), mc), getPaidPrincipal(), false);
return MathUtil.max(getEmiPlusChargeback().minus(getDueInterest(), mc), getPaidPrincipal(), false);
}
/**
* Gives back sum of all chargeback principal + chargeback interest
*
* @return
*/
public Money getTotalChargebackAmount() {
return getChargebackPrincipal().plus(getChargebackInterest(), mc);
}
/**
* Total paid amounts has everything: paid principal + paid interest + paid charge principal + paid charge interest
*
* @return
*/
public Money getTotalPaidAmount() {
return getPaidPrincipal().plus(getPaidInterest());
}
@@ -156,10 +225,20 @@ public class RepaymentPeriod {
return getEmi().isEqualTo(getTotalPaidAmount());
}
/**
* This method counts those interest amounts when there is no place in EMI. Which typically can happen if there is a
* not full paid early repayment. In this case we can count in the next repayment period.
*
* @return
*/
public Money getUnrecognizedInterest() {
return getCalculatedDueInterest().minus(getDueInterest(), mc);
}
public Money getCreditedAmounts() {
return interestPeriods.stream().map(InterestPeriod::getCreditedAmounts).reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc));
}
public Money getOutstandingLoanBalance() {
if (outstandingBalanceCalculation == null) {
outstandingBalanceCalculation = Memo.of(() -> {
@@ -190,8 +269,9 @@ public class RepaymentPeriod {
} else {
initialBalance = getZero(mc);
}
Money totalDisbursedAmount = getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount).reduce(getZero(mc),
(m1, m2) -> m1.plus(m2, mc));
Money totalDisbursedAmount = getInterestPeriods().stream() //
.map(InterestPeriod::getDisbursementAmount) //
.reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); //
return initialBalance.add(totalDisbursedAmount, mc);
}
@@ -67,9 +67,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationT
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
@@ -266,11 +266,11 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void testProcessCreditTransactionWithAllocationRulePrincipalPenaltyFeeInterest() {
// given
Loan loan = mock(Loan.class);
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
LoanTransaction chargebackTransaction = createChargebackTransaction(loan, 25.0);
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST);
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
LoanTransaction repayment = createRepayment(loan, chargebackTransaction, 10, 0, 20, 5);
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
@@ -280,7 +280,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
// when
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
underTest.processCreditTransaction(chargeBackTransaction, ctx);
underTest.processCreditTransaction(chargebackTransaction, ctx);
// verify principal
Mockito.verify(installment, times(1)).addToCreditedPrincipal(new BigDecimal("10.00"));
@@ -311,7 +311,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
Mockito.verify(chargebackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
penalty.capture());
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
@@ -323,11 +323,11 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void testProcessCreditTransactionWithAllocationRulePenaltyFeePrincipalInterest() {
// given
Loan loan = mock(Loan.class);
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
LoanTransaction chargebackTransaction = createChargebackTransaction(loan, 25.0);
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PENALTY, FEE, PRINCIPAL, INTEREST);
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
LoanTransaction repayment = createRepayment(loan, chargebackTransaction, 10, 0, 20, 5);
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
@@ -337,7 +337,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
// when
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
underTest.processCreditTransaction(chargeBackTransaction, ctx);
underTest.processCreditTransaction(chargebackTransaction, ctx);
// verify charges on installment
Mockito.verify(installment, times(1)).addToCreditedFee(new BigDecimal("20.00"));
@@ -360,7 +360,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
Mockito.verify(chargebackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
penalty.capture());
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(0)));
assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.valueOf(0)));
@@ -372,11 +372,11 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWithAdditionalInstallment() {
// given
Loan loan = mock(Loan.class);
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
LoanTransaction chargebackTransaction = createChargebackTransaction(loan, 25.0);
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST);
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
LoanTransaction repayment = createRepayment(loan, chargebackTransaction, 10, 0, 20, 5);
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
@@ -389,7 +389,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
// when
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder, null);
underTest.processCreditTransaction(chargeBackTransaction, ctx);
underTest.processCreditTransaction(chargebackTransaction, ctx);
// verify principal
Mockito.verify(installment2, times(1)).addToCreditedPrincipal(new BigDecimal("10.00"));
@@ -420,7 +420,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
Mockito.verify(chargebackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
penalty.capture());
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
@@ -35,16 +35,17 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -1145,7 +1146,7 @@ class ProgressiveEMICalculatorTest {
}
@Test
public void test_disbursedAmt1000_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() {
public void test_disbursedAmt1000_actual_actual_repayEvery1Month_verify_due_principal_amounts() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
@@ -1264,6 +1265,73 @@ class ProgressiveEMICalculatorTest {
checkDailyInterest(interestModel, dueDate, startDay, 31, 0.18, 5.83);
}
@Test
public void test_dailyInterest_chargeback_disbursedAmt1000_dayInYears360_daysInMonth30_repayIn1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(1000.0);
emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestModel, 0, 0, 1005.83, 0.0, 0.0, 5.83, 1000.0, 0.0);
checkPeriod(interestModel, 0, 1, 1005.83, 0.005833333333, 5.83, 1000.0, 0.0);
final LocalDate dueDate = LocalDate.of(2024, 2, 1);
final LocalDate startDay = LocalDate.of(2024, 1, 1);
// TODO: work on interest calculation
// emiCalculator.payInterest(interestModel, dueDate, startDay.plusDays(3), toMoney(0.56));
// emiCalculator.chargebackInterest(interestModel, startDay.plusDays(3), toMoney(0.0));
// emiCalculator.addBalanceCorrection(interestModel, startDay.plusDays(3), toMoney(0.0));
checkDailyInterest(interestModel, dueDate, startDay, 1, 0.19, 0.19);
checkDailyInterest(interestModel, dueDate, startDay, 2, 0.19, 0.38);
checkDailyInterest(interestModel, dueDate, startDay, 3, 0.18, 0.56);
checkDailyInterest(interestModel, dueDate, startDay, 4, 0.19, 0.75);
checkDailyInterest(interestModel, dueDate, startDay, 5, 0.19, 0.94);
checkDailyInterest(interestModel, dueDate, startDay, 6, 0.19, 1.13);
checkDailyInterest(interestModel, dueDate, startDay, 7, 0.19, 1.32);
checkDailyInterest(interestModel, dueDate, startDay, 8, 0.19, 1.51);
checkDailyInterest(interestModel, dueDate, startDay, 9, 0.18, 1.69);
checkDailyInterest(interestModel, dueDate, startDay, 10, 0.19, 1.88);
checkDailyInterest(interestModel, dueDate, startDay, 11, 0.19, 2.07);
checkDailyInterest(interestModel, dueDate, startDay, 12, 0.19, 2.26);
checkDailyInterest(interestModel, dueDate, startDay, 13, 0.19, 2.45);
checkDailyInterest(interestModel, dueDate, startDay, 14, 0.18, 2.63);
checkDailyInterest(interestModel, dueDate, startDay, 15, 0.19, 2.82);
checkDailyInterest(interestModel, dueDate, startDay, 16, 0.19, 3.01);
checkDailyInterest(interestModel, dueDate, startDay, 17, 0.19, 3.20);
checkDailyInterest(interestModel, dueDate, startDay, 18, 0.19, 3.39);
checkDailyInterest(interestModel, dueDate, startDay, 19, 0.19, 3.58);
checkDailyInterest(interestModel, dueDate, startDay, 20, 0.18, 3.76);
checkDailyInterest(interestModel, dueDate, startDay, 21, 0.19, 3.95);
checkDailyInterest(interestModel, dueDate, startDay, 22, 0.19, 4.14);
checkDailyInterest(interestModel, dueDate, startDay, 23, 0.19, 4.33);
checkDailyInterest(interestModel, dueDate, startDay, 24, 0.19, 4.52);
checkDailyInterest(interestModel, dueDate, startDay, 25, 0.18, 4.7);
checkDailyInterest(interestModel, dueDate, startDay, 26, 0.19, 4.89);
checkDailyInterest(interestModel, dueDate, startDay, 27, 0.19, 5.08);
checkDailyInterest(interestModel, dueDate, startDay, 28, 0.19, 5.27);
checkDailyInterest(interestModel, dueDate, startDay, 29, 0.19, 5.46);
checkDailyInterest(interestModel, dueDate, startDay, 30, 0.19, 5.65);
checkDailyInterest(interestModel, dueDate, startDay, 31, 0.18, 5.83);
}
@Test
public void test_dailyInterest_disbursedAmt2000_dayInYears360_daysInMonth30_repayIn2Month() {
@@ -2216,6 +2284,530 @@ class ProgressiveEMICalculatorTest {
checkPeriod(interestSchedule, 5, 867.65, 10.03, 857.62, 0.00, false);
}
@Nested
class ChargeBackPrincipalAmt100DayInYears360DaysInMonth30RepayEvery1MonthTests {
@Test
public void test_S1_full_chargeback_on_due_date_before_maturity_date() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
// repay 1st period
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(16.43));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(0.58));
// repay 2nd period
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(16.52));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(0.49));
// full chargeback on duedate
emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(17.01));
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 84.06);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.49, 33.53, 50.53);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.81);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 17.00);
checkPeriod(interestSchedule, 5, 0, 17.10, 0.005833333333, 0.1, 17.00, 0.0);
}
@Test
public void test_S2_S3_partial_and_full_chargeback_on_due_date_before_maturity_date() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
// repay 1st period
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(16.43));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(0.58));
// repay 2nd period
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(16.52));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(0.49));
// partial chargeback
emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(15.0));
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 82.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.48, 31.53, 50.52);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.80);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.99);
checkPeriod(interestSchedule, 5, 0, 17.09, 0.005833333333, 0.1, 16.99, 0.0);
// full chargeback
emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), toMoney(17.01));
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 99.06);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.58, 48.44, 50.62);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.3, 16.71, 33.91);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 17.1);
checkPeriod(interestSchedule, 5, 0, 17.20, 0.005833333333, 0.1, 17.1, 0.0);
}
@Test
public void test_S4_full_chargeback_in_middle_of_instalment_before_maturity_date() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
// repay 1st period
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(16.43));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(0.58));
// repay 2nd period
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(16.52));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1), toMoney(0.49));
// full chargeback on duedate
emiCalculator.chargebackPrincipal(interestSchedule, LocalDate.of(2024, 3, 15), toMoney(17.01));
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.002634408602, 0.18, 0.45, 33.57, 50.49);
checkPeriod(interestSchedule, 2, 1, 17.01, 0.003198924731, 0.27, 0.45, 33.57, 50.49);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.77);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.96);
checkPeriod(interestSchedule, 5, 0, 17.06, 0.005833333333, 0.1, 16.96, 0.0);
}
}
@Test
public void test_chargeback_principalAndInterest_Amt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
// repay 1st period
LocalDate txnDate = LocalDate.of(2024, 2, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
// repay 2nd period
txnDate = LocalDate.of(2024, 3, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
// chargeback
txnDate = LocalDate.of(2024, 3, 1);
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52));
emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49));
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 83.57);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.98, 33.04, 50.53);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.81);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 17.0);
checkPeriod(interestSchedule, 5, 0, 17.10, 0.005833333333, 0.1, 17.0, 0.0);
}
@Test
public void test_chargeback_principalAndInterest_Amt100_dayInYears360_daysInMonth30_repayEvery1Month__() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
// repay 1st period
LocalDate txnDate = LocalDate.of(2024, 2, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
// repay 2nd period
txnDate = LocalDate.of(2024, 3, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
// chargeback
txnDate = LocalDate.of(2024, 7, 1);
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52));
emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49));
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 0.59, 33.42, 0.0);
}
@Test
public void test_chargeback_less_principal_Amt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
// repay 1st period
LocalDate txnDate = LocalDate.of(2024, 2, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
// repay 2nd period
txnDate = LocalDate.of(2024, 3, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
// chargeback
txnDate = LocalDate.of(2024, 3, 1);
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(14.51));
emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49));
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 81.56);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.97, 31.04, 50.52);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.80);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.99);
checkPeriod(interestSchedule, 5, 0, 17.09, 0.005833333333, 0.1, 16.99, 0.0);
}
@Test
public void test_chargeback_less_principal_and_no_chargeback_interest_Amt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
// repay 1st period
LocalDate txnDate = LocalDate.of(2024, 2, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
// repay 2nd period
txnDate = LocalDate.of(2024, 3, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
// chargeback
txnDate = LocalDate.of(2024, 3, 1);
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(15.0));
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 82.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.48, 31.53, 50.52);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.80);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.99);
checkPeriod(interestSchedule, 5, 0, 17.09, 0.005833333333, 0.1, 16.99, 0.0);
}
@Test
public void test_multi_chargeback_Amt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
// repay 1st period
LocalDate txnDate = LocalDate.of(2024, 2, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
// repay 2nd period
txnDate = LocalDate.of(2024, 3, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
// chargeback 1st
txnDate = LocalDate.of(2024, 3, 1);
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(15.0));
// chargeback 2nd
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52));
emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49));
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 98.57);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 1.06, 47.96, 50.61);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.30, 16.71, 33.90);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 17.09);
checkPeriod(interestSchedule, 5, 0, 17.19, 0.005833333333, 0.10, 17.09, 0.0);
}
@Test
public void test_s5_chargeback_in_period_Amt100_dayInYears360_daysInMonth30_repayEvery1Month() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)),
repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)),
repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)),
repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)),
repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)),
repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.43);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.71);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 16.81, 16.90);
checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.1, 16.9, 0.0);
// repay 1st period
LocalDate txnDate = LocalDate.of(2024, 2, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.43));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.58));
// repay 2nd period
txnDate = LocalDate.of(2024, 3, 1);
emiCalculator.payPrincipal(interestSchedule, txnDate, txnDate, toMoney(16.52));
emiCalculator.payInterest(interestSchedule, txnDate, txnDate, toMoney(0.49));
// chargeback
txnDate = LocalDate.of(2024, 3, 15);
emiCalculator.chargebackPrincipal(interestSchedule, txnDate, toMoney(16.52));
emiCalculator.chargebackInterest(interestSchedule, txnDate, toMoney(0.49));
PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 4, 1), txnDate);
Assertions.assertEquals(33.08, toDouble(interestSchedule.repaymentPeriods().get(2).getDuePrincipal()));
Assertions.assertEquals(33.35, toDouble(dueAmounts.getDuePrincipal()));
Assertions.assertEquals(0.67, toDouble(dueAmounts.getDueInterest()));
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57);
checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.49, 16.52, 67.05);
checkPeriod(interestSchedule, 2, 0, 17.01, 0.002634408602, 0.18, 0.94, 33.08, 50.49);
checkPeriod(interestSchedule, 2, 1, 17.01, 0.003198924731, 0.76, 0.94, 33.08, 50.49);
checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.77);
checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 16.96);
checkPeriod(interestSchedule, 5, 0, 17.06, 0.005833333333, 0.10, 16.96, 0.0);
}
private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) {
final Money zeroAmount = Money.zero(currency);
return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount,
@@ -2234,11 +2826,10 @@ class ProgressiveEMICalculatorTest {
private static void checkDailyInterest(final ProgressiveLoanInterestScheduleModel interestModel, final LocalDate repaymentPeriodDueDate,
final LocalDate interestStartDay, final int dayOffset, final double dailyInterest, final double interest) {
Money previousInterest = emiCalculator
.getDueAmounts(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset - 1)).getDueInterest();
Money currentInterest = emiCalculator.getDueAmounts(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset))
.getDueInterest();
Money previousInterest = emiCalculator.getPeriodInterestTillDate(interestModel, repaymentPeriodDueDate,
interestStartDay.plusDays(dayOffset - 1), true);
Money currentInterest = emiCalculator.getPeriodInterestTillDate(interestModel, repaymentPeriodDueDate,
interestStartDay.plusDays(dayOffset), true);
Assertions.assertEquals(dailyInterest, toDouble(currentInterest.minus(previousInterest)));
Assertions.assertEquals(interest, toDouble(currentInterest));
}
@@ -2275,7 +2866,11 @@ class ProgressiveEMICalculatorTest {
private static void checkPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx,
final int interestIdx, final double emiValue, final double rateFactor, final double interestDue,
final double interestDueCumulated, final double principalDue, final double remaingBalance) {
Assertions.assertTrue(repaymentIdx < interestScheduleModel.repaymentPeriods().size(),
repaymentIdx + "th repaymentPeriod is not found.");
final RepaymentPeriod repaymentPeriod = interestScheduleModel.repaymentPeriods().get(repaymentIdx);
Assertions.assertTrue(interestIdx < repaymentPeriod.getInterestPeriods().size(),
repaymentIdx + "th repaymentPeriod's " + interestIdx + "th interest period is not found.");
final InterestPeriod interestPeriod = repaymentPeriod.getInterestPeriods().get(interestIdx);
Assertions.assertAll("Check period", () -> Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEmi())),
@@ -1635,7 +1635,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed",
"Loan transaction:" + transactionId + " chargeback not allowed as loan status is written off", transactionId);
}
if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isProgressiveSchedule()) {
throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed",
"Loan transaction:" + transactionId + " chargeback not allowed as loan product is interest recalculation enabled",
transactionId);
@@ -1672,10 +1672,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LoanTransactionRelationTypeEnum.CHARGEBACK);
this.loanTransactionRelationRepository.save(loanTransactionRelation);
newTransaction = this.loanTransactionRepository.saveAndFlush(newTransaction);
handleChargebackTransaction(loan, newTransaction, loanLifecycleStateMachine);
newTransaction = this.loanTransactionRepository.saveAndFlush(newTransaction);
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed(LoanApiConstants.noteParamName);
@@ -3423,15 +3423,21 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.build();
}
public void handleChargebackTransaction(final Loan loan, final LoanTransaction chargebackTransaction,
public void handleChargebackTransaction(final Loan loan, LoanTransaction chargebackTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor();
loan.addLoanTransaction(chargebackTransaction);
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction, new TransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled()) {
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(),
loan.retrieveListOfTransactionsForReprocessing(), loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges());
} else {
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
}
loan.updateLoanSummaryDerivedFields();
if (!loan.doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGEBACK, loan);
@@ -41,10 +41,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.starter.AdvancedPaymentScheduleTransactionProcessorCondition;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
@@ -36,9 +36,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.springframework.stereotype.Component;
@Component
@@ -65,6 +65,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdStatus;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
import org.apache.fineract.client.models.JournalEntryTransactionItem;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostChargesResponse;
@@ -268,6 +269,25 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
Assertions.assertTrue(actualValue);
}
protected GetLoansLoanIdTransactionsTemplateResponse getPrepayAmount(Long loanId, String date) {
return ok(fineractClient().loanTransactions.retrieveTransactionTemplate(loanId, "prepayLoan", DATETIME_PATTERN, date, "en"));
}
protected Long verifyPrepayAmountByRepayment(Long loanId, String date) {
GetLoansLoanIdTransactionsTemplateResponse prepayAmount = getPrepayAmount(loanId, date);
Double amountToPrepayLoan = prepayAmount.getAmount();
Long repaymentId = null;
if (amountToPrepayLoan != null && amountToPrepayLoan > 0) {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "repayment", date,
amountToPrepayLoan);
Assertions.assertNotNull(repayment);
Assertions.assertNotNull(repayment.getResourceId());
repaymentId = repayment.getResourceId();
}
verifyLoanStatus(loanId, LoanStatus.CLOSED_OBLIGATIONS_MET);
return repaymentId;
}
private String getNonByPassUserAuthKey(RequestSpecification requestSpec, ResponseSpecification responseSpec) {
// creates the user
UserHelper.getSimpleUserWithoutBypassPermission(requestSpec, responseSpec);
@@ -673,6 +693,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
}
protected void disburseLoan(Long loanId, BigDecimal amount, String date) {
log.info("Disbursing loan with id {} with amount {}", loanId, amount);
loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate(date).dateFormat(DATETIME_PATTERN)
.transactionAmount(amount).locale("en"));
}
@@ -1110,7 +1131,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
}
protected Installment unpaidInstallment(double principalAmount, double interestAmount, String dueDate) {
Double amount = principalAmount + interestAmount;
Double amount = BigDecimal.valueOf(principalAmount).add(BigDecimal.valueOf(interestAmount)).doubleValue();
return new Installment(principalAmount, interestAmount, null, null, amount, false, dueDate, null, null);
}
@@ -1161,6 +1182,10 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
assertEquals(actualMaturityDate, loanDetails.getTimeline().getActualMaturityDate());
}
protected void verifyLoanStatus(GetLoansLoanIdResponse loanDetails, LoanStatus loanStatus) {
assertEquals(loanStatus.getCode(), loanDetails.getStatus().getCode());
}
protected void verifyLoanStatus(long loanId, LoanStatus loanStatus) {
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
@@ -30,12 +30,17 @@ import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.CreditAllocationData;
import org.apache.fineract.client.models.CreditAllocationOrder;
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
import org.apache.fineract.client.models.GetDelinquencyRangesResponse;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
@@ -45,6 +50,8 @@ import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
@@ -58,14 +65,20 @@ import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuil
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.experimental.runners.Enclosed;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.runner.RunWith;
@RunWith(Enclosed.class)
@Slf4j
public class LoanTransactionChargebackTest extends BaseLoanIntegrationTest {
@@ -80,6 +93,7 @@ public class LoanTransactionChargebackTest extends BaseLoanIntegrationTest {
private final String amountVal = "1000";
private LocalDate todaysDate;
private String operationDate;
private static Long clientId;
@BeforeEach
public void setup() {
@@ -93,6 +107,8 @@ public class LoanTransactionChargebackTest extends BaseLoanIntegrationTest {
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
this.journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
this.accountHelper = new AccountHelper(requestSpec, responseSpec);
PostClientsResponse client = new ClientHelper(requestSpec, responseSpec).createClient(ClientHelper.defaultClientCreationRequest());
clientId = client.getResourceId();
this.todaysDate = Utils.getLocalDateOfTenant();
this.operationDate = Utils.dateFormatter.format(this.todaysDate);
@@ -592,6 +608,789 @@ public class LoanTransactionChargebackTest extends BaseLoanIntegrationTest {
}
}
@Nested
public class ProgressiveInterestBearingLoanWithInterestRecalculationTest {
Long applyApproveDisburseLoan(Long loanProductId) {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(clientId, loanProductId, "1 January 2024", 100.0, 7.0, 6, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(100.0), "01 January 2024");
});
return loanIdRef.get();
}
List<CreditAllocationData> chargebackCreditAllocationOrders(List<String> allocationIds) {
List<CreditAllocationOrder> creditAllocationOrders = new ArrayList<>(allocationIds.size());
for (int i = 0; i < allocationIds.size(); i++) {
String allocationId = allocationIds.get(i);
creditAllocationOrders.add(new CreditAllocationOrder().order(i + 1).creditAllocationRule(allocationId));
}
return List.of(new CreditAllocationData().transactionType("CHARGEBACK").creditAllocationOrder(creditAllocationOrders));
}
@Nested
public class WithoutChargebackAllocation {
final PostLoanProductsResponse loanProductWithoutChargebackAllocation = loanProductHelper
.createLoanProduct(create4IProgressive().isInterestRecalculationEnabled(true).daysInYearType(DaysInYearType.DAYS_360)
.daysInMonthType(DaysInMonthType.DAYS_30));
@Test
public void testS1FullChargebackBeforeMaturityDate() {
final Long loanId = applyApproveDisburseLoan(loanProductWithoutChargebackAllocation.getResourceId());
runAt("1 February 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 February 2024", 17.01);
});
runAt("1 March 2024", () -> {
Long repaymentId = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01).getResourceId();
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(16.62, 0.39, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.9, 0.10, "01 July 2024") //
); //
addChargebackForLoan(loanId, repaymentId, 17.01);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(33.53, 0.49, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(17.0, 0.10, "01 July 2024") //
); //
Long prepayId = verifyPrepayAmountByRepayment(loanId, "1 March 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, prepayId, "1 March 2024");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
verifyLoanStatus(loanDetails, LoanStatus.ACTIVE);
});
}
@Test
public void testS2AndS3PartialChargebackThenFullChargebackBeforeMaturityDate() {
final Long loanId = applyApproveDisburseLoan(loanProductWithoutChargebackAllocation.getResourceId());
AtomicReference<Long> repaymentFebruaryRef = new AtomicReference<>();
runAt("1 February 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 February 2024", 17.01);
repaymentFebruaryRef.set(repayment.getResourceId());
});
runAt("1 March 2024", () -> {
Long repaymentId = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01).getResourceId();
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(16.62, 0.39, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.9, 0.10, "01 July 2024") //
); //
addChargebackForLoan(loanId, repaymentFebruaryRef.get(), 15.0);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(31.53, 0.48, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.99, 0.10, "01 July 2024") //
); //
addChargebackForLoan(loanId, repaymentId, 17.01);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(48.44, 0.58, "01 April 2024"), //
unpaidInstallment(16.71, 0.30, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(17.10, 0.10, "01 July 2024") //
); //
Long prepayId = verifyPrepayAmountByRepayment(loanId, "1 March 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, prepayId, "1 March 2024");
});
}
@Test
public void testS4FullChargebackMiddleOfRepaymentPeriodBeforeMaturityDate() {
final Long loanId = applyApproveDisburseLoan(loanProductWithoutChargebackAllocation.getResourceId());
AtomicReference<Long> repaymentMarchId = new AtomicReference<>();
runAt("1 February 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 February 2024", 17.01);
});
runAt("1 March 2024", () -> {
repaymentMarchId
.set(loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01).getResourceId());
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(16.62, 0.39, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.9, 0.10, "01 July 2024") //
); //
});
runAt("15 March 2024", () -> {
addChargebackForLoan(loanId, repaymentMarchId.get(), 17.01);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(33.57, 0.45, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.96, 0.10, "01 July 2024") //
); //
Long repaymentId = verifyPrepayAmountByRepayment(loanId, "15 March 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentId, "15 March 2024");
});
}
@Test
public void testS7ChargebacksOnMaturityDate() {
final Long loanId = applyApproveDisburseLoan(loanProductWithoutChargebackAllocation.getResourceId());
runAt("1 February 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 February 2024", 17.01);
});
runAt("1 March 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01);
});
runAt("1 April 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 April 2024", 17.01);
});
runAt("1 May 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 May 2024", 17.01);
});
AtomicReference<Long> repaymentJuneRef = new AtomicReference<>();
runAt("1 June 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 June 2024", 17.01);
repaymentJuneRef.set(repayment.getResourceId());
});
AtomicReference<Long> repaymentJulyRef = new AtomicReference<>();
runAt("1 July 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 July 2024", 17.00);
repaymentJulyRef.set(repayment.getResourceId());
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
fullyRepaidInstallment(16.9, 0.10, "01 July 2024") //
); //
addChargebackForLoan(loanId, repaymentJulyRef.get(), 17.00);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
installment(33.9, 0.10, 17.0, false, "01 July 2024") //
); //
Long repaymentId = verifyPrepayAmountByRepayment(loanId, "01 July 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentId, "01 July 2024");
});
}
@Test
public void testS5AndS6ChargebacksAfterMaturityDateVerifyNPlus1ThPeriod() {
final Long loanId = applyApproveDisburseLoan(loanProductWithoutChargebackAllocation.getResourceId());
runAt("1 February 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 February 2024", 17.01);
});
runAt("1 March 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01);
});
runAt("1 April 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 April 2024", 17.01);
});
runAt("1 May 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 May 2024", 17.01);
});
AtomicReference<Long> repaymentJuneRef = new AtomicReference<>();
runAt("1 June 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 June 2024", 17.01);
repaymentJuneRef.set(repayment.getResourceId());
});
AtomicReference<Long> repaymentJulyRef = new AtomicReference<>();
runAt("1 July 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 July 2024", 17.00);
repaymentJulyRef.set(repayment.getResourceId());
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
fullyRepaidInstallment(16.9, 0.10, "01 July 2024") //
); //
});
runAt("15 July 2024", () -> {
addChargebackForLoan(loanId, repaymentJuneRef.get(), 17.01);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
fullyRepaidInstallment(16.9, 0.10, "01 July 2024"), //
unpaidInstallment(17.01, 0.0, "15 July 2024") //
); //
});
runAt("30 July 2024", () -> {
addChargebackForLoan(loanId, repaymentJulyRef.get(), 17.00);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
fullyRepaidInstallment(16.9, 0.10, "01 July 2024"), //
unpaidInstallment(34.01, 0.0, "30 July 2024") //
); //
Long repaymentId = verifyPrepayAmountByRepayment(loanId, "30 July 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentId, "30 July 2024");
});
}
}
@Nested
public class WithChargebackAllocationPrincipalInterestFeesPenalties {
final PostLoanProductsResponse loanProductWithChargebackAllocationPrincipalInterestFeesPenalties = loanProductHelper
.createLoanProduct(create4IProgressive().isInterestRecalculationEnabled(true).daysInYearType(DaysInYearType.DAYS_360)
.daysInMonthType(DaysInMonthType.DAYS_30)
.creditAllocation(chargebackCreditAllocationOrders(List.of("PRINCIPAL", "PENALTY", "FEE", "INTEREST"))));
@Test
public void testS1FullChargebackBeforeMaturityDate() {
final Long loanId = applyApproveDisburseLoan(
loanProductWithChargebackAllocationPrincipalInterestFeesPenalties.getResourceId());
runAt("1 February 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 February 2024", 17.01);
});
runAt("1 March 2024", () -> {
Long repaymentId = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01).getResourceId();
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(16.62, 0.39, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.9, 0.10, "01 July 2024") //
); //
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
addChargebackForLoan(loanId, repaymentId, 17.01);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(33.04, 0.98, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(17.0, 0.10, "01 July 2024") //
); //
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Chargeback", "01 March 2024", 83.57, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
Long prepayId = verifyPrepayAmountByRepayment(loanId, "1 March 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, prepayId, "1 March 2024");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
verifyLoanStatus(loanDetails, LoanStatus.ACTIVE);
});
}
@Test
public void testS2AndS3PartialChargebackThenFullChargebackBeforeMaturityDate() {
final Long loanId = applyApproveDisburseLoan(
loanProductWithChargebackAllocationPrincipalInterestFeesPenalties.getResourceId());
AtomicReference<Long> repaymentFebruaryRef = new AtomicReference<>();
runAt("1 February 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 February 2024", 17.01);
repaymentFebruaryRef.set(repayment.getResourceId());
});
runAt("1 March 2024", () -> {
runAt("1 March 2024", () -> {
Long repaymentMarchId = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01)
.getResourceId();
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(16.62, 0.39, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.9, 0.10, "01 July 2024") //
); //
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
addChargebackForLoan(loanId, repaymentFebruaryRef.get(), 15.0);
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(15.00, "Chargeback", "01 March 2024", 82.05, 15.0, 0.0, 0.0, 0.0, 0.0, 0.0, false));
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(31.53, 0.48, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.99, 0.10, "01 July 2024") //
); //
addChargebackForLoan(loanId, repaymentMarchId, 17.01);
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(15.00, "Chargeback", "01 March 2024", 82.05, 15.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Chargeback", "01 March 2024", 98.57, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(47.96, 1.06, "01 April 2024"), //
unpaidInstallment(16.71, 0.30, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(17.09, 0.10, "01 July 2024") //
); //
Long prepayId = verifyPrepayAmountByRepayment(loanId, "1 March 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, prepayId, "1 March 2024");
});
});
}
@Test
public void testS4FullChargebackMiddleOfRepaymentPeriodBeforeMaturityDate() {
final Long loanId = applyApproveDisburseLoan(
loanProductWithChargebackAllocationPrincipalInterestFeesPenalties.getResourceId());
AtomicReference<Long> repaymentMarchId = new AtomicReference<>();
runAt("1 February 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 February 2024", 17.01);
});
runAt("1 March 2024", () -> {
repaymentMarchId
.set(loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01).getResourceId());
});
runAt("15 March 2024", () -> {
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(16.62, 0.39, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.9, 0.10, "01 July 2024") //
);
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
addChargebackForLoan(loanId, repaymentMarchId.get(), 17.01);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(33.08, 0.94, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.96, 0.10, "01 July 2024") //
); //
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Chargeback", "15 March 2024", 83.57, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
Long repaymentId = verifyPrepayAmountByRepayment(loanId, "15 March 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentId, "15 March 2024");
});
}
@Test
public void testS5AndS6ChargebacksAfterMaturityDateVerifyNPlus1ThPeriod() {
final Long loanId = applyApproveDisburseLoan(
loanProductWithChargebackAllocationPrincipalInterestFeesPenalties.getResourceId());
runAt("1 February 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 February 2024", 17.01);
});
runAt("1 March 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01);
});
runAt("1 April 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 April 2024", 17.01);
});
runAt("1 May 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 May 2024", 17.01);
});
AtomicReference<Long> repaymentJuneRef = new AtomicReference<>();
runAt("1 June 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 June 2024", 17.01);
repaymentJuneRef.set(repayment.getResourceId());
});
AtomicReference<Long> repaymentJulyRef = new AtomicReference<>();
runAt("1 July 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 July 2024", 17.00);
repaymentJulyRef.set(repayment.getResourceId());
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
fullyRepaidInstallment(16.9, 0.10, "01 July 2024") //
); //
});
runAt("15 July 2024", () -> {
addChargebackForLoan(loanId, repaymentJuneRef.get(), 17.01);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
fullyRepaidInstallment(16.9, 0.10, "01 July 2024"), //
unpaidInstallment(16.81, 0.2, "15 July 2024") //
); //
});
runAt("30 July 2024", () -> {
addChargebackForLoan(loanId, repaymentJulyRef.get(), 17.00);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
fullyRepaidInstallment(16.9, 0.10, "01 July 2024"), //
unpaidInstallment(33.71, 0.3, "30 July 2024") //
); //
Long repaymentId = verifyPrepayAmountByRepayment(loanId, "30 July 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentId, "30 July 2024");
});
}
}
@Nested
public class WithChargebackAllocationInterestFeesPenaltiesPrincipal {
final PostLoanProductsResponse loanProductWithChargebackAllocationInterestFeesPenaltiesPrincipal = loanProductHelper
.createLoanProduct(create4IProgressive().isInterestRecalculationEnabled(true).daysInYearType(DaysInYearType.DAYS_360)
.daysInMonthType(DaysInMonthType.DAYS_30)
.creditAllocation(chargebackCreditAllocationOrders(List.of("PENALTY", "FEE", "INTEREST", "PRINCIPAL"))));
@Test
public void testS1FullChargebackBeforeMaturityDate() {
final Long loanId = applyApproveDisburseLoan(
loanProductWithChargebackAllocationInterestFeesPenaltiesPrincipal.getResourceId());
runAt("1 February 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 February 2024", 17.01);
});
runAt("1 March 2024", () -> {
Long repaymentId = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01).getResourceId();
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(16.62, 0.39, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.9, 0.10, "01 July 2024") //
); //
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
addChargebackForLoan(loanId, repaymentId, 17.01);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(33.04, 0.98, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(17.0, 0.10, "01 July 2024") //
); //
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Chargeback", "01 March 2024", 83.57, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
Long prepayId = verifyPrepayAmountByRepayment(loanId, "1 March 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, prepayId, "1 March 2024");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
verifyLoanStatus(loanDetails, LoanStatus.ACTIVE);
});
}
@Test
public void testS2AndS3PartialChargebackThenFullChargebackBeforeMaturityDate() {
final Long loanId = applyApproveDisburseLoan(
loanProductWithChargebackAllocationInterestFeesPenaltiesPrincipal.getResourceId());
AtomicReference<Long> repaymentFebruaryRef = new AtomicReference<>();
runAt("1 February 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 February 2024", 17.01);
repaymentFebruaryRef.set(repayment.getResourceId());
});
runAt("1 March 2024", () -> {
Long repaymentMarchId = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01)
.getResourceId();
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(16.62, 0.39, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.9, 0.10, "01 July 2024") //
); //
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
addChargebackForLoan(loanId, repaymentFebruaryRef.get(), 15.0);
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(15.00, "Chargeback", "01 March 2024", 81.47, 14.42, 0.58, 0.0, 0.0, 0.0, 0.0, false));
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(30.95, 1.06, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.99, 0.10, "01 July 2024") //
); //
addChargebackForLoan(loanId, repaymentMarchId, 17.01);
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(15.00, "Chargeback", "01 March 2024", 81.47, 14.42, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Chargeback", "01 March 2024", 97.99, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(47.38, 1.64, "01 April 2024"), //
unpaidInstallment(16.71, 0.30, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(17.09, 0.10, "01 July 2024") //
); //
Long prepayId = verifyPrepayAmountByRepayment(loanId, "1 March 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, prepayId, "1 March 2024");
});
}
@Test
public void testS4FullChargebackMiddleOfRepaymentPeriodBeforeMaturityDate() {
final Long loanId = applyApproveDisburseLoan(
loanProductWithChargebackAllocationInterestFeesPenaltiesPrincipal.getResourceId());
AtomicReference<Long> repaymentMarchId = new AtomicReference<>();
runAt("1 February 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 February 2024", 17.01);
});
runAt("1 March 2024", () -> {
repaymentMarchId
.set(loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01).getResourceId());
});
runAt("15 March 2024", () -> {
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(16.62, 0.39, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.9, 0.10, "01 July 2024") //
);
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
addChargebackForLoan(loanId, repaymentMarchId.get(), 17.01);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
unpaidInstallment(33.08, 0.94, "01 April 2024"), //
unpaidInstallment(16.72, 0.29, "01 May 2024"), //
unpaidInstallment(16.81, 0.20, "01 June 2024"), //
unpaidInstallment(16.96, 0.10, "01 July 2024") //
); //
verifyTransactions(loanId,
new TransactionExt(100.0, "Disbursement", "01 January 2024", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 February 2024", 83.57, 16.43, 0.58, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Repayment", "01 March 2024", 67.05, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false),
new TransactionExt(17.01, "Chargeback", "15 March 2024", 83.57, 16.52, 0.49, 0.0, 0.0, 0.0, 0.0, false));
Long repaymentId = verifyPrepayAmountByRepayment(loanId, "15 March 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentId, "15 March 2024");
});
}
@Test
public void testS5AndS6ChargebacksAfterMaturityDateVerifyNPlus1ThPeriod() {
final Long loanId = applyApproveDisburseLoan(
loanProductWithChargebackAllocationInterestFeesPenaltiesPrincipal.getResourceId());
runAt("1 February 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 February 2024", 17.01);
});
runAt("1 March 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01);
});
runAt("1 April 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 April 2024", 17.01);
});
runAt("1 May 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 May 2024", 17.01);
});
AtomicReference<Long> repaymentJuneRef = new AtomicReference<>();
runAt("1 June 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 June 2024", 17.01);
repaymentJuneRef.set(repayment.getResourceId());
});
AtomicReference<Long> repaymentJulyRef = new AtomicReference<>();
runAt("1 July 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 July 2024", 17.00);
repaymentJulyRef.set(repayment.getResourceId());
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
fullyRepaidInstallment(16.9, 0.10, "01 July 2024") //
); //
});
runAt("15 July 2024", () -> {
addChargebackForLoan(loanId, repaymentJuneRef.get(), 17.01);
// TODO verify TRANSACTIONS!!!!
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
fullyRepaidInstallment(16.9, 0.10, "01 July 2024"), //
unpaidInstallment(16.81, 0.2, "15 July 2024") //
); //
});
runAt("30 July 2024", () -> {
addChargebackForLoan(loanId, repaymentJulyRef.get(), 17.00);
// TODO verify TRANSACTIONS!!!!
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
fullyRepaidInstallment(16.9, 0.10, "01 July 2024"), //
unpaidInstallment(33.71, 0.3, "30 July 2024") //
); //
Long repaymentId = verifyPrepayAmountByRepayment(loanId, "30 July 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentId, "30 July 2024");
});
}
@Test
public void testS7ChargebacksOnMaturityDate() {
final Long loanId = applyApproveDisburseLoan(
loanProductWithChargebackAllocationInterestFeesPenaltiesPrincipal.getResourceId());
runAt("1 February 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 February 2024", 17.01);
});
runAt("1 March 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 March 2024", 17.01);
});
runAt("1 April 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 April 2024", 17.01);
});
runAt("1 May 2024", () -> {
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "01 May 2024", 17.01);
});
AtomicReference<Long> repaymentJuneRef = new AtomicReference<>();
runAt("1 June 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 June 2024", 17.01);
repaymentJuneRef.set(repayment.getResourceId());
});
AtomicReference<Long> repaymentJulyRef = new AtomicReference<>();
runAt("1 July 2024", () -> {
PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment",
"01 July 2024", 17.00);
repaymentJulyRef.set(repayment.getResourceId());
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
fullyRepaidInstallment(16.9, 0.10, "01 July 2024") //
); //
addChargebackForLoan(loanId, repaymentJulyRef.get(), 17.00);
verifyRepaymentSchedule(loanId, //
installment(100.0, null, "01 January 2024"), //
fullyRepaidInstallment(16.43, 0.58, "01 February 2024"), //
fullyRepaidInstallment(16.52, 0.49, "01 March 2024"), //
fullyRepaidInstallment(16.62, 0.39, "01 April 2024"), //
fullyRepaidInstallment(16.72, 0.29, "01 May 2024"), //
fullyRepaidInstallment(16.81, 0.20, "01 June 2024"), //
installment(33.8, 0.20, 17.0, false, "01 July 2024") //
); //
Long repaymentId = verifyPrepayAmountByRepayment(loanId, "01 July 2024");
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentId, "01 July 2024");
});
runAt("2 July 2024", () -> {
executeInlineCOB(loanId);
});
}
}
}
private Integer createAccounts(final Integer daysToSubtract, final Integer numberOfRepayments, final boolean withJournalEntries,
LoanProductTestBuilder loanProductTestBuilder) {
// Delinquency Bucket