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 78c7abf05c21d493082c5c310a6d5050052740a3 Author: adam.magyari <[email protected]> AuthorDate: Tue Feb 11 13:04:15 2025 +0100 FINERACT-2162: Progressive loan chargeback accrual handling --- .../service/LoanAccrualsProcessingServiceImpl.java | 2 +- .../LoanCOBCreateAccrualsTest.java | 168 +++++++++++++++++++-- 2 files changed, 157 insertions(+), 13 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java index a2e90154d..ba140eff1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -434,7 +434,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing boolean isPastPeriod = isAfterPeriod(tillDate, installment); boolean isInPeriod = isInPeriod(tillDate, installment, false); if (isPastPeriod || loan.isClosed() || loan.isOverPaid()) { - interest = installment.getInterestCharged(currency); + interest = installment.getInterestCharged(currency).minus(installment.getCreditedInterest()); } else { if (isInPeriod) { // first period first day is not accrued interest = scheduleGenerator.getPeriodInterestTillDate(installment, tillDate); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java index bb8288fbe..7ede4180a 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java @@ -20,14 +20,20 @@ package org.apache.fineract.integrationtests; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.CreditAllocationData; +import org.apache.fineract.client.models.CreditAllocationOrder; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.integrationtests.common.ClientHelper; +import org.junit.Ignore; import org.junit.Test; import org.junit.jupiter.api.Assertions; @@ -467,18 +473,14 @@ public class LoanCOBCreateAccrualsTest extends BaseLoanIntegrationTest { || "loanTransactionType.accrualAdjustment".equals(t.getType().getCode()))); // Accruals around installment due dates are as expected - Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( - t -> t.getDate().equals(LocalDate.of(2025, 1, 20)) && t.getType().getAccrual() && t.getAmount().equals(0.16D))); - Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( - t -> t.getDate().equals(LocalDate.of(2025, 1, 21)) && t.getType().getAccrual() && t.getAmount().equals(0.16D))); - Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( - t -> t.getDate().equals(LocalDate.of(2025, 2, 20)) && t.getType().getAccrual() && t.getAmount().equals(0.16D))); - Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( - t -> t.getDate().equals(LocalDate.of(2025, 2, 21)) && t.getType().getAccrual() && t.getAmount().equals(0.18D))); - Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( - t -> t.getDate().equals(LocalDate.of(2025, 3, 20)) && t.getType().getAccrual() && t.getAmount().equals(0.18D))); - Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( - t -> t.getDate().equals(LocalDate.of(2025, 3, 21)) && t.getType().getAccrual() && t.getAmount().equals(0.16D))); + validateTransactionsExist(loanDetails, // + transaction(0.16, "Accrual", "20 January 2025", 0.0, 0.0, 0.16, 0.0, 0.0, 0.0, 0.0), // + transaction(0.16, "Accrual", "21 January 2025", 0.0, 0.0, 0.16, 0.0, 0.0, 0.0, 0.0), // + transaction(0.16, "Accrual", "20 February 2025", 0.0, 0.0, 0.16, 0.0, 0.0, 0.0, 0.0), // + transaction(0.18, "Accrual", "21 February 2025", 0.0, 0.0, 0.18, 0.0, 0.0, 0.0, 0.0), // + transaction(0.18, "Accrual", "20 March 2025", 0.0, 0.0, 0.18, 0.0, 0.0, 0.0, 0.0), // + transaction(0.16, "Accrual", "21 March 2025", 0.0, 0.0, 0.16, 0.0, 0.0, 0.0, 0.0) // + ); }); } @@ -563,4 +565,146 @@ public class LoanCOBCreateAccrualsTest extends BaseLoanIntegrationTest { transaction(430.0d, "Disbursement", "20 December 2024", 430.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)); }); } + + @Test + public void testProgressiveChargeBackNoInterestRecalculation() { + AtomicReference<Long> loanIdRef = new AtomicReference<>(); + AtomicReference<Long> repaymentIdRef = new AtomicReference<>(); + + setup(); + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().isInterestRecalculationEnabled(false) + .creditAllocation(chargebackCreditAllocationOrders(List.of("PRINCIPAL", "PENALTY", "FEE", "INTEREST"))) + .currencyCode("USD")); + + runAt("20 December 2024", () -> { + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "20 December 2024", + 430.0, 26.0, 6, null); + + loanIdRef.set(loanId); + + disburseLoan(loanId, BigDecimal.valueOf(430), "20 December 2024"); + executeInlineCOB(loanId); + }); + runAt("20 January 2025", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addCharge(loanId, true, 5.0d, "20 January 2025"); + Long repaymentId = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "20 January 2025", 82.20).getResourceId(); + repaymentIdRef.set(repaymentId); + }); + runAt("2 February 2025", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addChargebackForLoan(loanId, repaymentIdRef.get(), 82.20); + }); + runAt("20 February 2025", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateTransactionsExist(loanDetails, // + transaction(0.26, "Accrual", "01 February 2025", 0.0, 0.0, 0.26, 0.0, 0.0, 0.0, 0.0), // + transaction(0.25, "Accrual", "02 February 2025", 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0), // + transaction(0.25, "Accrual", "03 February 2025", 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0)); // + }); + runAt("23 February 2025", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateTransactionsExist(loanDetails, // + transaction(0.25, "Accrual", "19 February 2025", 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0), // + transaction(0.26, "Accrual", "20 February 2025", 0.0, 0.0, 0.26, 0.0, 0.0, 0.0, 0.0), // + transaction(0.23, "Accrual", "21 February 2025", 0.0, 0.0, 0.23, 0.0, 0.0, 0.0, 0.0), // + transaction(0.22, "Accrual", "22 February 2025", 0.0, 0.0, 0.22, 0.0, 0.0, 0.0, 0.0)); // + }); + } + + @Ignore // TODO: enable when implementation is complete + @Test + public void testProgressiveChargeBackInterestRecalculation() { + AtomicReference<Long> loanIdRef = new AtomicReference<>(); + AtomicReference<Long> repaymentIdRef = new AtomicReference<>(); + + setup(); + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().isInterestRecalculationEnabled(true) + .creditAllocation(chargebackCreditAllocationOrders(List.of("PRINCIPAL", "PENALTY", "FEE", "INTEREST"))) + .currencyCode("USD")); + + runAt("20 December 2024", () -> { + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "20 December 2024", + 430.0, 26.0, 6, null); + + loanIdRef.set(loanId); + + disburseLoan(loanId, BigDecimal.valueOf(430), "20 December 2024"); + executeInlineCOB(loanId); + }); + runAt("20 January 2025", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addCharge(loanId, true, 5.0d, "20 January 2025"); + Long repaymentId = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "20 January 2025", 82.20).getResourceId(); + repaymentIdRef.set(repaymentId); + }); + runAt("2 February 2025", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addChargebackForLoan(loanId, repaymentIdRef.get(), 82.20); + }); + runAt("20 February 2025", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateTransactionsExist(loanDetails, // + transaction(0.26, "Accrual", "01 February 2025", 0.0, 0.0, 0.26, 0.0, 0.0, 0.0, 0.0), // + transaction(0.25, "Accrual", "02 February 2025", 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0), // + transaction(0.30, "Accrual", "03 February 2025", 0.0, 0.0, 0.30, 0.0, 0.0, 0.0, 0.0), // + transaction(0.30, "Accrual", "04 February 2025", 0.0, 0.0, 0.30, 0.0, 0.0, 0.0, 0.0)); // + }); + runAt("23 February 2025", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateTransactionsExist(loanDetails, // + transaction(0.30, "Accrual", "19 February 2025", 0.0, 0.0, 0.30, 0.0, 0.0, 0.0, 0.0), // + transaction(0.30, "Accrual", "20 February 2025", 0.0, 0.0, 0.30, 0.0, 0.0, 0.0, 0.0), // + transaction(0.23, "Accrual", "21 February 2025", 0.0, 0.0, 0.23, 0.0, 0.0, 0.0, 0.0), // + transaction(0.22, "Accrual", "22 February 2025", 0.0, 0.0, 0.22, 0.0, 0.0, 0.0, 0.0)); // + }); + } + + private List<CreditAllocationData> chargebackCreditAllocationOrders(List<String> allocationIds) { + List<CreditAllocationOrder> creditAllocationOrders = new ArrayList<>(allocationIds.size()); + for (int i = 0; i < allocationIds.size(); i++) { + String allocationId = allocationIds.get(i); + creditAllocationOrders.add(new CreditAllocationOrder().order(i + 1).creditAllocationRule(allocationId)); + } + return List.of(new CreditAllocationData().transactionType("CHARGEBACK").creditAllocationOrder(creditAllocationOrders)); + } + + private void validateTransactionsExist(GetLoansLoanIdResponse loanDetails, TransactionExt... transactions) { + Arrays.stream(transactions).forEach(tr -> { + boolean found = loanDetails.getTransactions().stream().anyMatch(item -> Objects.equals(item.getAmount(), tr.amount) // + && Objects.equals(item.getType().getValue(), tr.type) // + && Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter)) // + && Objects.equals(item.getOutstandingLoanBalance(), tr.outstandingPrincipal) // + && Objects.equals(item.getPrincipalPortion(), tr.principalPortion) // + && Objects.equals(item.getInterestPortion(), tr.interestPortion) // + && Objects.equals(item.getFeeChargesPortion(), tr.feePortion) // + && Objects.equals(item.getPenaltyChargesPortion(), tr.penaltyPortion) // + && Objects.equals(item.getOverpaymentPortion(), tr.overpaymentPortion) // + && Objects.equals(item.getUnrecognizedIncomePortion(), tr.unrecognizedPortion) // + ); + Assertions.assertTrue(found, "Required transaction not found: " + tr + " on loan " + loanDetails.getId()); + }); + } }
