diff --git a/src/main/java/zw/qantra/tm/configs/JacksonConfig.java b/src/main/java/zw/qantra/tm/configs/JacksonConfig.java
index f146914..58e68f2 100644
--- a/src/main/java/zw/qantra/tm/configs/JacksonConfig.java
+++ b/src/main/java/zw/qantra/tm/configs/JacksonConfig.java
@@ -1,24 +1,27 @@
package zw.qantra.tm.configs;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import com.fasterxml.jackson.datatype.joda.JodaModule;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Primary;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Service;
-@Configuration
+@Service
public class JacksonConfig {
-
- @Bean
- @Primary
- public ObjectMapper objectMapper() {
- ObjectMapper objectMapper = new ObjectMapper();
- objectMapper.registerModule(new JavaTimeModule());
- objectMapper.registerModule(new JodaModule());
- objectMapper.registerModule(new Jdk8Module());
+ @Autowired
+ private ObjectMapper nflowObjectMapper;
- return objectMapper;
+ @EventListener(ApplicationReadyEvent.class)
+ public void updateMapper() {
+ // It's crucial to create a new ObjectMapper instance here
+ // instead of modifying an autowired one.
+ nflowObjectMapper.registerModule(new JavaTimeModule());
+ nflowObjectMapper.registerModule(new JodaModule());
+ nflowObjectMapper.registerModule(new Jdk8Module());
+ nflowObjectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
}
diff --git a/src/main/java/zw/qantra/tm/configs/NflowConfiguration.java b/src/main/java/zw/qantra/tm/configs/NflowConfiguration.java
new file mode 100644
index 0000000..08d9ab0
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/configs/NflowConfiguration.java
@@ -0,0 +1,79 @@
+package zw.qantra.tm.configs;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
+import com.fasterxml.jackson.datatype.jsr310.JSR310Module;
+import io.nflow.engine.config.EngineConfiguration;
+import io.nflow.engine.config.NFlow;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import java.util.List;
+
+@Configuration
+public class NflowConfiguration {
+
+ /**
+ * Creates the primary, application-wide ObjectMapper.
+ *
+ * This bean is marked as {@code @Primary} to ensure it's the default for dependency injection
+ * throughout the application. It's configured using Spring Boot's {@link Jackson2ObjectMapperBuilder},
+ * which automatically registers modules for Java 8 Time, Optional, etc., if they are on the classpath.
+ * It also includes any custom {@link Module} beans defined in the application context.
+ *
+ *
+ * @param builder The default Jackson2ObjectMapperBuilder, pre-configured by Spring Boot.
+ * @param customModules A list of all custom Jackson {@link Module} beans found in the application context.
+ * @return A customized {@link com.fasterxml.jackson.databind.ObjectMapper}.
+ */
+ @Primary
+ @Bean
+ public ObjectMapper primaryObjectMapper(Jackson2ObjectMapperBuilder builder, List customModules) {
+ builder.modules(customModules);
+ builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ return builder.build();
+ }
+
+ /**
+ * Overrides the default ObjectMapper used by the nFlow REST API.
+ *
+ * This bean definition ensures that the nFlow REST endpoints use the same consistent,
+ * primary ObjectMapper as the rest of the application.
+ *
+ */
+ @Bean("nflowRestObjectMapper")
+ public ObjectMapper nflowRestObjectMapper(ObjectMapper primaryObjectMapper) {
+ return primaryObjectMapper;
+ }
+
+ /**
+ * Overrides the default ObjectMapper used by the nFlow Engine for variable serialization.
+ *
+ * This is the key fix for `InvalidDefinitionException` with `LocalDateTime` in workflow variables.
+ * It ensures the engine uses our fully configured primary ObjectMapper.
+ *
+ */
+ @Bean("nflowEngineObjectMapper")
+ public ObjectMapper nflowEngineObjectMapper(ObjectMapper primaryObjectMapper) {
+ return primaryObjectMapper;
+ }
+
+ @Bean
+ @NFlow
+ public EngineConfiguration.EngineObjectMapperSupplier nflowObjectMapper() {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
+ mapper.registerModule(new JodaModule());
+ mapper.registerModule(new Jdk8Module());
+ mapper.registerModule(new JSR310Module());
+ mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ return () -> mapper;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/zw/qantra/tm/configs/SecurityConfig.java b/src/main/java/zw/qantra/tm/configs/SecurityConfig.java
index 3afa522..ef9cb60 100644
--- a/src/main/java/zw/qantra/tm/configs/SecurityConfig.java
+++ b/src/main/java/zw/qantra/tm/configs/SecurityConfig.java
@@ -29,6 +29,7 @@ public class SecurityConfig {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
+ .requestMatchers("/configs/seed/**").permitAll()
.requestMatchers("/test/**").permitAll()
.requestMatchers("/explorer/**").permitAll()
.requestMatchers("/nflow/**").permitAll()
diff --git a/src/main/java/zw/qantra/tm/domain/controllers/OnboardingController.java b/src/main/java/zw/qantra/tm/domain/controllers/OnboardingController.java
index 009ab66..404ecfd 100644
--- a/src/main/java/zw/qantra/tm/domain/controllers/OnboardingController.java
+++ b/src/main/java/zw/qantra/tm/domain/controllers/OnboardingController.java
@@ -66,6 +66,7 @@ public class OnboardingController {
.setType(RegistrationWorkflow.TYPE)
.setExternalId(request.getUsername() + "-" + new Instant().getMillis())
.putStateVariable("body", request)
+ .setNextActivation(DateTime.now())
.build();
long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
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 82a1989..871c8e7 100644
--- a/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java
+++ b/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java
@@ -1,13 +1,23 @@
package zw.qantra.tm.domain.controllers;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.nflow.engine.internal.workflow.ObjectStringMapper;
+import io.nflow.engine.service.WorkflowInstanceService;
+import io.nflow.engine.workflow.instance.WorkflowInstance;
+import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
+import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
import lombok.RequiredArgsConstructor;
import net.kaczmarzyk.spring.data.jpa.domain.Equal;
import net.kaczmarzyk.spring.data.jpa.web.annotation.Or;
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
import net.kaczmarzyk.spring.data.jpa.web.annotation.Conjunction;
+import org.joda.time.DateTime;
+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;
@@ -15,9 +25,15 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
+import zw.qantra.tm.domain.workflows.TransactionWorkflow;
+import zw.qantra.tm.domain.workflows.WorkflowUtils;
import zw.qantra.tm.utils.LogUtils;
+import zw.qantra.tm.utils.Utils;
+import java.util.HashMap;
import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
@RestController
@RequestMapping("/public/transaction")
@@ -26,6 +42,10 @@ public class TransactonController {
Logger logger = LoggerFactory.getLogger(TransactonController.class);
private final TransactionService transactionService;
+ private final WorkflowInstanceService workflowInstanceService;
+ private final WorkflowInstanceFactory workflowInstanceFactory;
+ private final WorkflowUtils workflowUtils;
+ private final ObjectStringMapper objectStringMapper;
@GetMapping
public ResponseEntity> getAll(
@@ -65,25 +85,86 @@ public class TransactonController {
}
@GetMapping("/poll/{id}")
- public ResponseEntity poll(@PathVariable UUID id) {
- return ResponseEntity.ok(transactionService.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(transaction.getWorkflowId())
+ .setState("pollStatus") // The state you want to transition to
+ .setNextActivation(DateTime.now()) // Schedule immediate execution
+ .setStateText("Resumed from manual state")
+ .build();
+
+ WorkflowInstanceAction action = new WorkflowInstanceAction.Builder(update)
+ .setType(WorkflowInstanceAction.WorkflowActionType.externalChange)
+ .setExecutionEnd(DateTime.now())
+ .build();
+
+ // Apply the update
+ workflowInstanceService.updateWorkflowInstance(update, action);
+
+ CompletableFuture future = workflowUtils
+ .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) {
- LogUtils.logPrettyJson(logger, "incoming transaction", transaction);
- Transaction createdTransaction = transactionService.request(transaction);
- LogUtils.logPrettyJson(logger, "outgoing transaction", createdTransaction);
- return ResponseEntity.ok(createdTransaction);
+ @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("request", Utils.toJson(transaction)); }})
+ .setStateText("Resumed from manual state")
+ .build();
+
+ WorkflowInstanceAction action = new WorkflowInstanceAction.Builder(update)
+ .setType(WorkflowInstanceAction.WorkflowActionType.externalChange)
+ .setExecutionEnd(DateTime.now())
+ .build();
+
+ // Apply the update
+ workflowInstanceService.updateWorkflowInstance(update, action);
+
+ CompletableFuture future = workflowUtils
+ .waitForState(transaction.getWorkflowId(), new String[]{"gatewayPaymentSuccess", "failed"},
+ 15000, 50);
+ Object response = future.get();
+
+ LogUtils.logPrettyJson(logger, "outgoing transaction", response);
+ return ResponseEntity.ok(response);
}
@PostMapping
- public ResponseEntity create(@RequestBody Transaction transaction) {
+ public ResponseEntity confirm(@RequestBody Transaction transaction) throws ExecutionException, InterruptedException {
LogUtils.logPrettyJson(logger, "incoming transaction", transaction);
- Transaction createdTransaction = transactionService.confirm(transaction);
- LogUtils.logPrettyJson(logger, "outgoing transaction", createdTransaction);
- return ResponseEntity.ok(createdTransaction);
+ var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
+ .setType(TransactionWorkflow.TYPE)
+ .setExternalId(transaction.getUserId() + "-" + new Instant().getMillis())
+ .putStateVariable("confirmation", transaction)
+ .setNextActivation(DateTime.now())
+ .build();
+
+ long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
+
+ CompletableFuture future = workflowUtils
+ .waitForState(workflowId, new String[]{"confirmSuccess", "failed"},
+ 15000, 50);
+ Object response = future.get();
+ LogUtils.logPrettyJson(logger, "outgoing transaction", response);
+ return ResponseEntity.ok(response);
}
@PutMapping("/{id}")
diff --git a/src/main/java/zw/qantra/tm/domain/models/Transaction.java b/src/main/java/zw/qantra/tm/domain/models/Transaction.java
index bcb2e45..35eb3ba 100644
--- a/src/main/java/zw/qantra/tm/domain/models/Transaction.java
+++ b/src/main/java/zw/qantra/tm/domain/models/Transaction.java
@@ -80,6 +80,9 @@ public class Transaction extends BaseEntity {
private String erpPurchasePaymentRef;
private String erpCommissionJournalRef;
+ @Column(nullable = true)
+ private long workflowId;
+
@Enumerated(EnumType.STRING)
private RequestType type;
@Enumerated(EnumType.STRING)
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..84b0219 100644
--- a/src/main/java/zw/qantra/tm/domain/services/TransactionService.java
+++ b/src/main/java/zw/qantra/tm/domain/services/TransactionService.java
@@ -14,22 +14,9 @@ import org.springframework.data.jpa.domain.Specification;
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.AdditionalData;
-import zw.qantra.tm.domain.models.Provider;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.repositories.TransactionRepository;
-import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory;
-import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
-import zw.qantra.tm.domain.services.processors.handlers.CalculateChargesHandler;
-import zw.qantra.tm.domain.services.processors.handlers.ConfirmationLabelHandler;
-import zw.qantra.tm.domain.services.processors.handlers.ConfirmationLogicHandler;
-import zw.qantra.tm.domain.services.processors.handlers.FormattingHandler;
-import zw.qantra.tm.domain.services.processors.handlers.LogicPipeline;
-import zw.qantra.tm.domain.services.processors.handlers.RecipientsUpdateHandler;
-import zw.qantra.tm.domain.services.processors.handlers.ValidationHandler;
-import zw.qantra.tm.domain.services.processors.handlers.erp.*;
-import zw.qantra.tm.domain.models.TransactionEvent;
import zw.qantra.tm.exceptions.ApiException;
import zw.qantra.tm.utils.Utils;
@@ -40,230 +27,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/services/processors/handlers/erp/SalesInvoiceHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/SalesInvoiceHandler.java
index cbed519..e69de29 100644
--- a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/SalesInvoiceHandler.java
+++ b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/SalesInvoiceHandler.java
@@ -1,146 +0,0 @@
-package zw.qantra.tm.domain.services.processors.handlers.erp;
-
-import lombok.NoArgsConstructor;
-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.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;
-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.exceptions.ApiException;
-import zw.qantra.tm.rest.RestService;
-
-import java.nio.charset.StandardCharsets;
-import java.util.*;
-
-@Service
-@RequiredArgsConstructor
-public class SalesInvoiceHandler implements LogicHandler{
- Logger logger = LoggerFactory.getLogger(SalesInvoiceHandler.class);
-
- private final RestService restService;
- private final UserService userService;
- private final TransactionEventService transactionEventService;
- private final TransactionRepository transactionRepository;
-
- @Value("${erpnext.url}")
- private String erpnextUrl;
- @Value("${erpnext.api.key}")
- private String erpnextApiKey;
- @Value("${erpnext.api.secret}")
- private String erpnextApiSecret;
- @Value("${erpnext.username}")
- private String erpnextUsername;
- @Value("${erpnext.password}")
- private String erpnextPassword;
- @Value("${erpnext.company}")
- private String erpnextCompany;
-
- @Override
- public Transaction process(Transaction transaction) {
- logger.info("Processing sales invoice for transaction: {}", transaction.getReference());
- TransactionEvent transactionEvent = TransactionEvent.builder()
- .transactionId(transaction.getId().toString())
- .event(RequestType.SALES_INVOICE.name())
- .status(Status.PROCESSING)
- .build();
- transactionEventService.save(transactionEvent);
-
- if(transaction.getStatus() == Status.PENDING){
- try {
- logger.info("Creating sales invoice in ERPNext for successful transaction");
-
- // 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);
-
- String erpCustomerRef = "";
- User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null);
- if(user == null){
- erpCustomerRef = "Anonymous";
- }
-
- SalesInvoiceDto salesInvoiceDto = SalesInvoiceDto.builder()
- .doctype("Sales Invoice")
- .customer(erpCustomerRef)
- .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");
- } 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());
- }
- } else {
- logger.info("Transaction status is not SUCCESS ({}), skipping sales invoice creation", transaction.getStatus());
- }
-
- transactionEventService.save(transactionEvent);
-
- return transactionRepository.save(transaction);
- }
-}
diff --git a/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java b/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java
new file mode 100644
index 0000000..2c2e332
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java
@@ -0,0 +1,262 @@
+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 org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import zw.qantra.tm.domain.enums.Status;
+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
+public class TransactionWorkflow extends WorkflowDefinition {
+ Logger log = org.slf4j.LoggerFactory.getLogger(RegistrationWorkflow.class);
+
+ @Autowired
+ private WorkflowHandlerFactory workflowHandlerFactory;
+ @Autowired
+ private TransactionService transactionService;
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ public static final String TYPE = "transactionWorkflow";
+
+ public static final State CALCULATE_CHARGES = new State("calculateCharges", WorkflowStateType.start);
+ public static final State CLEANUP = new State("cleanup");
+ public static final State CONFIRM = new State("confirm");
+ public static final State CONFIRM_SUCCESS = new State("confirmSuccess", WorkflowStateType.manual);
+ 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 INTEGRATION = new State("integration");
+ public static final State PURCHASE_INVOICE_PAYMENT = new State("purchaseInvoicePayment");
+
+ public static final State FAILED = new State("failed", WorkflowStateType.manual);
+ public static final State DONE = new State("done", WorkflowStateType.end);
+
+ public TransactionWorkflow() {
+ super(TYPE, CALCULATE_CHARGES, FAILED);
+
+ permit(CALCULATE_CHARGES, CLEANUP);
+ permit(CLEANUP, CONFIRM);
+ permit(CONFIRM, CONFIRM_SUCCESS, FAILED);
+ permit(CONFIRM_SUCCESS, SALES_INVOICE);
+ permit(SALES_INVOICE, GATEWAY_PAYMENT);
+ permit(GATEWAY_PAYMENT, GATEWAY_PAYMENT_SUCCESS, FAILED);
+ permit(GATEWAY_PAYMENT_SUCCESS, POLL_STATUS);
+ permit(POLL_STATUS, JOURNAL_ENTRY, FAILED);
+ permit(JOURNAL_ENTRY, SALES_PAYMENT);
+ permit(SALES_PAYMENT, PURCHASE_INVOICE);
+ 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("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());
+ }
+ }
+
+ public NextAction cleanup(StateExecution execution) {
+ try {
+ Transaction transaction = execution.getVariable("body", Transaction.class);
+ process(transaction, "cleanup");
+ execution.setVariable("body", transaction);
+
+ return NextAction.moveToState(CONFIRM, "Cleanup complete");
+ } catch (Exception e) {
+ e.printStackTrace();
+ return NextAction.moveToState(FAILED, e.getMessage());
+ }
+ }
+
+ public NextAction confirm(StateExecution execution) {
+ try {
+ Transaction transaction = execution.getVariable("body", Transaction.class);
+
+ transaction.setWorkflowId(execution.getWorkflowInstanceId());
+ transaction = process(transaction, "confirm");
+ execution.setVariable("body", Utils.toJson(transaction));
+ 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) {
+ 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) {
+ 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 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) {
+ 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) {
+ HandlerInterface handlerInterface =
+ (HandlerInterface) workflowHandlerFactory.getWorkflowHandler(beanName);
+ try {
+ transaction = (Transaction) handlerInterface.process(transaction);
+ }catch (Exception e){
+ e.printStackTrace();
+ e.getCause().printStackTrace();
+ transaction.setStatus(Status.FAILED);
+ transaction.setResponseCode("01");
+ transaction.setErrorMessage(e.getMessage());
+ transactionService.getTransactionRepository().save(transaction);
+ }
+
+ return transaction;
+ }
+}
diff --git a/src/main/java/zw/qantra/tm/domain/workflows/WorkflowUtils.java b/src/main/java/zw/qantra/tm/domain/workflows/WorkflowUtils.java
index 37315a6..4618510 100644
--- a/src/main/java/zw/qantra/tm/domain/workflows/WorkflowUtils.java
+++ b/src/main/java/zw/qantra/tm/domain/workflows/WorkflowUtils.java
@@ -6,6 +6,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
+import io.nflow.engine.model.ModelObject;
import io.nflow.engine.service.WorkflowInstanceInclude.*;
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
import org.springframework.stereotype.Component;
@@ -55,12 +56,17 @@ public class WorkflowUtils {
}
public StateResponse getStateResponse(WorkflowInstance instance, String message){
- WorkflowInstanceAction action = instance.actions.get(instance.actions.size()-1);
+ WorkflowInstanceAction action;
+ try {
+ action = instance.actions.get(instance.actions.size()-1);
+ }catch (IndexOutOfBoundsException exception){
+ action = null;
+ }
return StateResponse.builder()
.state(instance.state)
.status(instance.status.toString())
.body(Utils.fromJson(instance.getStateVariable("body"), Map.class))
- .message(action.stateText)
+ .message(action != null ? action.stateText : null)
.workflowId(instance.id.toString())
.externalId(instance.externalId)
.build();
diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/CalculateChargesHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/CalculateChargesHandler.java
new file mode 100644
index 0000000..76ff75a
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/CalculateChargesHandler.java
@@ -0,0 +1,148 @@
+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.models.Charge;
+import zw.qantra.tm.domain.models.ChargeCondition;
+import zw.qantra.tm.domain.models.Transaction;
+import zw.qantra.tm.domain.services.ChargeService;
+import zw.qantra.tm.domain.services.processors.handlers.LogicHandler;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+
+@Service("calculateCharges")
+@RequiredArgsConstructor
+public class CalculateChargesHandler implements HandlerInterface {
+ Logger logger = LoggerFactory.getLogger(CalculateChargesHandler.class);
+
+ private final ChargeService chargeService;
+
+ public Transaction process(Transaction transaction) {
+ logger.info("processing charges");
+ transaction.setCharge(BigDecimal.ZERO);
+ transaction.setTax(BigDecimal.ZERO);
+ transaction.setGatewayCharge(BigDecimal.ZERO);
+
+ BigDecimal amountToCharge = transaction.getAmount();
+ BigDecimal chargeAmount = BigDecimal.ZERO;
+
+ List chargeList = chargeService.getChargeRepository().findAllByCurrency(transaction.getDebitCurrency());
+
+ List appropriateCharges = new ArrayList<>();
+ for (Charge charge : chargeList) {
+ boolean isPresent = false;
+ for (ChargeCondition chargeCondition: charge.getIncludes()){
+ List conditionTypes = Arrays.asList(chargeCondition.getCodes().split(","));
+ switch (chargeCondition.getType()){
+ case GATEWAY:
+ isPresent = conditionTypes.contains(transaction.getPaymentProcessorLabel());
+ break;
+ case PROVIDER:
+ isPresent = conditionTypes.contains(transaction.getIntegrationProcessorLabel());
+ break;
+ }
+ if(isPresent) break; // we've found a matching condition, no need to check rest of conditions
+ }
+ // if we did not find a match, means this charge does not apply to this transaction
+ // also implies there is no need to check exemptions
+ if(!isPresent) continue;
+
+ boolean isExempt = false;
+ for (ChargeCondition chargeCondition: charge.getExcludes()){
+ List conditionTypes = Arrays.asList(chargeCondition.getCodes().split(","));
+ switch (chargeCondition.getType()){
+ case GATEWAY:
+ isExempt = conditionTypes.contains(transaction.getPaymentProcessorLabel());
+ break;
+ case PROVIDER:
+ isExempt = conditionTypes.contains(transaction.getIntegrationProcessorLabel());
+ break;
+ }
+ if(isExempt) break; // we've found a matching condition, no need to check rest of conditions
+ }
+ if(isExempt) continue; // if we found a match, means this transaction is exempt from this charge
+
+ appropriateCharges.add(charge);
+ }
+
+ for (Charge charge : appropriateCharges) {
+ // if minimum amount chargeable is lower than the amount to charge, apply the min charge
+ if(charge.getMinimumAmount() != null) {
+ if(charge.getMinimumAmount().compareTo(amountToCharge) > 0) {
+ applyCharge(transaction, charge, charge.getMin());
+ continue;
+ }
+ }
+
+ // if maximum amount chargeable is higher than the amount to charge, apply the max charge
+ if(charge.getMaximumAmount() != null) {
+ if(charge.getMaximumAmount().compareTo(amountToCharge) < 0) {
+ applyCharge(transaction, charge, charge.getMax());
+ continue;
+ }
+ }
+
+ // calculate unrestricted charge amount
+ // if percentage rate is not present then it's a flat charge, else it's a percentage
+ if(charge.getPercentageRate() == null) {
+ if(charge.getFlat() == null)
+ continue;
+
+ chargeAmount = charge.getFlat();
+ }else {
+ BigDecimal fraction = charge.getPercentageRate().divide(
+ new BigDecimal(100), 4, RoundingMode.CEILING);
+ chargeAmount = amountToCharge.multiply(fraction)
+ .setScale(2, RoundingMode.CEILING);
+
+ // if it's a composite charge then add the flat component as well
+ if(charge.isComposite()) {
+ chargeAmount = chargeAmount.add(charge.getFlat())
+ .setScale(2, RoundingMode.CEILING);
+ }
+ }
+
+ // if minimum charge is set, check if the charge amount is less than the minimum
+ if(charge.getMin() != null) {
+ if(charge.getMin().compareTo(chargeAmount) > 0){
+ chargeAmount = charge.getMin();
+ }
+ }
+
+ // if maximum charge is set, check if the charge amount is more than the maximum
+ if(charge.getMax() != null) {
+ if(charge.getMax().compareTo(chargeAmount) < 0) {
+ chargeAmount = charge.getMax();
+ }
+ }
+
+ applyCharge(transaction, charge, chargeAmount);
+ }
+
+ // calculate total amount for ease of use later
+ transaction.setTotalAmount(
+ transaction.getAmount().add(
+ transaction.getCharge().add(
+ transaction.getTax().add(
+ transaction.getGatewayCharge()))));
+
+ return transaction;
+ }
+
+ void applyCharge(Transaction transaction, Charge charge, BigDecimal chargeAmount) {
+ if(chargeAmount == null) return;
+
+ switch (charge.getChargeLabel()) {
+ case FEE -> transaction.setCharge(chargeAmount);
+ case TAX -> transaction.setTax(chargeAmount);
+ case GATEWAY_FEE -> transaction.setGatewayCharge(chargeAmount);
+ }
+ }
+}
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
new file mode 100644
index 0000000..0955e62
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java
@@ -0,0 +1,97 @@
+package zw.qantra.tm.domain.workflows.handlers;
+
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber;
+import lombok.RequiredArgsConstructor;
+import org.aspectj.apache.bcel.generic.RET;
+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.models.Provider;
+import zw.qantra.tm.domain.models.Transaction;
+import zw.qantra.tm.domain.services.ProviderService;
+import zw.qantra.tm.domain.services.processors.handlers.ConfirmationLabelHandler;
+import zw.qantra.tm.exceptions.ApiException;
+import zw.qantra.tm.utils.Utils;
+
+import java.util.UUID;
+
+@Service("cleanup")
+@RequiredArgsConstructor
+public class CleanupHandler implements HandlerInterface {
+ private final Logger logger = LoggerFactory.getLogger(ConfirmationLabelHandler.class);
+
+ private final ProviderService providerService;
+
+ @Override
+ public Object process(Transaction transaction) throws Exception {
+ // MAKE SURE IMPORTANT FIELDS ARE SET
+ // =======================
+ if(transaction.getType().equals(RequestType.CONFIRM)) {
+ transaction.setTrace(Utils.getUid());
+ transaction.setReference(UUID.randomUUID().toString());
+ }
+
+ // UPDATE TRAN WITH CONFIRMATION HANDLER
+ // =======================
+ 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.getLabel();
+
+ transaction.setConfirmationProcessorLabel(label);
+ transaction.setProviderLabel(provider.getLabel());
+ logger.info("label: {}", label);
+
+ // FORMAT FIELDS CORRECTLY
+ // =======================
+ // strip credit account and make sure there's nothing funny
+ String creditAccount = transaction.getCreditAccount();
+ if (creditAccount != null && !creditAccount.isEmpty()) {
+ // Remove all non-hexadecimal characters (keep only 0-9, a-f, A-F)
+ String strippedAccount = creditAccount.replaceAll("[^0-9a-fA-F]", "");
+
+ // Only set if the stripped result is not empty
+ if (!strippedAccount.isEmpty()) {
+ transaction.setCreditAccount(strippedAccount);
+ } else {
+ // If stripping results in empty string, set to null
+ transaction.setCreditAccount(null);
+ }
+ }
+
+
+ // sanitize phone number formatting
+ String debitPhoneNumber = transaction.getDebitPhone();
+ String creditPhoneNumber = transaction.getCreditPhone();
+
+ PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
+
+ if(debitPhoneNumber != null && !debitPhoneNumber.isEmpty()){
+ try {
+ // phone must begin with '+'
+ Phonenumber.PhoneNumber zimPhoneNumber = phoneUtil.parse(debitPhoneNumber, "");
+ String formattedPhoneNumber = phoneUtil.format(zimPhoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
+ transaction.setDebitPhone(formattedPhoneNumber);
+ } catch (NumberParseException e) {
+ System.err.println("NumberParseException was thrown: " + e.toString());
+ }
+ }
+
+ if(creditPhoneNumber != null && !creditPhoneNumber.isEmpty()){
+ try {
+ Phonenumber.PhoneNumber zimPhoneNumber = phoneUtil.parse(creditPhoneNumber, "ZW");
+ String formattedPhoneNumber = phoneUtil.format(zimPhoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
+ transaction.setCreditPhone(formattedPhoneNumber);
+ } catch (NumberParseException e) {
+ System.err.println("NumberParseException was thrown: " + e.toString());
+ }
+ }
+
+ return transaction;
+ }
+}
diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/ConfirmHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/ConfirmHandler.java
new file mode 100644
index 0000000..0ceb137
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/ConfirmHandler.java
@@ -0,0 +1,56 @@
+package zw.qantra.tm.domain.workflows.handlers;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import zw.qantra.tm.domain.enums.Status;
+import zw.qantra.tm.domain.models.Transaction;
+import zw.qantra.tm.domain.models.TransactionEvent;
+import zw.qantra.tm.domain.services.TransactionEventService;
+import zw.qantra.tm.domain.services.TransactionService;
+import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory;
+import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
+import zw.qantra.tm.domain.services.processors.handlers.*;
+
+@Service("confirm")
+@RequiredArgsConstructor
+public class ConfirmHandler implements HandlerInterface {
+ private final TransactionEventService transactionEventService;
+ private final PaymentProcessorFactory paymentProcessorFactory;
+ private final TransactionService transactionService;
+
+ @Override
+ public Object process(Transaction transaction) throws Exception {
+ transactionService.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());
+ transactionService.save(transaction);
+ return transaction;
+ }
+
+ transactionEvent.setStatus(transaction.getStatus());
+ transactionEvent.setMessage(transaction.getErrorMessage());
+ transactionEventService.save(transactionEvent);
+
+ return transaction;
+ }
+}
diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/HandlerInterface.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/HandlerInterface.java
new file mode 100644
index 0000000..9bc52e4
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/HandlerInterface.java
@@ -0,0 +1,7 @@
+package zw.qantra.tm.domain.workflows.handlers;
+
+import zw.qantra.tm.domain.models.Transaction;
+
+public interface HandlerInterface {
+ Object process(Transaction transaction) throws Exception;
+}
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/PaymentHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/PaymentHandler.java
new file mode 100644
index 0000000..b423faa
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/PaymentHandler.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.Status;
+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.exceptions.ApiException;
+
+@Service("gatewayPayment")
+@RequiredArgsConstructor
+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 TransactionService transactionService;
+
+ @Override
+ public Object process(Transaction incomingTransaction) throws Exception {
+ Transaction transaction = incomingTransaction;
+ if(incomingTransaction.getId() != null){
+ transaction = transactionService.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());
+ }
+
+ transactionService.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());
+ transactionService.save(transaction);
+ return transaction;
+ }
+
+ transactionEvent.setStatus(transaction.getStatus());
+ transactionEvent.setMessage(transaction.getErrorMessage());
+ transactionEventService.save(transactionEvent);
+
+ return transaction;
+ }
+}
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
new file mode 100644
index 0000000..88cf946
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/PollStatusHandler.java
@@ -0,0 +1,74 @@
+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.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.exceptions.ApiException;
+
+@Service("pollStatus")
+@RequiredArgsConstructor
+public class PollStatusHandler implements HandlerInterface {
+
+ private final PaymentProcessorFactory paymentProcessorFactory;
+ private final TransactionEventService transactionEventService;
+ private final TransactionService transactionService;
+
+
+ @Override
+ public Object process(Transaction transaction) throws Exception {
+ Logger logger = LoggerFactory.getLogger(TransactionService.class);
+
+ // 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());
+
+ // 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();
+ transaction.setStatus(Status.FAILED);
+ transaction.setResponseCode("01");
+ transaction.setErrorMessage(e.getMessage());
+ }
+
+ transactionService.save(transaction);
+ return transaction;
+ }
+}
diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/WorkflowHandlerFactory.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/WorkflowHandlerFactory.java
new file mode 100644
index 0000000..492e078
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/WorkflowHandlerFactory.java
@@ -0,0 +1,43 @@
+package zw.qantra.tm.domain.workflows.handlers;
+
+import lombok.RequiredArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+import zw.qantra.tm.exceptions.ApiException;
+
+@Component
+@RequiredArgsConstructor
+public class WorkflowHandlerFactory implements ApplicationContextAware {
+ private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
+
+ private ApplicationContext applicationContext;
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ this.applicationContext = applicationContext;
+ }
+
+ public Object getWorkflowHandler(String beanName) {
+ Object handler;
+
+ try {
+ handler = applicationContext.getBean(beanName);
+ } catch (NoSuchBeanDefinitionException exception) {
+ exception.printStackTrace();
+ logger.error("Couldn't find handler: {}", exception.getMessage());
+ throw new ApiException("Couldn't find handler");
+ } catch (BeansException e) {
+ e.printStackTrace();
+ logger.error("Error retrieving handler bean: {}", e.getMessage());
+ throw new ApiException("Error retrieving handler bean");
+ }
+
+ return handler;
+ }
+
+}
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 94%
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 551c73a..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;
diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PaymentEntryHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PaymentEntryHandler.java
similarity index 93%
rename from src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PaymentEntryHandler.java
rename to src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PaymentEntryHandler.java
index efdd8e7..a50f0ea 100644
--- a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PaymentEntryHandler.java
+++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PaymentEntryHandler.java
@@ -1,9 +1,8 @@
-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;
@@ -17,9 +16,9 @@ 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;
@@ -27,9 +26,9 @@ import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.*;
-@Service
+@Service("salesPayment")
@RequiredArgsConstructor
-public class PaymentEntryHandler implements LogicHandler{
+public class PaymentEntryHandler implements HandlerInterface {
Logger logger = LoggerFactory.getLogger(PaymentEntryHandler.class);
private final RestService restService;
@@ -97,9 +96,7 @@ public class PaymentEntryHandler implements LogicHandler{
+public class PurchaseInvoiceHandler implements HandlerInterface {
Logger logger = LoggerFactory.getLogger(PurchaseInvoiceHandler.class);
private final RestService restService;
diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PurchasePaymentEntryHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PurchasePaymentEntryHandler.java
similarity index 96%
rename from src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PurchasePaymentEntryHandler.java
rename to src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PurchasePaymentEntryHandler.java
index ae53700..4a3e276 100644
--- a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/PurchasePaymentEntryHandler.java
+++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PurchasePaymentEntryHandler.java
@@ -1,9 +1,8 @@
-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;
@@ -19,9 +18,9 @@ import zw.qantra.tm.domain.models.User;
import zw.qantra.tm.domain.repositories.TransactionRepository;
import zw.qantra.tm.domain.services.ProviderService;
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;
@@ -30,9 +29,9 @@ import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.*;
-@Service
+@Service("purchaseInvoicePayment")
@RequiredArgsConstructor
-public class PurchasePaymentEntryHandler implements LogicHandler{
+public class PurchasePaymentEntryHandler implements HandlerInterface {
Logger logger = LoggerFactory.getLogger(PurchasePaymentEntryHandler.class);
private final RestService restService;
diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/SalesInvoiceHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/SalesInvoiceHandler.java
new file mode 100644
index 0000000..f86d0fc
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/SalesInvoiceHandler.java
@@ -0,0 +1,142 @@
+package zw.qantra.tm.domain.workflows.handlers.erp;
+
+import lombok.RequiredArgsConstructor;
+import org.joda.time.LocalDate;
+import org.joda.time.LocalDateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+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;
+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("salesInvoice")
+@RequiredArgsConstructor
+public class SalesInvoiceHandler implements HandlerInterface {
+ Logger logger = LoggerFactory.getLogger(SalesInvoiceHandler.class);
+
+ private final RestService restService;
+ private final UserService userService;
+ private final TransactionEventService transactionEventService;
+ private final TransactionService transactionService;
+
+ @Value("${erpnext.url}")
+ private String erpnextUrl;
+ @Value("${erpnext.api.key}")
+ private String erpnextApiKey;
+ @Value("${erpnext.api.secret}")
+ private String erpnextApiSecret;
+ @Value("${erpnext.username}")
+ private String erpnextUsername;
+ @Value("${erpnext.password}")
+ private String erpnextPassword;
+ @Value("${erpnext.company}")
+ private String erpnextCompany;
+
+ @Override
+ @Transactional
+ public Transaction process(Transaction transaction) {
+ logger.info("Processing sales invoice for transaction: {}", transaction.getReference());
+ TransactionEvent transactionEvent = TransactionEvent.builder()
+ .transactionId(transaction.getId().toString())
+ .event(RequestType.SALES_INVOICE.name())
+ .status(Status.PROCESSING)
+ .build();
+ transactionEventService.save(transactionEvent);
+
+ try {
+ logger.info("Creating sales invoice in ERPNext for successful transaction");
+
+ // 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);
+
+ 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 transactionService.save(transaction);
+ }
+}
diff --git a/src/main/resources/application-vusa.properties b/src/main/resources/application-vusa.properties
index a7af74a..b283d84 100644
--- a/src/main/resources/application-vusa.properties
+++ b/src/main/resources/application-vusa.properties
@@ -1,2 +1,12 @@
# sbz.merchant.url=http://localhost:24000/v1
sbz.merchant.url=https://api.stewardpay.co.zw/lab/v2
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/qpay?useLegacyDatetimeCode=false
+spring.datasource.username=postgres
+spring.datasource.password=example
+
+
+nflow.db.postgresql.driver=org.postgresql.Driver
+nflow.db.postgresql.url=jdbc:postgresql://localhost:5432/qpay
+nflow.db.postgresql.user=postgres
+nflow.db.postgresql.password=example
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 93d5e33..bb0365b 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,4 +1,5 @@
spring.application.name=tm
+spring.main.allow-bean-definition-overriding=true
debug=false
logging.level.org.springframework.boot.autoconfigure=INFO