diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeback.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeback.feature index e4adbe9f9..9ee0111f0 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeback.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeback.feature @@ -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" diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index a477465eb..87641b542 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -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; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java index a04de6c7b..84ed5d0ef 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java @@ -66,17 +66,12 @@ public class LoanScheduleService { final List existingTransactionIds, final List existingReversedTransactionIds) { existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); - /* - * LocalDate recalculateFrom = null; List 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(); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 701d05905..49df1228c 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -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 updatedTransactions = changedTransactionDetail.getNewTransactionMappings().values(); Optional 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 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 calculateChargebackAllocationMapPrincipalOnly(Money transactionAmount, MonetaryCurrency currency) { + Map 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 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 calculateChargebackAllocationMapByCreditAllocationRule(LoanTransaction loanTransaction, + TransactionCtx ctx) { + MonetaryCurrency currency = ctx.getCurrency(); + LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx); + // get the original allocation from the original transaction + Map originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction, currency); + LoanCreditAllocationRule chargebackAllocationRule = getChargebackAllocationRules(loanTransaction); + + // if there were earlier chargebacks then let's calculate the remaining amounts for each portion + Map 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 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 originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction, - originalAllocationNotAdjusted, loanTransaction, ctx, chargeBackAllocationRule); + originalAllocationNotAdjusted, loanTransaction, ctx, chargebackAllocationRule); // calculate the current chargeback allocation Map 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 adjustOriginalAllocationWithFormerChargebacks(LoanTransaction originalTransaction, - Map originalAllocation, LoanTransaction chargeBackTransaction, TransactionCtx ctx, - LoanCreditAllocationRule chargeBackAllocationRule) { + Map originalAllocation, LoanTransaction chargebackTransaction, TransactionCtx ctx, + LoanCreditAllocationRule chargebackAllocationRule) { // these are the list of existing transactions - List allTransactions = new ArrayList<>(chargeBackTransaction.getLoan().getLoanTransactions()); + List 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 comparator = LoanTransactionComparator.INSTANCE; List chargebacksForTheSameOriginal = chargebacks.stream() .filter(tr -> findChargebackOriginalTransaction(tr, ctx) == originalTransaction - && comparator.compare(tr, chargeBackTransaction) < 0) + && comparator.compare(tr, chargebackTransaction) < 0) .sorted(comparator).toList(); Map allocation = new HashMap<>(originalAllocation); for (LoanTransaction loanTransaction : chargebacksForTheSameOriginal) { Map 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 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 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 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)); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java index 859f6bfd0..4090dc1e5 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ProgressiveTransactionCtx.java @@ -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 { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java index b71e340d7..926310f76 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -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 diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java index d5fc0e49a..312a116b7 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java @@ -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 periods, @NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, List 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 installments, @NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, List loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc); + /** + * Find repayment period based on Due Date. + */ Optional 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); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index b82f6c7b1..aa27a757b 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -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 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 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); } }); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/EmiAdjustment.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiAdjustment.java similarity index 96% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/EmiAdjustment.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiAdjustment.java index 0766b5213..7673543dc 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/EmiAdjustment.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiAdjustment.java @@ -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; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EmiChangeOperation.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiChangeOperation.java similarity index 96% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EmiChangeOperation.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiChangeOperation.java index 9d404aa4b..c25639bee 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EmiChangeOperation.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiChangeOperation.java @@ -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; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java similarity index 63% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java index d21b1defa..9af92ffb8 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java @@ -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 { 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 { 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 { 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 { } } + /** + * 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()); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestRate.java similarity index 94% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestRate.java index 50e78716a..d128b5aa0 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestRate.java @@ -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; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/OutstandingDetails.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/OutstandingDetails.java similarity index 93% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/OutstandingDetails.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/OutstandingDetails.java index 7f9d722fe..6010c17a4 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/OutstandingDetails.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/OutstandingDetails.java @@ -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; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PeriodDueDetails.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/PeriodDueDetails.java similarity index 93% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PeriodDueDetails.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/PeriodDueDetails.java index ce9c8fe79..7f9a3a8ec 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PeriodDueDetails.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/PeriodDueDetails.java @@ -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; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java similarity index 85% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java index 74f38fd99..ded2ef328 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java @@ -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 changeOutstandingBalanceAndUpdateInterestPeriods(final LocalDate balanceChangeDate, @@ -201,7 +201,10 @@ public class ProgressiveLoanInterestScheduleModel { private Consumer updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount, final Money correctionAmount) { return repaymentPeriod -> { - final Optional interestPeriodOptional = findInterestPeriodForBalanceChange(repaymentPeriod, balanceChangeDate); + final boolean isChangeOnMaturityDate = isLastRepaymentPeriod(repaymentPeriod) + && balanceChangeDate.isEqual(repaymentPeriod.getDueDate()); + final Optional 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 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 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. * diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java similarity index 74% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java index 9274a6c7d..24edc1144 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java @@ -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); } diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java index 362a0f95e..ca646447c 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java @@ -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 interest = ArgumentCaptor.forClass(Money.class); ArgumentCaptor fee = ArgumentCaptor.forClass(Money.class); ArgumentCaptor 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 interest = ArgumentCaptor.forClass(Money.class); ArgumentCaptor fee = ArgumentCaptor.forClass(Money.class); ArgumentCaptor 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 interest = ArgumentCaptor.forClass(Money.class); ArgumentCaptor fee = ArgumentCaptor.forClass(Money.class); ArgumentCaptor 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))); diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index 011fe6585..e3ccfa56f 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -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 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 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 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 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 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 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 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 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 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 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 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())), diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 132f43c14..8f7e0d57f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java index 7504e3f8d..c72b44574 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java @@ -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; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java index 418d011af..767ae5c6e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java @@ -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 diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 67ec2d799..4f3b43f0e 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -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); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java index d0aebddff..5582c8974 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java @@ -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 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 chargebackCreditAllocationOrders(List allocationIds) { + List 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 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 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 repaymentJuneRef = new AtomicReference<>(); + runAt("1 June 2024", () -> { + PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", + "01 June 2024", 17.01); + repaymentJuneRef.set(repayment.getResourceId()); + }); + AtomicReference 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 repaymentJuneRef = new AtomicReference<>(); + runAt("1 June 2024", () -> { + PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", + "01 June 2024", 17.01); + repaymentJuneRef.set(repayment.getResourceId()); + }); + AtomicReference 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 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 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 repaymentJuneRef = new AtomicReference<>(); + runAt("1 June 2024", () -> { + PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", + "01 June 2024", 17.01); + repaymentJuneRef.set(repayment.getResourceId()); + }); + AtomicReference 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 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 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 repaymentJuneRef = new AtomicReference<>(); + runAt("1 June 2024", () -> { + PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", + "01 June 2024", 17.01); + repaymentJuneRef.set(repayment.getResourceId()); + }); + AtomicReference 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 repaymentJuneRef = new AtomicReference<>(); + runAt("1 June 2024", () -> { + PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", + "01 June 2024", 17.01); + repaymentJuneRef.set(repayment.getResourceId()); + }); + AtomicReference 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