diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 367b72af..f127d682 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,10 @@ name: Build on: push: - branches: [ main, develop ] + branches: + - main + - develop + - 'release/*' tags: - '[0-9]+.[0-9]+.[0-9]+*' pull_request: diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/config/BusinessRulesConfig.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/config/BusinessRulesConfig.java index 097c4819..fb6eb893 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/config/BusinessRulesConfig.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/config/BusinessRulesConfig.java @@ -69,8 +69,7 @@ private PipelineTask sanityCheckPipelineTask() { private PipelineTask preCleansingPipelineTask() { return new DefaultPipelineTask(List.of( - new DiscardZeroBalanceTxItemsTaskItem(), - new JournalAccountCreditEnrichmentTaskItem(organisationPublicApi) + new DiscardZeroBalanceTxItemsTaskItem() )); } @@ -79,7 +78,8 @@ private PipelineTask preValidationPipelineTask() { new AmountsFcyCheckTaskItem(), new AmountsLcyCheckTaskItem(), new AmountLcyBalanceZerosOutCheckTaskItem(), - new AmountFcyBalanceZerosOutCheckTaskItem() + new AmountFcyBalanceZerosOutCheckTaskItem(), + new JournalAccountCreditEnrichmentTaskItem(organisationPublicApi) )); } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/job/TxStatusUpdaterJob.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/job/TxStatusUpdaterJob.java new file mode 100644 index 00000000..50ba503c --- /dev/null +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/job/TxStatusUpdaterJob.java @@ -0,0 +1,71 @@ +package org.cardanofoundation.lob.app.accounting_reporting_core.job; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TxStatusUpdate; +import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.TransactionEntity; +import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.LedgerService; +import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.TransactionBatchService; + +@Service +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty(value = "lob.accounting_reporting_core.enabled", havingValue = "true", matchIfMissing = true) +public class TxStatusUpdaterJob { + + private final Map txStatusUpdatesMap = new ConcurrentHashMap<>(); + private final LedgerService ledgerService; + private final TransactionBatchService transactionBatchService; + + @Value("${lob.blockchain.tx-status-updater.max-map-size:1000}") + private int maxMapSize; + + // This Job collects all TxStatusUpdate events and updates the transactions in the database + @Scheduled( + fixedDelayString = "${ob.blockchain.tx-status-updater.fixed_delay:PT30S}", + initialDelayString = "${lob.blockchain.tx-status-updater.delay:PT30S}") + @Transactional + public void execute() { + Map updates; + synchronized (txStatusUpdatesMap) { + updates = new HashMap<>(txStatusUpdatesMap); + } + if(updates.isEmpty()) { + log.debug("No TxStatusUpdate events to process"); + return; + } + try { + log.info("Updating Status of {} transactions", updates.size()); + List transactionEntities = ledgerService.updateTransactionsWithNewStatuses(updates); + ledgerService.saveAllTransactionEntities(transactionEntities); + + transactionBatchService.updateBatchesPerTransactions(updates); + updates.forEach(txStatusUpdatesMap::remove); + } catch (Exception e) { + log.error("Failed to process TxStatusUpdates - entries will be retained in the map", e); + } + } + + public void addToStatusUpdateMap(Map updateMap) { + synchronized (txStatusUpdatesMap) { + txStatusUpdatesMap.putAll(updateMap); + } + if(txStatusUpdatesMap.size() > maxMapSize) { + log.warn("TxStatusUpdate map size exceeded the limit of {}. Current size: {}", maxMapSize, txStatusUpdatesMap.size()); + } + } + + +} diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/AccountingCorePresentationViewService.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/AccountingCorePresentationViewService.java index 13249209..b9580b25 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/AccountingCorePresentationViewService.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/AccountingCorePresentationViewService.java @@ -36,6 +36,7 @@ import org.cardanofoundation.lob.app.accounting_reporting_core.resource.views.*; import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.AccountingCoreService; import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.TransactionRepositoryGateway; +import org.cardanofoundation.lob.app.organisation.OrganisationPublicApiIF; import org.cardanofoundation.lob.app.organisation.domain.entity.OrganisationCostCenter; import org.cardanofoundation.lob.app.organisation.domain.entity.OrganisationProject; import org.cardanofoundation.lob.app.organisation.repository.CostCenterRepository; @@ -57,6 +58,7 @@ public class AccountingCorePresentationViewService { private final TransactionReconcilationRepository transactionReconcilationRepository; private final CostCenterRepository costCenterRepository; private final ProjectMappingRepository projectMappingRepository; + private final OrganisationPublicApiIF organisationPublicApiIF; /** * TODO: waiting for refactoring the layer to remove this @@ -519,7 +521,8 @@ public BigDecimal getAmountLcyTotalForAllDebitItems(TransactionEntity tx) { Set items = tx.getItems(); if (tx.getTransactionType().equals(TransactionType.Journal)) { - items = tx.getItems().stream().filter(txItems -> txItems.getOperationType().equals(OperationType.DEBIT)).collect(toSet()); + Optional dummyAccount = organisationPublicApiIF.findByOrganisationId(tx.getOrganisation().getId()).orElse(new org.cardanofoundation.lob.app.organisation.domain.entity.Organisation()).getDummyAccount(); + items = tx.getItems().stream().filter(txItems -> txItems.getAccountDebit().isPresent() && txItems.getAccountDebit().get().getCode().equals(dummyAccount.orElse(""))).collect(toSet()); } if (tx.getTransactionType().equals(TransactionType.FxRevaluation)) { diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/ExtractionItemService.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/ExtractionItemService.java index 052e973b..09ee1047 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/ExtractionItemService.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/ExtractionItemService.java @@ -21,6 +21,9 @@ import org.cardanofoundation.lob.app.accounting_reporting_core.repository.TransactionItemExtractionRepository; import org.cardanofoundation.lob.app.accounting_reporting_core.resource.views.ExtractionTransactionItemView; import org.cardanofoundation.lob.app.accounting_reporting_core.resource.views.ExtractionTransactionView; +import org.cardanofoundation.lob.app.organisation.OrganisationPublicApi; +import org.cardanofoundation.lob.app.organisation.domain.entity.OrganisationCostCenter; +import org.cardanofoundation.lob.app.organisation.domain.entity.OrganisationProject; @Service @Slf4j @@ -28,11 +31,14 @@ @Transactional public class ExtractionItemService { private final TransactionItemExtractionRepository transactionItemRepositoryImpl; + private final OrganisationPublicApi organisationPublicApi; @Transactional(readOnly = true) public ExtractionTransactionView findTransactionItems(LocalDate dateFrom, LocalDate dateTo, List accountCode, List costCenter, List project, List accountType, List accountSubType) { - List transactionItem = transactionItemRepositoryImpl.findByItemAccount(dateFrom, dateTo, accountCode, costCenter, project,accountType,accountSubType).stream().map(this::extractionTransactionItemViewBuilder).collect(Collectors.toList()); + List transactionItem = transactionItemRepositoryImpl.findByItemAccount(dateFrom, dateTo, accountCode, costCenter, project, accountType, accountSubType).stream().map(item -> { + return extractionTransactionItemViewBuilder(item); + }).collect(Collectors.toList()); return ExtractionTransactionView.createSuccess(transactionItem); } @@ -79,7 +85,9 @@ private ExtractionTransactionItemView extractionTransactionItemViewBuilder(Trans item.getDocument().flatMap(d -> d.getCounterparty().map(Counterparty::getCustomerCode)).orElse(null), item.getDocument().flatMap(d -> d.getCounterparty().map(Counterparty::getType)).isPresent() ? item.getDocument().flatMap(d -> d.getCounterparty().map(Counterparty::getType)).map(Object::toString).orElse(null) : null, item.getDocument().flatMap(document -> document.getCounterparty().flatMap(Counterparty::getName)).orElse(null), - item.getRejection().map(Rejection::getRejectionReason).orElse(null) + item.getRejection().map(Rejection::getRejectionReason).orElse(null), + organisationPublicApi.findCostCenter(item.getTransaction().getOrganisation().getId(),item.getCostCenter().map(CostCenter::getCustomerCode).orElse(null)).map(OrganisationCostCenter::getParentCustomerCode).orElse(null), + organisationPublicApi.findProject(item.getTransaction().getOrganisation().getId(),item.getProject().map(Project::getCustomerCode).orElse(null)).map(OrganisationProject::getParentCustomerCode).orElse(null) ); } } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/views/ExtractionTransactionItemView.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/views/ExtractionTransactionItemView.java index 270a5ae2..f868b5a9 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/views/ExtractionTransactionItemView.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/views/ExtractionTransactionItemView.java @@ -86,5 +86,8 @@ public class ExtractionTransactionItemView { private RejectionReason rejectionReason; + private String parentCostCenterCustomerCode; + + private String parentProjectCustomerCode; } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/DocumentConversionTaskItem.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/DocumentConversionTaskItem.java index 3fbfeb1d..c62434bc 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/DocumentConversionTaskItem.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/DocumentConversionTaskItem.java @@ -1,6 +1,7 @@ package org.cardanofoundation.lob.app.accounting_reporting_core.service.business_rules.items; import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.Source.ERP; import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.Source.LOB; import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TransactionViolationCode.*; import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.Violation.Severity.ERROR; @@ -76,6 +77,15 @@ private Optional enrichCurrency(String organisationId, val customerCurrencyCode = document.getCurrency().getCustomerCode(); if (isBlank(customerCurrencyCode)) { + tx.addViolation(TransactionViolation.builder() + .txItemId(txItem.getId()) + .code(CURRENCY_DATA_NOT_FOUND) + .severity(ERROR) + .source(ERP) + .processorModule(getClass().getSimpleName()) + .bag(Map.of("customerCode", customerCurrencyCode, "transactionNumber", tx.getTransactionInternalNumber())) + .build()); + return Optional.empty(); } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/JournalAccountCreditEnrichmentTaskItem.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/JournalAccountCreditEnrichmentTaskItem.java index 102f3072..e084e608 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/JournalAccountCreditEnrichmentTaskItem.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/JournalAccountCreditEnrichmentTaskItem.java @@ -1,6 +1,7 @@ package org.cardanofoundation.lob.app.accounting_reporting_core.service.business_rules.items; import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.OperationType.CREDIT; +import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.OperationType.DEBIT; import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.Source.ERP; import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.Source.LOB; import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TransactionType.Journal; @@ -69,6 +70,8 @@ public void run(TransactionEntity tx) { } Account accountDebit = txItem.getAccountDebit().orElseThrow(); txItem.setAccountCredit(Optional.of(accountDebit)); + // If we switch the account credit, we need to set the operation type to DEBIT + txItem.setOperationType(DEBIT); txItem.clearAccountCodeDebit(); } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreEventHandler.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreEventHandler.java index 2f344e11..1e642b9f 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreEventHandler.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreEventHandler.java @@ -1,6 +1,7 @@ package org.cardanofoundation.lob.app.accounting_reporting_core.service.internal; import java.time.LocalDate; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -17,8 +18,8 @@ import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.FatalError; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.ReportStatusUpdate; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.Transaction; -import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TxStatusUpdate; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.TransactionEntity; +import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.report.ReportEntity; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.event.extraction.TransactionBatchChunkEvent; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.event.extraction.TransactionBatchFailedEvent; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.event.extraction.TransactionBatchStartedEvent; @@ -28,6 +29,7 @@ import org.cardanofoundation.lob.app.accounting_reporting_core.domain.event.reconcilation.ReconcilationFailedEvent; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.event.reconcilation.ReconcilationFinalisationEvent; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.event.reconcilation.ReconcilationStartedEvent; +import org.cardanofoundation.lob.app.accounting_reporting_core.job.TxStatusUpdaterJob; import org.cardanofoundation.lob.app.accounting_reporting_core.service.business_rules.ProcessorFlags; import org.cardanofoundation.lob.app.support.modulith.EventMetadata; @@ -43,6 +45,7 @@ public class AccountingCoreEventHandler { private final TransactionBatchService transactionBatchService; private final TransactionReconcilationService transactionReconcilationService; private final ApplicationEventPublisher applicationEventPublisher; + private final TxStatusUpdaterJob txStatusUpdaterJob; @EventListener @@ -50,10 +53,7 @@ public class AccountingCoreEventHandler { public void handleLedgerUpdatedEvent(TxsLedgerUpdatedEvent event) { log.info("Received handleLedgerUpdatedEvent event, event: {}", event.getStatusUpdates()); - Map txStatusUpdatesMap = event.statusUpdatesMap(); - - ledgerService.updateTransactionsWithNewStatuses(txStatusUpdatesMap); - transactionBatchService.updateBatchesPerTransactions(txStatusUpdatesMap); + txStatusUpdaterJob.addToStatusUpdateMap(event.statusUpdatesMap()); log.info("Finished processing handleLedgerUpdatedEvent event, event: {}", event.getStatusUpdates()); } @@ -65,7 +65,8 @@ public void handleReportsLedgerUpdated(ReportsLedgerUpdatedEvent event) { Map reportStatusUpdatesMap = event.statusUpdatesMap(); - ledgerService.updateReportsWithNewStatuses(reportStatusUpdatesMap); + List reportEntities = ledgerService.updateReportsWithNewStatuses(reportStatusUpdatesMap); + ledgerService.saveAllReports(reportEntities); log.info("Finished processing handleReportsLedgerUpdated, event: {}", event); } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreService.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreService.java index 02c93b17..cc9f4a56 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreService.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreService.java @@ -53,7 +53,7 @@ public class AccountingCoreService { @Value("${lob.max.transaction.numbers.per.batch:600}") private int maxTransactionNumbersPerBatch = 600; - @Transactional + @Transactional(readOnly = true) public Either scheduleIngestion(UserExtractionParameters userExtractionParameters) { log.info("scheduleIngestion, parameters: {}", userExtractionParameters); @@ -69,7 +69,7 @@ public Either scheduleIngestion(UserExtractionParameters userExtr if (userExtractionParameters.getTransactionNumbers().size() > maxTransactionNumbersPerBatch) { return Either.left(Problem.builder() .withTitle("TOO_MANY_TRANSACTIONS") - .withDetail(STR."Too many transactions requested, maximum is \{maxTransactionNumbersPerBatch}") + .withDetail("Too many transactions requested, maximum is %s".formatted(maxTransactionNumbersPerBatch)) .withStatus(BAD_REQUEST) .build()); } @@ -84,7 +84,7 @@ public Either scheduleIngestion(UserExtractionParameters userExtr return Either.right(null); // all fine } - @Transactional + @Transactional(readOnly = true) public Either scheduleReconcilation(String organisationId, LocalDate fromDate, LocalDate toDate) { diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/LedgerService.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/LedgerService.java index 71d054ad..607f0287 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/LedgerService.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/LedgerService.java @@ -1,7 +1,7 @@ package org.cardanofoundation.lob.app.accounting_reporting_core.service.internal; -import static org.springframework.transaction.annotation.Propagation.SUPPORTS; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -50,8 +50,8 @@ public class LedgerService { @Value("${ledger.dispatch.batch.size:100}") private int dispatchBatchSize; - @Transactional - public void updateTransactionsWithNewStatuses(Map statuses) { + @Transactional(readOnly = true) + public List updateTransactionsWithNewStatuses(Map statuses) { log.info("Updating dispatch status for statusMapCount: {}", statuses.size()); Set txIds = statuses.keySet(); @@ -72,14 +72,17 @@ public void updateTransactionsWithNewStatuses(Map status tx.setLedgerDispatchReceipt(new LedgerDispatchReceipt(type, hash)); } } + return transactionEntities; - accountingCoreTransactionRepository.saveAll(transactionEntities); - - log.info("Updated dispatch status for statusMapCount: {} completed.", statuses.size()); } @Transactional - public void updateReportsWithNewStatuses(Map reportStatusUpdateMap) { + public void saveAllTransactionEntities(Collection transactionEntities) { + accountingCoreTransactionRepository.saveAll(transactionEntities); + } + + @Transactional(readOnly = true) + public List updateReportsWithNewStatuses(Map reportStatusUpdateMap) { log.info("Updating dispatch status for statusMapCount: {}", reportStatusUpdateMap.size()); Set reportIds = reportStatusUpdateMap.keySet(); @@ -101,12 +104,15 @@ public void updateReportsWithNewStatuses(Map reportS } } - reportRepository.saveAll(reports); - - log.info("Updated dispatch status for statusMapCount: {} completed.", reportStatusUpdateMap.size()); + return reports; } @Transactional + public void saveAllReports(Collection reports) { + reportRepository.saveAll(reports); + } + + @Transactional(readOnly = true) public void dispatchPending(int limit) { for (Organisation organisation : organisationPublicApi.listAll()) { Set dispatchTransactions = accountingCoreTransactionRepository.findDispatchableTransactions(organisation.getId(), Limit.of(limit)); @@ -119,7 +125,7 @@ public void dispatchPending(int limit) { } } - @Transactional(propagation = SUPPORTS) + @Transactional(readOnly = true) public void dispatchPendingTransactions(String organisationId, Set transactions) { log.info("dispatchTransactionToBlockchainPublisher, total tx count: {}", transactions.size()); @@ -145,7 +151,6 @@ public void dispatchPendingTransactions(String organisationId, } } - @Transactional(propagation = SUPPORTS) public void dispatchReports(String organisationId, Set reportEntities) { log.info("dispatchReports, total reports count: {}", reportEntities.size()); diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/ReportService.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/ReportService.java index af7b976d..e7ef25ac 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/ReportService.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/ReportService.java @@ -711,19 +711,21 @@ private void fillObjectRecursively(Object reportData, ReportTypeFieldEntity fiel return; } - Optional startSearchDate; + Optional startSearchDate = Optional.of(startDate);; BigDecimal totalAmount = BigDecimal.ZERO; if (field.isAccumulatedYearly()) { startSearchDate = Optional.of(LocalDate.of(startDate.getYear(), 1, 1)); - } else if (field.isAccumulated()) { - // TODO this calculation can be optimized by using already published reports + } + if (field.isAccumulated()) { startSearchDate = Optional.of(LocalDate.EPOCH); - } else if (field.isAccumulatedPreviousYear()) { - startSearchDate = Optional.of(LocalDate.of(startDate.getYear() - 1, 1, 1)); + } + if (field.isAccumulatedPreviousYear()) { + if(!field.isAccumulated()) { + startSearchDate = Optional.of(LocalDate.of(startDate.getYear() - 1, 1, 1)); + } + endDate = LocalDate.of(startDate.getYear() - 1, 12, 31); - }else { - startSearchDate = Optional.of(startDate); } totalAmount = addValuesFromTransactionItems(field, endDate, totalAmount, startSearchDate); @@ -844,11 +846,21 @@ private BigDecimal addValuesFromTransactionItems(ReportTypeFieldEntity field, Lo } BigDecimal amount = BigDecimal.ZERO; // adding the value if it's debit and subtracting it if it's Credit + // Account is on Debit if (transactionItemEntity.getAccountDebit().isPresent() && selfMap.containsKey(transactionItemEntity.getAccountDebit().get().getCode())) { - amount = amount.add(transactionItemEntity.getAmountLcy()); + if(transactionItemEntity.getOperationType() == org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.OperationType.DEBIT) { + amount = amount.add(transactionItemEntity.getAmountLcy()); + } else { + amount = amount.add(transactionItemEntity.getAmountLcy().negate()); + } } + if (transactionItemEntity.getAccountCredit().isPresent() && selfMap.containsKey(transactionItemEntity.getAccountCredit().get().getCode())) { - amount = amount.add(transactionItemEntity.getAmountLcy().negate()); + if(transactionItemEntity.getOperationType() == org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.OperationType.DEBIT) { + amount = amount.subtract(transactionItemEntity.getAmountLcy()); + } else { + amount = amount.subtract(transactionItemEntity.getAmountLcy().negate()); + } } return amount.stripTrailingZeros(); } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/TransactionBatchService.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/TransactionBatchService.java index e9831674..af07ffb9 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/TransactionBatchService.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/TransactionBatchService.java @@ -1,7 +1,6 @@ package org.cardanofoundation.lob.app.accounting_reporting_core.service.internal; import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TransactionBatchStatus.*; -import static org.springframework.transaction.annotation.Propagation.SUPPORTS; import java.time.Duration; import java.util.List; @@ -54,14 +53,13 @@ public class TransactionBatchService { private final DebouncerManager debouncerManager; private final AccountingCoreTransactionRepository accountingCoreTransactionRepository; - @Value("${batch.stats.debounce.duration:PT5S}") + @Value("${lob.accounting_reporting_core.debounce.duration:PT10S}") private Duration batchStatsDebounceDuration; public Optional findById(String batchId) { return transactionBatchRepository.findById(batchId); } - @Transactional public void createTransactionBatch(String batchId, String organisationId, UserExtractionParameters userExtractionParameters, @@ -104,7 +102,7 @@ public void updateTransactionBatchStatusAndStats(String batchId, debouncerManager.callInNewDebouncer(batchId, () -> invokeUpdateTransactionBatchStatusAndStats(batchId, Optional.ofNullable(totalTransactionsCount), entities), batchStatsDebounceDuration); } - @Transactional(propagation = SUPPORTS) + @Transactional public void failTransactionBatch(String batchId, UserExtractionParameters userExtractionParameters, Optional systemExtractionParameters, @@ -133,6 +131,7 @@ public void failTransactionBatch(String batchId, log.info("Transaction batch status updated, batchId: {}", batchId); } + @Transactional public void invokeUpdateTransactionBatchStatusAndStats(String batchId, Optional totalTransactionsCountO, Optional> transactionEntities) { @@ -184,7 +183,6 @@ public void invokeUpdateTransactionBatchStatusAndStats(String batchId, log.info("EXPENSIVE::Transaction batch status and statistics updated, batchId: {}", batchId); } - @Transactional public void updateBatchesPerTransactions(Map txStatusUpdates) { for (TxStatusUpdate txStatusUpdate : txStatusUpdates.values()) { String txId = txStatusUpdate.getTxId(); @@ -205,7 +203,7 @@ public void updateBatchesPerTransactions(Map txStatusUpd } } - @Transactional + @Transactional(readOnly = true) public List findAll() { return transactionBatchRepository.findAll(); } diff --git a/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/TxStatusUpdaterJobTest.java b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/TxStatusUpdaterJobTest.java new file mode 100644 index 00000000..ca0bdabe --- /dev/null +++ b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/TxStatusUpdaterJobTest.java @@ -0,0 +1,104 @@ +package org.cardanofoundation.lob.app.accounting_reporting_core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; + +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TxStatusUpdate; +import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.TransactionEntity; +import org.cardanofoundation.lob.app.accounting_reporting_core.job.TxStatusUpdaterJob; +import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.LedgerService; +import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.TransactionBatchService; + +@ExtendWith(MockitoExtension.class) +class TxStatusUpdaterJobTest { + + @Mock + private LedgerService ledgerService; + @Mock + private TransactionBatchService batchService; + @InjectMocks + private TxStatusUpdaterJob statusUpdaterJob; + + @Test + void addToStatusMapTest() throws NoSuchFieldException, IllegalAccessException { + TxStatusUpdate update = mock(TxStatusUpdate.class); + + statusUpdaterJob.addToStatusUpdateMap(Map.of("123", update)); + + Field field = TxStatusUpdaterJob.class.getDeclaredField("txStatusUpdatesMap"); + field.setAccessible(true); + Map stringTxStatusUpdateMap = (Map) field.get(statusUpdaterJob); + + assertNotNull(stringTxStatusUpdateMap); + assertTrue(stringTxStatusUpdateMap.containsKey("123")); + assertEquals(update, stringTxStatusUpdateMap.get("123")); + } + + @Test + void execute_empty() { + statusUpdaterJob.execute(); + + verifyNoInteractions(batchService, ledgerService); + } + + @Test + void execute_exception() throws NoSuchFieldException, IllegalAccessException { + TxStatusUpdate update = mock(TxStatusUpdate.class); + + statusUpdaterJob.addToStatusUpdateMap(Map.of("123", update)); + + doThrow(new RuntimeException()).when(ledgerService).saveAllTransactionEntities(any()); + + statusUpdaterJob.execute(); + + Field field = TxStatusUpdaterJob.class.getDeclaredField("txStatusUpdatesMap"); + field.setAccessible(true); + Map stringTxStatusUpdateMap = (Map) field.get(statusUpdaterJob); + // TxStatusUpdate stays within the map + assertNotNull(stringTxStatusUpdateMap); + assertTrue(stringTxStatusUpdateMap.containsKey("123")); + assertEquals(update, stringTxStatusUpdateMap.get("123")); + } + + @Test + void execute_success() throws NoSuchFieldException, IllegalAccessException { + TxStatusUpdate update = mock(TxStatusUpdate.class); + TransactionEntity entity = mock(TransactionEntity.class); + statusUpdaterJob.addToStatusUpdateMap(Map.of("123", update)); + + when(ledgerService.updateTransactionsWithNewStatuses(Map.of("123", update))).thenReturn(List.of(entity)); + + statusUpdaterJob.execute(); + + verify(ledgerService).updateTransactionsWithNewStatuses(Map.of("123", update)); + verify(ledgerService).saveAllTransactionEntities(List.of(entity)); + verify(batchService).updateBatchesPerTransactions(Map.of("123", update)); + // TxStatusUpdate must be removed from the internal map + Field field = TxStatusUpdaterJob.class.getDeclaredField("txStatusUpdatesMap"); + field.setAccessible(true); + Map stringTxStatusUpdateMap = (Map) field.get(statusUpdaterJob); + + assertFalse(stringTxStatusUpdateMap.containsKey("123")); + + } + +} diff --git a/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/model/AccountingCorePresentationConverterTest.java b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/model/AccountingCorePresentationConverterTest.java index ae68ba4d..2803d076 100644 --- a/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/model/AccountingCorePresentationConverterTest.java +++ b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/model/AccountingCorePresentationConverterTest.java @@ -3,6 +3,7 @@ import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TransactionViolationCode.CORE_CURRENCY_NOT_FOUND; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.math.BigDecimal; @@ -42,13 +43,17 @@ import org.cardanofoundation.lob.app.accounting_reporting_core.resource.views.TransactionView; import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.AccountingCoreService; import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.TransactionRepositoryGateway; +import org.cardanofoundation.lob.app.organisation.OrganisationPublicApiIF; +import org.cardanofoundation.lob.app.organisation.repository.CostCenterRepository; +import org.cardanofoundation.lob.app.organisation.repository.ProjectMappingRepository; @ExtendWith(MockitoExtension.class) class AccountingCorePresentationConverterTest { @Mock private TransactionRepositoryGateway transactionRepositoryGateway; - + @Mock + private OrganisationPublicApiIF organisationPublicApiIF; @Mock private AccountingCoreService accountingCoreService; @@ -56,6 +61,10 @@ class AccountingCorePresentationConverterTest { private TransactionBatchRepositoryGateway transactionBatchRepositoryGateway; @Mock private AccountingCoreTransactionRepository transactionRepository; + @Mock + private CostCenterRepository costCenterRepository; + @Mock + private ProjectMappingRepository projectMappingRepository; @InjectMocks private AccountingCorePresentationViewService accountingCorePresentationConverter; @@ -182,6 +191,8 @@ void testTransactionDetailSpecific() { @Test void testBatchDetail() { + org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.Organisation organisation = mock(org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.Organisation.class); + when(organisation.getId()).thenReturn("123"); TransactionItemEntity transactionItem = new TransactionItemEntity(); transactionItem.setId("txItemId"); transactionItem.setAmountLcy(BigDecimal.valueOf(100)); @@ -198,9 +209,11 @@ void testBatchDetail() { BatchStatistics batchStatistics = BatchStatistics.builder().total(10).pendingTransactions(1).invalidTransactions(8).approvedTransactions(5).publishedTransactions(6).readyToApproveTransactions(10).build(); TransactionEntity transaction1 = new TransactionEntity(); + transaction1.setOrganisation(organisation); transaction1.setId("tx-id1"); transaction1.setTransactionType(TransactionType.Journal); TransactionEntity transaction2 = new TransactionEntity(); + transaction2.setOrganisation(organisation); transaction2.setId("tx-id2"); transaction2.setTransactionType(TransactionType.VendorBill); @@ -224,6 +237,7 @@ void testBatchDetail() { transaction1.setBatchId(batchId); transaction2.setBatchId(batchId); + when(transactionBatchRepositoryGateway.findById(batchId)).thenReturn(Optional.of(transactionBatchEntity)); when(transactionRepository.findAllByBatchId(batchId, null, Pageable.unpaged())).thenReturn(new PageImpl<>(List.of(transaction1, transaction2))); diff --git a/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/ExtractionItemServiceTest.java b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/ExtractionItemServiceTest.java index 624a3511..2c16514a 100644 --- a/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/ExtractionItemServiceTest.java +++ b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/presentation_layer_service/ExtractionItemServiceTest.java @@ -21,6 +21,7 @@ import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.*; import org.cardanofoundation.lob.app.accounting_reporting_core.repository.TransactionItemExtractionRepository; import org.cardanofoundation.lob.app.accounting_reporting_core.resource.views.ExtractionTransactionView; +import org.cardanofoundation.lob.app.organisation.OrganisationPublicApi; @ExtendWith(MockitoExtension.class) class ExtractionItemServiceTest { @@ -28,6 +29,9 @@ class ExtractionItemServiceTest { @Mock private TransactionItemExtractionRepository transactionItemExtractionRepository; + @Mock + private OrganisationPublicApi organisationPublicApi; + @Test void findTransactionItemsTest() { val document = Document.builder() @@ -53,7 +57,9 @@ void findTransactionItemsTest() { item1.setTransaction(tx); Mockito.when(transactionItemExtractionRepository.findByItemAccount(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(List.of(item1)); - ExtractionItemService extractionItemService = new ExtractionItemService(transactionItemExtractionRepository); + Mockito.when(organisationPublicApi.findProject(Mockito.any(), Mockito.any())).thenReturn(Optional.empty()); + Mockito.when(organisationPublicApi.findCostCenter(Mockito.any(), Mockito.any())).thenReturn(Optional.empty()); + ExtractionItemService extractionItemService = new ExtractionItemService(transactionItemExtractionRepository, organisationPublicApi); ExtractionTransactionView result = extractionItemService.findTransactionItems(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); assertInstanceOf(ExtractionTransactionView.class, result); @@ -87,13 +93,15 @@ void findByItemAccountDateTest() { item1.setTransaction(tx); Mockito.when(transactionItemExtractionRepository.findByItemAccountDate(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(List.of(item1)); - ExtractionItemService extractionItemService = new ExtractionItemService(transactionItemExtractionRepository); + Mockito.when(organisationPublicApi.findProject(Mockito.any(), Mockito.any())).thenReturn(Optional.empty()); + Mockito.when(organisationPublicApi.findCostCenter(Mockito.any(), Mockito.any())).thenReturn(Optional.empty()); + ExtractionItemService extractionItemService = new ExtractionItemService(transactionItemExtractionRepository, organisationPublicApi); ExtractionTransactionView result = ExtractionTransactionView.createSuccess(extractionItemService.findTransactionItemsPublic(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())); assertInstanceOf(ExtractionTransactionView.class, result); assertEquals(1L, result.getTotal()); - assertEquals("item1",result.getTransactions().getFirst().getId()); - assertEquals("TxId1",result.getTransactions().getFirst().getTransactionID()); + assertEquals("item1", result.getTransactions().getFirst().getId()); + assertEquals("TxId1", result.getTransactions().getFirst().getTransactionID()); verifyNoMoreInteractions(transactionItemExtractionRepository); } diff --git a/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/DocumentConversionTaskItemTest.java b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/DocumentConversionTaskItemTest.java index d29c0c07..a9a05af9 100644 --- a/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/DocumentConversionTaskItemTest.java +++ b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/business_rules/items/DocumentConversionTaskItemTest.java @@ -217,4 +217,42 @@ public void testDocumentConversionWithMultipleViolations() { assertThat(transaction.getViolations()).anyMatch(v -> v.getCode() == CURRENCY_DATA_NOT_FOUND); } + @Test + public void testDocumentConversionWithNoCurrenciInDocument() { + val txId = "1"; + val txInternalNumber = "txn123"; + val organisationId = "org1"; + val customerCurrencyCode = "UNKNOWN_CURRENCY"; + val customerVatCode = "UNKNOWN_VAT"; + + val document = Document.builder() + .vat(Vat.builder() + .customerCode(customerVatCode) + .build()) + .currency(Currency.builder() + .customerCode("") + .build()) + .build(); + + val txItem = new TransactionItemEntity(); + txItem.setDocument(Optional.of(document)); + + val items = new LinkedHashSet(); + items.add(txItem); + + val transaction = new TransactionEntity(); + transaction.setId(txId); + transaction.setTransactionInternalNumber(txInternalNumber); + transaction.setOrganisation(Organisation.builder() + .id(organisationId) + .build()); + transaction.setItems(items); + + documentConversionTaskItem.run(transaction); + + assertThat(transaction.getViolations()).hasSize(2); + assertThat(transaction.getViolations()).anyMatch(v -> v.getCode() == VAT_DATA_NOT_FOUND); + assertThat(transaction.getViolations()).anyMatch(v -> v.getCode() == CURRENCY_DATA_NOT_FOUND); + } + } diff --git a/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreEventHandlerDuplicateEventsTest.java b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreEventHandlerDuplicateEventsTest.java index 7a69dc2e..5b7b1f2a 100644 --- a/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreEventHandlerDuplicateEventsTest.java +++ b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/AccountingCoreEventHandlerDuplicateEventsTest.java @@ -46,6 +46,7 @@ import org.cardanofoundation.lob.app.accounting_reporting_core.domain.event.ledger.TxsLedgerUpdatedEvent; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.event.reconcilation.ReconcilationFailedEvent; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.event.reconcilation.ReconcilationStartedEvent; +import org.cardanofoundation.lob.app.accounting_reporting_core.job.TxStatusUpdaterJob; import org.cardanofoundation.lob.app.accounting_reporting_core.repository.AccountingCoreTransactionRepository; import org.cardanofoundation.lob.app.accounting_reporting_core.repository.ReportRepository; import org.cardanofoundation.lob.app.accounting_reporting_core.repository.TransactionBatchRepository; @@ -55,7 +56,7 @@ @SpringBootTest(classes = {JaversConfig.class, TimeConfig.class, JpaConfig.class}) @TestPropertySource(properties = "spring.main.allow-bean-definition-overriding=true") @EnableAutoConfiguration -@ComponentScan(basePackages = {"org.cardanofoundation.lob.app.accounting_reporting_core","org.cardanofoundation.lob.app.organisation","org.cardanofoundation.lob.app.blockchain_reader","org.cardanofoundation.lob.app.support.security"}) +@ComponentScan(basePackages = {"org.cardanofoundation.lob.app.accounting_reporting_core","org.cardanofoundation.lob.app.organisation","org.cardanofoundation.lob.app.blockchain_reader","org.cardanofoundation.lob.app.support.security", "org.cardanofoundation.lob.app.accounting_reporting_core.job"}) class AccountingCoreEventHandlerDuplicateEventsTest { @Autowired @@ -67,6 +68,8 @@ class AccountingCoreEventHandlerDuplicateEventsTest { @Autowired private ReportRepository reportRepository; @Autowired + private TxStatusUpdaterJob txStatusUpdaterJob; + @Autowired private TransactionReconcilationRepository transactionReconcilationRepository; @BeforeEach @@ -76,7 +79,7 @@ void clearDatabase(@Autowired Flyway flyway){ } @Test - void testHandleLedgerUpdate() { + void testHandleLedgerUpdate() throws NoSuchFieldException, IllegalAccessException { TransactionBatchEntity transactionBatchEntity = new TransactionBatchEntity(); transactionBatchEntity.setId("batchId"); transactionBatchEntity.setFilteringParameters(FilteringParameters.builder() @@ -105,9 +108,11 @@ void testHandleLedgerUpdate() { // padding the transaction id to 64 characters to match the length of the id in the database .statusUpdates(Set.of(new TxStatusUpdate(String.format("%-64s", transactionEntity.getId()), LedgerDispatchStatus.MARK_DISPATCH, Set.of()))) .build(); + // sending events twice to check if the status is updated correctly and no exceptions are thrown accountingCoreEventHandler.handleLedgerUpdatedEvent(build); accountingCoreEventHandler.handleLedgerUpdatedEvent(build); + txStatusUpdaterJob.execute(); Optional txEntity = accountingCoreTransactionRepository.findById("txId"); Assertions.assertTrue(txEntity.isPresent()); diff --git a/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/ReportServiceTest.java b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/ReportServiceTest.java index 1be2a9c8..dc2b7165 100644 --- a/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/ReportServiceTest.java +++ b/accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/ReportServiceTest.java @@ -29,8 +29,10 @@ import org.junit.jupiter.api.Test; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.OperationType; +import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TxItemValidationStatus; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.report.IntervalType; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.report.Report; +import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.Account; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.TransactionItemEntity; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.report.BalanceSheetData; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.report.IncomeStatementData; @@ -1261,9 +1263,13 @@ void reportGenerate_generateBalanceSheet() { when(organisationChartOfAccount.getId()).thenReturn(organisationChartOfAccountId); when(organisationChartOfAccountId.getCustomerCode()).thenReturn("CustomerCode"); when(transactionItemRepository.findTransactionItemsByAccountCodeAndDateRange(List.of("CustomerCode"), startDate, endDate)).thenReturn(List.of(transactionItemEntity)); + when(transactionItemEntity.getStatus()).thenReturn(TxItemValidationStatus.OK); + when(transactionItemEntity.getAccountDebit()).thenReturn(Optional.of(Account.builder().code("CustomerCode").build())); + when(transactionItemEntity.getOperationType()).thenReturn(OperationType.DEBIT); when(transactionItemEntity.getAmountLcy()).thenReturn(BigDecimal.TEN); + Either result = reportService.reportGenerate(request); assertTrue(result.isRight()); @@ -1273,7 +1279,69 @@ void reportGenerate_generateBalanceSheet() { assertTrue(balanceSheetReportData.isPresent()); BalanceSheetData balanceSheetData = balanceSheetReportData.get(); BigDecimal profitForTheYear = balanceSheetData.getCapital().get().getProfitForTheYear().get(); - assertThat(profitForTheYear).isEqualTo(BigDecimal.ZERO); + assertThat(profitForTheYear).isEqualTo(BigDecimal.TEN); + + // Switching Operation Type to Credit - Should be minus 10 now + when(transactionItemEntity.getOperationType()).thenReturn(OperationType.CREDIT); + + result = reportService.reportGenerate(request); + + assertTrue(result.isRight()); + verify(reportTypeRepository, times(2)).findByOrganisationAndReportName(organisationId, BALANCE_SHEET.name()); + reportEntity = result.get(); + balanceSheetReportData = reportEntity.getBalanceSheetReportData(); + assertTrue(balanceSheetReportData.isPresent()); + balanceSheetData = balanceSheetReportData.get(); + profitForTheYear = balanceSheetData.getCapital().get().getProfitForTheYear().get(); + assertThat(profitForTheYear).isEqualTo(BigDecimal.valueOf(-10)); + + //Switching Item to AccountCredit - Still minus 10 + when(transactionItemEntity.getAccountDebit()).thenReturn(Optional.empty()); + when(transactionItemEntity.getAccountCredit()).thenReturn(Optional.of(Account.builder().code("CustomerCode").build())); + when(transactionItemEntity.getOperationType()).thenReturn(OperationType.DEBIT); + + result = reportService.reportGenerate(request); + + assertTrue(result.isRight()); + verify(reportTypeRepository, times(3)).findByOrganisationAndReportName(organisationId, BALANCE_SHEET.name()); + balanceSheetReportData = result.get().getBalanceSheetReportData(); + assertTrue(balanceSheetReportData.isPresent()); + balanceSheetData = balanceSheetReportData.get(); + profitForTheYear = balanceSheetData.getCapital().get().getProfitForTheYear().get(); + assertThat(profitForTheYear).isEqualTo(BigDecimal.valueOf(-10)); + + // Switching Operation Type to credit will be 10 + when(transactionItemEntity.getOperationType()).thenReturn(OperationType.CREDIT); + + result = reportService.reportGenerate(request); + + assertTrue(result.isRight()); + verify(reportTypeRepository, times(4)).findByOrganisationAndReportName(organisationId, BALANCE_SHEET.name()); + balanceSheetReportData = result.get().getBalanceSheetReportData(); + assertTrue(balanceSheetReportData.isPresent()); + balanceSheetData = balanceSheetReportData.get(); + profitForTheYear = balanceSheetData.getCapital().get().getProfitForTheYear().get(); + assertThat(profitForTheYear).isEqualTo(BigDecimal.valueOf(10)); + } + + @Test + void store_OrganisationNotFound() { + when(organisationPublicApi.findByOrganisationId("org123")).thenReturn(Optional.empty()); + + Either storeResponse = reportService.store("org123", IntervalType.YEAR, (short) 2025, 1, Optional.of((short) 1), Either.left(IncomeStatementData.builder().build())); + + assertTrue(storeResponse.isLeft()); + assertThat(storeResponse.getLeft().getTitle()).isEqualTo("ORGANISATION_NOT_FOUND"); + } + + @Test + void exist_reportNotFound() { + String reportId = Report.idControl("org123", INCOME_STATEMENT, IntervalType.YEAR, (short) 2025, Optional.of((short) 1)); + when(reportRepository.findLatestByIdControl("org123", reportId)).thenReturn(Optional.empty()); + + Either existsResponse = reportService.exist("org123", INCOME_STATEMENT, IntervalType.YEAR, (short) 2025, (short) 1); + assertTrue(existsResponse.isLeft()); + assertThat(existsResponse.getLeft().getTitle()).isEqualTo("REPORT_NOT_FOUND"); } diff --git a/blockchain_common/src/main/resources/api1_lob_blockchain_transaction_metadata_schema.json b/blockchain_common/src/main/resources/api1_lob_blockchain_transaction_metadata_schema.json index aa26a54d..a85284fd 100644 --- a/blockchain_common/src/main/resources/api1_lob_blockchain_transaction_metadata_schema.json +++ b/blockchain_common/src/main/resources/api1_lob_blockchain_transaction_metadata_schema.json @@ -48,7 +48,13 @@ "$ref": "#/definitions/countryCodePattern" }, "name": { - "type": "string" + "anyOf": [ + { "type": "string" }, + { + "type": "array", + "items": { "type": "string" } + } + ] }, "tax_id_number": { "type": "string" @@ -114,7 +120,13 @@ "type": "string" }, "name": { - "type": "string" + "anyOf": [ + { "type": "string" }, + { + "type": "array", + "items": { "type": "string" } + } + ] } }, "required": ["code", "name"] @@ -126,7 +138,13 @@ "type": "string" }, "name": { - "type": "string" + "anyOf": [ + { "type": "string" }, + { + "type": "array", + "items": { "type": "string" } + } + ] } }, "required": ["cust_code", "name"] @@ -138,7 +156,13 @@ "type": "string" }, "name": { - "type": "string" + "anyOf": [ + { "type": "string" }, + { + "type": "array", + "items": { "type": "string" } + } + ] } }, "required": ["cust_code", "name"] diff --git a/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/repository/TransactionEntityRepositoryGateway.java b/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/repository/TransactionEntityRepositoryGateway.java index ae8f43f0..96a23e27 100644 --- a/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/repository/TransactionEntityRepositoryGateway.java +++ b/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/repository/TransactionEntityRepositoryGateway.java @@ -47,24 +47,18 @@ public Set findAndLockTransactionsReadyToBeDispatched(String Set dispatchStatuses = BlockchainPublishStatus.toDispatchStatuses(); Limit limit = Limit.of(pullTransactionsBatchSize); - Set transactionsByStatus = transactionEntityRepository.findTransactionsByStatus( + Set transactionsByStatus = transactionEntityRepository.findFreeTransactionsByStatus( organisationId, dispatchStatuses, + LocalDateTime.now(clock).minus(lockTimeoutDuration), limit); if (transactionsByStatus.isEmpty()) { return transactionsByStatus; } // This logic could be moved to the repository, but for now it is easier to test it here - Set filteredTransactions = transactionsByStatus.stream().filter( - transactionEntity -> ( - transactionEntity.getLockedAt() - .map(lockedAt -> lockedAt.isBefore(LocalDateTime.now(clock).minus(lockTimeoutDuration))) - .orElse(true) // return true if lockedAt is not present - )) - .collect(toSet()); - filteredTransactions.forEach(tx -> tx.setLockedAt(LocalDateTime.now(clock))); - transactionEntityRepository.saveAll(filteredTransactions); - return filteredTransactions; + transactionsByStatus.forEach(tx -> tx.setLockedAt(LocalDateTime.now(clock))); + transactionEntityRepository.saveAll(transactionsByStatus); + return transactionsByStatus; } public Set findDispatchedTransactionsThatAreNotFinalizedYet(String organisationId, Limit limit) { @@ -111,4 +105,10 @@ public void storeTransaction(TransactionEntity transactionEntity) { public void storeTransactions(Set successfullyUpdatedTxEntities) { transactionEntityRepository.saveAll(successfullyUpdatedTxEntities); } + + public void unlockTransactions(Set transactionsBatch) { + transactionsBatch.forEach(transactionEntity -> + transactionEntity.setLockedAt(null)); + transactionEntityRepository.saveAll(transactionsBatch); + } } diff --git a/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/service/dispatch/BlockchainTransactionsDispatcher.java b/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/service/dispatch/BlockchainTransactionsDispatcher.java index 29faaef2..126427c2 100644 --- a/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/service/dispatch/BlockchainTransactionsDispatcher.java +++ b/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/service/dispatch/BlockchainTransactionsDispatcher.java @@ -2,6 +2,7 @@ import static org.cardanofoundation.lob.app.blockchain_publisher.domain.core.BlockchainPublishStatus.SUBMITTED; +import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -41,7 +42,7 @@ public class BlockchainTransactionsDispatcher { private final LedgerUpdatedEventPublisher ledgerUpdatedEventPublisher; private final DispatchingStrategy dispatchingStrategy; - @Value("${lob.blockchain_publisher.dispatcher.pullBatchSize:50}") + @Value("${lob.blockchain_publisher.dispatcher.pullBatchSize:500}") private int pullTransactionsBatchSize = 50; @PostConstruct @@ -58,7 +59,10 @@ public void dispatchTransactions() { String organisationId = organisation.getId(); Set transactionsBatch = transactionEntityRepositoryGateway.findAndLockTransactionsReadyToBeDispatched(organisationId, pullTransactionsBatchSize); Set transactionToDispatch = dispatchingStrategy.apply(organisationId, transactionsBatch); - + // unlock other transactions + HashSet toUnlock = new HashSet<>(transactionsBatch); + toUnlock.removeAll(transactionToDispatch); + transactionEntityRepositoryGateway.unlockTransactions(toUnlock); int dispatchTxCount = transactionToDispatch.size(); log.info("Dispatching txs for organisationId:{}, tx count:{}", organisationId, dispatchTxCount); if (dispatchTxCount > 0) { @@ -82,6 +86,7 @@ private void dispatchTransactionsBatch(String organisationId, int submittedTxCount = blockchainTransactions.submittedTransactions().size(); int remainingTxCount = blockchainTransactions.remainingTransactions().size(); + transactionEntityRepositoryGateway.unlockTransactions(blockchainTransactions.remainingTransactions()); log.info("Submitted tx count:{}, remainingTxCount:{}", submittedTxCount, remainingTxCount); } diff --git a/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/service/event_handle/BlockchainPublisherEventHandler.java b/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/service/event_handle/BlockchainPublisherEventHandler.java index 898040c6..d8bc5a30 100644 --- a/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/service/event_handle/BlockchainPublisherEventHandler.java +++ b/blockchain_publisher/src/main/java/org/cardanofoundation/lob/app/blockchain_publisher/service/event_handle/BlockchainPublisherEventHandler.java @@ -5,6 +5,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.event.ledger.ReportLedgerUpdateCommand; @@ -21,6 +22,7 @@ public class BlockchainPublisherEventHandler { // received when a ledger update command is published meaning accounting core has changed to the transaction status = MARK_DISPATCH @EventListener + @Async public void handleLedgerUpdateCommand(TransactionLedgerUpdateCommand command) { log.info("Received LedgerUpdateCommand: {}", command); @@ -31,6 +33,7 @@ public void handleLedgerUpdateCommand(TransactionLedgerUpdateCommand command) { } @EventListener + @Async public void handleLedgerUpdateCommand(ReportLedgerUpdateCommand command) { log.info("Received ReportLedgerUpdateCommand: {}", command); diff --git a/blockchain_publisher/src/main/resources/db/migration/postgresql/common/V1.0_100_4__lob_service_app_blockchain_publisher_module.sql b/blockchain_publisher/src/main/resources/db/migration/postgresql/common/V1.0_100_4__lob_service_app_blockchain_publisher_module.sql index de2a04d7..f440466d 100644 --- a/blockchain_publisher/src/main/resources/db/migration/postgresql/common/V1.0_100_4__lob_service_app_blockchain_publisher_module.sql +++ b/blockchain_publisher/src/main/resources/db/migration/postgresql/common/V1.0_100_4__lob_service_app_blockchain_publisher_module.sql @@ -82,9 +82,9 @@ CREATE TABLE blockchain_publisher_transaction_item ( FOREIGN KEY (transaction_id) REFERENCES blockchain_publisher_transaction (transaction_id), - fx_rate DECIMAL(12, 8) NOT NULL, + fx_rate DECIMAL(30, 15) NOT NULL, - amount_fcy DECIMAL(30, 8) NOT NULL, + amount_fcy DECIMAL(30, 15) NOT NULL, account_event_code VARCHAR(255) NOT NULL, account_event_name VARCHAR(255) NOT NULL, @@ -102,7 +102,7 @@ CREATE TABLE blockchain_publisher_transaction_item ( document_counterparty_type blockchain_publisher_counter_party_type, document_vat_customer_code VARCHAR(255), - document_vat_rate DECIMAL(12, 8), + document_vat_rate DECIMAL(30, 15), created_at TIMESTAMP WITHOUT TIME ZONE, updated_at TIMESTAMP WITHOUT TIME ZONE, diff --git a/blockchain_publisher/src/test/java/org/cardanofoundation/lob/app/blockchain_publisher/repository/TransactionEntityRepositoryGatewayTest.java b/blockchain_publisher/src/test/java/org/cardanofoundation/lob/app/blockchain_publisher/repository/TransactionEntityRepositoryGatewayTest.java index 865fbc56..d99fe1cc 100644 --- a/blockchain_publisher/src/test/java/org/cardanofoundation/lob/app/blockchain_publisher/repository/TransactionEntityRepositoryGatewayTest.java +++ b/blockchain_publisher/src/test/java/org/cardanofoundation/lob/app/blockchain_publisher/repository/TransactionEntityRepositoryGatewayTest.java @@ -72,7 +72,7 @@ void testFindAndLockTransactionsReadyToBeDispatchedLockNotExpired() { expiredLockTx.setLockedAt(LocalDateTime.now().minus(LOCK_TIMEOUT_DURATION.plusSeconds(1))); transactions.add(expiredLockTx); - when(transactionEntityRepository.findTransactionsByStatus(eq(ORG_ID), eq(dispatchStatuses), any(Limit.class))) + when(transactionEntityRepository.findFreeTransactionsByStatus(eq(ORG_ID), eq(dispatchStatuses), any(LocalDateTime.class), any(Limit.class))) .thenReturn(transactions); Set result = transactionEntityRepositoryGateway.findAndLockTransactionsReadyToBeDispatched(ORG_ID, BATCH_SIZE); @@ -80,7 +80,7 @@ void testFindAndLockTransactionsReadyToBeDispatchedLockNotExpired() { assertEquals(2, result.size()); Assertions.assertTrue(result.stream().allMatch(tx -> tx.getLockedAt().isPresent())); - verify(transactionEntityRepository).findTransactionsByStatus(ORG_ID, dispatchStatuses, Limit.of(BATCH_SIZE)); + verify(transactionEntityRepository).findFreeTransactionsByStatus(eq(ORG_ID), eq(dispatchStatuses), any(LocalDateTime.class), eq(Limit.of(BATCH_SIZE))); verify(transactionEntityRepository).saveAll(result); verifyNoMoreInteractions(transactionEntityRepository); } @@ -100,15 +100,15 @@ void testFindAndLockTransactionsReadyToBeDispatchedLockExpired() { expiredLockTx.setLockedAt(LocalDateTime.now().minus(LOCK_TIMEOUT_DURATION)); transactions.add(expiredLockTx); - when(transactionEntityRepository.findTransactionsByStatus(eq(ORG_ID), eq(dispatchStatuses), any(Limit.class))) + when(transactionEntityRepository.findFreeTransactionsByStatus(eq(ORG_ID), eq(dispatchStatuses), any(LocalDateTime.class), any(Limit.class))) .thenReturn(transactions); Set result = transactionEntityRepositoryGateway.findAndLockTransactionsReadyToBeDispatched(ORG_ID, BATCH_SIZE); - assertEquals(1, result.size()); + assertEquals(2, result.size()); Assertions.assertTrue(result.stream().allMatch(tx -> tx.getLockedAt() != null)); - verify(transactionEntityRepository).findTransactionsByStatus(ORG_ID, dispatchStatuses, Limit.of(BATCH_SIZE)); + verify(transactionEntityRepository).findFreeTransactionsByStatus(eq(ORG_ID), eq(dispatchStatuses), any(LocalDateTime.class),eq(Limit.of(BATCH_SIZE))); verify(transactionEntityRepository).saveAll(result); verifyNoMoreInteractions(transactionEntityRepository); } @@ -116,13 +116,13 @@ void testFindAndLockTransactionsReadyToBeDispatchedLockExpired() { @Test void testFindAndLockTransactionsReadyToBeDispatchedEmptyList() { Set dispatchStatuses = BlockchainPublishStatus.toDispatchStatuses(); - when(transactionEntityRepository.findTransactionsByStatus(eq(ORG_ID), eq(dispatchStatuses), any(Limit.class))) + when(transactionEntityRepository.findFreeTransactionsByStatus(eq(ORG_ID), eq(dispatchStatuses), any(LocalDateTime.class), any(Limit.class))) .thenReturn(Set.of()); Set result = transactionEntityRepositoryGateway.findAndLockTransactionsReadyToBeDispatched(ORG_ID, BATCH_SIZE); assertEquals(0, result.size()); - verify(transactionEntityRepository).findTransactionsByStatus(ORG_ID, dispatchStatuses, Limit.of(BATCH_SIZE)); + verify(transactionEntityRepository).findFreeTransactionsByStatus(eq(ORG_ID), eq(dispatchStatuses), any(LocalDateTime.class), eq(Limit.of(BATCH_SIZE))); verifyNoMoreInteractions(transactionEntityRepository); } diff --git a/blockchain_publisher/src/test/java/org/cardanofoundation/lob/app/blockchain_publisher/service/dispatch/BlockchainTransactionsDispatcherTest.java b/blockchain_publisher/src/test/java/org/cardanofoundation/lob/app/blockchain_publisher/service/dispatch/BlockchainTransactionsDispatcherTest.java index 34a57c24..259885b2 100644 --- a/blockchain_publisher/src/test/java/org/cardanofoundation/lob/app/blockchain_publisher/service/dispatch/BlockchainTransactionsDispatcherTest.java +++ b/blockchain_publisher/src/test/java/org/cardanofoundation/lob/app/blockchain_publisher/service/dispatch/BlockchainTransactionsDispatcherTest.java @@ -4,6 +4,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -81,6 +82,7 @@ void dispatchTransactionsNoTransactionToDispatch() { verify(organisationPublicApi).listAll(); verify(transactionEntityRepositoryGateway).findAndLockTransactionsReadyToBeDispatched("organisationId", 50); verify(dispatchingStrategy).apply("organisationId", Set.of()); + verify(transactionEntityRepositoryGateway).unlockTransactions(anySet()); verifyNoMoreInteractions(organisationPublicApi); verifyNoMoreInteractions(transactionEntityRepositoryGateway); verifyNoMoreInteractions(dispatchingStrategy); @@ -106,6 +108,7 @@ void dispatchTransactionsL1TransactionsProblem() { verify(transactionEntityRepositoryGateway).findAndLockTransactionsReadyToBeDispatched("organisationId", 50); verify(dispatchingStrategy).apply("organisationId", Set.of()); verify(l1TransactionCreator).pullBlockchainTransaction("organisationId", Set.of(transactionEntity)); + verify(transactionEntityRepositoryGateway).unlockTransactions(anySet()); verifyNoMoreInteractions(organisationPublicApi); verifyNoMoreInteractions(transactionEntityRepositoryGateway); verifyNoMoreInteractions(dispatchingStrategy); @@ -131,6 +134,7 @@ void dispatchTransactionsL1TransactionsEmpty() { verify(transactionEntityRepositoryGateway).findAndLockTransactionsReadyToBeDispatched("organisationId", 50); verify(dispatchingStrategy).apply("organisationId", Set.of()); verify(l1TransactionCreator).pullBlockchainTransaction("organisationId", Set.of(transactionEntity)); + verify(transactionEntityRepositoryGateway).unlockTransactions(anySet()); verifyNoMoreInteractions(organisationPublicApi); verifyNoMoreInteractions(transactionEntityRepositoryGateway); verifyNoMoreInteractions(dispatchingStrategy); @@ -156,6 +160,7 @@ void dispatchTransactionsL1TransactionsNull() { verify(transactionEntityRepositoryGateway).findAndLockTransactionsReadyToBeDispatched("organisationId", 50); verify(dispatchingStrategy).apply("organisationId", Set.of()); verify(l1TransactionCreator).pullBlockchainTransaction("organisationId", Set.of(transactionEntity)); + verify(transactionEntityRepositoryGateway).unlockTransactions(anySet()); verifyNoMoreInteractions(organisationPublicApi); verifyNoMoreInteractions(transactionEntityRepositoryGateway); verifyNoMoreInteractions(dispatchingStrategy); @@ -190,6 +195,7 @@ void dispatchTransactionsSuccess() throws ApiException { verify(l1TransactionCreator).pullBlockchainTransaction("organisationId", Set.of(transactionEntity)); verify(transactionSubmissionService).submitTransactionWithPossibleConfirmation(eq(new byte[0]), anyString()); verify(ledgerUpdatedEventPublisher).sendTxLedgerUpdatedEvents(null, new HashSet<>()); + verify(transactionEntityRepositoryGateway, times(2)).unlockTransactions(anySet()); verifyNoMoreInteractions(organisationPublicApi); verifyNoMoreInteractions(transactionEntityRepositoryGateway); verifyNoMoreInteractions(dispatchingStrategy); diff --git a/support/src/main/java/org/cardanofoundation/lob/app/support/reactive/Debouncer.java b/support/src/main/java/org/cardanofoundation/lob/app/support/reactive/Debouncer.java index 67745c65..b95c6b15 100644 --- a/support/src/main/java/org/cardanofoundation/lob/app/support/reactive/Debouncer.java +++ b/support/src/main/java/org/cardanofoundation/lob/app/support/reactive/Debouncer.java @@ -30,7 +30,7 @@ public synchronized void call() { future.cancel(false); // Cancel the previous task if it is pending. } future = scheduler.schedule(() -> - transactionalTaskRunner.runInNewTransaction(task), delay, TimeUnit.MILLISECONDS); + transactionalTaskRunner.runAfterTransaction(task), delay, TimeUnit.MILLISECONDS); } public void shutdown() { diff --git a/support/src/main/java/org/cardanofoundation/lob/app/support/reactive/TransactionalTaskRunner.java b/support/src/main/java/org/cardanofoundation/lob/app/support/reactive/TransactionalTaskRunner.java index c2138b26..12bc17df 100644 --- a/support/src/main/java/org/cardanofoundation/lob/app/support/reactive/TransactionalTaskRunner.java +++ b/support/src/main/java/org/cardanofoundation/lob/app/support/reactive/TransactionalTaskRunner.java @@ -1,14 +1,13 @@ package org.cardanofoundation.lob.app.support.reactive; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class TransactionalTaskRunner { - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void runInNewTransaction(Runnable task) { + @Transactional + public void runAfterTransaction(Runnable task) { task.run(); } } diff --git a/support/src/test/java/org/cardanofoundation/lob/app/support/reactive/DebouncerTest.java b/support/src/test/java/org/cardanofoundation/lob/app/support/reactive/DebouncerTest.java index 696573f9..3a1aa11a 100644 --- a/support/src/test/java/org/cardanofoundation/lob/app/support/reactive/DebouncerTest.java +++ b/support/src/test/java/org/cardanofoundation/lob/app/support/reactive/DebouncerTest.java @@ -39,7 +39,7 @@ public void shouldExecuteOnlyLastInvocation() throws InterruptedException { MILLISECONDS.sleep(200); // Verify that the task is executed only once - verify(taskRunner, times(1)).runInNewTransaction(task); + verify(taskRunner, times(1)).runAfterTransaction(task); } @Test @@ -51,7 +51,7 @@ public void shouldCancelPreviousTasks() throws InterruptedException { MILLISECONDS.sleep(200); // Wait more than the debouncing delay // Verify that the task is executed only once, ensuring the first call was cancelled - verify(taskRunner, times(1)).runInNewTransaction(task); + verify(taskRunner, times(1)).runAfterTransaction(task); } // @Test