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