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 d32ddc91df1887826ec31d68a2d87e9a4ff51272
Author: Oleksii Novikov <[email protected]>
AuthorDate: Thu Mar 6 19:40:33 2025 +0200

    FINERACT-2204: Fix accrual activity reversal logic: prevent duplicate 
reverse-replay, copy external ID correctly
---
 .../test/messaging/event/EventCheckHelper.java     |  4 +-
 .../fineract/test/stepdef/loan/LoanStepDef.java    | 70 ++++++++++++++++++++++
 .../resources/features/LoanAccrualActivity.feature |  2 +-
 .../LoanAccrualActivityProcessingServiceImpl.java  |  9 ++-
 4 files changed, 82 insertions(+), 3 deletions(-)

diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java
index f14fd300ec..9bbf5f27b5 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java
@@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.math.MathContext;
+import java.math.RoundingMode;
 import java.time.format.DateTimeFormatter;
 import java.util.List;
 import lombok.RequiredArgsConstructor;
@@ -198,7 +199,8 @@ public class EventCheckHelper {
                     Long clientIdExpected = body.getClientId();
                     BigDecimal principalDisbursedActual = 
loanAccountDataV1.getSummary().getPrincipalDisbursed();
                     Double principalDisbursedExpectedDouble = 
body.getSummary().getPrincipalDisbursed();
-                    BigDecimal principalDisbursedExpected = new 
BigDecimal(principalDisbursedExpectedDouble, MathContext.DECIMAL64);
+                    BigDecimal principalDisbursedExpected = new 
BigDecimal(principalDisbursedExpectedDouble, MathContext.DECIMAL64)
+                            .setScale(8, RoundingMode.HALF_DOWN);
                     String actualDisbursementDateActual = 
loanAccountDataV1.getTimeline().getActualDisbursementDate();
                     String actualDisbursementDateExpected = 
FORMATTER_EVENTS.format(body.getTimeline().getActualDisbursementDate());
                     String currencyCodeActual = 
loanAccountDataV1.getSummary().getCurrency().getCode();
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java
index 17f7e3edc1..3c784b926c 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java
@@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.awaitility.Awaitility.await;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.google.gson.Gson;
@@ -49,6 +50,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 import lombok.extern.slf4j.Slf4j;
@@ -124,6 +126,7 @@ import 
org.apache.fineract.test.messaging.event.loan.LoanRescheduledDueAdjustSch
 import org.apache.fineract.test.messaging.event.loan.LoanStatusChangedEvent;
 import 
org.apache.fineract.test.messaging.event.loan.transaction.LoanAccrualAdjustmentTransactionBusinessEvent;
 import 
org.apache.fineract.test.messaging.event.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
+import 
org.apache.fineract.test.messaging.event.loan.transaction.LoanAdjustTransactionBusinessEvent;
 import 
org.apache.fineract.test.messaging.event.loan.transaction.LoanChargeAdjustmentPostBusinessEvent;
 import 
org.apache.fineract.test.messaging.event.loan.transaction.LoanChargeOffEvent;
 import 
org.apache.fineract.test.messaging.event.loan.transaction.LoanChargeOffUndoEvent;
@@ -2384,6 +2387,73 @@ public class LoanStepDef extends AbstractStepDef {
                 .noneMatch(t -> date.equals(FORMATTER.format(t.getDate())) && 
"Accrual Activity".equals(t.getType().getValue()));
     }
 
+    @Then("LoanAdjustTransactionBusinessEvent is raised for the origin of 
Accrual Activity on {string} but not raised for the replayed one")
+    public void checkLoanAdjustTransactionBusinessEvent(String date) throws 
IOException {
+        Response<PostLoansResponse> loanCreateResponse = 
testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+        long loanId = loanCreateResponse.body().getLoanId();
+
+        Response<GetLoansLoanIdResponse> loanDetailsResponse = 
loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
+        ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
+
+        List<GetLoansLoanIdTransactions> transactions = 
loanDetailsResponse.body().getTransactions();
+
+        GetLoansLoanIdTransactions loadTransaction = transactions.stream()
+                .filter(t -> date.equals(FORMATTER.format(t.getDate())) && 
"Accrual Activity".equals(t.getType().getValue())).findFirst()
+                .orElseThrow(() -> new IllegalStateException(String.format("No 
Accrual Activity transaction found on %s", date)));
+        Long replayedTransactionId = loadTransaction.getId();
+
+        Set<GetLoansLoanIdLoanTransactionRelation> transactionRelations = 
loadTransaction.getTransactionRelations();
+        Long originalTransactionId = 
transactionRelations.stream().map(GetLoansLoanIdLoanTransactionRelation::getToLoanTransaction)
+                .filter(Objects::nonNull).findFirst().get();
+
+        
eventAssertion.assertEventRaised(LoanAdjustTransactionBusinessEvent.class, 
originalTransactionId);
+        
eventAssertion.assertEventNotRaised(LoanAdjustTransactionBusinessEvent.class, 
replayedTransactionId);
+    }
+
+    @Then("LoanAdjustTransactionBusinessEvent is not raised on {string}")
+    public void checkLoanAdjustTransactionBusinessEventNotCreated(String date) 
throws IOException {
+        Response<PostLoansResponse> loanCreateResponse = 
testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+        long loanId = loanCreateResponse.body().getLoanId();
+
+        Response<GetLoansLoanIdResponse> loanDetailsResponse = 
loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
+        ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
+
+        List<GetLoansLoanIdTransactions> transactions = 
loanDetailsResponse.body().getTransactions();
+
+        assertThat(transactions).as("Unexpected Accrual Adjustment transaction 
found on %s", date)
+                .noneMatch(t -> date.equals(FORMATTER.format(t.getDate())) && 
"Accrual Adjustment".equals(t.getType().getValue()));
+    }
+
+    @Then("External ID for the replayed Accrual Activity on {string} is 
present but is null for the original transaction")
+    public void checkExternalIdForReplayedAccrualActivity(String date) throws 
IOException {
+        Response<PostLoansResponse> loanCreateResponse = 
testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+        long loanId = loanCreateResponse.body().getLoanId();
+
+        Response<GetLoansLoanIdResponse> loanDetailsResponse = 
loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
+        ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
+
+        List<GetLoansLoanIdTransactions> transactions = 
loanDetailsResponse.body().getTransactions();
+
+        GetLoansLoanIdTransactions loadTransaction = transactions.stream()
+                .filter(t -> date.equals(FORMATTER.format(t.getDate())) && 
"Accrual Activity".equals(t.getType().getValue())).findFirst()
+                .orElseThrow(() -> new IllegalStateException(String.format("No 
Accrual Activity transaction found on %s", date)));
+        Long replayedTransactionId = loadTransaction.getId();
+
+        Set<GetLoansLoanIdLoanTransactionRelation> transactionRelations = 
loadTransaction.getTransactionRelations();
+        Long originalTransactionId = 
transactionRelations.stream().map(GetLoansLoanIdLoanTransactionRelation::getToLoanTransaction)
+                .filter(Objects::nonNull).findFirst().get();
+
+        Response<GetLoansLoanIdTransactionsTransactionIdResponse> 
replayedTransaction = loanTransactionsApi
+                .retrieveTransaction(loanId, replayedTransactionId, 
"").execute();
+        assertNotNull(String.format("Replayed transaction external id is null 
%n%s", replayedTransaction.body()),
+                replayedTransaction.body().getExternalId());
+
+        Response<GetLoansLoanIdTransactionsTransactionIdResponse> 
originalTransaction = loanTransactionsApi
+                .retrieveTransaction(loanId, originalTransactionId, 
"").execute();
+        assertNull(String.format("Original transaction external id is not null 
%n%s", originalTransaction.body()),
+                originalTransaction.body().getExternalId());
+    }
+
     @Then("LoanTransactionAccrualActivityPostBusinessEvent is raised on 
{string}")
     public void checkLoanTransactionAccrualActivityPostBusinessEvent(String 
date) throws IOException {
         Response<PostLoansResponse> loanCreateResponse = 
testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
diff --git 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature
 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature
index daaea1dd75..a6d07fad30 100644
--- 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature
+++ 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity.feature
@@ -6036,4 +6036,4 @@ Feature: LoanAccrualActivity
       | 29 June 2024     | Accrual          | 0.07   | 0.0       | 0.07     | 
0.0  | 0.0       | 0.0          | false    | false    |
       | 30 June 2024     | Accrual          | 0.06   | 0.0       | 0.06     | 
0.0  | 0.0       | 0.0          | false    | false    |
       | 01 July 2024     | Repayment        | 340.17 | 336.11    | 1.96     | 
0.0  | 0.0       | 0.0          | false    | false    |
-      | 01 July 2024     | Accrual Activity | 1.96   | 0.0       | 1.96     | 
0.0  | 0.0       | 0.0          | false    | false    |
\ No newline at end of file
+      | 01 July 2024     | Accrual Activity | 1.96   | 0.0       | 1.96     | 
0.0  | 0.0       | 0.0          | false    | false    |
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java
index 49c2b14983..c837a63dbf 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java
@@ -207,14 +207,21 @@ public class LoanAccrualActivityProcessingServiceImpl 
implements LoanAccrualActi
             
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
             
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
                     loanTransaction, 
LoanTransactionRelationTypeEnum.REPLAYED));
+
+            
newLoanTransaction.updateExternalId(loanTransaction.getExternalId());
+            loanTransaction.reverse();
+            loanTransaction.updateExternalId(null);
+            
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(loanTransaction);
+
             
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(newLoanTransaction);
             loan.addLoanTransaction(newLoanTransaction);
 
             LoanAdjustTransactionBusinessEvent.Data data = new 
LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
             data.setNewTransactionDetail(newLoanTransaction);
             businessEventNotifierService.notifyPostBusinessEvent(new 
LoanAdjustTransactionBusinessEvent(data));
+        } else {
+            reverseAccrualActivityTransaction(loanTransaction);
         }
-        reverseAccrualActivityTransaction(loanTransaction);
         return newLoanTransaction;
     }
 

Reply via email to