From 4c597b61fb6f460ff0755aa899f278f395c481f6 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Mon, 28 Nov 2022 21:24:57 +0100 Subject: [PATCH] FINERACT-1760: Raise transaction replayed business event --- .../cob/COBBusinessStepServiceImpl.java | 31 ++-- .../service/BusinessEventNotifierService.java | 2 + .../BusinessEventNotifierServiceImpl.java | 6 + .../domain/LoanAccountDomainServiceJpa.java | 7 + ...heduleRequestWritePlatformServiceImpl.java | 46 +----- .../LoanChargeWritePlatformServiceImpl.java | 7 + ...WritePlatformServiceJpaRepositoryImpl.java | 16 ++ ...playedTransactionBusinessEventService.java | 26 ++++ ...edTransactionBusinessEventServiceImpl.java | 59 ++++++++ ...COBBusinessStepServiceStepDefinitions.java | 10 ++ .../InitialisationTaskletStepDefinitions.java | 4 + ...onBusinessEventServiceIntegrationTest.java | 140 ++++++++++++++++++ .../test/resources/features/cob/cob.feature | 8 + 13 files changed, 310 insertions(+), 52 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceIntegrationTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java index e716c1ac3..2a62c5099 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java @@ -51,20 +51,27 @@ public class COBBusinessStepServiceImpl implements COBBusinessStepService { if (executionMap == null || executionMap.isEmpty()) { throw new BusinessStepException("Execution map is empty! COB Business step execution skipped!"); } - businessEventNotifierService.startExternalEventRecording(); - for (String businessStep : executionMap.values()) { - try { - ThreadLocalContextUtil.setActionContext(ActionContext.COB); - COBBusinessStep businessStepBean = (COBBusinessStep) applicationContext.getBean(businessStep); - item = businessStepBean.execute(item); - } catch (Exception e) { - throw new BusinessStepException("Error happened during business step execution", e); - } finally { - // Fallback to COB action context after each business step - ThreadLocalContextUtil.setActionContext(ActionContext.COB); + // Extra safety net to avoid event leaking + try { + businessEventNotifierService.startExternalEventRecording(); + + for (String businessStep : executionMap.values()) { + try { + ThreadLocalContextUtil.setActionContext(ActionContext.COB); + COBBusinessStep businessStepBean = (COBBusinessStep) applicationContext.getBean(businessStep); + item = businessStepBean.execute(item); + } catch (Exception e) { + throw new BusinessStepException("Error happened during business step execution", e); + } finally { + // Fallback to COB action context after each business step + ThreadLocalContextUtil.setActionContext(ActionContext.COB); + } } + businessEventNotifierService.stopExternalEventRecording(); + } catch (Exception e) { + businessEventNotifierService.resetEventRecording(); + throw e; } - businessEventNotifierService.stopExternalEventRecording(); return item; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java index d666b0eb6..d0176bc41 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierService.java @@ -50,4 +50,6 @@ public interface BusinessEventNotifierService { void startExternalEventRecording(); void stopExternalEventRecording(); + + void resetEventRecording(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java index e1369b22f..dca732ad9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java @@ -155,4 +155,10 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe recordedEvents.remove(); } } + + @Override + public void resetEventRecording() { + eventRecordingEnabled.set(false); + recordedEvents.remove(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 6652bcb1f..0846bbcaa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -91,6 +91,7 @@ import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; +import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService; import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; @@ -127,6 +128,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final DelinquencyWritePlatformService delinquencyWritePlatformService; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final ExternalIdFactory externalIdFactory; + private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; @Transactional @Override @@ -210,11 +212,14 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { if (changedTransactionDetail != null) { for (Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); // update loan with references to the newly created transactions loan.addLoanTransaction(mapEntry.getValue()); updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } if (StringUtils.isNotBlank(noteText)) { @@ -788,6 +793,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { loan.getLoanTransactions().add(mapEntry.getValue()); updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } saveAndFlushLoanWithDataIntegrityViolationChecks(loan); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java index 1a2244346..6396908d0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java @@ -30,6 +30,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper; @@ -78,10 +79,10 @@ import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanResch import org.apache.fineract.portfolio.loanaccount.rescheduleloan.exception.LoanRescheduleRequestNotFoundException; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; +import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService; import org.apache.fineract.useradministration.domain.AppUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.NonTransientDataAccessException; import org.springframework.orm.jpa.JpaSystemException; @@ -89,6 +90,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanRescheduleRequestWritePlatformService { private static final Logger LOG = LoggerFactory.getLogger(LoanRescheduleRequestWritePlatformServiceImpl.class); @@ -111,45 +113,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche private static final DefaultScheduledDateGenerator DEFAULT_SCHEDULED_DATE_GENERATOR = new DefaultScheduledDateGenerator(); private final LoanAccountDomainService loanAccountDomainService; private final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository; - - /** - * LoanRescheduleRequestWritePlatformServiceImpl constructor - * - * - **/ - @Autowired - public LoanRescheduleRequestWritePlatformServiceImpl(final CodeValueRepositoryWrapper codeValueRepositoryWrapper, - final PlatformSecurityContext platformSecurityContext, - final LoanRescheduleRequestDataValidator loanRescheduleRequestDataValidator, - final LoanRescheduleRequestRepository loanRescheduleRequestRepository, - final LoanRepaymentScheduleHistoryRepository loanRepaymentScheduleHistoryRepository, - final LoanScheduleHistoryWritePlatformService loanScheduleHistoryWritePlatformService, - final LoanTransactionRepository loanTransactionRepository, - final JournalEntryWritePlatformService journalEntryWritePlatformService, final LoanRepositoryWrapper loanRepositoryWrapper, - final LoanAssembler loanAssembler, final LoanUtilService loanUtilService, - final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, - final LoanScheduleGeneratorFactory loanScheduleFactory, final LoanSummaryWrapper loanSummaryWrapper, - final AccountTransfersWritePlatformService accountTransfersWritePlatformService, - final LoanAccountDomainService loanAccountDomainService, - final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository) { - this.codeValueRepositoryWrapper = codeValueRepositoryWrapper; - this.platformSecurityContext = platformSecurityContext; - this.loanRescheduleRequestDataValidator = loanRescheduleRequestDataValidator; - this.loanRescheduleRequestRepository = loanRescheduleRequestRepository; - this.loanRepaymentScheduleHistoryRepository = loanRepaymentScheduleHistoryRepository; - this.loanScheduleHistoryWritePlatformService = loanScheduleHistoryWritePlatformService; - this.loanTransactionRepository = loanTransactionRepository; - this.journalEntryWritePlatformService = journalEntryWritePlatformService; - this.loanRepositoryWrapper = loanRepositoryWrapper; - this.loanAssembler = loanAssembler; - this.loanUtilService = loanUtilService; - this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory; - this.loanScheduleFactory = loanScheduleFactory; - this.loanSummaryWrapper = loanSummaryWrapper; - this.accountTransfersWritePlatformService = accountTransfersWritePlatformService; - this.loanAccountDomainService = loanAccountDomainService; - this.repaymentScheduleInstallmentRepository = repaymentScheduleInstallmentRepository; - } + private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; /** * create a new instance of the LoanRescheduleRequest object from the JsonCommand object and persist @@ -490,6 +454,8 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche loan.addLoanTransaction(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index c187d86dd..745d76901 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -155,6 +155,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo private final ExternalIdFactory externalIdFactory; private final AccountTransferDetailRepository accountTransferDetailRepository; private final LoanChargeAssembler loanChargeAssembler; + private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) { return e.getFromDate().isBefore(loanCharge.getDueDate()) && !loanCharge.getDueDate().isAfter(e.getDueDate()); @@ -246,6 +247,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo loan.addLoanTransaction(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } this.loanRepositoryWrapper.save(loan); } @@ -740,6 +743,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo loan.addLoanTransaction(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } } @@ -1085,6 +1090,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo loan.addLoanTransaction(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } } } 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 19d680f0a..7cb647553 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 @@ -241,6 +241,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanAccountLockService loanAccountLockService; private final ExternalIdFactory externalIdFactory; + private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; @Transactional @Override @@ -457,6 +458,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanTransactionRepository.save(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } // auto create standing instruction @@ -732,6 +735,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanTransactionRepository.save(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); } @@ -1129,6 +1134,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.addLoanTransaction(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } final String noteText = command.stringValueOfParameterNamed("note"); @@ -1360,6 +1367,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.addLoanTransaction(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } final String noteText = command.stringValueOfParameterNamed("note"); @@ -2128,6 +2137,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanTransactionRepository.save(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } saveLoanWithDataIntegrityViolationChecks(loan); @@ -2268,8 +2279,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (command.entityId() != null && changedTransactionDetail != null) { for (Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + this.loanTransactionRepository.save(mapEntry.getValue()); updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { createLoanScheduleArchive(loan, scheduleGeneratorDTO); @@ -2326,6 +2340,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.addLoanTransaction(mapEntry.getValue()); this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccountDomainService.recalculateAccruals(loan); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventService.java new file mode 100644 index 000000000..614c9b719 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventService.java @@ -0,0 +1,26 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; + +public interface ReplayedTransactionBusinessEventService { + + void raiseTransactionReplayedEvents(ChangedTransactionDetail changedTransactionDetail); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java new file mode 100644 index 000000000..1359e00e6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceImpl.java @@ -0,0 +1,59 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ReplayedTransactionBusinessEventServiceImpl implements ReplayedTransactionBusinessEventService { + + private final BusinessEventNotifierService businessEventNotifierService; + private final LoanTransactionRepository loanTransactionRepository; + + @Override + public void raiseTransactionReplayedEvents(final ChangedTransactionDetail changedTransactionDetail) { + if (changedTransactionDetail == null || changedTransactionDetail.getNewTransactionMappings().isEmpty()) { + return; + } + // Extra safety net to avoid event leaking + try { + businessEventNotifierService.startExternalEventRecording(); + for (Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + LoanTransaction oldTransaction = loanTransactionRepository.findById(mapEntry.getKey()) + .orElseThrow(() -> new LoanTransactionNotFoundException(mapEntry.getKey())); + LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(oldTransaction); + data.setNewTransactionDetail(mapEntry.getValue()); + businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data)); + } + businessEventNotifierService.stopExternalEventRecording(); + } catch (Exception e) { + businessEventNotifierService.resetEventRecording(); + throw e; + } + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/COBBusinessStepServiceStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/COBBusinessStepServiceStepDefinitions.java index 866e13511..88cf32de9 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/COBBusinessStepServiceStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/COBBusinessStepServiceStepDefinitions.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import com.google.common.base.Splitter; import io.cucumber.java8.En; @@ -39,6 +40,7 @@ import org.apache.fineract.infrastructure.core.domain.ActionContext; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.mix.data.MixTaxonomyData; +import org.mockito.Mockito; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.context.ApplicationContext; @@ -147,6 +149,14 @@ public class COBBusinessStepServiceStepDefinitions implements En { ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); }); + Then("throw exception COBBusinessStepService.run method with verification", () -> { + assertThrows(BusinessStepException.class, () -> { + resultItem = this.businessStepService.run(this.executionMap, this.item); + }); + verify(businessEventNotifierService, Mockito.times(1)).resetEventRecording(); + ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); + }); + Then("The COBBusinessStepService.getCOBBusinessStepMap result exception", () -> { assertThrows(BeanCreationException.class, () -> { this.businessStepService.getCOBBusinessStepMap(this.clazz, this.jobName); diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/common/InitialisationTaskletStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/common/InitialisationTaskletStepDefinitions.java index 435266e52..cf4a41fe5 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/common/InitialisationTaskletStepDefinitions.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/common/InitialisationTaskletStepDefinitions.java @@ -24,6 +24,8 @@ import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import io.cucumber.java8.En; +import org.apache.fineract.infrastructure.core.domain.ActionContext; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper; import org.springframework.batch.repeat.RepeatStatus; @@ -56,12 +58,14 @@ public class InitialisationTaskletStepDefinitions implements En { Then("InitialisationTasklet.execute result should match", () -> { assertEquals(RepeatStatus.FINISHED, resultItem); assertEquals(appUser, SecurityContextHolder.getContext().getAuthentication().getPrincipal()); + ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); }); Then("throw exception InitialisationTasklet.execute method", () -> { assertThrows(RuntimeException.class, () -> { resultItem = this.initialisationTasklet.execute(null, null); }); + ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); }); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceIntegrationTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceIntegrationTest.java new file mode 100644 index 000000000..824f22c15 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/ReplayedTransactionBusinessEventServiceIntegrationTest.java @@ -0,0 +1,140 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +import java.util.Optional; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ReplayedTransactionBusinessEventServiceIntegrationTest { + + @Mock + private BusinessEventNotifierService businessEventNotifierService; + + @Mock + private LoanTransactionRepository loanTransactionRepository; + + private ReplayedTransactionBusinessEventService underTest; + + @BeforeEach + public void setUp() { + underTest = new ReplayedTransactionBusinessEventServiceImpl(businessEventNotifierService, loanTransactionRepository); + } + + @Test + public void testWhenParamIsNull() { + // given + ChangedTransactionDetail changedTransactionDetail = null; + // when + underTest.raiseTransactionReplayedEvents(changedTransactionDetail); + // then + verify(businessEventNotifierService, Mockito.times(0)).startExternalEventRecording(); + verify(businessEventNotifierService, Mockito.times(0)) + .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class)); + verify(businessEventNotifierService, Mockito.times(0)).stopExternalEventRecording(); + verify(businessEventNotifierService, Mockito.times(0)).resetEventRecording(); + } + + @Test + public void testWhenParamHasNoMapping() { + // given + ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); + // when + underTest.raiseTransactionReplayedEvents(changedTransactionDetail); + // then + verify(businessEventNotifierService, Mockito.times(0)).startExternalEventRecording(); + verify(businessEventNotifierService, Mockito.times(0)) + .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class)); + verify(businessEventNotifierService, Mockito.times(0)).stopExternalEventRecording(); + verify(businessEventNotifierService, Mockito.times(0)).resetEventRecording(); + } + + @Test + public void testWhenParamHasOneNewTransaction() { + // given + LoanTransaction oldLoanTransaction = Mockito.mock(LoanTransaction.class); + LoanTransaction newLoanTransaction = Mockito.mock(LoanTransaction.class); + lenient().when(loanTransactionRepository.findById(1L)).thenReturn(Optional.of(oldLoanTransaction)); + ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); + changedTransactionDetail.getNewTransactionMappings().put(1L, newLoanTransaction); + // when + underTest.raiseTransactionReplayedEvents(changedTransactionDetail); + // then + verify(businessEventNotifierService, Mockito.times(1)).startExternalEventRecording(); + verify(businessEventNotifierService, Mockito.times(1)) + .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class)); + verify(businessEventNotifierService, Mockito.times(1)).stopExternalEventRecording(); + verify(businessEventNotifierService, Mockito.times(0)).resetEventRecording(); + } + + @Test + public void testWhenParamHasTwoNewTransaction() { + // given + LoanTransaction oldLoanTransaction = Mockito.mock(LoanTransaction.class); + LoanTransaction newLoanTransaction = Mockito.mock(LoanTransaction.class); + lenient().when(loanTransactionRepository.findById(1L)).thenReturn(Optional.of(oldLoanTransaction)); + lenient().when(loanTransactionRepository.findById(2L)).thenReturn(Optional.of(oldLoanTransaction)); + ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); + changedTransactionDetail.getNewTransactionMappings().put(1L, newLoanTransaction); + changedTransactionDetail.getNewTransactionMappings().put(2L, newLoanTransaction); + // when + underTest.raiseTransactionReplayedEvents(changedTransactionDetail); + // then + verify(businessEventNotifierService, Mockito.times(1)).startExternalEventRecording(); + verify(businessEventNotifierService, Mockito.times(2)) + .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class)); + verify(businessEventNotifierService, Mockito.times(1)).stopExternalEventRecording(); + verify(businessEventNotifierService, Mockito.times(0)).resetEventRecording(); + } + + @Test + public void testWhenParamHasError() { + // given + doThrow(new RuntimeException()).when(businessEventNotifierService) + .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class)); + LoanTransaction oldLoanTransaction = Mockito.mock(LoanTransaction.class); + LoanTransaction newLoanTransaction = Mockito.mock(LoanTransaction.class); + lenient().when(loanTransactionRepository.findById(1L)).thenReturn(Optional.of(oldLoanTransaction)); + ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); + changedTransactionDetail.getNewTransactionMappings().put(1L, newLoanTransaction); + // when + assertThrows(RuntimeException.class, () -> underTest.raiseTransactionReplayedEvents(changedTransactionDetail)); + // then + verify(businessEventNotifierService, Mockito.times(1)).startExternalEventRecording(); + verify(businessEventNotifierService, Mockito.times(1)) + .notifyPostBusinessEvent(Mockito.any(LoanAdjustTransactionBusinessEvent.class)); + verify(businessEventNotifierService, Mockito.times(0)).stopExternalEventRecording(); + verify(businessEventNotifierService, Mockito.times(1)).resetEventRecording(); + } +} diff --git a/fineract-provider/src/test/resources/features/cob/cob.feature b/fineract-provider/src/test/resources/features/cob/cob.feature index 4818ee6d9..5645e271e 100644 --- a/fineract-provider/src/test/resources/features/cob/cob.feature +++ b/fineract-provider/src/test/resources/features/cob/cob.feature @@ -37,6 +37,14 @@ Feature: COB Examples: |executionMap| |null| + + @cob + Scenario Outline: COB Business Step Service - run test failure + Given The COBBusinessStepService.run method with executeMap + Then throw exception COBBusinessStepService.run method with verification + + Examples: + |executionMap| |2,notExist| |3,|