improvements to workflows

This commit is contained in:
2026-03-26 19:58:39 +02:00
parent 39881ba629
commit c6024b8127
8 changed files with 202 additions and 89 deletions

View File

@@ -0,0 +1,30 @@
package zw.qantra.tm.configs;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
@Configuration
public class DatabaseConfiguration {
// Application DataSource Properties
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource")
public DataSourceProperties applicationDataSourceProperties() {
return new DataSourceProperties();
}
// Application DataSource
@Bean
@Primary
public DataSource applicationDataSource() {
return applicationDataSourceProperties()
.initializeDataSourceBuilder()
.build();
}
}

View File

@@ -0,0 +1,34 @@
package zw.qantra.tm.configs;
import io.nflow.engine.config.NFlow;
import io.nflow.engine.config.db.PgDatabaseConfiguration;
import jakarta.annotation.Nonnull;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import javax.sql.DataSource;
@Configuration
public class NflowDatabaseConfig extends PgDatabaseConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.nflow")
public DataSourceProperties nflowDataSourceProperties() {
return new DataSourceProperties();
}
// Nflow DataSource - must override parent method and use @NFlow annotation
// This tells nflow to use this datasource instead of creating its own
@Override
@Bean
@NFlow
@Nonnull
public DataSource nflowDatasource(Environment env, BeanFactory appCtx) {
return nflowDataSourceProperties()
.initializeDataSourceBuilder()
.build();
}
}

View File

@@ -0,0 +1,21 @@
package zw.qantra.tm.configs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
@Configuration
public class RequestLoggingFilterConfig {
@Bean
public CommonsRequestLoggingFilter logFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
filter.setIncludeClientInfo(true);
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setMaxPayloadLength(128000); // Set max payload length to log
filter.setIncludeHeaders(false); // Can be set to true if headers are needed
filter.setAfterMessagePrefix("REQUEST DATA: ");
return filter;
}
}

View File

@@ -6,6 +6,7 @@ import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -16,6 +17,7 @@ import lombok.RequiredArgsConstructor;
import zw.qantra.tm.domain.dtos.SdkActionDto; import zw.qantra.tm.domain.dtos.SdkActionDto;
import zw.qantra.tm.domain.enums.Status; import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Transaction; import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.SettingService;
import zw.qantra.tm.domain.services.TransactionService; import zw.qantra.tm.domain.services.TransactionService;
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
import zw.qantra.tm.rest.RestService; import zw.qantra.tm.rest.RestService;
@@ -26,6 +28,7 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf
private final Logger logger = LoggerFactory.getLogger(EcocashRemotePaymentProcessor.class); private final Logger logger = LoggerFactory.getLogger(EcocashRemotePaymentProcessor.class);
private final RestService restService; private final RestService restService;
private final TransactionService transactionService; private final TransactionService transactionService;
private final SettingService settingService;
@Value("${sbz.aggregator.encryption-key}") @Value("${sbz.aggregator.encryption-key}")
private String encryptionKey; private String encryptionKey;
@@ -80,6 +83,15 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf
LinkedHashMap body = (LinkedHashMap) response.get("body"); LinkedHashMap body = (LinkedHashMap) response.get("body");
transaction.setSdkActionId((String) body.get("uid")); transaction.setSdkActionId((String) body.get("uid"));
// todo: REMOVE THIS
if(settingService.getBooleanSetting("SIMULATE_ECOCASH_SUCCESS")){
body.put("status", "SUCCESS");
LinkedHashMap hashMap = new LinkedHashMap();
hashMap.put("debitReference", RandomStringUtils.randomNumeric(10));
body.put("transaction", hashMap);
body.put("targetUrl", "/home");
}
if(body.get("status").equals("SUCCESS")){ if(body.get("status").equals("SUCCESS")){
transaction.setStatus(Status.SUCCESS); transaction.setStatus(Status.SUCCESS);
transaction.setResponseCode("00"); transaction.setResponseCode("00");
@@ -90,8 +102,12 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf
}else{ }else{
transaction.setStatus(Status.FAILED); transaction.setStatus(Status.FAILED);
transaction.setResponseCode("01"); transaction.setResponseCode("01");
LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction"); String errorMessage = "Problem processing payment. Please try again.";
transaction.setErrorMessage((String) transactionData.get("errorMessage")); if(body.get("transaction") != null) {
LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction");
errorMessage = (String) transactionData.get("errorMessage");
}
transaction.setErrorMessage(errorMessage);
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Network error during payment processing: {}", e.getMessage(), e); logger.error("Network error during payment processing: {}", e.getMessage(), e);

View File

@@ -9,6 +9,7 @@ import io.nflow.engine.workflow.definition.WorkflowStateType;
import io.nflow.engine.workflow.instance.WorkflowInstance; import io.nflow.engine.workflow.instance.WorkflowInstance;
import io.nflow.engine.workflow.instance.WorkflowInstanceAction; import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.LocalDateTime;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -19,9 +20,11 @@ import zw.qantra.tm.domain.services.TransactionService;
import zw.qantra.tm.utils.LogUtils; import zw.qantra.tm.utils.LogUtils;
import zw.qantra.tm.utils.Utils; import zw.qantra.tm.utils.Utils;
import java.sql.Timestamp;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@Component @Component
@@ -51,7 +54,10 @@ public class PollCronWorkflow extends WorkflowDefinition {
if (!transactions.isEmpty()) if (!transactions.isEmpty())
log.info("Transaction count {}", transactions.size()); log.info("Transaction count {}", transactions.size());
Integer attempts = 0;
Integer success = 0;
for (Transaction transaction: transactions) { for (Transaction transaction: transactions) {
attempts++;
try { try {
// polling logic // polling logic
WorkflowInstance update = new WorkflowInstance.Builder() WorkflowInstance update = new WorkflowInstance.Builder()
@@ -105,6 +111,8 @@ public class PollCronWorkflow extends WorkflowDefinition {
15000, 50); 15000, 50);
Object response1 = future1.get(); Object response1 = future1.get();
LogUtils.logPrettyJson(log, "integration result", response1); LogUtils.logPrettyJson(log, "integration result", response1);
success++;
}else { }else {
transaction.setPollingStatus(Status.FAILED); transaction.setPollingStatus(Status.FAILED);
transaction.setIntegrationStatus(Status.FAILED); transaction.setIntegrationStatus(Status.FAILED);
@@ -116,8 +124,13 @@ public class PollCronWorkflow extends WorkflowDefinition {
} }
} }
return NextAction.moveToStateAfter( log.info("Polling complete. {} transactions processed, {} successful", attempts, success);
PROCESS, DateTime.now().plusMinutes(2), Map<String, String> map = new HashMap<>();
map.put("attempts", attempts.toString());
map.put("success", success.toString());
execution.setVariable(LocalDateTime.now().toString(), map);
return NextAction.retryAfter(DateTime.now().plusMinutes(2),
String.format("Cron complete on %s transactions", transactions.size())); String.format("Cron complete on %s transactions", transactions.size()));
} }

View File

@@ -64,15 +64,15 @@ public class TransactionWorkflow extends WorkflowDefinition {
permit(RECIPIENTS_UPDATE, CONFIRM_SUCCESS); permit(RECIPIENTS_UPDATE, CONFIRM_SUCCESS);
permit(CONFIRM_SUCCESS, GATEWAY_PAYMENT); permit(CONFIRM_SUCCESS, GATEWAY_PAYMENT);
permit(GATEWAY_PAYMENT, GATEWAY_PAYMENT_SUCCESS, TRAN_FAILED); permit(GATEWAY_PAYMENT, GATEWAY_PAYMENT_SUCCESS, TRAN_FAILED);
permit(GATEWAY_PAYMENT_SUCCESS, INTEGRATION); permit(GATEWAY_PAYMENT_SUCCESS, POLL_STATUS);
permit(INTEGRATION, PURCHASE_INVOICE, TRAN_FAILED);
permit(PURCHASE_INVOICE, PURCHASE_INVOICE_PAYMENT);
permit(PURCHASE_INVOICE_PAYMENT, PURCHASE_INVOICE_PAYMENT_SUCCESS, DONE);
permit(PURCHASE_INVOICE_PAYMENT_SUCCESS, POLL_STATUS);
permit(POLL_STATUS, SALES_INVOICE, TRAN_FAILED); permit(POLL_STATUS, SALES_INVOICE, TRAN_FAILED);
permit(SALES_INVOICE, JOURNAL_ENTRY); permit(SALES_INVOICE, JOURNAL_ENTRY);
permit(JOURNAL_ENTRY, SALES_PAYMENT); permit(JOURNAL_ENTRY, SALES_PAYMENT);
permit(SALES_PAYMENT, SALES_PAYMENT_SUCCESS, DONE); 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);
} }
public NextAction calculateCharges(StateExecution execution) { public NextAction calculateCharges(StateExecution execution) {
@@ -170,76 +170,6 @@ public class TransactionWorkflow extends WorkflowDefinition {
log.info("Gateway payment initiation complete"); log.info("Gateway payment initiation complete");
} }
public NextAction integration(StateExecution execution) throws IOException {
LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class);
byte[] json = objectMapper.writeValueAsBytes(map);
Transaction transaction = objectMapper.readValue(json, Transaction.class);
try {
HandlerInterface handlerInterface =
(HandlerInterface) workflowHandlerFactory.getWorkflowHandler("integration");
transaction = (Transaction) handlerInterface.process(transaction);
execution.setVariable("body", Utils.toJson(transaction));
return NextAction.moveToState(PURCHASE_INVOICE, "Integration request successful");
} catch (Exception e) {
log.info(e.getMessage());
execution.setVariable("body", Utils.toJson(transaction));
return NextAction.moveToState(TRAN_FAILED, e.getMessage());
}
}
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));
// 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");
}
public NextAction pollStatus(StateExecution execution) throws IOException { public NextAction pollStatus(StateExecution execution) throws IOException {
// cannot cast direct to Transaction object because of some jackson mapper hullabaloo :( // cannot cast direct to Transaction object because of some jackson mapper hullabaloo :(
LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class);
@@ -329,4 +259,74 @@ public class TransactionWorkflow extends WorkflowDefinition {
public void salesPaymentSuccess(StateExecution execution) { public void salesPaymentSuccess(StateExecution execution) {
log.info("Sales payment completed successfully"); log.info("Sales payment completed successfully");
} }
public NextAction integration(StateExecution execution) throws IOException {
LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class);
byte[] json = objectMapper.writeValueAsBytes(map);
Transaction transaction = objectMapper.readValue(json, Transaction.class);
try {
HandlerInterface handlerInterface =
(HandlerInterface) workflowHandlerFactory.getWorkflowHandler("integration");
transaction = (Transaction) handlerInterface.process(transaction);
execution.setVariable("body", Utils.toJson(transaction));
return NextAction.moveToState(PURCHASE_INVOICE, "Integration request successful");
} catch (Exception e) {
log.info(e.getMessage());
execution.setVariable("body", Utils.toJson(transaction));
return NextAction.moveToState(TRAN_FAILED, e.getMessage());
}
}
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));
// 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");
}
} }

View File

@@ -46,10 +46,9 @@ erpnext.tax.account=VAT - Q
erpnext.tax.rate=15.0 erpnext.tax.rate=15.0
logging.level.root=WARN logging.level.root=WARN
nflow.db.postgresql.driver=org.postgresql.Driver spring.datasource.nflow.url=jdbc:postgresql://173.212.247.232:5532/nflow
nflow.db.postgresql.url=jdbc:postgresql://173.212.247.232:5532/qpay spring.datasource.nflow.username=postgres
nflow.db.postgresql.user=postgres spring.datasource.nflow.password=zdDZMzq6F4B4L1IUl
nflow.db.postgresql.password=zdDZMzq6F4B4L1IUl
powertel.clientid=powertel_zesa powertel.clientid=powertel_zesa

View File

@@ -52,10 +52,10 @@ jwt.secret=your-super-secret-jwt-key-here-make-it-very-long-and-secure-in-produc
jwt.expiration=86400000 jwt.expiration=86400000
logging.level.root=WARN logging.level.root=WARN
nflow.db.postgresql.driver=org.postgresql.Driver spring.datasource.nflow.url=jdbc:postgresql://62.171.179.108:5432/nflow
nflow.db.postgresql.url=jdbc:postgresql://62.171.179.108:5432/qpay spring.datasource.nflow.username=postgres
nflow.db.postgresql.user=postgres spring.datasource.nflow.password=zdDZMzq6F4B4L1IUla
nflow.db.postgresql.password=zdDZMzq6F4B4L1IUla spring.main.allow-bean-definition-overriding=true
# Email Configuration # Email Configuration
infobip.url=https://z3m696.api.infobip.com/sms/2/text/advanced infobip.url=https://z3m696.api.infobip.com/sms/2/text/advanced