From f7332d826d9bdb1c9d376e4c87b90b43e5a834c3 Mon Sep 17 00:00:00 2001 From: Janos Meszaros Date: Wed, 5 Feb 2025 15:03:28 +0100 Subject: [PATCH] FINERACT-2162: Chargeback interest bearing loans Added tests to cover the charbeback cases where no allocation rule is set but interest recalc is enabled --- .../resources/features/LoanChargeback.feature | 276 ++++++ .../LoanRepaymentScheduleInstallment.java | 16 +- .../service/LoanScheduleService.java | 17 +- ...edPaymentScheduleTransactionProcessor.java | 191 ++++- .../impl/ProgressiveTransactionCtx.java | 2 +- .../ProgressiveLoanScheduleGenerator.java | 18 +- .../loanproduct/calc/EMICalculator.java | 63 +- .../calc/ProgressiveEMICalculator.java | 94 ++- .../calc}/data/EmiAdjustment.java | 2 +- .../calc/{ => data}/EmiChangeOperation.java | 2 +- .../calc}/data/InterestPeriod.java | 57 +- .../calc}/data/InterestRate.java | 2 +- .../calc}/data/OutstandingDetails.java | 2 +- .../calc}/data/PeriodDueDetails.java | 2 +- .../ProgressiveLoanInterestScheduleModel.java | 80 +- .../calc}/data/RepaymentPeriod.java | 116 ++- ...ymentScheduleTransactionProcessorTest.java | 2 +- .../calc/ProgressiveEMICalculatorTest.java | 615 +++++++++++++- ...WritePlatformServiceJpaRepositoryImpl.java | 20 +- ...gressiveLoanInterestRefundServiceImpl.java | 2 +- .../ProgressiveLoanSummaryDataProvider.java | 4 +- .../BaseLoanIntegrationTest.java | 27 +- .../LoanTransactionChargebackTest.java | 799 ++++++++++++++++++ 23 files changed, 2261 insertions(+), 148 deletions(-) rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/{loanaccount/loanschedule => loanproduct/calc}/data/EmiAdjustment.java (96%) rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/{ => data}/EmiChangeOperation.java (96%) rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/{loanaccount/loanschedule => loanproduct/calc}/data/InterestPeriod.java (65%) rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/{loanaccount/loanschedule => loanproduct/calc}/data/InterestRate.java (94%) rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/{loanaccount/loanschedule => loanproduct/calc}/data/OutstandingDetails.java (93%) rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/{loanaccount/loanschedule => loanproduct/calc}/data/PeriodDueDetails.java (93%) rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/{loanaccount/loanschedule => loanproduct/calc}/data/ProgressiveLoanInterestScheduleModel.java (86%) rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/{loanaccount/loanschedule => loanproduct/calc}/data/RepaymentPeriod.java (71%) 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 4b7805a2768..09a31c608ea 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeback.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanChargeback.feature @@ -4015,3 +4015,279 @@ Feature: LoanChargeback | 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 | 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 | \ No newline at end of file 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 a477465eb03..62c239e92e6 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 @@ -853,14 +853,6 @@ public void addToInterest(final LocalDate transactionDate, final Money transacti 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 +861,14 @@ public void addToCreditedInterest(final BigDecimal amount) { } } + 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 86cb820a7db..b1dc9d55401 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 ChangedTransactionDetail recalculateScheduleFromLastTransaction(final Loa 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.repaymentScheduleDetail().isInterestRecalculationEnabled() && !loan.isChargedOff()) { - regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO); - } else { - regenerateRepaymentSchedule(loan, generatorDTO); + if (!loan.isProgressiveSchedule()) { + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && !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 e5175898563..2623ff93cb3 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,11 +86,11 @@ 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.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.domain.AllocationType; import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.DueType; @@ -404,7 +404,9 @@ protected LoanTransaction findChargebackOriginalTransaction(LoanTransaction char 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(); @@ -413,7 +415,9 @@ protected LoanTransaction findChargebackOriginalTransaction(LoanTransaction char 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"); @@ -421,8 +425,109 @@ protected LoanTransaction findChargebackOriginalTransaction(LoanTransaction char 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; + } + + 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)) { + if (chargebackAllocation.get(PRINCIPAL).isGreaterThanZero()) { + emiCalculator.chargebackPrincipal(model, loanTransaction.getTransactionDate(), chargebackAllocation.get(PRINCIPAL)); + } + + // interest + 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); + + LoanRepaymentScheduleInstallment instalment = lastInstallmentDueDate.isEqual(loanTransaction.getDateOf()) + ? ctx.getInstallments().stream().filter(i -> i.getDueDate().isEqual(loanTransaction.getDateOf())).findAny() + .orElseThrow() + : ctx.getInstallments().stream().filter(i -> !loanTransaction.getTransactionDate().isBefore(i.getFromDate()) + && i.getDueDate().isAfter(loanTransaction.getTransactionDate())).findAny().orElseThrow(); + // special because principal and interest dues are already updated. + recognizeAmountsAfterChargebackWithInterestRecalculation(ctx, instalment, chargebackAllocation); + } else { + // N+1 + LoanRepaymentScheduleInstallment instalment = ctx.getInstallments().stream() + .filter(LoanRepaymentScheduleInstallment::isAdditional).findAny() + .or(() -> createAdditionalInstalment(loanTransaction, ctx)).orElseThrow(); + // generic + recognizeAmountsAfterChargeback(ctx, loanTransaction.getTransactionDate(), instalment, chargebackAllocation); + if (instalment.getDueDate().isBefore(loanTransaction.getTransactionDate())) { + instalment.updateDueDate(loanTransaction.getTransactionDate()); + } + } + + allocateOverpayment(loanTransaction, ctx); + } + + private Optional createAdditionalInstalment(LoanTransaction loanTransaction, + ProgressiveTransactionCtx ctx) { + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loanTransaction.getLoan(), + (ctx.getInstallments().size() + 1), loanTransaction.getTransactionDate(), loanTransaction.getTransactionDate(), ZERO, ZERO, + ZERO, ZERO, false, null); + installment.markAsAdditional(); + loanTransaction.getLoan().addLoanRepaymentScheduleInstallment(installment); + return Optional.of(installment); + } + + private Map calculateChargebackAllocationMapByCreditAllocationRule(LoanTransaction loanTransaction, + ProgressiveTransactionCtx ctx) { + MonetaryCurrency currency = ctx.getCurrency(); + LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx); + // get the original allocation from the opriginal 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) { + // TODO refactor if needed + if (loanTransaction.getLoan().isInterestBearing() + && loanTransaction.getLoan().getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { + processCreditTransactionWithEmiCalculator(loanTransaction, (ProgressiveTransactionCtx) ctx); + } else if (hasNoCustomCreditAllocationRule(loanTransaction)) { super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments()); } else { loanTransaction.resetDerivedComponents(); @@ -433,7 +538,13 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, Transac Money transactionAmount = loanTransaction.getAmount(currency); Money totalOverpaid = ctx.getOverpaymentHolder().getMoneyObject(); Money amountToDistribute = MathUtil.negativeToZero(transactionAmount).minus(totalOverpaid); + // transaction amount should be greater than or equal to 0 + // total overpaid amount should be greater than or equal to 0 + // amountToDistribute = transactionAmount - totalOverpaid Money overpaymentAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute)); + // overpaymentAmount = negativeToZero ( transactionAmount - (transactionAmount - totalOverpaid) ) + // overpaymentAmount = negativeToZero ( totalOverpaid ) + // TODO does above lines make sense???? loanTransaction.setOverPayments(overpaymentAmount); if (!transactionAmount.isGreaterThanZero()) { return; @@ -443,7 +554,7 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, Transac } 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); @@ -506,9 +617,8 @@ private Map adjustOriginalAllocationWithFormerChargebacks List allTransactions = new ArrayList<>(chargeBackTransaction.getLoan().getLoanTransactions()); // Remove the current chargeback from the list - if (chargeBackTransaction.getId() != null) { - allTransactions.remove(chargeBackTransaction); - } else { + allTransactions.remove(chargeBackTransaction); + if (ctx.getChangedTransactionDetail() != null) { Long oldId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(chargeBackTransaction); allTransactions.remove(allTransactions.stream().filter(tr -> Objects.equals(tr.getId(), oldId)).findFirst().get()); } @@ -540,8 +650,39 @@ private Map adjustOriginalAllocationWithFormerChargebacks return allocation; } - private void recognizeAmountsAfterChargeback(final TransactionCtx ctx, final LocalDate transactionDate, - final LoanRepaymentScheduleInstallment installment, final Map chargebackAllocation) { + 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 recognizeAmountsAfterChargebackWithInterestRecalculation(TransactionCtx ctx, LoanRepaymentScheduleInstallment installment, + Map chargebackAllocation) { + Money principal = chargebackAllocation.get(PRINCIPAL); + if (principal.isGreaterThanZero()) { + installment.addToCreditedPrincipal(principal.getAmount()); + } + Money interest = chargebackAllocation.get(INTEREST); + if (principal.isGreaterThanZero()) { + installment.addToCreditedInterest(interest.getAmount()); + } + recognizeFeePenaltiesAmountsAfterChargeback(ctx, installment, chargebackAllocation); + } + + private void recognizeAmountsAfterChargeback(TransactionCtx ctx, LocalDate transactionDate, + LoanRepaymentScheduleInstallment installment, Map chargebackAllocation) { final Money principal = chargebackAllocation.get(PRINCIPAL); if (principal != null && principal.isGreaterThanZero()) { installment.addToCreditedPrincipal(principal.getAmount()); @@ -553,21 +694,7 @@ private void recognizeAmountsAfterChargeback(final TransactionCtx ctx, final Loc 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 @@ -612,6 +739,11 @@ private Predicate hasMatchingToLoanTransaction(Long id, 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); @@ -1227,9 +1359,8 @@ private void handleAccelerateMaturityChargeOff(final LoanTransaction loanTransac if (!installments.isEmpty() && transactionDate.isBefore(loan.getMaturityDate())) { if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx && loanTransaction.getLoan().isInterestRecalculationEnabled()) { - 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(); @@ -1295,7 +1426,7 @@ private void handleZeroInterestChargeOff(final LoanTransaction loanTransaction, 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); 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 859f6bfd035..4090dc1e5a3 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.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 b71e340d7ec..926310f76ff 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.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 OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency .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 Money getPeriodInterestTillDate(@NotNull LoanRepaymentScheduleInstallment 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 900950ca60b..94f424c1fd7 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,98 @@ 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 Bank 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); @@ -78,5 +127,9 @@ Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel int 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 5d09c43682c..9332db20179 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.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.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 void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, Loc 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.getCalculatedEmi())) { + rp.setEmi(rp.getTotalCountablePaidAmount()); 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 PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleMo @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 @@ -290,7 +342,18 @@ private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate( .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); @@ -327,19 +390,20 @@ private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestSchedu 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::getCalculatedEmi).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.getTotalCountablePaidAmount())) { + 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 0766b5213a0..7673543dcde 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 9d404aa4b1c..c25639bee08 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 65% 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 d21b1defad3..fa4186466b1 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 @@ @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,9 +95,17 @@ public void addDisbursementAmount(final Money disbursementAmount) { 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(); @@ -85,7 +115,7 @@ public Money getCalculatedDueInterest() { .multiply(getRateFactorTillPeriodDueDate(), mc) // .divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) // .multiply(BigDecimal.valueOf(getLength()), mc); // - return Money.of(outstandingLoanBalance.getCurrencyData(), interestDueTillRepaymentDueDate, mc); + return chargebackInterest.plus(interestDueTillRepaymentDueDate, mc); } public long getLength() { @@ -116,6 +146,13 @@ public void updateOutstandingLoanBalance() { } } + /** + * Include principal like amounts (all disbursement amount + chargeback principal) + */ + public Money getDisbursedAmounts() { + 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 50e78716aac..d128b5aa076 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 7f9d722fe01..6010c17a446 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 ce9c8fe796e..7f9a3a8ec86 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 86% 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 528d8572556..23c2a4413be 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; @@ -201,7 +201,10 @@ Optional findRepaymentPeriodForBalanceChange(final LocalDate ba 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,14 @@ private Consumer updateInterestPeriodOnRepaymentPeriod(final Lo } private Optional findInterestPeriodForBalanceChange(final RepaymentPeriod repaymentPeriod, - final LocalDate balanceChangeDate) { + final LocalDate balanceChangeDate, final boolean isChangeOnMaturityDate) { if (repaymentPeriod == null || balanceChangeDate == null) { return Optional.empty(); } + if (isChangeOnMaturityDate) { + var lastRepaymentPeriod = repaymentPeriod.getInterestPeriods().get(repaymentPeriod.getInterestPeriods().size() - 1); + return lastRepaymentPeriod.getLength() == 0 ? Optional.of(lastRepaymentPeriod) : Optional.empty(); + } return repaymentPeriod.getInterestPeriods().stream()// .filter(interestPeriod -> balanceChangeDate.isEqual(interestPeriod.getDueDate()))// .findFirst(); @@ -231,8 +238,7 @@ void insertInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate 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 +254,20 @@ private void insertInterestPausePeriods(final RepaymentPeriod repaymentPeriod, f 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)); @@ -283,30 +285,65 @@ private InterestPeriod findPreviousInterestPeriod(final RepaymentPeriod repaymen } } + /** + * 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::getDisbursedAmounts).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 +351,15 @@ public boolean isEmpty() { .isEmpty(); // } + @NotNull + public RepaymentPeriod getLastRepaymentPeriod() { + return repaymentPeriods.get(repaymentPeriods.size() - 1); + } + + public boolean isLastRepaymentPeriod(@NotNull RepaymentPeriod repaymentPeriod) { + return getLastRepaymentPeriod().getDueDate().equals(repaymentPeriod.getDueDate()); + } + /** * 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 71% 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 139ef71abc2..f9e483c70a0 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,9 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.portfolio.loanaccount.loanschedule.data; - -import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod; +package org.apache.fineract.portfolio.loanproduct.calc.data; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; @@ -73,8 +71,7 @@ public RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate d 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); } @@ -91,7 +88,7 @@ public RepaymentPeriod(RepaymentPeriod previous, RepaymentPeriod 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)); } } @@ -99,6 +96,11 @@ public Optional getPrevious() { 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); @@ -110,6 +112,11 @@ private BigDecimal calculateRateFactorPlus1() { 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) { @@ -128,6 +135,11 @@ private Money calculateCalculatedDueInterest() { 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 @@ -138,27 +150,101 @@ public Money getDueInterest() { return dueInterestCalculation.get(); } + /** + * Gives back an EMI amount which includes chargeback amounts as well + * + * @return + */ + public Money getCalculatedEmi() { + return getEmi().plus(getTotalChargebackAmount(), mc); // + } + + /** + * Gives back principal due + charge back principal based on (EMI - Calculated Due Interest) + * + * @return + */ public Money getCalculatedDuePrincipal() { - return getEmi().minus(getCalculatedDueInterest(), mc); + return getCalculatedEmi().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(getCalculatedEmi().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()); } + /** + * These contain all (principal + interest) amounts except chargeback amounts + * + * @return + */ + public Money getTotalCountablePaidAmount() { + return getTotalPaidAmount().minus(getTotalChargebackAmount()); + } + public boolean isFullyPaid() { - return getEmi().isEqualTo(getTotalPaidAmount()); + return getCalculatedEmi().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 getDisbursedAmounts() { + return interestPeriods.stream().map(InterestPeriod::getDisbursedAmounts).reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); + } + public Money getOutstandingLoanBalance() { if (outstandingBalanceCalculation == null) { outstandingBalanceCalculation = Memo.of(() -> { @@ -189,8 +275,9 @@ public Money getInitialBalanceForEmiRecalculation() { } 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); } @@ -208,13 +295,6 @@ public InterestPeriod getLastInterestPeriod() { return interestPeriods.get(interestPeriods.size() - 1); } - public Optional findInterestPeriod(@NotNull LocalDate transactionDate) { - return interestPeriods.stream() // - .filter(interestPeriod -> isInPeriod(transactionDate, interestPeriod.getFromDate(), interestPeriod.getDueDate(), - isFirstRepaymentPeriod() && interestPeriod.isFirstInterestPeriod()))// - .reduce((one, two) -> two); - } - public boolean isFirstRepaymentPeriod() { return previous == null; } 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 4b7d1e98aa2..4af832b3765 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.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; 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 011fe65851d..e3ccfa56fa2 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.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 @@ public void test_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEver } @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 @@ public void test_dailyInterest_disbursedAmt1000_dayInYears360_daysInMonth30_repa 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 @@ public void test_actual_actual_repayment_schedule_disbursement_near_month_end_re 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 @@ private static LoanRepaymentScheduleInstallment createPeriod(int periodId, Local 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 @@ private static void checkPeriod(final ProgressiveLoanInterestScheduleModel inter 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 b089fab992d..614863b68da 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 CommandProcessingResult chargebackLoanTransaction(final Long loanId, fina 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.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && !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 CommandProcessingResult chargebackLoanTransaction(final Long loanId, fina 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 CommandProcessingResult makeRefund(final Long loanId, final LoanTransacti .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.getLoanTransactions().stream().filter(LoanTransaction::isNotReversed).toList(), 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 7504e3f8d7f..c72b4457480 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.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 418d011afc7..767ae5c6e40 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.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 b463b0d6af0..6bfa5753c79 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.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; @@ -267,6 +268,25 @@ protected void verifyLoanStatus(GetLoansLoanIdResponse loanDetails, Function 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); @@ -672,6 +692,7 @@ protected void verifyLastClosedBusinessDate(Long loanId, String lastClosedBusine } 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")); } @@ -1109,7 +1130,7 @@ protected Installment fullyRepaidInstallment(double principalAmount, double inte } 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); } @@ -1160,6 +1181,10 @@ protected void checkMaturityDates(long loanId, LocalDate expectedMaturityDate, L 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 3327b9a468a..9eda2e81bd5 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,14 +30,19 @@ 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.Arrays; import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +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; @@ -47,6 +52,8 @@ 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; @@ -60,14 +67,20 @@ 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 { @@ -82,6 +95,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() { @@ -95,6 +109,8 @@ public void setup() { 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); @@ -594,6 +610,789 @@ public void applyMultipleLoanTransactionChargeback(LoanProductTestBuilder loanPr } } + @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