diff --git a/CHANGELOG.md b/CHANGELOG.md index 361e809..a6bba7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,20 +4,12 @@ All notable changes to this project will be documented in this file. ## [0.0.1-RELEASE] - 2026-03-26 -### Added - New database configuration classes for enhanced data source management - Request logging filter configuration for improved request tracing - Nflow database configuration for workflow persistence - -### Changed - Improved workflow definitions in `PollCronWorkflow` and `TransactionWorkflow` - Enhanced ecocash remote payment processor with additional features - Optimized application properties for both lab and production environments - Refactored transaction workflow with 128 lines of improvements and optimizations (190 insertions, 77 deletions) -### Details -- **Author**: Vusumuzi Khoza -- **Commit**: c6024b812734158283918eba40fb51efe1f8452a -- **Files Changed**: 8 files modified - diff --git a/src/main/java/zw/qantra/tm/domain/workflows/PurchaseInvoiceWorkflow.java b/src/main/java/zw/qantra/tm/domain/workflows/PurchaseInvoiceWorkflow.java new file mode 100644 index 0000000..ce7066b --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/workflows/PurchaseInvoiceWorkflow.java @@ -0,0 +1,82 @@ +package zw.qantra.tm.domain.workflows; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.nflow.engine.workflow.curated.State; +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 lombok.extern.slf4j.Slf4j; +import org.joda.time.DateTime; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import zw.qantra.tm.domain.models.Transaction; +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 +@Slf4j +public class PurchaseInvoiceWorkflow extends WorkflowDefinition { + @Autowired + private WorkflowHandlerFactory workflowHandlerFactory; + @Autowired + private ObjectMapper objectMapper; + + public static final String TYPE = "purchaseInvoiceWorkflow"; + + public static final State PURCHASE_INVOICE = new State("purchaseInvoice", WorkflowStateType.start); + public static final State PURCHASE_INVOICE_PAYMENT = new State("purchaseInvoicePayment"); + + public static final State TRAN_FAILED = new State("failed", WorkflowStateType.manual); + public static final State DONE = new State("done", WorkflowStateType.end); + + public PurchaseInvoiceWorkflow() { + super(TYPE, PURCHASE_INVOICE, TRAN_FAILED); + + permit(PURCHASE_INVOICE, PURCHASE_INVOICE_PAYMENT); + permit(PURCHASE_INVOICE_PAYMENT, DONE); + } + + public NextAction purchaseInvoice(StateExecution execution) throws Exception { + try { + LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + HandlerInterface handlerInterface = + (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("purchaseInvoice"); + transaction = (Transaction) handlerInterface.process(transaction); + + execution.setVariable("body", Utils.toJson(transaction)); + return NextAction.moveToState(PURCHASE_INVOICE_PAYMENT, + "Purchase invoice successful - id: " + transaction.getErpPurchaseRef()); + + }catch (Exception e) { + e.printStackTrace(); + return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage()); + } + } + + public NextAction purchaseInvoicePayment(StateExecution execution) { + try { + LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + HandlerInterface handlerInterface = + (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("purchaseInvoicePayment"); + transaction = (Transaction) handlerInterface.process(transaction); + + execution.setVariable("body", Utils.toJson(transaction)); + + return NextAction.moveToState(DONE, + "Purchase invoice payment generated - id: " + transaction.getErpPurchasePaymentRef()); + }catch (Exception e) { + e.printStackTrace(); + return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage()); + } + } +} diff --git a/src/main/java/zw/qantra/tm/domain/workflows/SalesInvoiceWorkflow.java b/src/main/java/zw/qantra/tm/domain/workflows/SalesInvoiceWorkflow.java new file mode 100644 index 0000000..3ff494e --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/workflows/SalesInvoiceWorkflow.java @@ -0,0 +1,107 @@ +package zw.qantra.tm.domain.workflows; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.nflow.engine.workflow.curated.State; +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 lombok.extern.slf4j.Slf4j; +import org.joda.time.DateTime; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.services.TransactionService; +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 +@Slf4j +public class SalesInvoiceWorkflow extends WorkflowDefinition { + @Autowired + private WorkflowHandlerFactory workflowHandlerFactory; + @Autowired + private TransactionService transactionService; + @Autowired + private ObjectMapper objectMapper; + + public static final String TYPE = "salesInvoiceWorkflow"; + + public static final State SALES_INVOICE = new State("salesInvoice", WorkflowStateType.start); + public static final State JOURNAL_ENTRY = new State("journalEntry"); + public static final State SALES_PAYMENT = new State("salesPayment"); + + public static final State TRAN_FAILED = new State("failed", WorkflowStateType.manual); + public static final State DONE = new State("done", WorkflowStateType.end); + + SalesInvoiceWorkflow() { + super(TYPE, SALES_INVOICE, TRAN_FAILED); + + permit(SALES_INVOICE, JOURNAL_ENTRY); + permit(JOURNAL_ENTRY, SALES_PAYMENT); + permit(SALES_PAYMENT, DONE); + } + + public NextAction salesInvoice(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); + + HandlerInterface handlerInterface = + (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("salesInvoice"); + transaction = (Transaction) handlerInterface.process(transaction); + + execution.setVariable("body", Utils.toJson(transaction)); + return NextAction.moveToState(JOURNAL_ENTRY, + "Sales invoice generated - id:" + transaction.getErpSalesRef()); + }catch (Exception e) { + e.printStackTrace(); + return NextAction.retryAfter(new DateTime().plusMinutes(1), 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); + + HandlerInterface handlerInterface = + (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("journalEntry"); + transaction = (Transaction) handlerInterface.process(transaction); + + 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) { + try { + LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + HandlerInterface handlerInterface = + (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("salesPayment"); + transaction = (Transaction) handlerInterface.process(transaction); + + execution.setVariable("body", Utils.toJson(transaction)); + + return NextAction.moveToState(DONE, + "Sales payment successful - id: " + transaction.getErpSalesPaymentRef()); + }catch (Exception e) { + e.printStackTrace(); + return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage()); + } + } + +} 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 f04262b..4bfee5b 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java @@ -2,12 +2,15 @@ package zw.qantra.tm.domain.workflows; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.nflow.engine.service.WorkflowInstanceService; import io.nflow.engine.workflow.curated.State; 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 io.nflow.engine.workflow.instance.WorkflowInstanceFactory; import org.joda.time.DateTime; +import org.joda.time.Instant; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -32,6 +35,10 @@ public class TransactionWorkflow extends WorkflowDefinition { private TransactionService transactionService; @Autowired private ObjectMapper objectMapper; + @Autowired + private WorkflowInstanceService workflowInstanceService; + @Autowired + private WorkflowInstanceFactory workflowInstanceFactory; public static final String TYPE = "transactionWorkflow"; @@ -43,14 +50,9 @@ public class TransactionWorkflow extends WorkflowDefinition { 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 POLL_STATUS = new State("pollStatus"); - public static final State SALES_INVOICE = new State("salesInvoice"); - public static final State JOURNAL_ENTRY = new State("journalEntry"); - public static final State SALES_PAYMENT = new State("salesPayment"); - public static final State SALES_PAYMENT_SUCCESS = new State("salesPaymentSuccess", WorkflowStateType.manual); - public static final State PURCHASE_INVOICE = new State("purchaseInvoice"); + public static final State POLL_STATUS_SUCCESS = new State("pollStatusSuccess", WorkflowStateType.manual); public static final State INTEGRATION = new State("integration"); - public static final State PURCHASE_INVOICE_PAYMENT = new State("purchaseInvoicePayment"); - public static final State PURCHASE_INVOICE_PAYMENT_SUCCESS = new State("purchaseInvoicePaymentSuccess", WorkflowStateType.manual); + public static final State INTEGRATION_STATUS_SUCCESS = new State("integrationStatusSuccess"); public static final State TRAN_FAILED = new State("failed", WorkflowStateType.manual); public static final State DONE = new State("done", WorkflowStateType.end); @@ -65,14 +67,10 @@ public class TransactionWorkflow extends WorkflowDefinition { permit(CONFIRM_SUCCESS, GATEWAY_PAYMENT); permit(GATEWAY_PAYMENT, GATEWAY_PAYMENT_SUCCESS, TRAN_FAILED); permit(GATEWAY_PAYMENT_SUCCESS, POLL_STATUS); - permit(POLL_STATUS, SALES_INVOICE, TRAN_FAILED); - permit(SALES_INVOICE, JOURNAL_ENTRY); - permit(JOURNAL_ENTRY, SALES_PAYMENT); - permit(SALES_PAYMENT, SALES_PAYMENT_SUCCESS); - permit(SALES_PAYMENT_SUCCESS, INTEGRATION); - permit(INTEGRATION, PURCHASE_INVOICE, TRAN_FAILED); - permit(PURCHASE_INVOICE, PURCHASE_INVOICE_PAYMENT); - permit(PURCHASE_INVOICE_PAYMENT, PURCHASE_INVOICE_PAYMENT_SUCCESS, DONE); + permit(POLL_STATUS, POLL_STATUS_SUCCESS, TRAN_FAILED); + permit(POLL_STATUS_SUCCESS, INTEGRATION, TRAN_FAILED); + permit(INTEGRATION, INTEGRATION_STATUS_SUCCESS, TRAN_FAILED); + permit(INTEGRATION_STATUS_SUCCESS, DONE); } public NextAction calculateCharges(StateExecution execution) { @@ -167,7 +165,25 @@ public class TransactionWorkflow extends WorkflowDefinition { } public void gatewayPaymentSuccess(StateExecution execution) { - log.info("Gateway payment initiation complete"); + try { + LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); + byte[] json = objectMapper.writeValueAsBytes(map); + Transaction transaction = objectMapper.readValue(json, Transaction.class); + + var instance = workflowInstanceFactory.newWorkflowInstanceBuilder() + .setType(SalesInvoiceWorkflow.TYPE) + .setExternalId(transaction.getUserId() + "-" + new Instant().getMillis()) + .setBusinessKey(execution.getBusinessKey()) + .putStateVariable("body", Utils.toJson(transaction)) + .setNextActivation(DateTime.now()) + .build(); + + long workflowId = workflowInstanceService.insertWorkflowInstance(instance); + + log.info("Gateway payment initiation complete"); + }catch (Exception e) { + e.printStackTrace(); + } } public NextAction pollStatus(StateExecution execution) throws IOException { @@ -182,7 +198,7 @@ public class TransactionWorkflow extends WorkflowDefinition { transaction = (Transaction) handlerInterface.process(transaction); execution.setVariable("body", Utils.toJson(transaction)); - return NextAction.moveToState(SALES_INVOICE, "Polling complete"); + return NextAction.moveToState(POLL_STATUS_SUCCESS, "Polling complete"); } catch (Exception e) { log.info(e.getMessage()); execution.setVariable("body", Utils.toJson(transaction)); @@ -190,74 +206,8 @@ public class TransactionWorkflow extends WorkflowDefinition { } } - public NextAction salesInvoice(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); - - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("salesInvoice"); - transaction = (Transaction) handlerInterface.process(transaction); - - execution.setVariable("body", Utils.toJson(transaction)); - return NextAction.moveToState(JOURNAL_ENTRY, - "Sales invoice generated - id:" + transaction.getErpSalesRef()); - }catch (Exception e) { - e.printStackTrace(); - return NextAction.retryAfter(new DateTime().plusMinutes(1), 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); - - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("journalEntry"); - transaction = (Transaction) handlerInterface.process(transaction); - - 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) { - try { - LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); - byte[] json = objectMapper.writeValueAsBytes(map); - Transaction transaction = objectMapper.readValue(json, Transaction.class); - - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("salesPayment"); - transaction = (Transaction) handlerInterface.process(transaction); - - execution.setVariable("body", Utils.toJson(transaction)); - - // it's possible to have a flow that ends up here though - // integration has already happened - if(transaction.getIntegrationStatus() == Status.SUCCESS){ - return NextAction.moveToState(DONE, - "Integration already complete"); - } - - return NextAction.moveToState(SALES_PAYMENT_SUCCESS, - "Sales payment successful - id: " + transaction.getErpSalesPaymentRef()); - }catch (Exception e) { - e.printStackTrace(); - return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage()); - } - } - - public void salesPaymentSuccess(StateExecution execution) { - log.info("Sales payment completed successfully"); + public void pollStatusSuccess(StateExecution execution) { + log.info("Polling complete"); } public NextAction integration(StateExecution execution) throws IOException { @@ -271,7 +221,7 @@ public class TransactionWorkflow extends WorkflowDefinition { transaction = (Transaction) handlerInterface.process(transaction); execution.setVariable("body", Utils.toJson(transaction)); - return NextAction.moveToState(PURCHASE_INVOICE, "Integration request successful"); + return NextAction.moveToState(INTEGRATION_STATUS_SUCCESS, "Integration request successful"); } catch (Exception e) { log.info(e.getMessage()); execution.setVariable("body", Utils.toJson(transaction)); @@ -279,54 +229,27 @@ public class TransactionWorkflow extends WorkflowDefinition { } } - public NextAction purchaseInvoice(StateExecution execution) throws Exception { + public NextAction integrationStatusSuccess(StateExecution execution) { try { LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); byte[] json = objectMapper.writeValueAsBytes(map); Transaction transaction = objectMapper.readValue(json, Transaction.class); - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("purchaseInvoice"); - transaction = (Transaction) handlerInterface.process(transaction); + var instance = workflowInstanceFactory.newWorkflowInstanceBuilder() + .setType(PurchaseInvoiceWorkflow.TYPE) + .setExternalId(transaction.getUserId() + "-" + new Instant().getMillis()) + .setBusinessKey(execution.getBusinessKey()) + .putStateVariable("body", Utils.toJson(transaction)) + .setNextActivation(DateTime.now()) + .build(); - execution.setVariable("body", Utils.toJson(transaction)); - return NextAction.moveToState(PURCHASE_INVOICE_PAYMENT, - "Purchase invoice successful - id: " + transaction.getErpPurchaseRef()); + long workflowId = workflowInstanceService.insertWorkflowInstance(instance); + log.info("Transaction workflow complete"); }catch (Exception e) { e.printStackTrace(); - return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage()); } + return NextAction.moveToState(DONE, "Transaction workflow complete"); } - public NextAction purchaseInvoicePayment(StateExecution execution) { - try { - LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); - byte[] json = objectMapper.writeValueAsBytes(map); - Transaction transaction = objectMapper.readValue(json, Transaction.class); - - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("purchaseInvoicePayment"); - transaction = (Transaction) handlerInterface.process(transaction); - - execution.setVariable("body", Utils.toJson(transaction)); - - // it's possible to have a flow that ends up event though - // polling has already happened - if(transaction.getPollingStatus() == Status.SUCCESS){ - return NextAction.moveToState(DONE, - "Purchase invoice payment generated - id: " + transaction.getErpPurchasePaymentRef() + ". " + - "Not proceeding because polling already complete"); - } - return NextAction.moveToState(PURCHASE_INVOICE_PAYMENT_SUCCESS, - "Purchase invoice payment generated - id: " + transaction.getErpPurchasePaymentRef()); - }catch (Exception e) { - e.printStackTrace(); - return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage()); - } - } - - public void purchaseInvoicePaymentSuccess(StateExecution execution) { - log.info("Purchase invoice payment complete"); - } }