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; }
