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 49feaab3e57eecb7b9a5f19a9c23c20b19502dbf
Author: adam.magyari <[email protected]>
AuthorDate: Fri Mar 28 12:23:12 2025 +0100

    FINERACT-2226: progressive loan charge-off reverse replay events fix
---
 ...eplayedTransactionBusinessEventServiceImpl.java |  10 --
 .../LoanWritePlatformServiceJpaRepositoryImpl.java |   1 +
 .../ExternalBusinessEventTest.java                 | 154 +++++++++++++++++++++
 3 files changed, 155 insertions(+), 10 deletions(-)

diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java
index f31cbe6923..e82d3bd6b0 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java
@@ -20,8 +20,6 @@ package org.apache.fineract.portfolio.loanaccount.service;
 
 import lombok.RequiredArgsConstructor;
 import 
org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
-import 
org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualAdjustmentTransactionBusinessEvent;
-import 
org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
 import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
 import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
 import 
org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
@@ -51,14 +49,6 @@ public class ReplayedTransactionBusinessEventServiceImpl 
implements ReplayedTran
                     final LoanAdjustTransactionBusinessEvent.Data data = new 
LoanAdjustTransactionBusinessEvent.Data(oldTransaction);
                     data.setNewTransactionDetail(newTransaction);
                     businessEventNotifierService.notifyPostBusinessEvent(new 
LoanAdjustTransactionBusinessEvent(data));
-                } else {
-                    if (newTransaction.isAccrual()) {
-                        businessEventNotifierService
-                                .notifyPostBusinessEvent(new 
LoanAccrualTransactionCreatedBusinessEvent(newTransaction));
-                    } else {
-                        businessEventNotifierService
-                                .notifyPostBusinessEvent(new 
LoanAccrualAdjustmentTransactionBusinessEvent(newTransaction));
-                    }
                 }
             }
             businessEventNotifierService.stopExternalEventRecording();
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 5da8d282ea..aa5551219e 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
@@ -2944,6 +2944,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
 
         journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
         businessEventNotifierService.notifyPostBusinessEvent(new 
LoanChargeOffPostBusinessEvent(chargeOffTransaction));
+        
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
 existingTransactionIds);
         return new CommandProcessingResultBuilder() //
                 .withCommandId(command.commandId()) //
                 .withEntityId(chargeOffTransaction.getId()) //
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
index 7f7ea4a8b8..7baa0996e2 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
@@ -30,6 +30,7 @@ import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -856,6 +857,59 @@ public class ExternalBusinessEventTest extends 
BaseLoanIntegrationTest {
         });
     }
 
+    @Test
+    public void testProgressiveLoanReverseReplayChargeOffEvents() {
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+
+        final AtomicReference<Long> loanIdRef = new AtomicReference<>();
+        final AtomicReference<Long> repaymentTransactionIdRef = new 
AtomicReference<>();
+
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+
+        runAt("01 January 2025", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "01 January 2025",
+                    1000.0, 7.0, 6, null);
+
+            loanIdRef.set(loanId);
+
+            disburseLoan(loanId, BigDecimal.valueOf(1000), "01 January 2025");
+        });
+        runAt("01 February 2025", () -> {
+            final Long loanId = loanIdRef.get();
+            executeInlineCOB(loanId);
+            final PostLoansLoanIdTransactionsResponse 
postLoansLoanIdTransactionsResponse = 
loanTransactionHelper.makeLoanRepayment(loanId,
+                    "Repayment", "01 February 2025", 260.0);
+            
repaymentTransactionIdRef.set(postLoansLoanIdTransactionsResponse.getResourceId());
+        });
+        runAt("04 February 2025", () -> {
+            final Long loanId = loanIdRef.get();
+            executeInlineCOB(loanId);
+            chargeOffLoan(loanId, "04 February 2025");
+        });
+        runAt("05 February 2025", () -> {
+            final Long loanId = loanIdRef.get();
+            executeInlineCOB(loanId);
+
+            configureLoanAdjustTransactionBusinessEvent(true);
+            configureLoanAccrualTransactionCreatedBusinessEvent(true);
+            deleteAllExternalEvents();
+
+            loanTransactionHelper.reverseLoanTransaction(loanId, 
repaymentTransactionIdRef.get(), "01 February 2025");
+
+            List<ExternalEventDTO> allExternalEvents = 
ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
+            // Verify no BulkEvent was created
+            Assertions.assertEquals(0, allExternalEvents.stream().filter(e -> 
e.getType().equals("BulkBusinessEvent")).count());
+            verifyBusinessEvents(//
+                    new 
LoanAdjustTransactionBusinessEvent("LoanAdjustTransactionBusinessEvent", "05 
February 2025",
+                            "loanTransactionType.chargeOff", "2025-02-04"), //
+                    new 
LoanAdjustTransactionBusinessEvent("LoanAdjustTransactionBusinessEvent", "05 
February 2025",
+                            "loanTransactionType.repayment", "2025-02-01"), //
+                    new 
LoanTransactionBusinessEvent("LoanAccrualTransactionCreatedBusinessEvent", "05 
February 2025", 0.15, 0.0, 0.0, 0.15,
+                            0.0, 0.0)//
+            );
+        });
+    }
+
     @Nested
     class ExternalIdGenerationTest {
 
@@ -1034,6 +1088,14 @@ public class ExternalBusinessEventTest extends 
BaseLoanIntegrationTest {
         
externalEventHelper.configureBusinessEvent("LoanTransactionInterestRefundPostBusinessEvent",
 enabled);
     }
 
+    private void configureLoanAdjustTransactionBusinessEvent(boolean enabled) {
+        
externalEventHelper.configureBusinessEvent("LoanAdjustTransactionBusinessEvent",
 enabled);
+    }
+
+    private void configureLoanAccrualTransactionCreatedBusinessEvent(boolean 
enabled) {
+        
externalEventHelper.configureBusinessEvent("LoanAccrualTransactionCreatedBusinessEvent",
 enabled);
+    }
+
     public void verifyBusinessEvents(BusinessEvent... businessEvents) {
         List<ExternalEventDTO> allExternalEvents = 
ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
         logBusinessEvents(allExternalEvents);
@@ -1157,4 +1219,96 @@ public class ExternalBusinessEventTest extends 
BaseLoanIntegrationTest {
             return numberOfMatches == expectedTypes.size();
         }
     }
+
+    public static class LoanAdjustTransactionBusinessEvent extends 
BusinessEvent {
+
+        private String transactionTypeCode;
+        private String transactionDate;
+        private Double oldAmount;
+        private Double newAmount;
+        private Double oldPrincipalPortion;
+        private Double newPrincipalPortion;
+        private Double oldInterestPortion;
+        private Double newInterestPortion;
+        private Double oldFeePortion;
+        private Double newFeePortion;
+        private Double oldPenaltyPortion;
+        private Double newPenaltyPortion;
+
+        public LoanAdjustTransactionBusinessEvent(String type, String 
businessDate, String transactionTypeCode, String transactionDate) {
+            super(type, businessDate);
+            this.transactionTypeCode = transactionTypeCode;
+            this.transactionDate = transactionDate;
+        }
+
+        public LoanAdjustTransactionBusinessEvent(String type, String 
businessDate, String transactionTypeCode, String transactionDate,
+                Double oldAmount, Double newAmount) {
+            super(type, businessDate);
+            this.transactionTypeCode = transactionTypeCode;
+            this.transactionDate = transactionDate;
+            this.oldAmount = oldAmount;
+            this.newAmount = newAmount;
+        }
+
+        public LoanAdjustTransactionBusinessEvent(String type, String 
businessDate, String transactionTypeCode, String transactionDate,
+                Double oldAmount, Double newAmount, Double 
oldPrincipalPortion, Double newPrincipalPortion, Double oldInterestPortion,
+                Double newInterestPortion, Double oldFeePortion, Double 
newFeePortion, Double oldPenaltyPortion, Double newPenaltyPortion) {
+            super(type, businessDate);
+            this.transactionTypeCode = transactionTypeCode;
+            this.transactionDate = transactionDate;
+            this.oldAmount = oldAmount;
+            this.newAmount = newAmount;
+            this.oldPrincipalPortion = oldPrincipalPortion;
+            this.newPrincipalPortion = newPrincipalPortion;
+            this.oldInterestPortion = oldInterestPortion;
+            this.newInterestPortion = newInterestPortion;
+            this.oldFeePortion = oldFeePortion;
+            this.newFeePortion = newFeePortion;
+            this.oldPenaltyPortion = oldPenaltyPortion;
+            this.newPenaltyPortion = newPenaltyPortion;
+        }
+
+        @Override
+        boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter 
formatter) {
+            final Object transactionToAdjust = 
externalEvent.getPayLoad().get("transactionToAdjust");
+            final Map<?, Object> transActionToAdjustMap = transactionToAdjust 
instanceof Map ? (Map<String, Object>) transactionToAdjust
+                    : Collections.emptyMap();
+
+            Object actualOldAmount = transActionToAdjustMap.get("amount");
+            Object actualOldPrincipalPortion = 
transActionToAdjustMap.get("principalPortion");
+            Object actualOldInterestPortion = 
transActionToAdjustMap.get("interestPortion");
+            Object actualOldFeePortion = 
transActionToAdjustMap.get("feeChargesPortion");
+            Object actualOldPenaltyPortion = 
transActionToAdjustMap.get("penaltyChargesPortion");
+
+            final Object newTransactionDetail = 
externalEvent.getPayLoad().get("newTransactionDetail");
+            final Map<?, Object> newTransactionDetailMap = 
newTransactionDetail instanceof Map ? (Map<String, Object>) newTransactionDetail
+                    : Collections.emptyMap();
+
+            Object actualNewAmount = newTransactionDetailMap.get("amount");
+            Object actualNewPrincipalPortion = 
newTransactionDetailMap.get("principalPortion");
+            Object actualNewInterestPortion = 
newTransactionDetailMap.get("interestPortion");
+            Object actualNewFeePortion = 
newTransactionDetailMap.get("feeChargesPortion");
+            Object actualNewPenaltyPortion = 
newTransactionDetailMap.get("penaltyChargesPortion");
+
+            final Object actualTransactionDate = 
transActionToAdjustMap.get("date");
+            final Object transactionType = transActionToAdjustMap.get("type");
+            final Map<?, Object> transactionTypeMap = transactionType 
instanceof Map ? (Map<String, Object>) transactionType
+                    : Collections.emptyMap();
+            final Object actualTransactionTypeCode = 
transactionTypeMap.get("code");
+
+            return super.verify(externalEvent, formatter)//
+                    && Objects.equals(actualTransactionTypeCode, 
transactionTypeCode)
+                    && Objects.equals(actualTransactionDate, transactionDate)//
+                    && (oldAmount == null || Objects.equals(actualOldAmount, 
oldAmount))//
+                    && (newAmount == null || Objects.equals(actualNewAmount, 
newAmount))//
+                    && (oldPrincipalPortion == null || 
Objects.equals(actualOldPrincipalPortion, oldPrincipalPortion))//
+                    && (newPrincipalPortion == null || 
Objects.equals(actualNewPrincipalPortion, newPrincipalPortion))//
+                    && (oldInterestPortion == null || 
Objects.equals(actualOldInterestPortion, oldInterestPortion))//
+                    && (newInterestPortion == null || 
Objects.equals(actualNewInterestPortion, newInterestPortion))//
+                    && (oldFeePortion == null || 
Objects.equals(actualOldFeePortion, oldFeePortion))//
+                    && (newFeePortion == null || 
Objects.equals(actualNewFeePortion, newFeePortion))//
+                    && (oldPenaltyPortion == null || 
Objects.equals(actualOldPenaltyPortion, oldPenaltyPortion))//
+                    && (newPenaltyPortion == null || 
Objects.equals(actualNewPenaltyPortion, newPenaltyPortion));
+        }
+    }
 }

Reply via email to