From 5140fec68c172af389f4a27dc76ebef0b055110c Mon Sep 17 00:00:00 2001 From: Mateusz Czeladka Date: Wed, 7 Aug 2024 17:24:55 +0200 Subject: [PATCH] feat: ability to reject multiple transactions. --- .../domain/core/TransactionProblem.java | 16 ++ .../TransactionBatchRepositoryGateway.java | 5 +- .../TransactionRepositoryGateway.java | 249 +++++++++++++----- .../resource/AccountingCoreResource.java | 58 ++-- ...AccountingCorePresentationViewService.java | 167 ++++++------ ...sactionApprove.java => TransactionId.java} | 11 +- ...sApprove.java => TransactionsRequest.java} | 10 +- .../views/TransactionProcessView.java | 1 + .../internal/AccountingCoreService.java | 32 +-- 9 files changed, 315 insertions(+), 234 deletions(-) create mode 100644 accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/domain/core/TransactionProblem.java rename accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/{TransactionApprove.java => TransactionId.java} (58%) rename accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/{TransactionsApprove.java => TransactionsRequest.java} (87%) diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/domain/core/TransactionProblem.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/domain/core/TransactionProblem.java new file mode 100644 index 00000000..0ac2a9ca --- /dev/null +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/domain/core/TransactionProblem.java @@ -0,0 +1,16 @@ +package org.cardanofoundation.lob.app.accounting_reporting_core.domain.core; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.zalando.problem.Problem; + +@RequiredArgsConstructor +@ToString +@Getter +public class TransactionProblem { + + private final String id; + private final Problem problem; + +} diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/repository/TransactionBatchRepositoryGateway.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/repository/TransactionBatchRepositoryGateway.java index adda5f00..5c7e1136 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/repository/TransactionBatchRepositoryGateway.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/repository/TransactionBatchRepositoryGateway.java @@ -22,10 +22,8 @@ public Optional findById(String batchId) { return transactionBatchRepository.findById(batchId); } + // TODO: Pagination need to be implemented public List findByOrganisationId(String organisationId) { - /** - * Todo: Pagination need to be implemented. - */ return transactionBatchRepository.findAllByFilteringParametersOrganisationId(organisationId); } @@ -36,4 +34,5 @@ public List findByFilter(BatchSearchRequest body) { public Long findByFilterCount(BatchSearchRequest body) { return transactionBatchRepository.findByFilterCount(body); } + } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/repository/TransactionRepositoryGateway.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/repository/TransactionRepositoryGateway.java index d5bb3aa2..1ee4457e 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/repository/TransactionRepositoryGateway.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/repository/TransactionRepositoryGateway.java @@ -4,21 +4,27 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; +import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TransactionProblem; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TransactionType; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.ValidationStatus; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.Rejection; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.TransactionEntity; +import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.TransactionId; +import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.TransactionsRequest; import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.LedgerService; +import org.springframework.dao.DataAccessException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.zalando.problem.Problem; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.ValidationStatus.FAILED; +import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TransactionStatus.FAIL; +import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; @Service @Slf4j @@ -30,119 +36,220 @@ public class TransactionRepositoryGateway { private final TransactionRepository transactionRepository; private final LedgerService ledgerService; - @Transactional - public Either approveTransaction(String transactionId) { + @Transactional(propagation = REQUIRES_NEW) + // TODO optimise performance because we have to load transaction from db each time and we don't save it in bulk + private Either approveTransaction(String transactionId) { log.info("Approving transaction: {}", transactionId); val txM = transactionRepository.findById(transactionId); if (txM.isEmpty()) { - return Either.left(Problem.builder() + val problem = Problem.builder() .withTitle("TX_NOT_FOUND") .withDetail(STR."Transaction with id \{transactionId} not found") .with("transactionId", transactionId) - .build() - ); + .build(); + + return Either.left(new TransactionProblem(transactionId, problem)); } val tx = txM.orElseThrow(); - if (tx.getAutomatedValidationStatus() == FAILED) { - return Either.left(Problem.builder() + if (tx.getStatus() == FAIL) { + val problem = Problem.builder() .withTitle("CANNOT_APPROVE_FAILED_TX") .withDetail(STR."Cannot approve a failed transaction, transactionId: \{transactionId}") .with("transactionId", transactionId) - .build() - ); + .build(); + + return Either.left(new TransactionProblem(transactionId, problem)); } tx.setTransactionApproved(true); val savedTx = transactionRepository.save(tx); - val organisationId = savedTx.getOrganisation().getId(); - if (savedTx.getTransactionApproved()) { - ledgerService.checkIfThereAreTransactionsToDispatch(organisationId, Set.of(savedTx)); + return Either.right(savedTx); + } + + @Transactional(propagation = REQUIRES_NEW) + // TODO optimise performance because we have to load transaction from db each time and we don't save it in bulk + private Either approveTransactionsDispatch(String transactionId) { + log.info("Approving transaction to dispatch: {}", transactionId); + + val txM = transactionRepository.findById(transactionId); + + if (txM.isEmpty()) { + val problem = Problem.builder() + .withTitle("TX_NOT_FOUND") + .withDetail(STR."Transaction with id \{transactionId} not found") + .with("transactionId", transactionId) + .build(); - return Either.right(savedTx.getTransactionApproved()); + return Either.left(new TransactionProblem(transactionId, problem)); } - return Either.right(false); - } + val tx = txM.orElseThrow(); - @Transactional - public Set approveTransactions(String organisationId, Set transactionIds) { - log.info("Approving transactions: {}", transactionIds); + if (tx.getStatus() == FAIL) { + val problem = Problem.builder() + .withTitle("CANNOT_APPROVE_FAILED_TX") + .withDetail(STR."Cannot approve a failed transaction, transactionId: \{transactionId}") + .with("transactionId", transactionId) + .build(); - val transactions = transactionRepository.findAllById(transactionIds) - .stream() - .filter(tx -> tx.getAutomatedValidationStatus() != FAILED) - .peek(tx -> tx.setTransactionApproved(true)) - .collect(Collectors.toSet()); + return Either.left(new TransactionProblem(transactionId, problem)); + } - val savedTxs = transactionRepository.saveAll(transactions); + if (!tx.getTransactionApproved()) { + val problem = Problem.builder() + .withTitle("TX_NOT_APPROVED") + .withDetail(STR."Cannot approve a transaction that has not been approved before, transactionId: \{transactionId}") + .with("transactionId", transactionId) + .build(); + + return Either.left(new TransactionProblem(transactionId, problem)); + } + + tx.setLedgerDispatchApproved(true); - ledgerService.checkIfThereAreTransactionsToDispatch(organisationId, Set.copyOf(savedTxs)); + val savedTx = transactionRepository.save(tx); - return savedTxs.stream().map(TransactionEntity::getId).collect(Collectors.toSet()); + return Either.right(savedTx); } - @Transactional - public Set approveTransactionsDispatch(String organisationId, Set transactionIds) { - log.info("Approving transactions dispatch: {}", transactionIds); + public List> approveTransactions(TransactionsRequest transactionsRequest) { + val organisationId = transactionsRequest.getOrganisationId(); - val transactions = transactionRepository.findAllById(transactionIds) + val transactionIds = transactionsRequest.getTransactionIds() .stream() - .filter(tx -> tx.getAutomatedValidationStatus() != FAILED) - .peek(tx -> tx.setLedgerDispatchApproved(true)) + .map(TransactionId::getId) .collect(Collectors.toSet()); - val savedTxs = transactionRepository.saveAll(transactions); + val transactionsApprovalResponseListE = new ArrayList>(); + for (val transactionId : transactionIds) { + try { + val transactionEntities = approveTransaction(transactionId); - ledgerService.checkIfThereAreTransactionsToDispatch(organisationId, Set.copyOf(savedTxs)); + transactionsApprovalResponseListE.add(transactionEntities); + } catch (DataAccessException dae) { + log.error("Error approving transaction: {}", transactionId, dae); - return savedTxs.stream().map(TransactionEntity::getId).collect(Collectors.toSet()); - } + val problem = Problem.builder() + .withTitle("DB_ERROR") + .withDetail(STR."DB error approving transaction: \{transactionId}") + .with("transactionId", transactionId) + .with("error", dae.getMessage()) + .build(); - @Transactional - public Either approveTransactionDispatch(String transactionId) { - log.info("Approving transaction dispatch: {}", transactionId); + transactionsApprovalResponseListE.add(Either.left(new TransactionProblem(transactionId, problem))); + } + } - val txM = transactionRepository.findById(transactionId); + val transactionSuccesses = transactionsApprovalResponseListE.stream() + .filter(Either::isRight) + .map(Either::get) + .collect(Collectors.toSet()); - if (txM.isEmpty()) { - return Either.left(Problem.builder() - .withTitle("TX_NOT_FOUND") - .withDetail(STR."Transaction with id \{transactionId} not found") - .with("transactionId", transactionId) - .build() - ); - } + ledgerService.checkIfThereAreTransactionsToDispatch(organisationId, transactionSuccesses); - val tx = txM.orElseThrow(); + return transactionsApprovalResponseListE; + } - if (tx.getAutomatedValidationStatus() == FAILED) { - return Either.left(Problem.builder() - .withTitle("CANNOT_APPROVE_FAILED_TX") - .withDetail(STR."Cannot approve dispatch for a failed transaction, transactionId: \{transactionId}") - .with("transactionId", transactionId) - .build() - ); - } + public List> approveTransactionsDispatch(TransactionsRequest transactionsRequest) { + val organisationId = transactionsRequest.getOrganisationId(); - tx.setLedgerDispatchApproved(true); + val transactionIds = transactionsRequest.getTransactionIds() + .stream() + .map(TransactionId::getId) + .collect(Collectors.toSet()); - val savedTx = transactionRepository.save(tx); + val transactionsApprovalResponseListE = new ArrayList>(); + for (val transactionId : transactionIds) { + try { + val transactionEntities = approveTransactionsDispatch(transactionId); - if (savedTx.getLedgerDispatchApproved()) { - ledgerService.checkIfThereAreTransactionsToDispatch(savedTx.getOrganisation().getId(), Set.of(savedTx)); + transactionsApprovalResponseListE.add(transactionEntities); + } catch (DataAccessException dae) { + log.error("Error approving transaction publish: {}", transactionId, dae); - return Either.right(savedTx.getLedgerDispatchApproved()); + val problem = Problem.builder() + .withTitle("DB_ERROR") + .withDetail(STR."DB error approving transaction publish:\{transactionId}") + .with("transactionId", transactionId) + .with("error", dae.getMessage()) + .build(); + + transactionsApprovalResponseListE.add(Either.left(new TransactionProblem(transactionId, problem))); + } } - return Either.right(false); + val transactionSuccesses = transactionsApprovalResponseListE.stream() + .filter(Either::isRight) + .map(Either::get) + .collect(Collectors.toSet()); + + ledgerService.checkIfThereAreTransactionsToDispatch(organisationId, transactionSuccesses); + + return transactionsApprovalResponseListE; } +// @Transactional +// public Set approveTransactionsDispatch(String organisationId, Set transactionIds) { +// log.info("Approving transactions dispatch: {}", transactionIds); +// +// val transactions = transactionRepository.findAllById(transactionIds) +// .stream() +// .filter(tx -> tx.getAutomatedValidationStatus() != FAILED) +// .peek(tx -> tx.setLedgerDispatchApproved(true)) +// .collect(Collectors.toSet()); +// +// val savedTxs = transactionRepository.saveAll(transactions); +// +// ledgerService.checkIfThereAreTransactionsToDispatch(organisationId, Set.copyOf(savedTxs)); +// +// return savedTxs.stream().map(TransactionEntity::getId).collect(Collectors.toSet()); +// } + +// @Transactional +// public Either approveTransactionDispatch(String transactionId) { +// log.info("Approving transaction dispatch: {}", transactionId); +// +// val txM = transactionRepository.findById(transactionId); +// +// if (txM.isEmpty()) { +// return Either.left(Problem.builder() +// .withTitle("TX_NOT_FOUND") +// .withDetail(STR."Transaction with id \{transactionId} not found") +// .with("transactionId", transactionId) +// .build() +// ); +// } +// +// val tx = txM.orElseThrow(); +// +// if (tx.getAutomatedValidationStatus() == FAILED) { +// return Either.left(Problem.builder() +// .withTitle("CANNOT_APPROVE_FAILED_TX") +// .withDetail(STR."Cannot approve dispatch for a failed transaction, transactionId: \{transactionId}") +// .with("transactionId", transactionId) +// .build() +// ); +// } +// +// tx.setLedgerDispatchApproved(true); +// +// val savedTx = transactionRepository.save(tx); +// +// if (savedTx.getLedgerDispatchApproved()) { +// ledgerService.checkIfThereAreTransactionsToDispatch(savedTx.getOrganisation().getId(), Set.of(savedTx)); +// +// return Either.right(savedTx.getLedgerDispatchApproved()); +// } +// +// return Either.right(false); +// } + public Either changeTransactionComment(String txId, String userComment) { val txM = transactionRepository.findById(txId); @@ -205,16 +312,15 @@ public Optional findById(String transactionId) { } public List findByAllId(Set transactionIds) { - return transactionRepository.findAllById(transactionIds); } - private static Set transactionIds(Set transactions) { - return transactions - .stream() - .map(TransactionEntity::getId) - .collect(Collectors.toSet()); - } +// private static Set transactionIds(Set transactions) { +// return transactions +// .stream() +// .map(TransactionEntity::getId) +// .collect(Collectors.toSet()); +// } public List findAllByStatus(String organisationId, List validationStatuses, @@ -225,4 +331,5 @@ public List findAllByStatus(String organisationId, public List listAll() { return transactionRepository.findAll(); } + } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/AccountingCoreResource.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/AccountingCoreResource.java index 74719853..df96fdd7 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/AccountingCoreResource.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/AccountingCoreResource.java @@ -18,7 +18,7 @@ import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.BatchSearchRequest; import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.ExtractionRequest; import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.SearchRequest; -import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.TransactionsApprove; +import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.TransactionsRequest; import org.cardanofoundation.lob.app.accounting_reporting_core.resource.views.BatchView; import org.cardanofoundation.lob.app.accounting_reporting_core.resource.views.BatchsDetailView; import org.cardanofoundation.lob.app.accounting_reporting_core.resource.views.TransactionProcessView; @@ -27,7 +27,6 @@ import org.json.JSONException; import org.json.JSONObject; import org.springframework.http.HttpStatusCode; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.zalando.problem.Problem; @@ -35,6 +34,9 @@ import java.util.List; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.zalando.problem.Status.OK; + @RestController @CrossOrigin(origins = "http://localhost:3000") @RequestMapping("/api") @@ -48,10 +50,10 @@ public class AccountingCoreResource { @Tag(name = "Transactions", description = "Transactions API") @Operation(description = "Transaction list", responses = { @ApiResponse(content = - {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE, array = @ArraySchema(schema = @Schema(implementation = TransactionView.class)))} + {@Content(mediaType = APPLICATION_JSON_VALUE, array = @ArraySchema(schema = @Schema(implementation = TransactionView.class)))} ) }) - @PostMapping(value = "/transactions", produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "/transactions", produces = APPLICATION_JSON_VALUE) public ResponseEntity listAllAction(@Valid @RequestBody SearchRequest body) { List transactions = accountingCorePresentationService.allTransactions(body); @@ -61,10 +63,10 @@ public ResponseEntity listAllAction(@Valid @RequestBody SearchRequest body) { @Tag(name = "Transactions", description = "Transactions API") @Operation(description = "Transaction detail", responses = { @ApiResponse(content = - {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = TransactionView.class))} + {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = TransactionView.class))} ) }) - @GetMapping(value = "/transactions/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "/transactions/{id}", produces = APPLICATION_JSON_VALUE) public ResponseEntity transactionDetailSpecific(@Valid @PathVariable("id") @Parameter(example = "7e9e8bcbb38a283b41eab57add98278561ab51d23a16f3e3baf3daa461b84ab4") String id) { val transactionEntity = accountingCorePresentationService.transactionDetailSpecific(id); @@ -84,10 +86,10 @@ public ResponseEntity transactionDetailSpecific(@Valid @PathVariable("id") @P @Tag(name = "Transactions", description = "Transactions API") @Operation(description = "Transaction types", responses = { @ApiResponse(content = - {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(example = "[{\"id\":\"CardCharge\",\"title\":\"Card Charge\"},{\"id\":\"VendorBill\",\"title\":\"Vendor Bill\"},{\"id\":\"CardRefund\",\"title\":\"Card Refund\"},{\"id\":\"Journal\",\"title\":\"Journal\"},{\"id\":\"FxRevaluation\",\"title\":\"Fx Revaluation\"},{\"id\":\"Transfer\",\"title\":\"Transfer\"},{\"id\":\"CustomerPayment\",\"title\":\"Customer Payment\"},{\"id\":\"ExpenseReport\",\"title\":\"Expense Report\"},{\"id\":\"VendorPayment\",\"title\":\"Vendor Payment\"},{\"id\":\"BillCredit\",\"title\":\"Bill Credit\"}]"))} + {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(example = "[{\"id\":\"CardCharge\",\"title\":\"Card Charge\"},{\"id\":\"VendorBill\",\"title\":\"Vendor Bill\"},{\"id\":\"CardRefund\",\"title\":\"Card Refund\"},{\"id\":\"Journal\",\"title\":\"Journal\"},{\"id\":\"FxRevaluation\",\"title\":\"Fx Revaluation\"},{\"id\":\"Transfer\",\"title\":\"Transfer\"},{\"id\":\"CustomerPayment\",\"title\":\"Customer Payment\"},{\"id\":\"ExpenseReport\",\"title\":\"Expense Report\"},{\"id\":\"VendorPayment\",\"title\":\"Vendor Payment\"},{\"id\":\"BillCredit\",\"title\":\"Bill Credit\"}]"))} ) }) - @GetMapping(value = "/transaction-types", produces = MediaType.APPLICATION_JSON_VALUE, name = "Transaction types") + @GetMapping(value = "/transaction-types", produces = APPLICATION_JSON_VALUE, name = "Transaction types") public ResponseEntity transactionType() throws JSONException { JSONArray jsonArray = new JSONArray(); @@ -106,20 +108,20 @@ public ResponseEntity transactionType() throws JSONException { @Tag(name = "Transactions", description = "Transactions API") @Operation(description = "Rejection types", responses = { @ApiResponse(content = - {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE, array = @ArraySchema(schema = @Schema(implementation = RejectionCode.class)))} + {@Content(mediaType = APPLICATION_JSON_VALUE, array = @ArraySchema(schema = @Schema(implementation = RejectionCode.class)))} ) }) - @GetMapping(value = "/rejection-types", produces = MediaType.APPLICATION_JSON_VALUE, name = "Rejection types") + @GetMapping(value = "/rejection-types", produces = APPLICATION_JSON_VALUE, name = "Rejection types") public ResponseEntity rejectionTypes() { return ResponseEntity.ok().body(RejectionCode.values()); } @Tag(name = "Transactions", description = "Transactions API") - @PostMapping(value = "/extraction", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "/extraction", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE) @Operation(description = "Trigger the extraction from the ERP system(s)", responses = { @ApiResponse(content = - {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(example = "{\"event\": \"EXTRACTION\",\"message\":\"We have received your extraction request now. Please review imported transactions from the batch list.\"}"))}, responseCode = "202" ) @@ -159,35 +161,29 @@ public ResponseEntity extractionTrigger(@Valid @RequestBody ExtractionRequest .body(response.toString()); } - @Tag(name = "Transactions", description = "Transactions API") - @PostMapping(value = "/transactions/approve", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) - @Operation(description = "Approve transactions", + @Tag(name = "Transactions Approval", description = "Transactions Approval API") + @PostMapping(value = "/transactions/approve", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE) + @Operation(description = "Approve one or more transactions", responses = { @ApiResponse(content = { - @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, array = @ArraySchema(schema = @Schema(implementation = TransactionProcessView.class))) + @Content(mediaType = APPLICATION_JSON_VALUE, array = @ArraySchema(schema = @Schema(implementation = TransactionProcessView.class))) }) } ) - public ResponseEntity approveTransaction(@Valid @RequestBody TransactionsApprove transactionsApprove) { - val resul = accountingCorePresentationService.approveTransactions(transactionsApprove.getTransactionApproves()); - - if (resul.isEmpty()) { - return ResponseEntity - .status(HttpStatusCode.valueOf(404)) - .body(resul); - } + public ResponseEntity approveTransactions(@Valid @RequestBody TransactionsRequest transactionsRequest) { + val transactionProcessViewsResult = accountingCorePresentationService.approveTransactions(transactionsRequest); return ResponseEntity - .status(HttpStatusCode.valueOf(202)) - .body(resul); + .status(HttpStatusCode.valueOf(OK.getStatusCode())) + .body(transactionProcessViewsResult); } @Tag(name = "Batchs", description = "Batchs API") - @PostMapping(value = "/batchs", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "/batchs", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE) @Operation(description = "Batch list", responses = { @ApiResponse(content = { - @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, array = @ArraySchema(schema = @Schema(implementation = BatchsDetailView.class))) + @Content(mediaType = APPLICATION_JSON_VALUE, array = @ArraySchema(schema = @Schema(implementation = BatchsDetailView.class))) }) } ) @@ -203,13 +199,13 @@ public ResponseEntity listAllBatch(@Valid @RequestBody BatchSearchRequest bod } @Tag(name = "Batchs", description = "Batchs API") - @GetMapping(value = "/batchs/{batchId}", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = "/batchs/{batchId}", produces = APPLICATION_JSON_VALUE) @Operation(description = "Batch detail", responses = { @ApiResponse(content = { - @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = BatchView.class)) + @Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(implementation = BatchView.class)) }), - @ApiResponse(responseCode = "404", description = "Error: response status is 404", content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(example = "{\"title\": \"BATCH_NOT_FOUND\",\"status\": 404,\"detail\": \"Batch with id: {batchId} could not be found\"" + + @ApiResponse(responseCode = "404", description = "Error: response status is 404", content = {@Content(mediaType = APPLICATION_JSON_VALUE, schema = @Schema(example = "{\"title\": \"BATCH_NOT_FOUND\",\"status\": 404,\"detail\": \"Batch with id: {batchId} could not be found\"" + "}"))}) } ) diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/model/AccountingCorePresentationViewService.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/model/AccountingCorePresentationViewService.java index 53eca2a4..993301d7 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/model/AccountingCorePresentationViewService.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/model/AccountingCorePresentationViewService.java @@ -1,6 +1,5 @@ package org.cardanofoundation.lob.app.accounting_reporting_core.resource.model; -import io.vavr.control.Either; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; @@ -11,52 +10,57 @@ import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.BatchSearchRequest; import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.ExtractionRequest; import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.SearchRequest; -import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.TransactionApprove; +import org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests.TransactionsRequest; import org.cardanofoundation.lob.app.accounting_reporting_core.resource.views.*; import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.AccountingCoreService; import org.jmolecules.ddd.annotation.Service; -import org.springframework.dao.DataAccessException; import org.springframework.transaction.annotation.Transactional; -import org.zalando.problem.Problem; -import org.zalando.problem.Status; -import org.zalando.problem.ThrowableProblem; import java.math.BigDecimal; import java.time.LocalDate; -import java.util.*; +import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; +import static java.math.BigDecimal.ZERO; +import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.Counterparty.Type.VENDOR; + @Service -@org.springframework.stereotype.Service @Slf4j @RequiredArgsConstructor @Transactional(readOnly = true) // presentation layer service public class AccountingCorePresentationViewService { + private final TransactionRepositoryGateway transactionRepositoryGateway; private final AccountingCoreService accountingCoreService; private final TransactionBatchRepositoryGateway transactionBatchRepositoryGateway; public List allTransactions(SearchRequest body) { - List transactions = transactionRepositoryGateway.findAllByStatus(body.getOrganisationId(), body.getStatus(), body.getTransactionType()); + val transactions = transactionRepositoryGateway.findAllByStatus( + body.getOrganisationId(), + body.getStatus(), + body.getTransactionType() + ); - return - transactions.stream().map(this::getTransactionView).toList() - ; + return transactions.stream() + .map(this::getTransactionView) + .toList(); } public Optional transactionDetailSpecific(String transactionId) { + val transactionEntity = transactionRepositoryGateway.findById(transactionId); - Optional transactionEntity = transactionRepositoryGateway.findById(transactionId); return transactionEntity.map(this::getTransactionView); } public Optional batchDetail(String batchId) { return transactionBatchRepositoryGateway.findById(batchId).map(transactionBatchEntity -> { - val transactions = this.getTransaction(transactionBatchEntity); - val statistic = this.getStatisticts(transactionBatchEntity.getBatchStatistics()); + val statistic = this.getStatistics(transactionBatchEntity.getBatchStatistics()); val filteringParameters = this.getFilteringParameters(transactionBatchEntity.getFilteringParameters()); + return new BatchView( transactionBatchEntity.getId(), transactionBatchEntity.getCreatedAt().toString(), @@ -68,25 +72,22 @@ public Optional batchDetail(String batchId) { statistic, filteringParameters, transactions - ); } ); } - private BatchStatisticsView getStatisticts(Optional batchStatistics) { - - Optional statistics = batchStatistics.stream().findFirst(); + private BatchStatisticsView getStatistics(Optional batchStatistics) { + val statisticsM = batchStatistics.stream().findFirst(); return new BatchStatisticsView( - statistics.flatMap(BatchStatistics::getApprovedTransactionsCount).orElse(0), - Math.abs(statistics.flatMap(BatchStatistics::getTotalTransactionsCount).orElse(0) - statistics.flatMap(BatchStatistics::getDispatchedTransactionsCount).orElse(0)), - statistics.flatMap(BatchStatistics::getFailedTransactionsCount).orElse(0), - statistics.flatMap(BatchStatistics::getDispatchedTransactionsCount).orElse(0), - statistics.flatMap(BatchStatistics::getCompletedTransactionsCount).orElse(0), - statistics.flatMap(BatchStatistics::getTotalTransactionsCount).orElse(0) + statisticsM.flatMap(BatchStatistics::getApprovedTransactionsCount).orElse(0), + Math.abs(statisticsM.flatMap(BatchStatistics::getTotalTransactionsCount).orElse(0) - statisticsM.flatMap(BatchStatistics::getDispatchedTransactionsCount).orElse(0)), + statisticsM.flatMap(BatchStatistics::getFailedTransactionsCount).orElse(0), + statisticsM.flatMap(BatchStatistics::getDispatchedTransactionsCount).orElse(0), + statisticsM.flatMap(BatchStatistics::getCompletedTransactionsCount).orElse(0), + statisticsM.flatMap(BatchStatistics::getTotalTransactionsCount).orElse(0) ); - } private FilteringParametersView getFilteringParameters(FilteringParameters filteringParameters) { @@ -101,28 +102,29 @@ private FilteringParametersView getFilteringParameters(FilteringParameters filte } public BatchsDetailView listAllBatch(BatchSearchRequest body) { - - BatchsDetailView batchDetail = new BatchsDetailView(); - - List batches = transactionBatchRepositoryGateway.findByFilter(body).stream().map( - - transactionBatchEntity -> new BatchView( - transactionBatchEntity.getId(), - transactionBatchEntity.getCreatedAt().toString(), - transactionBatchEntity.getUpdatedAt().toString(), - transactionBatchEntity.getCreatedBy(), - transactionBatchEntity.getUpdatedBy(), - transactionBatchEntity.getOrganisationId(), - transactionBatchEntity.getStatus(), - this.getStatisticts(transactionBatchEntity.getBatchStatistics()), - this.getFilteringParameters(transactionBatchEntity.getFilteringParameters()), - Set.of() - ) - ).toList(); - - batchDetail.setBatchs(batches); - batchDetail.setTotal(Long.valueOf(transactionBatchRepositoryGateway.findByFilterCount(body))); - return batchDetail; + val batchDetailView = new BatchsDetailView(); + + val batches = transactionBatchRepositoryGateway.findByFilter(body) + .stream() + .map( + transactionBatchEntity -> new BatchView( + transactionBatchEntity.getId(), + transactionBatchEntity.getCreatedAt().toString(), + transactionBatchEntity.getUpdatedAt().toString(), + transactionBatchEntity.getCreatedBy(), + transactionBatchEntity.getUpdatedBy(), + transactionBatchEntity.getOrganisationId(), + transactionBatchEntity.getStatus(), + this.getStatistics(transactionBatchEntity.getBatchStatistics()), + this.getFilteringParameters(transactionBatchEntity.getFilteringParameters()), + Set.of() + ) + ).toList(); + + batchDetailView.setBatchs(batches); + batchDetailView.setTotal(transactionBatchRepositoryGateway.findByFilterCount(body)); + + return batchDetailView; } @Transactional @@ -136,37 +138,28 @@ public void extractionTrigger(ExtractionRequest body) { .build(); accountingCoreService.scheduleIngestion(fp); - } - public List approveTransactions(List transactionApproves) { - - val transactionProcessViews = new ArrayList(List.of()); - for (val transactionAp : transactionApproves) { + public List approveTransactions(TransactionsRequest transactionsRequest) { + return transactionRepositoryGateway.approveTransactions(transactionsRequest) + .stream() + .map(txEntityE -> txEntityE.fold(txProblem -> { + return TransactionProcessView.createFail(txProblem.getId(), txProblem.getProblem()); + }, success -> { + return TransactionProcessView.createSucess(success.getId()); + })) + .toList(); + } - try { - Either approveTransactionE = accountingCoreService.approveTransaction(transactionAp.getId()); - val resu = approveTransactionE.fold(problem -> { - return TransactionProcessView.createFail(transactionAp.getId(), problem); + public List approveTransactionsPublish(TransactionsRequest transactionsRequest) { + return transactionRepositoryGateway.approveTransactions(transactionsRequest) + .stream() + .map(txEntityE -> txEntityE.fold(txProblem -> { + return TransactionProcessView.createFail(txProblem.getId(), txProblem.getProblem()); }, success -> { - return TransactionProcessView.createSucess(transactionAp.getId()); - }); - - transactionProcessViews.add(resu); - } catch (DataAccessException exception) { - val problem = Problem.builder() - .withTitle("TRANSACTION_DB_ERROR") - .withDetail(STR."DAtabse serialsation problem for the ID: \{transactionAp.getId()}") - .with("transactionId", transactionAp.getId()) - .withStatus(Status.INTERNAL_SERVER_ERROR) - .with("cause", exception.getMessage()) - .build(); - transactionProcessViews.add(TransactionProcessView.createFail(transactionAp.getId(), problem)); - } - - } - - return transactionProcessViews; + return TransactionProcessView.createSucess(success.getId()); + })) + .toList(); } private Set getTransaction(TransactionBatchEntity transactionBatchEntity) { @@ -184,16 +177,15 @@ private TransactionView getTransactionView(TransactionEntity transactionEntity) transactionEntity.getAutomatedValidationStatus(), transactionEntity.getTransactionApproved(), transactionEntity.getLedgerDispatchApproved(), - getAmountLcy(transactionEntity), + getAmountLcyTotalForAllItems(transactionEntity), getTransactionItemView(transactionEntity), - getViolation(transactionEntity), + getViolations(transactionEntity), transactionEntity.getStatus() ); } private Set getTransactionItemView(TransactionEntity transaction) { return transaction.getItems().stream().map(item -> { - return new TransactionItemView( item.getId(), item.getAccountDebit().map(account -> account.getCode()).orElse(""), @@ -216,18 +208,15 @@ private Set getTransactionItemView(TransactionEntity transa item.getDocument().map(Document::getNum).orElse(""), item.getDocument().map(document -> document.getCurrency().getCustomerCode()).orElse(""), item.getDocument().flatMap(document -> document.getVat().map(Vat::getCustomerCode)).orElse(""), - item.getDocument().flatMap(document -> document.getVat().flatMap(Vat::getRate)).orElse(BigDecimal.ZERO), + item.getDocument().flatMap(document -> document.getVat().flatMap(Vat::getRate)).orElse(ZERO), item.getDocument().flatMap(d -> d.getCounterparty().map(Counterparty::getCustomerCode)).orElse(""), - item.getDocument().flatMap(d -> d.getCounterparty().map(Counterparty::getType)).orElse(org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.Counterparty.Type.VENDOR), + item.getDocument().flatMap(d -> d.getCounterparty().map(Counterparty::getType)).orElse(VENDOR), item.getDocument().flatMap(document -> document.getCounterparty().flatMap(Counterparty::getName)).orElse("") - - ); }).collect(Collectors.toSet()); } - private Set getViolation(TransactionEntity transaction) { - + private Set getViolations(TransactionEntity transaction) { return transaction.getViolations().stream().map(violation -> new ViolationView( violation.getSeverity(), violation.getSource(), @@ -237,12 +226,10 @@ private Set getViolation(TransactionEntity transaction) { )).collect(Collectors.toSet()); } - private BigDecimal getAmountLcy(TransactionEntity tx) { - BigDecimal total = BigDecimal.ZERO; - for (val txItem : tx.getItems()) { - total = total.add(txItem.getAmountLcy()); - } - return total; + public BigDecimal getAmountLcyTotalForAllItems(TransactionEntity tx) { + return tx.getItems().stream() + .map(TransactionItemEntity::getAmountLcy) + .reduce(ZERO, BigDecimal::add); } } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionApprove.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionId.java similarity index 58% rename from accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionApprove.java rename to accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionId.java index 55218efd..f968d902 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionApprove.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionId.java @@ -1,17 +1,16 @@ package org.cardanofoundation.lob.app.accounting_reporting_core.resource.requests; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; @Getter @Setter @AllArgsConstructor @NoArgsConstructor -public class TransactionApprove { - //48335c2b63cffcef2a3cd0678b65c4fb16420f51110033024209957fbd58ec4e +@EqualsAndHashCode +public class TransactionId { + @Schema(example = "7e9e8bcbb38a283b41eab57add98278561ab51d23a16f3e3baf3daa461b84ab4") private String id; + } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionsApprove.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionsRequest.java similarity index 87% rename from accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionsApprove.java rename to accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionsRequest.java index 27c4834a..7d4e02d4 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionsApprove.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/requests/TransactionsRequest.java @@ -7,13 +7,14 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import java.util.List; +import java.util.Set; @Getter @Setter @AllArgsConstructor @NoArgsConstructor -public class TransactionsApprove { +public class TransactionsRequest { + @ArraySchema(arraySchema = @Schema(example = "[ {" + "\"id\": \"7e9e8bcbb38a283b41eab57add98278561ab51d23a16f3e3baf3daa461b84ab4\"}," + "{\"id\": \"7bce71783ff8e6501b33ce9797097f5633c069f17e4731d96467cdb311693fcb\"}," + @@ -23,5 +24,8 @@ public class TransactionsApprove { "{\"id\": \"48335c2b63cffcef2a3cd0678b65c4fb16420f51110033024209957fbd58ec4e\"}" + "]")) - private List transactionApproves; + private String organisationId; + + private Set transactionIds; + } diff --git a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/views/TransactionProcessView.java b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/views/TransactionProcessView.java index 06ec9503..22a2824c 100644 --- a/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/views/TransactionProcessView.java +++ b/accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/resource/views/TransactionProcessView.java @@ -26,4 +26,5 @@ public static TransactionProcessView createSucess(String id) { public static TransactionProcessView createFail(String id, Problem error) { return new TransactionProcessView(id, false, Optional.of(error)); } + } 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 e243b0dd..eb7465a0 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 @@ -7,20 +7,17 @@ import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.UserExtractionParameters; import org.cardanofoundation.lob.app.accounting_reporting_core.domain.event.ScheduledIngestionEvent; import org.cardanofoundation.lob.app.accounting_reporting_core.repository.TransactionBatchRepository; -import org.cardanofoundation.lob.app.accounting_reporting_core.repository.TransactionRepositoryGateway; import org.cardanofoundation.lob.app.accounting_reporting_core.service.business_rules.ProcessorFlags; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.zalando.problem.Problem; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.Source.LOB; -import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.ValidationStatus.FAILED; +import static org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TransactionStatus.FAIL; import static org.zalando.problem.Status.NOT_FOUND; @Service @@ -31,7 +28,6 @@ public class AccountingCoreService { private final ApplicationEventPublisher applicationEventPublisher; private final TransactionBatchRepository transactionBatchRepository; private final ERPIncomingDataProcessor erpIncomingDataProcessor; - private final TransactionRepositoryGateway transactionRepositoryGateway; @Transactional public void scheduleIngestion(UserExtractionParameters userExtractionParameters) { @@ -60,7 +56,7 @@ public Either scheduleReIngestionForFailed(String batchId) { val txBatch = txBatchM.get(); val txs = txBatch.getTransactions().stream() - .filter(tx -> tx.getAutomatedValidationStatus() == FAILED) + .filter(tx -> tx.getStatus() == FAIL) // reprocess only the ones that have not been approved to dispatch yet, actually it is just a sanity check because it should never happen // and we should never allow approving failed transactions .filter(tx -> !tx.allApprovalsPassedForTransactionDispatch()) @@ -84,28 +80,4 @@ public Either scheduleReIngestionForFailed(String batchId) { return Either.right(true); } - @Transactional(propagation = Propagation.REQUIRES_NEW) - public Either approveTransaction(String txId) { - return transactionRepositoryGateway.approveTransaction(txId); - } - - @Transactional - public Either approveTransactionDispatch(String txId) { - return transactionRepositoryGateway.approveTransactionDispatch(txId); - } - - @Transactional - public Set approveTransactions(String organisationId, Set transactionIds) { - log.info("approveTransactions, transactionIds: {}", transactionIds); - - return transactionRepositoryGateway.approveTransactions(organisationId, transactionIds); - } - - @Transactional - public Set approveTransactionsDispatch(String organisationId, Set transactionIds) { - log.info("approveTransactionsDispatch, transactionIds: {}", transactionIds); - - return transactionRepositoryGateway.approveTransactionsDispatch(organisationId, transactionIds); - } - }