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());
+        });
+    }
 }

Reply via email to