From c6024b812734158283918eba40fb51efe1f8452a Mon Sep 17 00:00:00 2001 From: Vusumuzi Khoza Date: Thu, 26 Mar 2026 19:58:39 +0200 Subject: [PATCH] improvements to workflows --- .../tm/configs/DatabaseConfiguration.java | 30 ++++ .../tm/configs/NflowDatabaseConfig.java | 34 ++++ .../configs/RequestLoggingFilterConfig.java | 21 +++ .../EcocashRemotePaymentProcessor.java | 22 ++- .../tm/domain/workflows/PollCronWorkflow.java | 17 +- .../domain/workflows/TransactionWorkflow.java | 152 +++++++++--------- src/main/resources/application-lab.properties | 7 +- src/main/resources/application.properties | 8 +- 8 files changed, 202 insertions(+), 89 deletions(-) create mode 100644 src/main/java/zw/qantra/tm/configs/DatabaseConfiguration.java create mode 100644 src/main/java/zw/qantra/tm/configs/NflowDatabaseConfig.java create mode 100644 src/main/java/zw/qantra/tm/configs/RequestLoggingFilterConfig.java diff --git a/src/main/java/zw/qantra/tm/configs/DatabaseConfiguration.java b/src/main/java/zw/qantra/tm/configs/DatabaseConfiguration.java new file mode 100644 index 0000000..525a98d --- /dev/null +++ b/src/main/java/zw/qantra/tm/configs/DatabaseConfiguration.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/configs/NflowDatabaseConfig.java b/src/main/java/zw/qantra/tm/configs/NflowDatabaseConfig.java new file mode 100644 index 0000000..92d95f5 --- /dev/null +++ b/src/main/java/zw/qantra/tm/configs/NflowDatabaseConfig.java @@ -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(); + } +} diff --git a/src/main/java/zw/qantra/tm/configs/RequestLoggingFilterConfig.java b/src/main/java/zw/qantra/tm/configs/RequestLoggingFilterConfig.java new file mode 100644 index 0000000..33d4366 --- /dev/null +++ b/src/main/java/zw/qantra/tm/configs/RequestLoggingFilterConfig.java @@ -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; + } +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/payments/EcocashRemotePaymentProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/payments/EcocashRemotePaymentProcessor.java index 331595a..dd31f64 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/payments/EcocashRemotePaymentProcessor.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/payments/EcocashRemotePaymentProcessor.java @@ -6,6 +6,7 @@ import java.security.NoSuchAlgorithmException; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; +import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.enums.Status; 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.processors.TransactionProcessorInterface; 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 RestService restService; private final TransactionService transactionService; + private final SettingService settingService; @Value("${sbz.aggregator.encryption-key}") private String encryptionKey; @@ -79,7 +82,16 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf LinkedHashMap body = (LinkedHashMap) response.get("body"); 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")){ transaction.setStatus(Status.SUCCESS); transaction.setResponseCode("00"); @@ -90,8 +102,12 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf }else{ transaction.setStatus(Status.FAILED); transaction.setResponseCode("01"); - LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction"); - transaction.setErrorMessage((String) transactionData.get("errorMessage")); + String errorMessage = "Problem processing payment. Please try again."; + if(body.get("transaction") != null) { + LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction"); + errorMessage = (String) transactionData.get("errorMessage"); + } + transaction.setErrorMessage(errorMessage); } } catch (Exception e) { logger.error("Network error during payment processing: {}", e.getMessage(), e); diff --git a/src/main/java/zw/qantra/tm/domain/workflows/PollCronWorkflow.java b/src/main/java/zw/qantra/tm/domain/workflows/PollCronWorkflow.java index 5b72f92..e5ba77c 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/PollCronWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/workflows/PollCronWorkflow.java @@ -9,6 +9,7 @@ import io.nflow.engine.workflow.definition.WorkflowStateType; import io.nflow.engine.workflow.instance.WorkflowInstance; import io.nflow.engine.workflow.instance.WorkflowInstanceAction; import org.joda.time.DateTime; +import org.joda.time.LocalDateTime; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; 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.Utils; +import java.sql.Timestamp; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; @Component @@ -51,7 +54,10 @@ public class PollCronWorkflow extends WorkflowDefinition { if (!transactions.isEmpty()) log.info("Transaction count {}", transactions.size()); + Integer attempts = 0; + Integer success = 0; for (Transaction transaction: transactions) { + attempts++; try { // polling logic WorkflowInstance update = new WorkflowInstance.Builder() @@ -105,6 +111,8 @@ public class PollCronWorkflow extends WorkflowDefinition { 15000, 50); Object response1 = future1.get(); LogUtils.logPrettyJson(log, "integration result", response1); + + success++; }else { transaction.setPollingStatus(Status.FAILED); transaction.setIntegrationStatus(Status.FAILED); @@ -116,8 +124,13 @@ public class PollCronWorkflow extends WorkflowDefinition { } } - return NextAction.moveToStateAfter( - PROCESS, DateTime.now().plusMinutes(2), + log.info("Polling complete. {} transactions processed, {} successful", attempts, success); + Map 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())); } 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 d0529be..f04262b 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java @@ -64,15 +64,15 @@ public class TransactionWorkflow extends WorkflowDefinition { permit(RECIPIENTS_UPDATE, CONFIRM_SUCCESS); permit(CONFIRM_SUCCESS, GATEWAY_PAYMENT); permit(GATEWAY_PAYMENT, GATEWAY_PAYMENT_SUCCESS, TRAN_FAILED); - permit(GATEWAY_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(PURCHASE_INVOICE_PAYMENT_SUCCESS, POLL_STATUS); + 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, 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) { @@ -170,76 +170,6 @@ public class TransactionWorkflow extends WorkflowDefinition { 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 { // cannot cast direct to Transaction object because of some jackson mapper hullabaloo :( LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); @@ -329,4 +259,74 @@ public class TransactionWorkflow extends WorkflowDefinition { public void salesPaymentSuccess(StateExecution execution) { 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"); + } } diff --git a/src/main/resources/application-lab.properties b/src/main/resources/application-lab.properties index 2946fd1..43c40f0 100644 --- a/src/main/resources/application-lab.properties +++ b/src/main/resources/application-lab.properties @@ -46,10 +46,9 @@ erpnext.tax.account=VAT - Q erpnext.tax.rate=15.0 logging.level.root=WARN -nflow.db.postgresql.driver=org.postgresql.Driver -nflow.db.postgresql.url=jdbc:postgresql://173.212.247.232:5532/qpay -nflow.db.postgresql.user=postgres -nflow.db.postgresql.password=zdDZMzq6F4B4L1IUl +spring.datasource.nflow.url=jdbc:postgresql://173.212.247.232:5532/nflow +spring.datasource.nflow.username=postgres +spring.datasource.nflow.password=zdDZMzq6F4B4L1IUl powertel.clientid=powertel_zesa diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c89b7f8..ccd49df 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -52,10 +52,10 @@ jwt.secret=your-super-secret-jwt-key-here-make-it-very-long-and-secure-in-produc jwt.expiration=86400000 logging.level.root=WARN -nflow.db.postgresql.driver=org.postgresql.Driver -nflow.db.postgresql.url=jdbc:postgresql://62.171.179.108:5432/qpay -nflow.db.postgresql.user=postgres -nflow.db.postgresql.password=zdDZMzq6F4B4L1IUla +spring.datasource.nflow.url=jdbc:postgresql://62.171.179.108:5432/nflow +spring.datasource.nflow.username=postgres +spring.datasource.nflow.password=zdDZMzq6F4B4L1IUla +spring.main.allow-bean-definition-overriding=true # Email Configuration infobip.url=https://z3m696.api.infobip.com/sms/2/text/advanced