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