progress
This commit is contained in:
@@ -1,24 +1,27 @@
|
|||||||
package zw.qantra.tm.configs;
|
package zw.qantra.tm.configs;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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.jdk8.Jdk8Module;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import com.fasterxml.jackson.datatype.joda.JodaModule;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.context.annotation.Primary;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Configuration
|
@Service
|
||||||
public class JacksonConfig {
|
public class JacksonConfig {
|
||||||
|
@Autowired
|
||||||
@Bean
|
private ObjectMapper nflowObjectMapper;
|
||||||
@Primary
|
|
||||||
public ObjectMapper objectMapper() {
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
objectMapper.registerModule(new JavaTimeModule());
|
|
||||||
objectMapper.registerModule(new JodaModule());
|
|
||||||
objectMapper.registerModule(new Jdk8Module());
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
src/main/java/zw/qantra/tm/configs/NflowConfiguration.java
Normal file
79
src/main/java/zw/qantra/tm/configs/NflowConfiguration.java
Normal file
@@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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<Module> customModules) {
|
||||||
|
builder.modules(customModules);
|
||||||
|
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the default ObjectMapper used by the nFlow REST API.
|
||||||
|
* <p>
|
||||||
|
* This bean definition ensures that the nFlow REST endpoints use the same consistent,
|
||||||
|
* primary ObjectMapper as the rest of the application.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Bean("nflowRestObjectMapper")
|
||||||
|
public ObjectMapper nflowRestObjectMapper(ObjectMapper primaryObjectMapper) {
|
||||||
|
return primaryObjectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the default ObjectMapper used by the nFlow Engine for variable serialization.
|
||||||
|
* <p>
|
||||||
|
* This is the key fix for `InvalidDefinitionException` with `LocalDateTime` in workflow variables.
|
||||||
|
* It ensures the engine uses our fully configured primary ObjectMapper.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ public class SecurityConfig {
|
|||||||
http
|
http
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/configs/seed/**").permitAll()
|
||||||
.requestMatchers("/test/**").permitAll()
|
.requestMatchers("/test/**").permitAll()
|
||||||
.requestMatchers("/explorer/**").permitAll()
|
.requestMatchers("/explorer/**").permitAll()
|
||||||
.requestMatchers("/nflow/**").permitAll()
|
.requestMatchers("/nflow/**").permitAll()
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ public class OnboardingController {
|
|||||||
.setType(RegistrationWorkflow.TYPE)
|
.setType(RegistrationWorkflow.TYPE)
|
||||||
.setExternalId(request.getUsername() + "-" + new Instant().getMillis())
|
.setExternalId(request.getUsername() + "-" + new Instant().getMillis())
|
||||||
.putStateVariable("body", request)
|
.putStateVariable("body", request)
|
||||||
|
.setNextActivation(DateTime.now())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
|
long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
package zw.qantra.tm.domain.controllers;
|
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 lombok.RequiredArgsConstructor;
|
||||||
import net.kaczmarzyk.spring.data.jpa.domain.Equal;
|
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.Or;
|
||||||
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
||||||
import net.kaczmarzyk.spring.data.jpa.web.annotation.Conjunction;
|
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.jpa.domain.Specification;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -15,9 +23,15 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
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.LogUtils;
|
||||||
|
import zw.qantra.tm.utils.Utils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/public/transaction")
|
@RequestMapping("/public/transaction")
|
||||||
@@ -26,6 +40,10 @@ public class TransactonController {
|
|||||||
Logger logger = LoggerFactory.getLogger(TransactonController.class);
|
Logger logger = LoggerFactory.getLogger(TransactonController.class);
|
||||||
|
|
||||||
private final TransactionService transactionService;
|
private final TransactionService transactionService;
|
||||||
|
private final WorkflowInstanceService workflowInstanceService;
|
||||||
|
private final WorkflowInstanceFactory workflowInstanceFactory;
|
||||||
|
private final WorkflowUtils workflowUtils;
|
||||||
|
private final ObjectStringMapper objectStringMapper;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<Page<Transaction>> getAll(
|
public ResponseEntity<Page<Transaction>> getAll(
|
||||||
@@ -64,26 +82,80 @@ public class TransactonController {
|
|||||||
return ResponseEntity.ok(transactionService.findById(id));
|
return ResponseEntity.ok(transactionService.findById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/poll/{id}")
|
@GetMapping("/poll/{workflowId}")
|
||||||
public ResponseEntity<Transaction> poll(@PathVariable UUID id) {
|
public ResponseEntity<Object> poll(@PathVariable String workflowId) throws ExecutionException, InterruptedException {
|
||||||
return ResponseEntity.ok(transactionService.poll(id));
|
// Create an update to move the workflow to the next state
|
||||||
|
WorkflowInstance update = new WorkflowInstance.Builder()
|
||||||
|
.setId(Long.parseLong(workflowId))
|
||||||
|
.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<Object> future = workflowUtils
|
||||||
|
.waitForState(Long.parseLong(workflowId), new String[]{"done", "failed"},
|
||||||
|
15000, 50);
|
||||||
|
Object response = future.get();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/request")
|
@PostMapping("/request")
|
||||||
public ResponseEntity<Transaction> request(@RequestBody Transaction transaction) {
|
public ResponseEntity<Object> request(@RequestBody Transaction transaction) throws ExecutionException, InterruptedException {
|
||||||
LogUtils.logPrettyJson(logger, "incoming transaction", transaction);
|
LogUtils.logPrettyJson(logger, "incoming transaction", transaction);
|
||||||
Transaction createdTransaction = transactionService.request(transaction);
|
// Create an update to move the workflow to the next state
|
||||||
LogUtils.logPrettyJson(logger, "outgoing transaction", createdTransaction);
|
WorkflowInstance update = new WorkflowInstance.Builder()
|
||||||
return ResponseEntity.ok(createdTransaction);
|
.setId(transaction.getWorkflowId())
|
||||||
|
.setState("salesInvoice") // The state you want to transition to
|
||||||
|
.setNextActivation(DateTime.now()) // Schedule immediate execution
|
||||||
|
.setStateVariables(new HashMap<>(){{ put("body", 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<Object> 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
|
@PostMapping
|
||||||
public ResponseEntity<Transaction> create(@RequestBody Transaction transaction) {
|
public ResponseEntity<Object> confirm(@RequestBody Transaction transaction) throws ExecutionException, InterruptedException {
|
||||||
LogUtils.logPrettyJson(logger, "incoming transaction", transaction);
|
LogUtils.logPrettyJson(logger, "incoming transaction", transaction);
|
||||||
Transaction createdTransaction = transactionService.confirm(transaction);
|
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
|
||||||
LogUtils.logPrettyJson(logger, "outgoing transaction", createdTransaction);
|
.setType(TransactionWorkflow.TYPE)
|
||||||
return ResponseEntity.ok(createdTransaction);
|
.setExternalId(transaction.getUserId() + "-" + new Instant().getMillis())
|
||||||
|
.putStateVariable("incoming", transaction)
|
||||||
|
.setNextActivation(DateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
|
||||||
|
|
||||||
|
CompletableFuture<Object> 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}")
|
@PutMapping("/{id}")
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ public class Transaction extends BaseEntity {
|
|||||||
private String erpPurchasePaymentRef;
|
private String erpPurchasePaymentRef;
|
||||||
private String erpCommissionJournalRef;
|
private String erpCommissionJournalRef;
|
||||||
|
|
||||||
|
@Column(nullable = true)
|
||||||
|
private long workflowId;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private RequestType type;
|
private RequestType type;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
|
|||||||
transaction.setTargetUrl((String) body.get("targetUrl"));
|
transaction.setTargetUrl((String) body.get("targetUrl"));
|
||||||
|
|
||||||
LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction");
|
LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction");
|
||||||
transaction.setDebitRef((String) transactionData.get("creditReference"));
|
transaction.setDebitRef((String) transactionData.get("reference"));
|
||||||
|
|
||||||
transactionService.save(transaction);
|
transactionService.save(transaction);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
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.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;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class TransactionWorkflow extends WorkflowDefinition {
|
||||||
|
Logger log = org.slf4j.LoggerFactory.getLogger(RegistrationWorkflow.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private WorkflowHandlerFactory workflowHandlerFactory;
|
||||||
|
@Autowired
|
||||||
|
private TransactionService transactionService;
|
||||||
|
|
||||||
|
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 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 BILL_PAYMENT = new State("billPayment");
|
||||||
|
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, SALES_PAYMENT, FAILED);
|
||||||
|
permit(SALES_PAYMENT, PURCHASE_INVOICE);
|
||||||
|
permit(PURCHASE_INVOICE, BILL_PAYMENT);
|
||||||
|
permit(BILL_PAYMENT, PURCHASE_INVOICE_PAYMENT, FAILED);
|
||||||
|
permit(PURCHASE_INVOICE_PAYMENT, DONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction calculateCharges(StateExecution execution) {
|
||||||
|
try {
|
||||||
|
Transaction transaction = execution.getVariable("incoming", Transaction.class);
|
||||||
|
process(transaction, "calculateCharges");
|
||||||
|
execution.setVariable("body", transaction);
|
||||||
|
|
||||||
|
return NextAction.moveToState(CLEANUP, "Charge calculation complete");
|
||||||
|
} catch (Exception e) {
|
||||||
|
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) {
|
||||||
|
return NextAction.moveToState(FAILED, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction confirm(StateExecution execution) {
|
||||||
|
try {
|
||||||
|
Transaction transaction = execution.getVariable("body", Transaction.class);
|
||||||
|
|
||||||
|
transaction.setWorkflowId(execution.getWorkflowInstanceId());
|
||||||
|
process(transaction, "confirm");
|
||||||
|
execution.setVariable("body", Utils.toJson(transaction));
|
||||||
|
return NextAction.moveToState(CONFIRM_SUCCESS, "Cleanup complete");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return NextAction.moveToState(FAILED, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void confirmSuccess(StateExecution execution) {
|
||||||
|
log.info("Confirm successful");
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction salesInvoice(StateExecution execution) {
|
||||||
|
try {
|
||||||
|
Transaction transaction = execution.getVariable("body", Transaction.class);
|
||||||
|
|
||||||
|
process(transaction, "salesInvoice");
|
||||||
|
execution.setVariable("body", Utils.toJson(transaction));
|
||||||
|
return NextAction.moveToState(GATEWAY_PAYMENT, "Sales invoice generated");
|
||||||
|
}catch (Exception e) {
|
||||||
|
return NextAction.moveToState(FAILED, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction gatewayPayment(StateExecution execution) {
|
||||||
|
try {
|
||||||
|
Transaction transaction = execution.getVariable("body", Transaction.class);
|
||||||
|
|
||||||
|
transaction = process(transaction, "gatewayPayment");
|
||||||
|
execution.setVariable("body", Utils.toJson(transaction));
|
||||||
|
return NextAction.moveToState(GATEWAY_PAYMENT_SUCCESS, "Sales invoice generated");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return NextAction.moveToState(FAILED, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void gatewayPaymentSuccess(StateExecution execution) {
|
||||||
|
log.info("Gateway payment successful");
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction pollStatus(StateExecution execution) {
|
||||||
|
try {
|
||||||
|
Transaction transaction = execution.getVariable("body", Transaction.class);
|
||||||
|
|
||||||
|
transaction = process(transaction, "pollStatus");
|
||||||
|
execution.setVariable("body", Utils.toJson(transaction));
|
||||||
|
return NextAction.moveToState(SALES_PAYMENT, "Polling complete");
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return NextAction.moveToState(FAILED, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction salesPayment(StateExecution execution) {
|
||||||
|
return NextAction.moveToState(PURCHASE_INVOICE, "Sales invoice generated");
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction purchaseInvoice(StateExecution execution) {
|
||||||
|
return NextAction.moveToState(BILL_PAYMENT, "Purchase invoice generated");
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction billPayment(StateExecution execution) {
|
||||||
|
return NextAction.moveToState(PURCHASE_INVOICE_PAYMENT, "Bill payment successful");
|
||||||
|
}
|
||||||
|
|
||||||
|
public NextAction purchaseInvoicePayment(StateExecution execution) {
|
||||||
|
return NextAction.moveToState(DONE, "Purchase invoice payment successful");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import io.nflow.engine.model.ModelObject;
|
||||||
import io.nflow.engine.service.WorkflowInstanceInclude.*;
|
import io.nflow.engine.service.WorkflowInstanceInclude.*;
|
||||||
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -55,12 +56,17 @@ public class WorkflowUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public StateResponse getStateResponse(WorkflowInstance instance, String message){
|
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()
|
return StateResponse.builder()
|
||||||
.state(instance.state)
|
.state(instance.state)
|
||||||
.status(instance.status.toString())
|
.status(instance.status.toString())
|
||||||
.body(Utils.fromJson(instance.getStateVariable("body"), Map.class))
|
.body(Utils.fromJson(instance.getStateVariable("body"), Map.class))
|
||||||
.message(action.stateText)
|
.message(action != null ? action.stateText : null)
|
||||||
.workflowId(instance.id.toString())
|
.workflowId(instance.id.toString())
|
||||||
.externalId(instance.externalId)
|
.externalId(instance.externalId)
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -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<Charge> chargeList = chargeService.getChargeRepository().findAllByCurrency(transaction.getDebitCurrency());
|
||||||
|
|
||||||
|
List<Charge> appropriateCharges = new ArrayList<>();
|
||||||
|
for (Charge charge : chargeList) {
|
||||||
|
boolean isPresent = false;
|
||||||
|
for (ChargeCondition chargeCondition: charge.getIncludes()){
|
||||||
|
List<String> 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<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
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);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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.domain.services.processors.handlers.*;
|
||||||
|
import zw.qantra.tm.domain.services.processors.handlers.CalculateChargesHandler;
|
||||||
|
import zw.qantra.tm.domain.services.processors.handlers.erp.*;
|
||||||
|
import zw.qantra.tm.exceptions.ApiException;
|
||||||
|
|
||||||
|
@Service("gatewayPayment")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GatewayPaymentHandler 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 SalesInvoiceHandler salesInvoiceHandler;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
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.domain.services.processors.handlers.erp.*;
|
||||||
|
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());
|
||||||
|
transactionService.save(transaction);
|
||||||
|
|
||||||
|
// if transaction wasn't successful, throw and halt workflow
|
||||||
|
if(transaction.getStatus() != Status.SUCCESS){
|
||||||
|
throw new ApiException("Transaction failed: " + transaction.getErrorMessage() );
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
transaction.setStatus(Status.FAILED);
|
||||||
|
transaction.setResponseCode("01");
|
||||||
|
transaction.setErrorMessage(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
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.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.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 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<String, String> 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);
|
||||||
|
|
||||||
|
User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null);
|
||||||
|
if(user == null)
|
||||||
|
throw new ApiException("Could not find user");
|
||||||
|
|
||||||
|
SalesInvoiceDto salesInvoiceDto = SalesInvoiceDto.builder()
|
||||||
|
.doctype("Sales Invoice")
|
||||||
|
.customer(user.getErpCustomerRef())
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,12 @@
|
|||||||
# sbz.merchant.url=http://localhost:24000/v1
|
# sbz.merchant.url=http://localhost:24000/v1
|
||||||
sbz.merchant.url=https://api.stewardpay.co.zw/lab/v2
|
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
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
spring.application.name=tm
|
spring.application.name=tm
|
||||||
|
spring.main.allow-bean-definition-overriding=true
|
||||||
debug=false
|
debug=false
|
||||||
|
|
||||||
logging.level.org.springframework.boot.autoconfigure=INFO
|
logging.level.org.springframework.boot.autoconfigure=INFO
|
||||||
|
|||||||
Reference in New Issue
Block a user