This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit d21a1a98a29e4910b834bc74bcac3eee866d59f4 Author: mariiaKraievska <[email protected]> AuthorDate: Fri Oct 10 19:16:47 2025 +0300 FINERACT-2354: First step - basic implementation of re-aging for Interest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change --- .../test/resources/features/LoanReAging.feature | 318 --------------------- .../portfolio/loanaccount/domain/Loan.java | 3 + .../service/LoanDownPaymentHandlerServiceImpl.java | 3 +- ...EmbeddableProgressiveLoanScheduleGenerator.java | 6 +- ...dvancedPaymentScheduleTransactionProcessor.java | 262 +++++++++++++---- .../portfolio/loanproduct/calc/EMICalculator.java | 5 + .../loanproduct/calc/ProgressiveEMICalculator.java | 130 +++++++++ .../domain/LoanScheduleGeneratorTest.java | 2 +- .../calc/ProgressiveEMICalculatorTest.java | 5 +- .../domain/LoanAccountDomainServiceJpa.java | 3 +- .../LoanChargeWritePlatformServiceImpl.java | 11 +- .../LoanWritePlatformServiceJpaRepositoryImpl.java | 28 +- .../ProgressiveLoanInterestRefundServiceImpl.java | 3 +- .../ReprocessLoanTransactionsServiceImpl.java | 9 +- .../service/reaging/LoanReAgingServiceImpl.java | 26 +- .../service/reaging/LoanReAgingValidator.java | 17 +- .../service/reaging/LoanReAgingValidatorTest.java | 28 -- 17 files changed, 407 insertions(+), 452 deletions(-) diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature index 2c6b058982..8585adb3e1 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature @@ -3588,75 +3588,6 @@ Feature: LoanReAging | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | Snooze fee | false | Specified due date | 15 February 2024 | Flat | 10.0 | 10.0 | 0.0 | 0.0 | - @TestRailId:C4077 @AdvancedPaymentAllocation - Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - Charge-back before re-aging - UC3 - When Admin sets the business date to "01 January 2024" - When Admin creates a client with random data - When 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_INTEREST_RECALC_EMI_360_30_CHARGEBACK_INTEREST_PENALTY_FEE_PRINCIPAL | 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" - When Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount - 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 | | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 16.9 | 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.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - 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 makes "REPAYMENT_ADJUSTMENT_CHARGEBACK" chargeback with 17.01 EUR transaction amount - 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 | | 67.14 | 32.86 | 1.16 | 0.0 | 0.0 | 34.02 | 0.0 | 0.0 | 0.0 | 34.02 | - | 3 | 31 | 01 April 2024 | | 50.52 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.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 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 116.43 | 2.72 | 0.0 | 0.0 | 119.15 | 17.01 | 0.0 | 0.0 | 102.14 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | - | 01 February 2024 | Chargeback | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 100.0 | false | - When Admin sets the business date to "15 March 2024" - When Admin creates a Loan re-aging transaction with the following data: - | frequencyNumber | frequencyType | startDate | numberOfInstallments | - | 1 | MONTHS | 01 April 2024| 6 | - Then Loan Repayment schedule has 8 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 | 15 March 2024 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | - | 3 | 31 | 01 April 2024 | | 84.53 | 15.47 | 1.74 | 0.0 | 0.0 | 17.21 | 0.0 | 0.0 | 0.0 | 17.21 | - | 4 | 30 | 01 May 2024 | | 67.81 | 16.72 | 0.49 | 0.0 | 0.0 | 17.21 | 0.0 | 0.0 | 0.0 | 17.21 | - | 5 | 31 | 01 June 2024 | | 51.0 | 16.81 | 0.4 | 0.0 | 0.0 | 17.21 | 0.0 | 0.0 | 0.0 | 17.21 | - | 6 | 30 | 01 July 2024 | | 34.09 | 16.91 | 0.3 | 0.0 | 0.0 | 17.21 | 0.0 | 0.0 | 0.0 | 17.21 | - | 7 | 31 | 01 August 2024 | | 17.08 | 17.01 | 0.2 | 0.0 | 0.0 | 17.21 | 0.0 | 0.0 | 0.0 | 17.21 | - | 8 | 31 | 01 September 2024| | 0.0 | 17.08 | 0.1 | 0.0 | 0.0 | 17.18 | 0.0 | 0.0 | 0.0 | 17.18 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 116.43 | 3.81 | 0.0 | 0.0 | 120.24 | 17.01| 0.0 | 0.0 | 103.23 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | - | 01 February 2024 | Chargeback | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 100.0 | false | - | 15 March 2024 | Re-age | 100.84 | 100.0 | 0.84 | 0.0 | 0.0 | 0.0 | false | - @TestRailId:C4083 @AdvancedPaymentAllocation Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - N+1 Scenario - UC4 When Admin sets the business date to "01 January 2024" @@ -4128,180 +4059,6 @@ Feature: LoanReAging | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | | 15 March 2024 | Re-age | 84.28 | 83.57 | 0.71 | 0.0 | 0.0 | 0.0 | true | - @TestRailId:C4089 @AdvancedPaymentAllocation - Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - Charge-off scenario (zero interest) - UC10 - When Admin sets the business date to "01 January 2024" - When Admin creates a client with random data - When Admin set "LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule - When 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_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF | 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" - When Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount - 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| | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 16.9 | 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.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - 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 - 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 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 16.9 | 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.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 17.01 | 0.0 | 0.0 | 85.04 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | - When Admin sets the business date to "01 March 2024" - And Admin does charge-off the loan on "01 March 2024" - 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 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 3 | 31 | 01 April 2024 | | 50.04 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.03 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 16.02 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 6 | 30 | 01 July 2024 | | 0.0 | 16.02 | 0.0 | 0.0 | 0.0 | 16.02 | 0.0 | 0.0 | 0.0 | 16.02 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 1.07 | 0.0 | 0.0 | 101.07 | 17.01 | 0.0 | 0.0 | 84.06 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | - | 01 March 2024 | Accrual | 1.07 | 0.0 | 1.07 | 0.0 | 0.0 | 0.0 | false | - | 01 March 2024 | Charge-off | 84.06 | 83.57 | 0.49 | 0.0 | 0.0 | 0.0 | false | - When Admin sets the business date to "15 March 2024" - When Admin creates a Loan re-aging transaction with the following data: - | frequencyNumber | frequencyType | startDate | numberOfInstallments | - | 1 | MONTHS | 01 April 2024 | 6 | - Then Loan Repayment schedule has 8 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 | 15 March 2024 | 83.57 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | - | 3 | 31 | 01 April 2024 | | 70.05 | 13.52 | 0.49 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - | 4 | 30 | 01 May 2024 | | 56.04 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - | 5 | 31 | 01 June 2024 | | 42.03 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - | 6 | 30 | 01 July 2024 | | 28.02 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - | 7 | 31 | 01 August 2024 | | 14.01 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - | 8 | 31 | 01 September 2024| | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 1.07 | 0.0 | 0.0 | 101.07| 17.01 | 0.0 | 0.0 | 84.06 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | - | 01 March 2024 | Charge-off | 84.06 | 83.57 | 0.49 | 0.0 | 0.0 | 0.0 | false | - | 01 March 2024 | Accrual | 1.07 | 0.0 | 1.07 | 0.0 | 0.0 | 0.0 | false | - | 15 March 2024 | Re-age | 84.06 | 83.57 | 0.49 | 0.0 | 0.0 | 0.0 | false | - - @TestRailId:C4090 @AdvancedPaymentAllocation - Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - Charge-off scenario (accelerate maturity) - UC11 - When Admin sets the business date to "01 January 2024" - When Admin creates a client with random data - When Admin set "LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule - When 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_INTEREST_RECALCULATION_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR | 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" - When Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount - 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| | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 16.9 | 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.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - 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 - 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 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 16.9 | 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.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 17.01 | 0.0 | 0.0 | 85.04 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | - When Admin sets the business date to "01 March 2024" - And Admin does charge-off the loan on "01 March 2024" - Then Loan Repayment schedule has 2 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 | | 0.0 | 83.57 | 0.49 | 0.0 | 0.0 | 84.06 | 0.0 | 0.0 | 0.0 | 84.06 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 1.07 | 0.0 | 0.0 | 101.07 | 17.01 | 0.0 | 0.0 | 84.06 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | - | 01 March 2024 | Accrual | 1.07 | 0.0 | 1.07 | 0.0 | 0.0 | 0.0 | false | - | 01 March 2024 | Charge-off | 84.06 | 83.57 | 0.49 | 0.0 | 0.0 | 0.0 | false | - When Admin sets the business date to "15 March 2024" - When Admin creates a Loan re-aging transaction with the following data: - | frequencyNumber | frequencyType | startDate | numberOfInstallments | - | 1 | MONTHS | 01 April 2024 | 6 | - Then Loan Repayment schedule has 8 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 | 15 March 2024 | 83.57 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | - | 3 | 31 | 01 April 2024 | | 70.05 | 13.52 | 0.49 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - | 4 | 30 | 01 May 2024 | | 56.04 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - | 5 | 31 | 01 June 2024 | | 42.03 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - | 6 | 30 | 01 July 2024 | | 28.02 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - | 7 | 31 | 01 August 2024 | | 14.01 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - | 8 | 31 | 01 September 2024| | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | 0.0 | 0.0 | 0.0 | 14.01 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 1.07 | 0.0 | 0.0 | 101.07| 17.01 | 0.0 | 0.0 | 84.06 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | - | 01 March 2024 | Accrual | 1.07 | 0.0 | 1.07 | 0.0 | 0.0 | 0.0 | false | - | 01 March 2024 | Charge-off | 84.06 | 83.57 | 0.49 | 0.0 | 0.0 | 0.0 | false | - | 15 March 2024 | Re-age | 84.06 | 83.57 | 0.49 | 0.0 | 0.0 | 0.0 | false | - @TestRailId:C4091 @AdvancedPaymentAllocation Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - Fees and Interest Split after re-aging - UC12 When Admin sets the business date to "01 January 2024" @@ -4392,78 +4149,3 @@ Feature: LoanReAging Then Loan Charges tab has the following data: | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | Snooze fee | false | Specified due date | 15 May 2024 | Flat | 10.0 | 0.0 | 0.0 | 10.0 | - - @TestRailId:C4110 @AdvancedPaymentAllocation - Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - backdated re-aging transaction - UC16 - When Admin sets the business date to "01 January 2024" - When Admin creates a client with random data - When 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_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 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" - When Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount - 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 | | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 16.9 | 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.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - 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 - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | - 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 - 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.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | - | 5 | 31 | 01 June 2024 | | 16.9 | 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.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 2.05 | 0.0 | 0.0 | 102.05 | 34.02 | 0.0 | 0.0 | 68.03 | - Then Loan Transactions tab has the following data: - | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | - | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | - | 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | - | 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | - When Admin sets the business date to "01 June 2024" - # Backdated re-aging - created in June but effective from April 01 - When Admin creates a Loan re-aging transaction with the following data: - | frequencyNumber | frequencyType | startDate | numberOfInstallments | - | 1 | MONTHS | 01 April 2024 | 6 | - Then Loan Repayment schedule has 8 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 | | 56.0 | 11.05 | 0.39 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 | - | 4 | 30 | 01 May 2024 | | 44.95 | 11.05 | 0.39 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 | - | 5 | 31 | 01 June 2024 | | 33.9 | 11.05 | 0.39 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 | - | 6 | 30 | 01 July 2024 | | 22.66 | 11.24 | 0.2 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 | - | 7 | 31 | 01 August 2024 | | 11.35 | 11.31 | 0.13 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 | - | 8 | 31 | 01 September 2024| | 0.0 | 11.35 | 0.07 | 0.0 | 0.0 | 11.42 | 0.0 | 0.0 | 0.0 | 11.42 | - Then Loan Repayment schedule has the following data in Total row: - | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | - | 100.0 | 2.64 | 0.0 | 0.0 | 102.64 | 34.02 | 0.0 | 0.0 | 68.62 | - Then 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 | Re-age | 67.44 | 67.05 | 0.39 | 0.0 | 0.0 | 0.0 | false | true | \ No newline at end of file diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 0054f3de3e..31a6f971c0 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -1824,4 +1824,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> { return getLoanTransactions().stream().anyMatch(t -> t.isContractTermination() && t.isNotReversed()); } + public boolean hasReAgingTransaction() { + return getLoanTransactions().stream().anyMatch(t -> t.isReAge() && t.isNotReversed()); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java index a2bb34a7c6..40698ceab9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java @@ -157,7 +157,8 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) { loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() + || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } reprocessLoanTransactionsService.reprocessTransactions(loan); diff --git a/fineract-progressive-loan-embeddable-schedule-generator/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan-embeddable-schedule-generator/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGenerator.java index 90622ab2c0..e2968663f9 100644 --- a/fineract-progressive-loan-embeddable-schedule-generator/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan-embeddable-schedule-generator/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGenerator.java @@ -34,12 +34,10 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaym public class EmbeddableProgressiveLoanScheduleGenerator { private final ProgressiveLoanScheduleGenerator scheduleGenerator; - private final ScheduledDateGenerator scheduledDateGenerator; - private final EMICalculator emiCalculator; public EmbeddableProgressiveLoanScheduleGenerator() { - this.emiCalculator = new ProgressiveEMICalculator(); - this.scheduledDateGenerator = new DefaultScheduledDateGenerator(); + final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); + final EMICalculator emiCalculator = new ProgressiveEMICalculator(scheduledDateGenerator); this.scheduleGenerator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator, new NoopInterestScheduleModelRepositoryWrapper()); } 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 bc97009799..e8e2b1d577 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 @@ -47,6 +47,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.OptionalInt; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -97,6 +98,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParamet import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService; @@ -2890,76 +2892,149 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep MonetaryCurrency currency = ctx.getCurrency(); List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments(); - AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency)); - installments.forEach(i -> { - Money principalOutstanding = i.getPrincipalOutstanding(currency); - if (principalOutstanding.isGreaterThanZero()) { - outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding)); - i.addToPrincipal(loanTransaction.getTransactionDate(), principalOutstanding.negated()); + // re-aging logic for interest-bearing loans + if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx + && loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) { + handleReAgeWithInterestRecalculationEnabled(loanTransaction, progressiveTransactionCtx); + } else if (loanTransaction.getLoan().isInterestBearing() && !loanTransaction.getLoan().isInterestRecalculationEnabled()) { + // TODO: implement interestRecalculation = false logic + throw new NotImplementedException( + "Logic for re-aging when interest bearing loan has interestRecalculation disabled is not implemented"); + } else { + AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency)); + installments.forEach(i -> { + Money principalOutstanding = i.getPrincipalOutstanding(currency); + if (principalOutstanding.isGreaterThanZero()) { + outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding)); + i.addToPrincipal(loanTransaction.getTransactionDate(), principalOutstanding.negated()); + } + }); + + loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(), Money.zero(currency), Money.zero(currency), + Money.zero(currency)); + + if (outstandingPrincipalBalance.get().isZero()) { + loanTransaction.reverse(); + return; } - }); - loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(), Money.zero(currency), Money.zero(currency), - Money.zero(currency)); + Money calculatedPrincipal = Money.zero(currency); + Money adjustCalculatedPrincipal = Money.zero(currency); + if (outstandingPrincipalBalance.get().isGreaterThanZero()) { + calculatedPrincipal = outstandingPrincipalBalance.get() + .dividedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(), MoneyHelper.getMathContext()); + Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProductRelatedDetail() + .getInstallmentAmountInMultiplesOf(); + if (installmentAmountInMultiplesOf != null) { + calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf); + } + adjustCalculatedPrincipal = outstandingPrincipalBalance.get() + .minus(calculatedPrincipal.multipliedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())); + } + + Optional<LoanRepaymentScheduleInstallment> lastNormalInstallmentOptional = installments.stream().filter(i -> !i.isDownPayment()) + .filter(i -> i.getDueDate().isBefore(loanTransaction.getTransactionDate())).reduce((first, second) -> second); + + int reAgedInstallmentNumber; + LocalDate fromDate; + Loan loan; + if (lastNormalInstallmentOptional.isEmpty()) { + LoanRepaymentScheduleInstallment firstNormalInstallment = installments.stream().filter(i -> !i.isDownPayment()) + .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).orElseThrow(); + reAgedInstallmentNumber = firstNormalInstallment.getInstallmentNumber(); + fromDate = firstNormalInstallment.getFromDate(); + loan = firstNormalInstallment.getLoan(); + } else { + LoanRepaymentScheduleInstallment lastNormalInstallment = lastNormalInstallmentOptional.get(); + reAgedInstallmentNumber = lastNormalInstallment.getInstallmentNumber() + 1; + fromDate = lastNormalInstallment.getDueDate(); + loan = lastNormalInstallment.getLoan(); + } - if (outstandingPrincipalBalance.get().isZero()) { - loanTransaction.reverse(); - return; - } + LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan, + reAgedInstallmentNumber, fromDate, loanTransaction.getLoanReAgeParameter().getStartDate(), + calculatedPrincipal.getAmount()); + insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate()); - Money calculatedPrincipal = Money.zero(currency); - Money adjustCalculatedPrincipal = Money.zero(currency); - if (outstandingPrincipalBalance.get().isGreaterThanZero()) { - calculatedPrincipal = outstandingPrincipalBalance.get() - .dividedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(), MoneyHelper.getMathContext()); - Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProductRelatedDetail() - .getInstallmentAmountInMultiplesOf(); - if (installmentAmountInMultiplesOf != null) { - calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf); + for (int i = 1; i < loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(); i++) { + LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(), + reAgedInstallment.getDueDate()); + int nextReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber() + 1; + reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(), + nextReAgedInstallmentNumber, reAgedInstallment.getDueDate(), calculatedDueDate, calculatedPrincipal.getAmount()); + if (i + 1 == loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()) { + reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal); + } + insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate()); } - adjustCalculatedPrincipal = outstandingPrincipalBalance.get() - .minus(calculatedPrincipal.multipliedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())); + int lastReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber(); + List<LoanRepaymentScheduleInstallment> toRemove = installments.stream().filter(i -> i != null && !i.isAdditional() + && i.getInstallmentNumber() != null && i.getInstallmentNumber() > lastReAgedInstallmentNumber).toList(); + toRemove.forEach(installments::remove); + reprocessInstallments(installments); } + } - Optional<LoanRepaymentScheduleInstallment> lastNormalInstallmentOptional = installments.stream().filter(i -> !i.isDownPayment()) - .filter(i -> i.getDueDate().isBefore(loanTransaction.getTransactionDate())).reduce((first, second) -> second); + private void cleanupInstallmentsAndRepaymentPeriodsAfterReAging(final ProgressiveTransactionCtx ctx) { + final List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments(); + final List<RepaymentPeriod> repaymentPeriods = ctx.getModel().repaymentPeriods(); - int reAgedInstallmentNumber; - LocalDate fromDate; - Loan loan; - if (lastNormalInstallmentOptional.isEmpty()) { - LoanRepaymentScheduleInstallment firstNormalInstallment = installments.stream().filter(i -> !i.isDownPayment()) - .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).orElseThrow(); - reAgedInstallmentNumber = firstNormalInstallment.getInstallmentNumber(); - fromDate = firstNormalInstallment.getFromDate(); - loan = firstNormalInstallment.getLoan(); - } else { - LoanRepaymentScheduleInstallment lastNormalInstallment = lastNormalInstallmentOptional.get(); - reAgedInstallmentNumber = lastNormalInstallment.getInstallmentNumber() + 1; - fromDate = lastNormalInstallment.getDueDate(); - loan = lastNormalInstallment.getLoan(); + // Find the last re-aged installment number + final OptionalInt lastReAgedInstallmentNumberOpt = installments.stream().filter(LoanRepaymentScheduleInstallment::isReAged) + .mapToInt(LoanRepaymentScheduleInstallment::getInstallmentNumber).max(); + + if (lastReAgedInstallmentNumberOpt.isPresent()) { + final int lastReAgedInstallmentNumber = lastReAgedInstallmentNumberOpt.getAsInt(); + final LoanRepaymentScheduleInstallment lastReAgedInstallment = installments.stream() + .filter(i -> i.getInstallmentNumber().equals(lastReAgedInstallmentNumber)).findFirst().orElse(null); + // Remove installments with numbers greater than the last re-aged installment + final List<LoanRepaymentScheduleInstallment> installmentsToRemove = installments.stream().filter(i -> i != null + && !i.isAdditional() && i.getInstallmentNumber() != null && i.getInstallmentNumber() > lastReAgedInstallmentNumber) + .toList(); + if (lastReAgedInstallment != null) { + final List<RepaymentPeriod> repaymentPeriodsToRemove = repaymentPeriods.stream() + .filter(rp -> !rp.getFromDate().isBefore(lastReAgedInstallment.getDueDate())).toList(); + repaymentPeriodsToRemove.forEach(repaymentPeriods::remove); + } + installmentsToRemove.forEach(installments::remove); } + } - LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan, - reAgedInstallmentNumber, fromDate, loanTransaction.getLoanReAgeParameter().getStartDate(), calculatedPrincipal.getAmount()); - insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate()); + private void updateInstallmentsByModelForReAging(final LoanTransaction loanTransaction, final ProgressiveTransactionCtx ctx) { + ctx.getModel().repaymentPeriods().forEach(rp -> { + final LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream().filter( + ri -> ri.getFromDate().equals(rp.getFromDate()) && ri.getDueDate().equals(rp.getDueDate()) && !ri.isDownPayment()) + .findFirst().orElse(null); + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + if (installment != null) { + installment.setFromDate(rp.getFromDate()); + installment.setDueDate(rp.getDueDate()); - for (int i = 1; i < loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(); i++) { - LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(), - reAgedInstallment.getDueDate()); - int nextReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber() + 1; - reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(), - nextReAgedInstallmentNumber, reAgedInstallment.getDueDate(), calculatedDueDate, calculatedPrincipal.getAmount()); - if (i + 1 == loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()) { - reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal); + if (rp.getEmi().isZero()) { + installment.updatePrincipal(BigDecimal.ZERO); + installment.updateInterestCharged(BigDecimal.ZERO); + } else { + installment.updatePrincipal(rp.getDuePrincipal().getAmount()); + installment.updateInterestCharged(rp.getDueInterest().getAmount()); + } + installment.setReAged(true); + installment.updateObligationsMet(ctx.getCurrency(), transactionDate); + } else { + final LoanRepaymentScheduleInstallment lastInstallment = ctx.getInstallments().getLast(); + final LoanRepaymentScheduleInstallment newInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment( + loanTransaction.getLoan(), lastInstallment.getInstallmentNumber() + 1, rp.getFromDate(), rp.getDueDate(), + rp.getDuePrincipal().getAmount()); + + if (rp.getDueInterest().isGreaterThanZero()) { + newInstallment.addToInterest(transactionDate, rp.getDueInterest()); + } + + newInstallment.updateObligationsMet(ctx.getCurrency(), transactionDate); + ctx.getInstallments().add(newInstallment); } - insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate()); - } - int lastReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber(); - List<LoanRepaymentScheduleInstallment> toRemove = installments.stream().filter(i -> i != null && !i.isAdditional() - && i.getInstallmentNumber() != null && i.getInstallmentNumber() > lastReAgedInstallmentNumber).toList(); - toRemove.forEach(installments::remove); - reprocessInstallments(installments); + }); + + cleanupInstallmentsAndRepaymentPeriodsAfterReAging(ctx); } private void reprocessInstallments(final List<LoanRepaymentScheduleInstallment> installments) { @@ -3211,4 +3286,75 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep return false; } } + + private void handleReAgeWithInterestRecalculationEnabled(final LoanTransaction loanTransaction, final ProgressiveTransactionCtx ctx) { + final MonetaryCurrency currency = ctx.getCurrency(); + final Loan loan = loanTransaction.getLoan(); + final MathContext mc = MoneyHelper.getMathContext(); + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments(); + final List<RepaymentPeriod> repaymentPeriods = ctx.getModel().repaymentPeriods(); + final LocalDate reAgingStartDate = loanTransaction.getLoanReAgeParameter().getStartDate(); + + final List<RepaymentPeriod> periodsBeforeReAging = repaymentPeriods.stream() + .filter(rp -> rp.getFromDate().isBefore(reAgingStartDate) && !rp.isFullyPaid()).toList(); + + final RepaymentPeriod lastPeriod = periodsBeforeReAging.getLast(); + + if (!lastPeriod.getDueDate().isEqual(reAgingStartDate)) { + // TODO: implement logic when re-aging changes the due dates + throw new NotImplementedException("Logic when re-aging changes the due dates not implemented"); + } + + final BigDecimal interestBeforeReAging = emiCalculator + .getPeriodInterestTillDate(ctx.getModel(), lastPeriod.getDueDate(), transactionDate, false).getAmount(); + + final AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency)); + installments.forEach(i -> { + final Money principalOutstanding = i.getPrincipalOutstanding(currency); + if (principalOutstanding.isGreaterThanZero()) { + outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding)); + } + }); + + final AtomicReference<BigDecimal> interestFromZeroedInstallments = new AtomicReference<>(interestBeforeReAging); + + installments.stream() + .filter(installment -> !installment.isObligationsMet() && !installment.getDueDate().isAfter(lastPeriod.getFromDate())) + .forEach(installment -> { + final BigDecimal currentInterest = interestFromZeroedInstallments.get(); + final BigDecimal additionalInterest = MathUtil.nullToZero(installment.getInterestOutstanding(currency).getAmount() + .add(MathUtil.nullToZero(installment.getCreditedInterest()).negate())); + interestFromZeroedInstallments.set(currentInterest.add(additionalInterest)); + }); + + final BigDecimal interestRate = loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(); + + final LoanApplicationTerms loanApplicationTerms = new LoanApplicationTerms.Builder().currency(currency.getCurrencyData()) + .repaymentsStartingFromDate(reAgingStartDate).principal(outstandingPrincipalBalance.get()) + .loanTermFrequency(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()) + .loanTermPeriodFrequencyType(loanTransaction.getLoanReAgeParameter().getFrequencyType()) + .numberOfRepayments(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()) + .repaymentEvery(loanTransaction.getLoanReAgeParameter().getFrequencyNumber()) + .repaymentPeriodFrequencyType(loanTransaction.getLoanReAgeParameter().getFrequencyType()) + .interestRatePerPeriod(interestRate) + .interestRatePeriodFrequencyType(loan.getLoanRepaymentScheduleDetail().getRepaymentPeriodFrequencyType()) + .annualNominalInterestRate(interestRate).daysInMonthType(loan.getLoanProduct().fetchDaysInMonthType()) + .daysInYearType(loan.getLoanProduct().fetchDaysInYearType()).inArrearsTolerance(Money.zero(currency, mc)) + .isDownPaymentEnabled(false).downPaymentPercentage(ZERO).seedDate(reAgingStartDate) + .interestRecognitionOnDisbursementDate( + loan.getLoanProduct().getLoanProductRelatedDetail().isInterestRecognitionOnDisbursementDate()) + .daysInYearCustomStrategy(loan.getLoanProduct().getLoanProductRelatedDetail().getDaysInYearCustomStrategy()) + .interestMethod(loan.getLoanProductRelatedDetail().getInterestMethod()).allowPartialPeriodInterestCalculation( + loan.getLoanProduct().getLoanProductRelatedDetail().isAllowPartialPeriodInterestCalculation()) + .mc(mc).build(); + + // Update the existing model with re-aged periods + emiCalculator.updateModelRepaymentPeriodsDuringReAge(ctx.getModel(), loanTransaction, loanApplicationTerms, mc); + updateInstallmentsByModelForReAging(loanTransaction, ctx); + + loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(), + Money.of(currency, interestFromZeroedInstallments.get()), Money.zero(currency), Money.zero(currency)); + reprocessInstallments(installments); + } } 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 7ed0283c64..ed091679e9 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,6 +27,8 @@ import java.util.Optional; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; 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; @@ -140,4 +142,7 @@ public interface EMICalculator { * interest paused. */ void applyInterestPause(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate fromDate, LocalDate endDate); + + void updateModelRepaymentPeriodsDuringReAge(ProgressiveLoanInterestScheduleModel scheduleModel, LoanTransaction loanTransaction, + LoanApplicationTerms loanApplicationTerms, MathContext mc); } 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 b007774176..af1ca44069 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 @@ -40,6 +40,7 @@ import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; @@ -47,7 +48,11 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; 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; @@ -66,6 +71,8 @@ public final class ProgressiveEMICalculator implements EMICalculator { private static final BigDecimal DIVISOR_100 = new BigDecimal("100"); private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7); + private final ScheduledDateGenerator scheduledDateGenerator; + @Override @NotNull public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods, @@ -559,6 +566,129 @@ public final class ProgressiveEMICalculator implements EMICalculator { } } + @Override + public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveLoanInterestScheduleModel scheduleModel, + final LoanTransaction loanTransaction, final LoanApplicationTerms loanApplicationTerms, final MathContext mc) { + final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter(); + final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate(); + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods(); + + moveOutstandingAmountsFromPeriodsBeforeReAging(existingRepaymentPeriods, reAgingStartDate); + + final LocalDate periodStartDate = calculateFirstReAgedPeriodStartDate(loanReAgeParameter); + + final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryReAgedScheduleModel(loanApplicationTerms, + mc, periodStartDate, transactionDate); + + mergeNewInterestScheduleModelWithExistingOne(scheduleModel, temporaryReAgedScheduleModel, loanTransaction); + } + + /** + * * Merging the new temporary model of re-aged repayment periods and existing one together. After that recalculate + * the balances of the updated model and also recalculate the EMI if the EMI of the last repayment period differs + * significantly from other periods. + */ + private void mergeNewInterestScheduleModelWithExistingOne(final ProgressiveLoanInterestScheduleModel scheduleModel, + final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel, final LoanTransaction loanTransaction) { + final List<RepaymentPeriod> newPeriods = temporaryReAgedScheduleModel.repaymentPeriods(); + + if (newPeriods.isEmpty()) { + return; + } + + final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods(); + final LocalDate reAgingStartDate = loanTransaction.getLoanReAgeParameter().getStartDate(); + + final Optional<RepaymentPeriod> firstExistingRepaymentPeriodOpt = existingRepaymentPeriods.stream() + .filter(period -> period.getDueDate().equals(reAgingStartDate)).findFirst(); + + for (final RepaymentPeriod newPeriod : newPeriods) { + final Optional<RepaymentPeriod> existingRepaymentPeriodOpt = existingRepaymentPeriods.stream().filter( + period -> period.getFromDate().equals(newPeriod.getFromDate()) && period.getDueDate().equals(newPeriod.getDueDate())) + .findFirst(); + Optional<RepaymentPeriod> previousExistingRepaymentPeriodOpt = Optional.empty(); + if (existingRepaymentPeriodOpt.isPresent() && firstExistingRepaymentPeriodOpt.isPresent() + && existingRepaymentPeriodOpt.get().equals(firstExistingRepaymentPeriodOpt.get())) { + previousExistingRepaymentPeriodOpt = existingRepaymentPeriodOpt.get().getPrevious(); + } + + final Money newPrincipal = newPeriod.getDuePrincipal(); + final Money newInterest = newPeriod.getDueInterest(); + + final RepaymentPeriod rp = RepaymentPeriod.create( + previousExistingRepaymentPeriodOpt.orElseGet(existingRepaymentPeriods::getLast), newPeriod.getFromDate(), + newPeriod.getDueDate(), newPrincipal.add(newInterest), MoneyHelper.getMathContext(), + loanTransaction.getLoan().getLoanProductRelatedDetail()); + rp.setTotalDisbursedAmount(scheduleModel.repaymentPeriods().getFirst().getTotalDisbursedAmount()); + + existingRepaymentPeriodOpt.ifPresent(existingRepaymentPeriods::remove); + existingRepaymentPeriods.add(rp); + calculateRateFactorForRepaymentPeriod(rp, scheduleModel); + } + + final RepaymentPeriod lastReAgedInstallment = newPeriods.getLast(); + final List<RepaymentPeriod> reAgedRepaymentPeriods = existingRepaymentPeriods.stream() + .filter(repaymentPeriod -> (!repaymentPeriod.getFromDate().isBefore(reAgingStartDate) + || repaymentPeriod.getDueDate().isEqual(reAgingStartDate)) + && !repaymentPeriod.getDueDate().isAfter(lastReAgedInstallment.getDueDate())) + .toList(); + + calculateOutstandingBalance(scheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, loanTransaction.getTransactionDate()); + checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, reAgedRepaymentPeriods); + } + + /** + * * Generates temporary interestScheduleModel with re-aged repayment periods + */ + @NotNull + private ProgressiveLoanInterestScheduleModel generateTemporaryReAgedScheduleModel(final LoanApplicationTerms loanApplicationTerms, + final MathContext mc, final LocalDate periodStartDate, final LocalDate transactionDate) { + final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc, + periodStartDate, loanApplicationTerms, null); + final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetailMinimumData(), null, + loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc); + + addDisbursement(temporaryReAgedScheduleModel, EmiChangeOperation.disburse(transactionDate, loanApplicationTerms.getPrincipal())); + return temporaryReAgedScheduleModel; + } + + /** + * * Based on the re-aging start date and frequency data calculates start date for the first re-aged period, which + * is used to generate re-aged repayment periods + */ + @NotNull + private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanReAgeParameter loanReAgeParameter) { + final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate(); + return switch (loanReAgeParameter.getFrequencyType()) { + case DAYS -> reAgingStartDate.minusDays(loanReAgeParameter.getFrequencyNumber()); + case WEEKS -> reAgingStartDate.minusWeeks(loanReAgeParameter.getFrequencyNumber()); + case MONTHS -> reAgingStartDate.minusMonths(loanReAgeParameter.getFrequencyNumber()); + case YEARS -> reAgingStartDate.minusYears(loanReAgeParameter.getFrequencyNumber()); + case WHOLE_TERM -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WHOLE_TERM"); + case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID"); + }; + } + + /** + * * Zeroing out the EMI of the repayment periods, that are before re-aging and not been fully paid. And decreases + * the balance correction amount (added during interest recalculation for the business date) by the amount of the + * principal that was moved. + */ + private static void moveOutstandingAmountsFromPeriodsBeforeReAging(final List<RepaymentPeriod> existingRepaymentPeriods, + final LocalDate reAgingStartDate) { + final List<RepaymentPeriod> periodsBeforeReAging = existingRepaymentPeriods.stream() + .filter(rp -> rp.getFromDate().isBefore(reAgingStartDate) && !rp.isFullyPaid()).toList(); + + periodsBeforeReAging.forEach(rp -> { + final InterestPeriod lastInterestPeriod = rp.getInterestPeriods().getLast(); + lastInterestPeriod.addBalanceCorrectionAmount(rp.getOutstandingPrincipal().negated()); + rp.setEmi(rp.getTotalPaidAmount()); + }); + } + private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate tillDate) { Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()) .reduce((first, second) -> second); diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java index 7e8b93f369..e192fe5fe1 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java @@ -44,7 +44,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class LoanScheduleGeneratorTest { - private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(); + private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(mock(ScheduledDateGenerator.class)); private static final ApplicationCurrency APPLICATION_CURRENCY = new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$"); private static final CurrencyData CURRENCY = APPLICATION_CURRENCY.toData(); private static final BigDecimal DISBURSEMENT_AMOUNT = BigDecimal.valueOf(192.22); 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 085b205011..9d7c0ae4ba 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 @@ -18,6 +18,8 @@ */ package org.apache.fineract.portfolio.loanproduct.calc; +import static org.mockito.Mockito.mock; + import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; @@ -39,6 +41,7 @@ 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.domain.LoanScheduleModelRepaymentPeriod; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestScheduleModelParserServiceGsonImpl; import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod; import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails; @@ -64,7 +67,7 @@ import org.springframework.lang.NonNull; @ExtendWith(MockitoExtension.class) class ProgressiveEMICalculatorTest { - private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(); + private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(mock(ScheduledDateGenerator.class)); private static MockedStatic<ThreadLocalContextUtil> threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class); private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index e263a8c629..3af967027a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -860,7 +860,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) { loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() + || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } loan.getLoanTransactions().add(refundTransaction); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index b255354ee0..6f3ff467da 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -278,7 +278,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo if (reprocessRequired) { if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() + || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } @@ -857,7 +858,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo if (reprocessRequired) { addInstallmentIfPenaltyAppliedAfterLastDueDate(loan, lastChargeDate); if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() + || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } @@ -888,7 +890,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo loan.addLoanTransaction(loanChargeAdjustmentTransaction); if (loan.isInterestBearingAndInterestRecalculationEnabled()) { if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() + || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } @@ -1446,7 +1449,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo && DateUtils.isBefore(loanCharge.getDueLocalDate(), businessDate)) { loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } // Waive of charges whose due date falls after latest 'repayment' transaction don't require entire loan schedule 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 6068df09a0..f8225d9686 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 @@ -994,7 +994,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) { loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } @@ -1304,6 +1304,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf validateLoanTransactionAmountChargeBack(loanTransaction, newTransaction); + if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled()) { + if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) + || loan.hasContractTerminationTransaction() + || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { + final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); + loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); + } + } + // Store the Loan Transaction Relation LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, newTransaction, LoanTransactionRelationTypeEnum.CHARGEBACK); @@ -2273,11 +2282,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf updateDisbursementDateAndAmountForTranche(loan, loanDisbursementDetails, command, changes, scheduleGeneratorDTO); } else { loan.getLoanProductRelatedDetail().setPrincipal(loan.getPrincipalAmountForRepaymentSchedule()); - if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) { loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() + || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); reprocessLoanTransactionsService.processPostDisbursementTransactions(loan); } @@ -2432,7 +2441,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) { loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() + || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } } @@ -3000,7 +3010,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) { loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() + || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } loan.addLoanTransaction(interestRefundTxn); @@ -3264,7 +3275,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) { loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() + || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } reprocessLoanTransactionsService.reprocessTransactions(loan); @@ -3373,7 +3385,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) { loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } @@ -3430,7 +3442,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf writeOffTransaction.reverse(); loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO, loan); if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } 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 ec46968932..584b747f52 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 @@ -81,9 +81,8 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS LocalDate relatedRefundTransactionDate, List<LoanTransaction> transactionsToReprocess) { List<LoanRepaymentScheduleInstallment> installmentsToReprocess = new ArrayList<>( loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList()); - if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java index 91b2940ca5..f906ba679c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java @@ -165,8 +165,13 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa ProgressiveLoanInterestScheduleModel model = savedModel .orElseGet(() -> advancedProcessor.calculateInterestScheduleModel(loan.getId(), loanTransaction.getTransactionDate())); - transactionCtx = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), - loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), model); + final ProgressiveTransactionCtx progressiveTransactionCtx = new ProgressiveTransactionCtx(loan.getCurrency(), + loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), + new ChangedTransactionDetail(), model); + progressiveTransactionCtx.setChargedOff(loan.isChargedOff()); + progressiveTransactionCtx.setWrittenOff(loan.isClosedWrittenOff()); + progressiveTransactionCtx.setContractTerminated(loan.isContractTermination()); + transactionCtx = progressiveTransactionCtx; } else { transactionCtx = new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java index a01487a3c4..ba9703f4ec 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java @@ -24,6 +24,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.codes.domain.CodeValue; @@ -95,16 +96,25 @@ public class LoanReAgingServiceImpl { LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command); reAgeTransaction.setLoanReAgeParameter(reAgeParameter); loanTransactionRepository.saveAndFlush(reAgeTransaction); - - if (reAgeTransaction.getTransactionDate().isBefore(reAgeTransaction.getSubmittedOnDate())) { + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory + .determineProcessor(loan.transactionProcessingStrategy()); + if (reAgeTransaction.getTransactionDate().isBefore(reAgeTransaction.getSubmittedOnDate()) + && !loan.isInterestBearingAndInterestRecalculationEnabled()) { reprocessLoanTransactionsService.reprocessTransactionsWithPostTransactionChecks(loan, reAgeTransaction.getTransactionDate()); + } else if (loan.isInterestBearingAndInterestRecalculationEnabled()) { + if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) + || loan.hasContractTerminationTransaction() + || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { + final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); + loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); + } + final List<LoanTransaction> loanTransactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan); + loanTransactions.add(reAgeTransaction); + reprocessLoanTransactionsService.reprocessParticularTransactions(loan, loanTransactions); } else { - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory - .determineProcessor(loan.transactionProcessingStrategy()); loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction, new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); - } loan.updateLoanScheduleDependentDerivedFields(); persistNote(loan, command, changes); @@ -135,13 +145,13 @@ public class LoanReAgingServiceImpl { if (reAgeTransaction == null) { throw new LoanTransactionNotFoundException("Re-Age transaction for loan was not found"); } - reverseReAgeTransaction(reAgeTransaction, command); - loanTransactionRepository.saveAndFlush(reAgeTransaction); if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy()) - || loan.hasContractTerminationTransaction())) { + || loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) { final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); } + reverseReAgeTransaction(reAgeTransaction, command); + loanTransactionRepository.saveAndFlush(reAgeTransaction); reprocessLoanTransactionsService.reprocessTransactions(loan); loan.updateLoanScheduleDependentDerivedFields(); persistNote(loan, command, changes); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java index 5a3d4fb64a..4d9f75142f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java @@ -33,7 +33,6 @@ import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -68,7 +67,7 @@ public class LoanReAgingValidator { .notExceedingLengthOf(100); LocalDate startDate = command.localDateValueOfParameterNamed(LoanReAgingApiConstants.startDate); - if (loan.isProgressiveSchedule() && !loan.isInterestBearing()) { + if (loan.isProgressiveSchedule()) { baseDataValidator.reset().parameter(LoanReAgingApiConstants.startDate).value(startDate).notNull() .validateDateAfterOrEqual(loan.getDisbursementDate()); } else { @@ -105,14 +104,6 @@ public class LoanReAgingValidator { } private void validateReAgeBusinessRules(Loan loan) { - // validate reaging shouldn't happen before maturity - // on progressive loans it can - if (!(loan.isProgressiveSchedule() && !loan.isInterestBearing()) - && DateUtils.isBefore(getBusinessLocalDate(), loan.getMaturityDate())) { - throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.cannot.be.submitted.before.maturity", - "Loan cannot be re-aged before maturity", loan.getId()); - } - // validate reaging is only available for progressive schedule & advanced payment allocation LoanScheduleType loanScheduleType = LoanScheduleType.valueOf(loan.getLoanProductRelatedDetail().getLoanScheduleType().name()); boolean isProgressiveSchedule = LoanScheduleType.PROGRESSIVE.equals(loanScheduleType); @@ -127,12 +118,6 @@ public class LoanReAgingValidator { loan.getId()); } - // validate reaging is only available for non-interest bearing loans - if (loan.isInterestBearing()) { - throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.supported.only.for.non.interest.loans", - "Loan reaging is only available for non-interest bearing loans", loan.getId()); - } - // validate reaging is only done on an active loan if (!loan.getStatus().isActive()) { throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.supported.only.for.active.loans", diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidatorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidatorTest.java index 4eb4c534bb..a734e9ba53 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidatorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidatorTest.java @@ -288,20 +288,6 @@ class LoanReAgingValidatorTest { .isEqualTo("validation.msg.loan.reAge.numberOfInstallments.not.greater.than.zero"); } - @Test - public void testValidateReAge_ShouldThrowException_WhenLoanIsBeforeMaturity() { - // given - ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate))); - Loan loan = loan(); - JsonCommand command = jsonCommand(); - // when - GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class, - () -> underTest.validateReAge(loan, command)); - // then - assertThat(result).isNotNull(); - assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reage.cannot.be.submitted.before.maturity"); - } - @Test public void testValidateReAge_ShouldThrowException_WhenStartDateIsBeforeMaturity() { // given @@ -350,20 +336,6 @@ class LoanReAgingValidatorTest { .isEqualTo("error.msg.loan.reage.supported.only.for.progressive.loan.schedule.type"); } - @Test - public void testValidateReAge_ShouldThrowException_WhenLoanIsInterestBearing() { - // given - Loan loan = loan(); - given(loan.isInterestBearing()).willReturn(true); - JsonCommand command = jsonCommand(); - // when - GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class, - () -> underTest.validateReAge(loan, command)); - // then - assertThat(result).isNotNull(); - assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reage.supported.only.for.non.interest.loans"); - } - @Test public void testValidateReAge_ShouldThrowException_WhenLoanIsNotActive() { // given
