FINERACT-2162: Chargeback implementation for progressive interest recalculation enabled loan.
This commit is contained in:
committed by
Adam Saghy
parent
22f7972f60
commit
cdbf0df571
@@ -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"
|
||||
|
||||
+11
-8
@@ -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;
|
||||
|
||||
+6
-11
@@ -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();
|
||||
}
|
||||
|
||||
+187
-41
@@ -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));
|
||||
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+12
-6
@@ -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
|
||||
|
||||
+57
-5
@@ -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);
|
||||
}
|
||||
|
||||
+83
-24
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
+50
-12
@@ -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());
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
+1
-1
@@ -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;
|
||||
+68
-20
@@ -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.
|
||||
*
|
||||
+89
-9
@@ -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);
|
||||
}
|
||||
|
||||
+13
-13
@@ -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)));
|
||||
|
||||
+605
-10
@@ -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())),
|
||||
|
||||
+13
-7
@@ -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);
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+26
-1
@@ -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);
|
||||
|
||||
|
||||
+799
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user