From 77a6dd69a6de3b04cf2a4259c676d231c563b950 Mon Sep 17 00:00:00 2001 From: Vusumuzi Khoza Date: Tue, 21 Oct 2025 23:47:15 +0200 Subject: [PATCH] completed transaction workflow --- .../controllers/TransactonController.java | 27 ++- .../domain/services/TransactionService.java | 222 ------------------ .../domain/workflows/TransactionWorkflow.java | 197 +++++++++++----- .../workflows/handlers/CleanupHandler.java | 1 + .../handlers/IntegrationHandler.java | 71 ++++++ ...aymentHandler.java => PaymentHandler.java} | 7 +- .../workflows/handlers/PollStatusHandler.java | 5 +- .../handlers/erp/JournalEntryHandler.java | 15 +- .../handlers/erp/PaymentEntryHandler.java | 17 +- .../handlers/erp/PurchaseInvoiceHandler.java | 17 +- .../erp/PurchasePaymentEntryHandler.java} | 90 ++++--- .../handlers/erp/SalesInvoiceHandler.java | 145 ++++++------ 12 files changed, 384 insertions(+), 430 deletions(-) create mode 100644 src/main/java/zw/qantra/tm/domain/workflows/handlers/IntegrationHandler.java rename src/main/java/zw/qantra/tm/domain/workflows/handlers/{GatewayPaymentHandler.java => PaymentHandler.java} (90%) rename src/main/java/zw/qantra/tm/domain/{services/processors => workflows}/handlers/erp/JournalEntryHandler.java (91%) rename src/main/java/zw/qantra/tm/domain/{services/processors => workflows}/handlers/erp/PaymentEntryHandler.java (92%) rename src/main/java/zw/qantra/tm/domain/{services/processors => workflows}/handlers/erp/PurchaseInvoiceHandler.java (93%) rename src/main/java/zw/qantra/tm/domain/{services/processors/handlers/erp/SalesInvoiceHandler.java => workflows/handlers/erp/PurchasePaymentEntryHandler.java} (55%) diff --git a/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java b/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java index cbe3f2a..871c8e7 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java @@ -16,6 +16,8 @@ import org.joda.time.Instant; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import zw.qantra.tm.domain.enums.AuthType; +import zw.qantra.tm.domain.enums.RequestType; import zw.qantra.tm.domain.services.TransactionService; import zw.qantra.tm.domain.models.Transaction; @@ -82,11 +84,12 @@ public class TransactonController { return ResponseEntity.ok(transactionService.findById(id)); } - @GetMapping("/poll/{workflowId}") - public ResponseEntity poll(@PathVariable String workflowId) throws ExecutionException, InterruptedException { + @GetMapping("/poll/{id}") + public ResponseEntity poll(@PathVariable UUID id) throws ExecutionException, InterruptedException { + Transaction transaction = transactionService.findById(id); // Create an update to move the workflow to the next state WorkflowInstance update = new WorkflowInstance.Builder() - .setId(Long.parseLong(workflowId)) + .setId(transaction.getWorkflowId()) .setState("pollStatus") // The state you want to transition to .setNextActivation(DateTime.now()) // Schedule immediate execution .setStateText("Resumed from manual state") @@ -101,22 +104,28 @@ public class TransactonController { workflowInstanceService.updateWorkflowInstance(update, action); CompletableFuture future = workflowUtils - .waitForState(Long.parseLong(workflowId), new String[]{"done", "failed"}, + .waitForState(transaction.getWorkflowId(), new String[]{"done", "failed"}, 15000, 50); Object response = future.get(); return ResponseEntity.ok(response); } - @PostMapping("/request") - public ResponseEntity request(@RequestBody Transaction transaction) throws ExecutionException, InterruptedException { - LogUtils.logPrettyJson(logger, "incoming transaction", transaction); + @GetMapping("/request/{id}/{authType}") + public ResponseEntity request(@PathVariable UUID id, @PathVariable AuthType authType) + throws ExecutionException, InterruptedException { + LogUtils.logPrettyJson(logger, "incoming transaction", id + " - " + authType); + Transaction transaction = transactionService.findById(id); + transaction.setType(RequestType.REQUEST); + transaction.setAuthType(authType); + transactionService.save(transaction); + // Create an update to move the workflow to the next state WorkflowInstance update = new WorkflowInstance.Builder() .setId(transaction.getWorkflowId()) .setState("salesInvoice") // The state you want to transition to .setNextActivation(DateTime.now()) // Schedule immediate execution - .setStateVariables(new HashMap<>(){{ put("body", Utils.toJson(transaction)); }}) + .setStateVariables(new HashMap<>(){{ put("request", Utils.toJson(transaction)); }}) .setStateText("Resumed from manual state") .build(); @@ -144,7 +153,7 @@ public class TransactonController { var instance = workflowInstanceFactory.newWorkflowInstanceBuilder() .setType(TransactionWorkflow.TYPE) .setExternalId(transaction.getUserId() + "-" + new Instant().getMillis()) - .putStateVariable("incoming", transaction) + .putStateVariable("confirmation", transaction) .setNextActivation(DateTime.now()) .build(); diff --git a/src/main/java/zw/qantra/tm/domain/services/TransactionService.java b/src/main/java/zw/qantra/tm/domain/services/TransactionService.java index 8021590..53b85d6 100644 --- a/src/main/java/zw/qantra/tm/domain/services/TransactionService.java +++ b/src/main/java/zw/qantra/tm/domain/services/TransactionService.java @@ -40,230 +40,8 @@ public class TransactionService { @Getter private final TransactionRepository transactionRepository; - private final PaymentProcessorFactory paymentProcessorFactory; - private final ProviderService providerService; - private final ChargeService chargeService; private final AdditionalDataService additionalDataService; - private final RecipientService recipientService; - private final TransactionEventService transactionEventService; - private final SalesInvoiceHandler salesInvoiceHandler; - private final JournalEntryHandler journalEntryHandler; - private final PaymentEntryHandler paymentEntryHandler; - private final PurchaseInvoiceHandler purchaseInvoiceHandler; - private final PurchasePaymentEntryHandler purchasePaymentEntryHandler; - - public Transaction integration(Transaction transaction) { - transaction.setType(RequestType.INTEGRATION); - - TransactionEvent transactionEvent = TransactionEvent.builder() - .transactionId(transaction.getId().toString()) - .event(transaction.getType().name()) - .status(Status.PROCESSING) - .build(); - transactionEventService.save(transactionEvent); - - LogicPipeline transactionLogic = - new LogicPipeline<>(new ValidationHandler()) - .addHandler(journalEntryHandler) // todo: do this in a separate thread - .addHandler(paymentEntryHandler); // todo: do this in a separate thread - transaction = transactionLogic.execute(transaction); - - Provider provider = providerService.getProviderRepository() - .findByClientId(transaction.getBillClientId()); - if(provider == null){ - throw new ApiException("Provider not found for billClientId: " + transaction.getBillClientId()); - } - String label = transaction.getType() + "_" + - provider.getIntegrationProcessorLabel(); - logger.info("label: {}", label); - transaction.setIntegrationProcessorLabel(provider.getIntegrationProcessorLabel()); // for reference purposes - - TransactionProcessorInterface transactionProcessorInterface = - (TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor(label); - - try { - transactionProcessorInterface.process(transaction); - transaction.setIntegrationStatus(transaction.getStatus()); - transactionRepository.save(transaction); - }catch (Exception e){ - e.getCause().printStackTrace(); - transaction.setStatus(Status.FAILED); - transaction.setResponseCode("01"); - transaction.setErrorMessage(e.getMessage()); - transactionRepository.save(transaction); - return transaction; - } - - // run post integration logic - LogicPipeline postTransactionLogic = - new LogicPipeline<>(purchaseInvoiceHandler) // todo: do this in a separate thread - .addHandler(purchasePaymentEntryHandler); // todo: do this in a separate thread - transaction = postTransactionLogic.execute(transaction); - - transactionEvent.setStatus(transaction.getStatus()); - transactionEvent.setMessage(transaction.getErrorMessage()); - transactionEventService.save(transactionEvent); - - return transaction; - } - - public Transaction poll(UUID uuid){ - if(!transactionRepository.existsById(uuid)){ - throw new ApiException("Transaction not found for ID: " + uuid); - } - - Transaction transaction = transactionRepository.findById(uuid).get(); - // if the transaction is already successful, return it - if(transaction.getIntegrationStatus() == Status.SUCCESS){ - return transaction; - } - - TransactionEvent transactionEvent = TransactionEvent.builder() - .transactionId(transaction.getId().toString()) - .event(RequestType.POLL.name()) - .status(Status.PROCESSING) - .build(); - transactionEventService.save(transactionEvent); - - // no persistence happens during polling unless the transaction status changes - String label = "REQUEST_" + transaction.getPaymentProcessorLabel() + "_" + transaction.getAuthType(); - logger.info("label: {}", label); - - TransactionProcessorInterface transactionProcessorInterface = - (TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor(label); - try { - // find out if the gateway tran was successful - transactionProcessorInterface.poll(transaction); - - transactionEvent.setStatus(transaction.getStatus()); - transactionEvent.setMessage(transaction.getErrorMessage()); - transactionEventService.save(transactionEvent); - - transaction.setPollingStatus(transaction.getStatus()); - transactionRepository.save(transaction); - - // if tran was successful push integration to the aggregator - if(transaction.getStatus() == Status.SUCCESS){ - transaction.setType(RequestType.INTEGRATION); - integration(transaction); - } - } catch (Exception e) { - e.printStackTrace(); - transaction.setStatus(Status.FAILED); - transaction.setResponseCode("01"); - transaction.setErrorMessage(e.getMessage()); - } - - return transaction; - } - - public Transaction request(Transaction incomingTransaction) { - // if payment request (not confirm), then fetch already existing transaction record - Transaction transaction = incomingTransaction; - if(incomingTransaction.getId() != null){ - transaction = getTransactionRepository().findById(incomingTransaction.getId()).orElseThrow(() -> - new ApiException("Transaction not found for ID: " + incomingTransaction.getId())); - transaction.setType(incomingTransaction.getType()); // type should be REQUEST here - transaction.setAuthType(incomingTransaction.getAuthType()); - } - - // run pre transaction logic - LogicPipeline preTransactionLogic = - new LogicPipeline<>(new CalculateChargesHandler(chargeService)) - .addHandler(new FormattingHandler()) - .addHandler(new ValidationHandler()); - transaction = preTransactionLogic.execute(transaction); - - transactionRepository.save(transaction); - - TransactionEvent transactionEvent = TransactionEvent.builder() - .transactionId(transaction.getId().toString()) - .event(transaction.getType().name()) - .status(Status.PROCESSING) - .build(); - transactionEventService.save(transactionEvent); - - String label = transaction.getType() + "_" + transaction.getPaymentProcessorLabel() + "_" + transaction.getAuthType(); - logger.info("label: {}", label); - - TransactionProcessorInterface transactionProcessorInterface = - (TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor(label); - - try { - // MAIN TRANSACTION LOGIC! - transactionProcessorInterface.process(transaction); - transaction.setPaymentStatus(transaction.getStatus()); - - }catch (Exception e){ - e.printStackTrace(); - transaction.setStatus(Status.FAILED); - transaction.setResponseCode("01"); - transaction.setErrorMessage(e.getMessage()); - transactionRepository.save(transaction); - return transaction; - } - - // run post transaction logic - LogicPipeline postTransactionLogic = - new LogicPipeline<>(new RecipientsUpdateHandler(recipientService)) - .addHandler(salesInvoiceHandler);// todo: do this in a separate thread - transaction = postTransactionLogic.execute(transaction); - - transactionEvent.setStatus(transaction.getStatus()); - transactionEvent.setMessage(transaction.getErrorMessage()); - transactionEventService.save(transactionEvent); - - return transaction; - } - - public Transaction confirm(Transaction transaction) { - // run pre transaction logic - LogicPipeline preTransactionLogic = - new LogicPipeline<>(new CalculateChargesHandler(chargeService)) - .addHandler(new ConfirmationLabelHandler(providerService)) - .addHandler(new ConfirmationLogicHandler()) - .addHandler(new FormattingHandler()) - .addHandler(new ValidationHandler()); - transaction = preTransactionLogic.execute(transaction); - transactionRepository.save(transaction); - - TransactionEvent transactionEvent = TransactionEvent.builder() - .transactionId(transaction.getId().toString()) - .event(transaction.getType().name()) - .status(Status.PROCESSING) - .build(); - transactionEventService.save(transactionEvent); - - TransactionProcessorInterface transactionProcessorInterface = - (TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor( - transaction.getConfirmationProcessorLabel()); - - try { - // MAIN TRANSACTION LOGIC! - transactionProcessorInterface.process(transaction); - transaction.setConfirmationStatus(transaction.getStatus()); - - }catch (Exception e){ - e.printStackTrace(); - transaction.setStatus(Status.FAILED); - transaction.setResponseCode("01"); - transaction.setErrorMessage(e.getMessage()); - transactionRepository.save(transaction); - return transaction; - } - - // run post transaction logic - LogicPipeline postTransactionLogic = - new LogicPipeline<>(new RecipientsUpdateHandler(recipientService)); - transaction = postTransactionLogic.execute(transaction); - - transactionEvent.setStatus(transaction.getStatus()); - transactionEvent.setMessage(transaction.getErrorMessage()); - transactionEventService.save(transactionEvent); - - return transaction; - } public Transaction findById(UUID id) { Transaction transaction = transactionRepository.findById(id).orElseThrow(() -> new ApiException("Transaction not found for ID: " + id)); diff --git a/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java b/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java index 8db7953..2c2e332 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java @@ -6,6 +6,7 @@ import io.nflow.engine.workflow.definition.NextAction; import io.nflow.engine.workflow.definition.StateExecution; import io.nflow.engine.workflow.definition.WorkflowDefinition; import io.nflow.engine.workflow.definition.WorkflowStateType; +import org.joda.time.DateTime; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -16,6 +17,8 @@ import zw.qantra.tm.domain.workflows.handlers.HandlerInterface; import zw.qantra.tm.domain.workflows.handlers.WorkflowHandlerFactory; import zw.qantra.tm.utils.Utils; +import java.util.LinkedHashMap; + @Component public class TransactionWorkflow extends WorkflowDefinition { Logger log = org.slf4j.LoggerFactory.getLogger(RegistrationWorkflow.class); @@ -24,6 +27,8 @@ public class TransactionWorkflow extends WorkflowDefinition { private WorkflowHandlerFactory workflowHandlerFactory; @Autowired private TransactionService transactionService; + @Autowired + private ObjectMapper objectMapper; public static final String TYPE = "transactionWorkflow"; @@ -34,10 +39,11 @@ public class TransactionWorkflow extends WorkflowDefinition { public static final State SALES_INVOICE = new State("salesInvoice"); public static final State GATEWAY_PAYMENT = new State("gatewayPayment"); public static final State GATEWAY_PAYMENT_SUCCESS = new State("gatewayPaymentSuccess", WorkflowStateType.manual); + public static final State JOURNAL_ENTRY = new State("journalEntry"); public static final State POLL_STATUS = new State("pollStatus"); public static final State SALES_PAYMENT = new State("salesPayment"); public static final State PURCHASE_INVOICE = new State("purchaseInvoice"); - public static final State BILL_PAYMENT = new State("billPayment"); + public static final State INTEGRATION = new State("integration"); public static final State PURCHASE_INVOICE_PAYMENT = new State("purchaseInvoicePayment"); public static final State FAILED = new State("failed", WorkflowStateType.manual); @@ -53,21 +59,23 @@ public class TransactionWorkflow extends WorkflowDefinition { permit(SALES_INVOICE, GATEWAY_PAYMENT); permit(GATEWAY_PAYMENT, GATEWAY_PAYMENT_SUCCESS, FAILED); permit(GATEWAY_PAYMENT_SUCCESS, POLL_STATUS); - permit(POLL_STATUS, SALES_PAYMENT, FAILED); + permit(POLL_STATUS, JOURNAL_ENTRY, FAILED); + permit(JOURNAL_ENTRY, SALES_PAYMENT); permit(SALES_PAYMENT, PURCHASE_INVOICE); - permit(PURCHASE_INVOICE, BILL_PAYMENT); - permit(BILL_PAYMENT, PURCHASE_INVOICE_PAYMENT, FAILED); + permit(PURCHASE_INVOICE, INTEGRATION); + permit(INTEGRATION, PURCHASE_INVOICE_PAYMENT, FAILED); permit(PURCHASE_INVOICE_PAYMENT, DONE); } public NextAction calculateCharges(StateExecution execution) { try { - Transaction transaction = execution.getVariable("incoming", Transaction.class); + Transaction transaction = execution.getVariable("confirmation", Transaction.class); process(transaction, "calculateCharges"); execution.setVariable("body", transaction); return NextAction.moveToState(CLEANUP, "Charge calculation complete"); } catch (Exception e) { + e.printStackTrace(); return NextAction.moveToState(FAILED, e.getMessage()); } } @@ -80,6 +88,7 @@ public class TransactionWorkflow extends WorkflowDefinition { return NextAction.moveToState(CONFIRM, "Cleanup complete"); } catch (Exception e) { + e.printStackTrace(); return NextAction.moveToState(FAILED, e.getMessage()); } } @@ -89,73 +98,149 @@ public class TransactionWorkflow extends WorkflowDefinition { Transaction transaction = execution.getVariable("body", Transaction.class); transaction.setWorkflowId(execution.getWorkflowInstanceId()); - process(transaction, "confirm"); + transaction = process(transaction, "confirm"); execution.setVariable("body", Utils.toJson(transaction)); - return NextAction.moveToState(CONFIRM_SUCCESS, "Cleanup complete"); - } catch (Exception e) { - return NextAction.moveToState(FAILED, e.getMessage()); - } - } - - public void confirmSuccess(StateExecution execution) { - log.info("Confirm successful"); - } - - public NextAction salesInvoice(StateExecution execution) { - try { - Transaction transaction = execution.getVariable("body", Transaction.class); - - process(transaction, "salesInvoice"); - execution.setVariable("body", Utils.toJson(transaction)); - return NextAction.moveToState(GATEWAY_PAYMENT, "Sales invoice generated"); - }catch (Exception e) { - return NextAction.moveToState(FAILED, e.getMessage()); - } - } - - public NextAction gatewayPayment(StateExecution execution) { - try { - Transaction transaction = execution.getVariable("body", Transaction.class); - - transaction = process(transaction, "gatewayPayment"); - execution.setVariable("body", Utils.toJson(transaction)); - return NextAction.moveToState(GATEWAY_PAYMENT_SUCCESS, "Sales invoice generated"); - } catch (Exception e) { - return NextAction.moveToState(FAILED, e.getMessage()); - } - } - - public void gatewayPaymentSuccess(StateExecution execution) { - log.info("Gateway payment successful"); - } - - public NextAction pollStatus(StateExecution execution) { - try { - Transaction transaction = execution.getVariable("body", Transaction.class); - - transaction = process(transaction, "pollStatus"); - execution.setVariable("body", Utils.toJson(transaction)); - return NextAction.moveToState(SALES_PAYMENT, "Polling complete"); + return NextAction.moveToState(CONFIRM_SUCCESS, "Transaction confirmed successfully"); } catch (Exception e) { e.printStackTrace(); return NextAction.moveToState(FAILED, e.getMessage()); } } + public void confirmSuccess(StateExecution execution) { + log.info("Confirm complete"); + } + + public NextAction salesInvoice(StateExecution execution) { + try { + // cannot cast direct to Transaction object because of some jackson mapper hullabaloo :( + LinkedHashMap map = execution.getVariable("request", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + transaction = process(transaction, "salesInvoice"); + execution.setVariable("body", Utils.toJson(transaction)); + return NextAction.moveToState(GATEWAY_PAYMENT, + "Sales invoice generated - id:" + transaction.getErpSalesRef()); + }catch (Exception e) { + e.printStackTrace(); + return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage()); + } + } + + public NextAction gatewayPayment(StateExecution execution) { + try { + // cannot cast direct to Transaction object because of some jackson mapper hullabaloo :( + LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + transaction = process(transaction, "gatewayPayment"); + execution.setVariable("body", Utils.toJson(transaction)); + return NextAction.moveToState(GATEWAY_PAYMENT_SUCCESS, "Gateway payment initiated"); + } catch (Exception e) { + e.printStackTrace(); + return NextAction.moveToState(FAILED, e.getMessage()); + } + } + + public void gatewayPaymentSuccess(StateExecution execution) { + log.info("Gateway payment initiation complete"); + } + + public NextAction pollStatus(StateExecution execution) { + try { + // cannot cast direct to Transaction object because of some jackson mapper hullabaloo :( + LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + transaction = process(transaction, "pollStatus"); + execution.setVariable("body", Utils.toJson(transaction)); + return NextAction.moveToState(JOURNAL_ENTRY, "Polling complete"); + } catch (Exception e) { + e.printStackTrace(); + return NextAction.moveToState(FAILED, e.getMessage()); + } + } + + public NextAction journalEntry(StateExecution execution) { + try { + LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + transaction = process(transaction, "journalEntry"); + execution.setVariable("body", Utils.toJson(transaction)); + return NextAction.moveToState(SALES_PAYMENT, + "Journal Entry successful - id: " + transaction.getErpJournalRef()); + }catch (Exception e) { + e.printStackTrace(); + return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage()); + } + } + public NextAction salesPayment(StateExecution execution) { - return NextAction.moveToState(PURCHASE_INVOICE, "Sales invoice generated"); + try { + LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + transaction = process(transaction, "salesPayment"); + execution.setVariable("body", Utils.toJson(transaction)); + return NextAction.moveToState(PURCHASE_INVOICE, + "Sales payment successful - id: " + transaction.getErpSalesPaymentRef()); + }catch (Exception e) { + e.printStackTrace(); + return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage()); + } } public NextAction purchaseInvoice(StateExecution execution) { - return NextAction.moveToState(BILL_PAYMENT, "Purchase invoice generated"); + try { + LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + transaction = process(transaction, "purchaseInvoice"); + execution.setVariable("body", Utils.toJson(transaction)); + return NextAction.moveToState(INTEGRATION, + "Purchase invoice generated - id: " + transaction.getErpPurchaseRef()); + }catch (Exception e) { + e.printStackTrace(); + return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage()); + } } - public NextAction billPayment(StateExecution execution) { - return NextAction.moveToState(PURCHASE_INVOICE_PAYMENT, "Bill payment successful"); + public NextAction integration(StateExecution execution) { + try { + LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + transaction = process(transaction, "integration"); + execution.setVariable("body", Utils.toJson(transaction)); + return NextAction.moveToState(PURCHASE_INVOICE_PAYMENT, "Integration request successful"); + }catch (Exception e) { + e.printStackTrace(); + return NextAction.moveToState(FAILED, e.getMessage()); + } } public NextAction purchaseInvoicePayment(StateExecution execution) { - return NextAction.moveToState(DONE, "Purchase invoice payment successful"); + try { + LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + transaction = process(transaction, "purchaseInvoicePayment"); + execution.setVariable("body", Utils.toJson(transaction)); + return NextAction.moveToState(DONE, + "Purchase invoice payment generated - id: " + transaction.getErpPurchaseRef()); + }catch (Exception e) { + e.printStackTrace(); + return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage()); + } } public Transaction process(Transaction transaction, String beanName) { diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java index 2b4f5a4..0955e62 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java +++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java @@ -44,6 +44,7 @@ public class CleanupHandler implements HandlerInterface { String label = transaction.getType() + "_" + provider.getLabel(); transaction.setConfirmationProcessorLabel(label); + transaction.setProviderLabel(provider.getLabel()); logger.info("label: {}", label); // FORMAT FIELDS CORRECTLY diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/IntegrationHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/IntegrationHandler.java new file mode 100644 index 0000000..afa84b4 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/IntegrationHandler.java @@ -0,0 +1,71 @@ +package zw.qantra.tm.domain.workflows.handlers; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.enums.RequestType; +import zw.qantra.tm.domain.enums.Status; +import zw.qantra.tm.domain.models.Provider; +import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.models.TransactionEvent; +import zw.qantra.tm.domain.services.*; +import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory; +import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; +import zw.qantra.tm.domain.services.processors.handlers.LogicPipeline; +import zw.qantra.tm.domain.services.processors.handlers.ValidationHandler; +import zw.qantra.tm.exceptions.ApiException; + +@Service("integration") +@RequiredArgsConstructor +public class IntegrationHandler implements HandlerInterface { + Logger logger = LoggerFactory.getLogger(TransactionService.class); + + private final PaymentProcessorFactory paymentProcessorFactory; + private final ProviderService providerService; + private final RecipientService recipientService; + private final TransactionEventService transactionEventService; + private final TransactionService transactionService; + + @Override + public Object process(Transaction transaction) throws Exception { + transaction.setType(RequestType.INTEGRATION); + + TransactionEvent transactionEvent = TransactionEvent.builder() + .transactionId(transaction.getId().toString()) + .event(transaction.getType().name()) + .status(Status.PROCESSING) + .build(); + transactionEventService.save(transactionEvent); + + Provider provider = providerService.getProviderRepository() + .findByClientId(transaction.getBillClientId()); + if(provider == null){ + throw new ApiException("Provider not found for billClientId: " + transaction.getBillClientId()); + } + String label = transaction.getType() + "_" + + provider.getIntegrationProcessorLabel(); + logger.info("label: {}", label); + transaction.setIntegrationProcessorLabel(provider.getIntegrationProcessorLabel()); // for reference purposes + + TransactionProcessorInterface transactionProcessorInterface = + (TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor(label); + + try { + transactionProcessorInterface.process(transaction); + transaction.setIntegrationStatus(transaction.getStatus()); + }catch (Exception e){ + e.getCause().printStackTrace(); + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("01"); + transaction.setErrorMessage(e.getMessage()); + } + + transactionEvent.setStatus(transaction.getStatus()); + transactionEvent.setMessage(transaction.getErrorMessage()); + transactionEventService.save(transactionEvent); + + transactionService.save(transaction); + return transaction; + } +} diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/GatewayPaymentHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/PaymentHandler.java similarity index 90% rename from src/main/java/zw/qantra/tm/domain/workflows/handlers/GatewayPaymentHandler.java rename to src/main/java/zw/qantra/tm/domain/workflows/handlers/PaymentHandler.java index 07b7e53..b423faa 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/GatewayPaymentHandler.java +++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/PaymentHandler.java @@ -10,22 +10,17 @@ import zw.qantra.tm.domain.models.TransactionEvent; import zw.qantra.tm.domain.services.*; import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory; import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; -import zw.qantra.tm.domain.services.processors.handlers.*; -import zw.qantra.tm.domain.services.processors.handlers.CalculateChargesHandler; -import zw.qantra.tm.domain.services.processors.handlers.erp.*; import zw.qantra.tm.exceptions.ApiException; @Service("gatewayPayment") @RequiredArgsConstructor -public class GatewayPaymentHandler implements HandlerInterface { +public class PaymentHandler implements HandlerInterface { Logger logger = LoggerFactory.getLogger(TransactionService.class); private final PaymentProcessorFactory paymentProcessorFactory; private final ChargeService chargeService; private final RecipientService recipientService; private final TransactionEventService transactionEventService; - - private final SalesInvoiceHandler salesInvoiceHandler; private final TransactionService transactionService; @Override diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/PollStatusHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/PollStatusHandler.java index 8841fe9..8c74349 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/PollStatusHandler.java +++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/PollStatusHandler.java @@ -54,11 +54,13 @@ public class PollStatusHandler implements HandlerInterface { transactionEventService.save(transactionEvent); transaction.setPollingStatus(transaction.getStatus()); - transactionService.save(transaction); // if transaction wasn't successful, throw and halt workflow if(transaction.getStatus() != Status.SUCCESS){ throw new ApiException("Transaction failed: " + transaction.getErrorMessage() ); + }else { + transaction.setPaymentStatus(transaction.getStatus()); + transaction.setErrorMessage(null); } } catch (Exception e) { e.printStackTrace(); @@ -67,6 +69,7 @@ public class PollStatusHandler implements HandlerInterface { transaction.setErrorMessage(e.getMessage()); } + transactionService.save(transaction); return transaction; } } diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/JournalEntryHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/JournalEntryHandler.java similarity index 91% rename from src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/JournalEntryHandler.java rename to src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/JournalEntryHandler.java index aaf42a9..76d0ffc 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/JournalEntryHandler.java +++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/JournalEntryHandler.java @@ -1,17 +1,14 @@ -package zw.qantra.tm.domain.services.processors.handlers.erp; +package zw.qantra.tm.domain.workflows.handlers.erp; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import zw.qantra.tm.domain.dtos.erp.JournalEntryAccountDto; import zw.qantra.tm.domain.dtos.erp.JournalEntryDto; -import zw.qantra.tm.domain.dtos.erp.SalesInvoiceDto; -import zw.qantra.tm.domain.dtos.erp.SalesInvoiceItemDto; import zw.qantra.tm.domain.enums.RequestType; import zw.qantra.tm.domain.enums.Status; import zw.qantra.tm.domain.models.Transaction; @@ -19,18 +16,18 @@ import zw.qantra.tm.domain.models.TransactionEvent; import zw.qantra.tm.domain.models.User; import zw.qantra.tm.domain.repositories.TransactionRepository; import zw.qantra.tm.domain.services.TransactionEventService; -import zw.qantra.tm.domain.services.TransactionService; import zw.qantra.tm.domain.services.UserService; import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; +import zw.qantra.tm.domain.workflows.handlers.HandlerInterface; import zw.qantra.tm.exceptions.ApiException; import zw.qantra.tm.rest.RestService; import java.nio.charset.StandardCharsets; import java.util.*; -@Service +@Service("journalEntry") @RequiredArgsConstructor -public class JournalEntryHandler implements LogicHandler{ +public class JournalEntryHandler implements HandlerInterface { Logger logger = LoggerFactory.getLogger(JournalEntryHandler.class); private final RestService restService; @@ -100,10 +97,6 @@ public class JournalEntryHandler implements LogicHandler{ +public class PaymentEntryHandler implements HandlerInterface { Logger logger = LoggerFactory.getLogger(PaymentEntryHandler.class); private final RestService restService; @@ -95,9 +94,9 @@ public class PaymentEntryHandler implements LogicHandler{ +public class PurchaseInvoiceHandler implements HandlerInterface { Logger logger = LoggerFactory.getLogger(PurchaseInvoiceHandler.class); private final RestService restService; @@ -98,10 +99,6 @@ public class PurchaseInvoiceHandler implements LogicHandler{ - Logger logger = LoggerFactory.getLogger(SalesInvoiceHandler.class); +public class PurchasePaymentEntryHandler implements HandlerInterface { + Logger logger = LoggerFactory.getLogger(PurchasePaymentEntryHandler.class); private final RestService restService; private final UserService userService; private final TransactionEventService transactionEventService; private final TransactionRepository transactionRepository; + private final ProviderService providerService; @Value("${erpnext.url}") private String erpnextUrl; @@ -50,20 +52,26 @@ public class SalesInvoiceHandler implements LogicHandler loginRequest = Map.of( + "usr", erpnextUsername, + "pwd", erpnextPassword); + + String sid = ""; + try { - logger.info("Creating sales invoice in ERPNext for successful transaction"); + ResponseEntity response = restService.postAsEntity( + erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); - // login to erp - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", "application/json"); - headers.add("Accept", "application/json"); - - Map loginRequest = Map.of( - "usr", erpnextUsername, - "pwd", erpnextPassword); - - String sid = ""; - - try { - ResponseEntity response = restService.postAsEntity( - erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); - - sid = response.getHeaders().getFirst("Set-Cookie"); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to login to ERPNext"); - } - - // create customer in erp - String basicAuth = "Basic " + Base64.getEncoder().encodeToString( - (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); - headers.add("Authorization", basicAuth); - headers.add("Cookie", sid); - - User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null); - if(user == null) - throw new ApiException("Could not find user"); - - SalesInvoiceDto salesInvoiceDto = SalesInvoiceDto.builder() - .doctype("Sales Invoice") - .customer(user.getErpCustomerRef()) - .postingDate(transaction.getCreatedAt().toLocalDate().toString()) - .dueDate(transaction.getCreatedAt().toLocalDate().toString()) - .company(erpnextCompany) - .docstatus(1) - .items(Arrays.asList(SalesInvoiceItemDto.builder() - .itemCode(transaction.getProviderLabel()) - .qty(1) - .rate(transaction.getAmount().add(transaction.getGatewayCharge())) - .amount(transaction.getAmount().add(transaction.getGatewayCharge())) - .build())) - .build(); - - try { - LinkedHashMap response = restService.postAs( - erpnextUrl + "/v2/document/Sales Invoice", salesInvoiceDto, headers, LinkedHashMap.class); - - LinkedHashMap data = (LinkedHashMap) response.get("data"); - String transactionId = (String) data.get("name"); - - transaction.setErpSalesRef(transactionId); - transactionEvent.setStatus(Status.SUCCESS); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to create sales invoice in ERPNext"); - } - - logger.info("Sales invoice created successfully in ERPNext"); + sid = response.getHeaders().getFirst("Set-Cookie"); } catch (Exception e) { - logger.error("Failed to create sales invoice in ERPNext: {}", e.getMessage(), e); - // Don't fail the transaction, just log the error - // The ERP invoice creation is a side effect, not critical to the payment flow - transactionEvent.setStatus(Status.FAILED); - transactionEvent.setMessage(e.getMessage()); + e.printStackTrace(); + throw new ApiException("Failed to login to ERPNext"); } - } else { - logger.info("Transaction status is not SUCCESS ({}), skipping sales invoice creation", transaction.getStatus()); + + // create customer in erp + String basicAuth = "Basic " + Base64.getEncoder().encodeToString( + (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); + headers.add("Authorization", basicAuth); + headers.add("Cookie", sid); + + String erpCustomerRef = ""; + User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null); + erpCustomerRef = (user == null) ? "Anonymous" : user.getErpCustomerRef(); + + SalesInvoiceDto salesInvoiceDto = SalesInvoiceDto.builder() + .doctype("Sales Invoice") + .customer(erpCustomerRef) + .postingDate(new LocalDateTime().toLocalDate().toString()) + .dueDate(new LocalDateTime().toLocalDate().toString()) + .company(erpnextCompany) + .docstatus(1) + .items(Arrays.asList(SalesInvoiceItemDto.builder() + .itemCode(transaction.getProviderLabel()) + .qty(1) + .rate(transaction.getAmount().add(transaction.getGatewayCharge())) + .amount(transaction.getAmount().add(transaction.getGatewayCharge())) + .build())) + .build(); + + try { + LinkedHashMap response = restService.postAs( + erpnextUrl + "/v2/document/Sales Invoice", salesInvoiceDto, headers, LinkedHashMap.class); + + LinkedHashMap data = (LinkedHashMap) response.get("data"); + String transactionId = (String) data.get("name"); + + transaction.setErpSalesRef(transactionId); + transactionEvent.setStatus(Status.SUCCESS); + } catch (Exception e) { + e.printStackTrace(); + throw new ApiException("Failed to create sales invoice in ERPNext"); + } + + logger.info("Sales invoice created successfully in ERPNext"); + } catch (Exception e) { + logger.error("Failed to create sales invoice in ERPNext: {}", e.getMessage(), e); + // Don't fail the transaction, just log the error + // The ERP invoice creation is a side effect, not critical to the payment flow + transactionEvent.setStatus(Status.FAILED); + transactionEvent.setMessage(e.getMessage()); } transactionEventService.save(transactionEvent); - return transactionRepository.save(transaction); + return transactionService.save(transaction); } }