improvements to workflows

This commit is contained in:
2026-03-27 19:44:54 +02:00
parent c3ba11caea
commit 19813f8e98
4 changed files with 236 additions and 132 deletions

View File

@@ -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

View File

@@ -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());
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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");
}
}