From 979b586eb2b3da982b80bee6d1ff1ba38950172c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soma=20S=C3=B6r=C3=B6s?= Date: Tue, 7 Jan 2025 19:38:08 +0100 Subject: [PATCH] FINERACT-1971: Fix Accrual Activity reversal on Progressive interest bearing loans during loan reopening --- ...RepaymentScheduleTransactionProcessor.java | 22 ++- ...TransactionAccrualActivityPostingTest.java | 149 ++++++++++++++++++ 2 files changed, 159 insertions(+), 12 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index dbe127bfed6..bae506da01b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -218,24 +218,22 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur protected void calculateAccrualActivity(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments) { - loanTransaction.resetDerivedComponents(); - // determine how much is outstanding total and breakdown for principal, interest and charges - final Money principalPortion = Money.zero(currency); - Money interestPortion = Money.zero(currency); - Money feeChargesPortion = Money.zero(currency); - Money penaltychargesPortion = Money.zero(currency); + final int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); final LoanRepaymentScheduleInstallment currentInstallment = installments.stream() .filter(installment -> LoanRepaymentScheduleProcessingWrapper.isInPeriod(loanTransaction.getTransactionDate(), installment, installment.getInstallmentNumber().equals(firstNormalInstallmentNumber))) .findFirst().orElseThrow(); - - interestPortion = interestPortion.plus(currentInstallment.getInterestCharged(currency)); - feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesCharged(currency)); - penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesCharged(currency)); - - loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion); + if (loanTransaction.getDateOf().isEqual(currentInstallment.getDueDate()) || installments.stream() + .filter(i -> !i.isAdditional() && !i.isDownPayment()).noneMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)) { + loanTransaction.resetDerivedComponents(); + final Money principalPortion = Money.zero(currency); + Money interestPortion = currentInstallment.getInterestCharged(currency); + Money feeChargesPortion = currentInstallment.getFeeChargesCharged(currency); + Money penaltyChargesPortion = currentInstallment.getPenaltyChargesCharged(currency); + loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } } private void recalculateAccrualActivityTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction, diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java index 622394ea89c..66301d4ac55 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java @@ -42,15 +42,18 @@ import org.apache.fineract.client.models.ChargeToGLAccountMapper; import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings; import org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PostChargesRequest; import org.apache.fineract.client.models.PostChargesResponse; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest; import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdRequest; import org.apache.fineract.client.models.PostLoansRequest; +import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.integrationtests.common.BusinessStepHelper; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.SchedulerJobHelper; @@ -910,6 +913,152 @@ public void testAccrualActivityPostingForMultiDisburseLoan() { }); } + Long interestBearingProgressiveLoanProductId = null; + + public void createInterestBearingProgressiveNoInterestRecalculationAutoDownPayment25percentLoanProductIfNotExists() { + if (interestBearingProgressiveLoanProductId == null) { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive() + .currencyCode("USD").enableAccrualActivityPosting(true).enableDownPayment(true) + .disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25.0)).enableAutoRepaymentForDownPayment(true) + .currencyCode("USD").daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.ACTUAL) + .isInterestRecalculationEnabled(false).description( + "Interest bearing Progressive Loan USD, Auto Down Payment 25%, Accrual Activity Posting, NO InterestRecalculation")); + interestBearingProgressiveLoanProductId = loanProductsResponse.getResourceId(); + } + } + + /* + * using Interest bearing Progressive Loan USD, Auto Down Payment 25%, Accrual Activity Posting, NO + * InterestRecalculation 9.99 yearly interest 6 repayment 400 USD principal apply, approve and disburse on 1 January + * 2024 auto down payment 100 USD on 1 January 2024 repayment 370USD on 2 January 2024 verify Accrual and Accrual + * Activity transaction creation verify that the loan become overpaid reverse the repayment on same day verify + * transaction reversals + */ + @Test + public void testInterestBearingProgressiveNoInterestRecalculationAutoDownPayment25percentReopenDueReverseRepayment1() { + createInterestBearingProgressiveNoInterestRecalculationAutoDownPayment25percentLoanProductIfNotExists(); + AtomicReference loanIdRef = new AtomicReference<>(null); + runAt("1 January 2024", () -> { + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + interestBearingProgressiveLoanProductId, "01 January 2024", 400.0, 9.99, 6, null)); + Long loanId = postLoansResponse.getLoanId(); + Assertions.assertNotNull(loanId); + loanIdRef.set(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(400.0, "01 January 2024")); + disburseLoan(loanId, BigDecimal.valueOf(400.0), "01 January 2024"); + verifyTransactions(loanId, // + transaction(400.0, "Disbursement", "01 January 2024"), // + transaction(100.0, "Down Payment", "01 January 2024") // + ); + }); + runAt("2 January 2024", () -> { + Long loanId = loanIdRef.get(); + Long repaymentId = loanTransactionHelper.makeLoanRepayment("02 January 2024", 370.0f, loanId.intValue()).getResourceId(); + Assertions.assertNotNull(repaymentId); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertNotNull(loanDetails); + Assertions.assertNotNull(loanDetails.getStatus()); + Assertions.assertNotNull(loanDetails.getStatus().getOverpaid()); + Assertions.assertTrue(loanDetails.getStatus().getOverpaid()); + + verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), + transaction(100.0, "Down Payment", "01 January 2024"), transaction(8.76, "Accrual", "02 January 2024"), + transaction(8.76, "Accrual Activity", "02 January 2024"), transaction(370.0, "Repayment", "02 January 2024")); + loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "02 January 2024"); + }); + } + + /* + * using Interest bearing Progressive Loan USD, Auto Down Payment 25%, Accrual Activity Posting, NO + * InterestRecalculation 9.99 yearly interest 6 repayment 400 USD principal apply, approve and disburse on 1 January + * 2024 auto down payment 100 USD on 1 January 2024 repayment 370USD on 1 January 2024 verify Accrual and Accrual + * Activity transaction creation verify that the loan become overpaid reverse the repayment on same day verify + * transaction reversals + */ + @Test + public void testInterestBearingProgressiveNoInterestRecalculationAutoDownPayment25percentReopenDueReverseRepayment2() { + createInterestBearingProgressiveNoInterestRecalculationAutoDownPayment25percentLoanProductIfNotExists(); + runAt("1 January 2024", () -> { + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + interestBearingProgressiveLoanProductId, "01 January 2024", 400.0, 9.99, 6, null)); + Long loanId = postLoansResponse.getLoanId(); + Assertions.assertNotNull(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(400.0, "01 January 2024")); + disburseLoan(loanId, BigDecimal.valueOf(400.0), "01 January 2024"); + verifyTransactions(loanId, // + transaction(400.0, "Disbursement", "01 January 2024"), // + transaction(100.0, "Down Payment", "01 January 2024") // + ); + Long repaymentId = loanTransactionHelper.makeLoanRepayment("01 January 2024", 370.0f, loanId.intValue()).getResourceId(); + Assertions.assertNotNull(repaymentId); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertNotNull(loanDetails); + Assertions.assertNotNull(loanDetails.getStatus()); + Assertions.assertNotNull(loanDetails.getStatus().getOverpaid()); + Assertions.assertTrue(loanDetails.getStatus().getOverpaid()); + + verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), + transaction(100.0, "Down Payment", "01 January 2024"), transaction(8.76, "Accrual", "01 January 2024"), + transaction(8.76, "Accrual Activity", "01 January 2024"), transaction(370.0, "Repayment", "01 January 2024")); + + loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "01 January 2024"); + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertNotNull(loanDetails); + Assertions.assertNotNull(loanDetails.getStatus()); + Assertions.assertNotNull(loanDetails.getStatus().getActive()); + Assertions.assertTrue(loanDetails.getStatus().getActive()); + verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), + transaction(100.0, "Down Payment", "01 January 2024"), transaction(8.76, "Accrual", "01 January 2024"), + reversedTransaction(370.0, "Repayment", "01 January 2024")); + + }); + } + + /* + * using Interest bearing Progressive Loan USD, Auto Down Payment 25%, Accrual Activity Posting, NO + * InterestRecalculation 9.99 yearly interest 6 repayment 400 USD principal apply, approve and disburse on 1 January + * 2024 auto down payment 100 USD on 1 January 2024 charge 30USD fee on 1 January 2024 repayment 370USD on 1 January + * 2024 verify Accrual and Accrual Activity transaction creation verify that the loan become overpaid reverse the + * repayment on same day verify transaction reversals + */ + @Test + public void testInterestBearingProgressiveNoInterestRecalculationAutoDownPayment25percentReopenDueReverseRepayment3() { + createInterestBearingProgressiveNoInterestRecalculationAutoDownPayment25percentLoanProductIfNotExists(); + runAt("1 January 2024", () -> { + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + interestBearingProgressiveLoanProductId, "01 January 2024", 400.0, 9.99, 6, null)); + Long loanId = postLoansResponse.getLoanId(); + Assertions.assertNotNull(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(400.0, "01 January 2024")); + disburseLoan(loanId, BigDecimal.valueOf(400.0), "01 January 2024"); + verifyTransactions(loanId, // + transaction(400.0, "Disbursement", "01 January 2024"), // + transaction(100.0, "Down Payment", "01 January 2024") // + ); + addCharge(loanId, false, 30.0, "01 January 2024"); + Long repaymentId = loanTransactionHelper.makeLoanRepayment("01 January 2024", 370.0f, loanId.intValue()).getResourceId(); + Assertions.assertNotNull(repaymentId); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertNotNull(loanDetails); + Assertions.assertNotNull(loanDetails.getStatus()); + Assertions.assertNotNull(loanDetails.getStatus().getOverpaid()); + Assertions.assertTrue(loanDetails.getStatus().getOverpaid()); + + verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), + transaction(100.0, "Down Payment", "01 January 2024"), transaction(38.76, "Accrual", "01 January 2024"), + transaction(38.76, "Accrual Activity", "01 January 2024"), transaction(370.0, "Repayment", "01 January 2024")); + loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentId.intValue(), "01 January 2024"); + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertNotNull(loanDetails); + Assertions.assertNotNull(loanDetails.getStatus()); + Assertions.assertNotNull(loanDetails.getStatus().getActive()); + Assertions.assertTrue(loanDetails.getStatus().getActive()); + verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 January 2024"), + transaction(100.0, "Down Payment", "01 January 2024"), transaction(38.76, "Accrual", "01 January 2024"), + reversedTransaction(370.0, "Repayment", "01 January 2024")); + }); + } + @Test public void test() { final String disbursementDay = "01 January 2023";