completed transaction workflow

This commit is contained in:
2025-10-21 23:47:15 +02:00
parent 2b6ef3ab14
commit 77a6dd69a6
12 changed files with 384 additions and 430 deletions

View File

@@ -16,6 +16,8 @@ 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;
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.services.TransactionService;
import zw.qantra.tm.domain.models.Transaction; import zw.qantra.tm.domain.models.Transaction;
@@ -82,11 +84,12 @@ public class TransactonController {
return ResponseEntity.ok(transactionService.findById(id)); return ResponseEntity.ok(transactionService.findById(id));
} }
@GetMapping("/poll/{workflowId}") @GetMapping("/poll/{id}")
public ResponseEntity<Object> poll(@PathVariable String workflowId) throws ExecutionException, InterruptedException { public ResponseEntity<Object> poll(@PathVariable UUID id) throws ExecutionException, InterruptedException {
Transaction transaction = transactionService.findById(id);
// Create an update to move the workflow to the next state // Create an update to move the workflow to the next state
WorkflowInstance update = new WorkflowInstance.Builder() WorkflowInstance update = new WorkflowInstance.Builder()
.setId(Long.parseLong(workflowId)) .setId(transaction.getWorkflowId())
.setState("pollStatus") // The state you want to transition to .setState("pollStatus") // The state you want to transition to
.setNextActivation(DateTime.now()) // Schedule immediate execution .setNextActivation(DateTime.now()) // Schedule immediate execution
.setStateText("Resumed from manual state") .setStateText("Resumed from manual state")
@@ -101,22 +104,28 @@ public class TransactonController {
workflowInstanceService.updateWorkflowInstance(update, action); workflowInstanceService.updateWorkflowInstance(update, action);
CompletableFuture<Object> future = workflowUtils CompletableFuture<Object> future = workflowUtils
.waitForState(Long.parseLong(workflowId), new String[]{"done", "failed"}, .waitForState(transaction.getWorkflowId(), new String[]{"done", "failed"},
15000, 50); 15000, 50);
Object response = future.get(); Object response = future.get();
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
@PostMapping("/request") @GetMapping("/request/{id}/{authType}")
public ResponseEntity<Object> request(@RequestBody Transaction transaction) throws ExecutionException, InterruptedException { public ResponseEntity<Object> request(@PathVariable UUID id, @PathVariable AuthType authType)
LogUtils.logPrettyJson(logger, "incoming transaction", transaction); 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 // Create an update to move the workflow to the next state
WorkflowInstance update = new WorkflowInstance.Builder() WorkflowInstance update = new WorkflowInstance.Builder()
.setId(transaction.getWorkflowId()) .setId(transaction.getWorkflowId())
.setState("salesInvoice") // The state you want to transition to .setState("salesInvoice") // The state you want to transition to
.setNextActivation(DateTime.now()) // Schedule immediate execution .setNextActivation(DateTime.now()) // Schedule immediate execution
.setStateVariables(new HashMap<>(){{ put("body", Utils.toJson(transaction)); }}) .setStateVariables(new HashMap<>(){{ put("request", Utils.toJson(transaction)); }})
.setStateText("Resumed from manual state") .setStateText("Resumed from manual state")
.build(); .build();
@@ -144,7 +153,7 @@ public class TransactonController {
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder() var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
.setType(TransactionWorkflow.TYPE) .setType(TransactionWorkflow.TYPE)
.setExternalId(transaction.getUserId() + "-" + new Instant().getMillis()) .setExternalId(transaction.getUserId() + "-" + new Instant().getMillis())
.putStateVariable("incoming", transaction) .putStateVariable("confirmation", transaction)
.setNextActivation(DateTime.now()) .setNextActivation(DateTime.now())
.build(); .build();

View File

@@ -40,230 +40,8 @@ public class TransactionService {
@Getter @Getter
private final TransactionRepository transactionRepository; private final TransactionRepository transactionRepository;
private final PaymentProcessorFactory paymentProcessorFactory;
private final ProviderService providerService;
private final ChargeService chargeService;
private final AdditionalDataService additionalDataService; 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<Transaction, Transaction> 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<Transaction, Transaction> 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<Transaction, Transaction> 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<Transaction, Transaction> 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<Transaction, Transaction> 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<Transaction, Transaction> 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) { public Transaction findById(UUID id) {
Transaction transaction = transactionRepository.findById(id).orElseThrow(() -> new ApiException("Transaction not found for ID: " + id)); Transaction transaction = transactionRepository.findById(id).orElseThrow(() -> new ApiException("Transaction not found for ID: " + id));

View File

@@ -6,6 +6,7 @@ import io.nflow.engine.workflow.definition.NextAction;
import io.nflow.engine.workflow.definition.StateExecution; import io.nflow.engine.workflow.definition.StateExecution;
import io.nflow.engine.workflow.definition.WorkflowDefinition; import io.nflow.engine.workflow.definition.WorkflowDefinition;
import io.nflow.engine.workflow.definition.WorkflowStateType; import io.nflow.engine.workflow.definition.WorkflowStateType;
import org.joda.time.DateTime;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -16,6 +17,8 @@ import zw.qantra.tm.domain.workflows.handlers.HandlerInterface;
import zw.qantra.tm.domain.workflows.handlers.WorkflowHandlerFactory; import zw.qantra.tm.domain.workflows.handlers.WorkflowHandlerFactory;
import zw.qantra.tm.utils.Utils; import zw.qantra.tm.utils.Utils;
import java.util.LinkedHashMap;
@Component @Component
public class TransactionWorkflow extends WorkflowDefinition { public class TransactionWorkflow extends WorkflowDefinition {
Logger log = org.slf4j.LoggerFactory.getLogger(RegistrationWorkflow.class); Logger log = org.slf4j.LoggerFactory.getLogger(RegistrationWorkflow.class);
@@ -24,6 +27,8 @@ public class TransactionWorkflow extends WorkflowDefinition {
private WorkflowHandlerFactory workflowHandlerFactory; private WorkflowHandlerFactory workflowHandlerFactory;
@Autowired @Autowired
private TransactionService transactionService; private TransactionService transactionService;
@Autowired
private ObjectMapper objectMapper;
public static final String TYPE = "transactionWorkflow"; public static final String TYPE = "transactionWorkflow";
@@ -34,10 +39,11 @@ public class TransactionWorkflow extends WorkflowDefinition {
public static final State SALES_INVOICE = new State("salesInvoice"); public static final State SALES_INVOICE = new State("salesInvoice");
public static final State GATEWAY_PAYMENT = new State("gatewayPayment"); 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 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 POLL_STATUS = new State("pollStatus");
public static final State SALES_PAYMENT = new State("salesPayment"); public static final State SALES_PAYMENT = new State("salesPayment");
public static final State PURCHASE_INVOICE = new State("purchaseInvoice"); public static final State PURCHASE_INVOICE = new State("purchaseInvoice");
public static final State BILL_PAYMENT = new State("billPayment"); public static final State INTEGRATION = new State("integration");
public static final State PURCHASE_INVOICE_PAYMENT = new State("purchaseInvoicePayment"); public static final State PURCHASE_INVOICE_PAYMENT = new State("purchaseInvoicePayment");
public static final State FAILED = new State("failed", WorkflowStateType.manual); public static final State FAILED = new State("failed", WorkflowStateType.manual);
@@ -53,21 +59,23 @@ public class TransactionWorkflow extends WorkflowDefinition {
permit(SALES_INVOICE, GATEWAY_PAYMENT); permit(SALES_INVOICE, GATEWAY_PAYMENT);
permit(GATEWAY_PAYMENT, GATEWAY_PAYMENT_SUCCESS, FAILED); permit(GATEWAY_PAYMENT, GATEWAY_PAYMENT_SUCCESS, FAILED);
permit(GATEWAY_PAYMENT_SUCCESS, POLL_STATUS); permit(GATEWAY_PAYMENT_SUCCESS, POLL_STATUS);
permit(POLL_STATUS, SALES_PAYMENT, FAILED); permit(POLL_STATUS, JOURNAL_ENTRY, FAILED);
permit(JOURNAL_ENTRY, SALES_PAYMENT);
permit(SALES_PAYMENT, PURCHASE_INVOICE); permit(SALES_PAYMENT, PURCHASE_INVOICE);
permit(PURCHASE_INVOICE, BILL_PAYMENT); permit(PURCHASE_INVOICE, INTEGRATION);
permit(BILL_PAYMENT, PURCHASE_INVOICE_PAYMENT, FAILED); permit(INTEGRATION, PURCHASE_INVOICE_PAYMENT, FAILED);
permit(PURCHASE_INVOICE_PAYMENT, DONE); permit(PURCHASE_INVOICE_PAYMENT, DONE);
} }
public NextAction calculateCharges(StateExecution execution) { public NextAction calculateCharges(StateExecution execution) {
try { try {
Transaction transaction = execution.getVariable("incoming", Transaction.class); Transaction transaction = execution.getVariable("confirmation", Transaction.class);
process(transaction, "calculateCharges"); process(transaction, "calculateCharges");
execution.setVariable("body", transaction); execution.setVariable("body", transaction);
return NextAction.moveToState(CLEANUP, "Charge calculation complete"); return NextAction.moveToState(CLEANUP, "Charge calculation complete");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace();
return NextAction.moveToState(FAILED, e.getMessage()); return NextAction.moveToState(FAILED, e.getMessage());
} }
} }
@@ -80,6 +88,7 @@ public class TransactionWorkflow extends WorkflowDefinition {
return NextAction.moveToState(CONFIRM, "Cleanup complete"); return NextAction.moveToState(CONFIRM, "Cleanup complete");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace();
return NextAction.moveToState(FAILED, e.getMessage()); return NextAction.moveToState(FAILED, e.getMessage());
} }
} }
@@ -89,73 +98,149 @@ public class TransactionWorkflow extends WorkflowDefinition {
Transaction transaction = execution.getVariable("body", Transaction.class); Transaction transaction = execution.getVariable("body", Transaction.class);
transaction.setWorkflowId(execution.getWorkflowInstanceId()); transaction.setWorkflowId(execution.getWorkflowInstanceId());
process(transaction, "confirm"); transaction = process(transaction, "confirm");
execution.setVariable("body", Utils.toJson(transaction)); execution.setVariable("body", Utils.toJson(transaction));
return NextAction.moveToState(CONFIRM_SUCCESS, "Cleanup complete"); return NextAction.moveToState(CONFIRM_SUCCESS, "Transaction confirmed successfully");
} 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) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
return NextAction.moveToState(FAILED, e.getMessage()); 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) { public NextAction salesPayment(StateExecution execution) {
return NextAction.moveToState(PURCHASE_INVOICE, "Sales invoice generated"); 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) { public NextAction purchaseInvoice(StateExecution execution) {
return NextAction.moveToState(BILL_PAYMENT, "Purchase invoice generated"); 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 billPayment(StateExecution execution) { public NextAction integration(StateExecution execution) {
return NextAction.moveToState(PURCHASE_INVOICE_PAYMENT, "Bill payment successful"); 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) { public NextAction purchaseInvoicePayment(StateExecution execution) {
return NextAction.moveToState(DONE, "Purchase invoice payment successful"); 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) { public Transaction process(Transaction transaction, String beanName) {

View File

@@ -44,6 +44,7 @@ public class CleanupHandler implements HandlerInterface {
String label = transaction.getType() + "_" + provider.getLabel(); String label = transaction.getType() + "_" + provider.getLabel();
transaction.setConfirmationProcessorLabel(label); transaction.setConfirmationProcessorLabel(label);
transaction.setProviderLabel(provider.getLabel());
logger.info("label: {}", label); logger.info("label: {}", label);
// FORMAT FIELDS CORRECTLY // FORMAT FIELDS CORRECTLY

View File

@@ -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;
}
}

View File

@@ -10,22 +10,17 @@ import zw.qantra.tm.domain.models.TransactionEvent;
import zw.qantra.tm.domain.services.*; import zw.qantra.tm.domain.services.*;
import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory; import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory;
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; 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; import zw.qantra.tm.exceptions.ApiException;
@Service("gatewayPayment") @Service("gatewayPayment")
@RequiredArgsConstructor @RequiredArgsConstructor
public class GatewayPaymentHandler implements HandlerInterface { public class PaymentHandler implements HandlerInterface {
Logger logger = LoggerFactory.getLogger(TransactionService.class); Logger logger = LoggerFactory.getLogger(TransactionService.class);
private final PaymentProcessorFactory paymentProcessorFactory; private final PaymentProcessorFactory paymentProcessorFactory;
private final ChargeService chargeService; private final ChargeService chargeService;
private final RecipientService recipientService; private final RecipientService recipientService;
private final TransactionEventService transactionEventService; private final TransactionEventService transactionEventService;
private final SalesInvoiceHandler salesInvoiceHandler;
private final TransactionService transactionService; private final TransactionService transactionService;
@Override @Override

View File

@@ -54,11 +54,13 @@ public class PollStatusHandler implements HandlerInterface {
transactionEventService.save(transactionEvent); transactionEventService.save(transactionEvent);
transaction.setPollingStatus(transaction.getStatus()); transaction.setPollingStatus(transaction.getStatus());
transactionService.save(transaction);
// if transaction wasn't successful, throw and halt workflow // if transaction wasn't successful, throw and halt workflow
if(transaction.getStatus() != Status.SUCCESS){ if(transaction.getStatus() != Status.SUCCESS){
throw new ApiException("Transaction failed: " + transaction.getErrorMessage() ); throw new ApiException("Transaction failed: " + transaction.getErrorMessage() );
}else {
transaction.setPaymentStatus(transaction.getStatus());
transaction.setErrorMessage(null);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@@ -67,6 +69,7 @@ public class PollStatusHandler implements HandlerInterface {
transaction.setErrorMessage(e.getMessage()); transaction.setErrorMessage(e.getMessage());
} }
transactionService.save(transaction);
return transaction; return transaction;
} }
} }

View File

@@ -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 lombok.RequiredArgsConstructor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.dtos.erp.JournalEntryAccountDto; import zw.qantra.tm.domain.dtos.erp.JournalEntryAccountDto;
import zw.qantra.tm.domain.dtos.erp.JournalEntryDto; 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.RequestType;
import zw.qantra.tm.domain.enums.Status; import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Transaction; import zw.qantra.tm.domain.models.Transaction;
@@ -19,18 +16,18 @@ import zw.qantra.tm.domain.models.TransactionEvent;
import zw.qantra.tm.domain.models.User; import zw.qantra.tm.domain.models.User;
import zw.qantra.tm.domain.repositories.TransactionRepository; import zw.qantra.tm.domain.repositories.TransactionRepository;
import zw.qantra.tm.domain.services.TransactionEventService; 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.UserService;
import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; 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.exceptions.ApiException;
import zw.qantra.tm.rest.RestService; import zw.qantra.tm.rest.RestService;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
@Service @Service("journalEntry")
@RequiredArgsConstructor @RequiredArgsConstructor
public class JournalEntryHandler implements LogicHandler<Transaction, Transaction>{ public class JournalEntryHandler implements HandlerInterface {
Logger logger = LoggerFactory.getLogger(JournalEntryHandler.class); Logger logger = LoggerFactory.getLogger(JournalEntryHandler.class);
private final RestService restService; private final RestService restService;
@@ -100,10 +97,6 @@ public class JournalEntryHandler implements LogicHandler<Transaction, Transactio
headers.add("Authorization", basicAuth); headers.add("Authorization", basicAuth);
headers.add("Cookie", sid); 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");
JournalEntryAccountDto expenseEntry = JournalEntryAccountDto.builder() JournalEntryAccountDto expenseEntry = JournalEntryAccountDto.builder()
.doctype("Journal Entry Account") .doctype("Journal Entry Account")
.account(gatewayExpenseAccount) .account(gatewayExpenseAccount)

View File

@@ -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 lombok.RequiredArgsConstructor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity; 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.models.User;
import zw.qantra.tm.domain.repositories.TransactionRepository; import zw.qantra.tm.domain.repositories.TransactionRepository;
import zw.qantra.tm.domain.services.TransactionEventService; 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.UserService;
import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; 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.exceptions.ApiException;
import zw.qantra.tm.rest.RestService; import zw.qantra.tm.rest.RestService;
@@ -27,9 +26,9 @@ import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
@Service @Service("salesPayment")
@RequiredArgsConstructor @RequiredArgsConstructor
public class PaymentEntryHandler implements LogicHandler<Transaction, Transaction>{ public class PaymentEntryHandler implements HandlerInterface {
Logger logger = LoggerFactory.getLogger(PaymentEntryHandler.class); Logger logger = LoggerFactory.getLogger(PaymentEntryHandler.class);
private final RestService restService; private final RestService restService;
@@ -95,9 +94,9 @@ public class PaymentEntryHandler implements LogicHandler<Transaction, Transactio
headers.add("Authorization", basicAuth); headers.add("Authorization", basicAuth);
headers.add("Cookie", sid); headers.add("Cookie", sid);
String erpCustomerRef = "";
User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null); User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null);
if(user == null) erpCustomerRef = (user == null) ? "Anonymous" : user.getErpCustomerRef();
throw new ApiException("Could not find user");
BigDecimal amount = transaction.getAmount().add(transaction.getGatewayCharge()); BigDecimal amount = transaction.getAmount().add(transaction.getGatewayCharge());
@@ -112,13 +111,13 @@ public class PaymentEntryHandler implements LogicHandler<Transaction, Transactio
.company(erpnextCompany) .company(erpnextCompany)
.docstatus(1) .docstatus(1)
.paymentType("Receive") .paymentType("Receive")
.party(user.getErpCustomerRef()) .party(erpCustomerRef)
.partyType("Customer") .partyType("Customer")
.paidFrom(accountsReceivableAccount) .paidFrom(accountsReceivableAccount)
.paidTo(cashAccount) .paidTo(cashAccount)
.paidAmount(amount) .paidAmount(amount)
.receivedAmount(amount) .receivedAmount(amount)
.referenceNo(transaction.getDebitRef()) .referenceNo(transaction.getSdkActionId())
.referenceDate(transaction.getCreatedAt().toLocalDate().toString()) .referenceDate(transaction.getCreatedAt().toLocalDate().toString())
.postingDate(transaction.getCreatedAt().toLocalDate().toString()) .postingDate(transaction.getCreatedAt().toLocalDate().toString())
.references(Arrays.asList(paymentReferenceDto)) .references(Arrays.asList(paymentReferenceDto))

View File

@@ -1,14 +1,15 @@
package zw.qantra.tm.domain.services.processors.handlers.erp; package zw.qantra.tm.domain.workflows.handlers.erp;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.dtos.erp.*; import zw.qantra.tm.domain.dtos.erp.PurchaseInvoiceDto;
import zw.qantra.tm.domain.dtos.erp.PurchaseInvoiceItemDto;
import zw.qantra.tm.domain.dtos.erp.PurchaseTaxDto;
import zw.qantra.tm.domain.enums.RequestType; import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.enums.Status; import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Provider; import zw.qantra.tm.domain.models.Provider;
@@ -18,9 +19,9 @@ import zw.qantra.tm.domain.models.User;
import zw.qantra.tm.domain.repositories.TransactionRepository; import zw.qantra.tm.domain.repositories.TransactionRepository;
import zw.qantra.tm.domain.services.ProviderService; import zw.qantra.tm.domain.services.ProviderService;
import zw.qantra.tm.domain.services.TransactionEventService; 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.UserService;
import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; 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.exceptions.ApiException;
import zw.qantra.tm.rest.RestService; import zw.qantra.tm.rest.RestService;
@@ -29,9 +30,9 @@ import java.math.RoundingMode;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
@Service @Service("purchaseInvoice")
@RequiredArgsConstructor @RequiredArgsConstructor
public class PurchaseInvoiceHandler implements LogicHandler<Transaction, Transaction>{ public class PurchaseInvoiceHandler implements HandlerInterface {
Logger logger = LoggerFactory.getLogger(PurchaseInvoiceHandler.class); Logger logger = LoggerFactory.getLogger(PurchaseInvoiceHandler.class);
private final RestService restService; private final RestService restService;
@@ -98,10 +99,6 @@ public class PurchaseInvoiceHandler implements LogicHandler<Transaction, Transac
headers.add("Authorization", basicAuth); headers.add("Authorization", basicAuth);
headers.add("Cookie", sid); 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");
PurchaseInvoiceItemDto purchaseInvoiceItemDto = PurchaseInvoiceItemDto.builder() PurchaseInvoiceItemDto purchaseInvoiceItemDto = PurchaseInvoiceItemDto.builder()
.doctype("Purchase Invoice Item") .doctype("Purchase Invoice Item")
.uom("Nos") .uom("Nos")

View File

@@ -1,42 +1,44 @@
package zw.qantra.tm.domain.services.processors.handlers.erp; package zw.qantra.tm.domain.workflows.handlers.erp;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.dtos.erp.PaymentEntryDto;
import zw.qantra.tm.domain.dtos.erp.SalesInvoiceDto; import zw.qantra.tm.domain.dtos.erp.PaymentReferenceDto;
import zw.qantra.tm.domain.dtos.erp.SalesInvoiceItemDto;
import zw.qantra.tm.domain.enums.RequestType; import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.enums.Status; 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.Transaction;
import zw.qantra.tm.domain.models.TransactionEvent; import zw.qantra.tm.domain.models.TransactionEvent;
import zw.qantra.tm.domain.models.User; import zw.qantra.tm.domain.models.User;
import zw.qantra.tm.domain.repositories.TransactionRepository; 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.TransactionEventService;
import zw.qantra.tm.domain.services.TransactionService;
import zw.qantra.tm.domain.services.UserService; import zw.qantra.tm.domain.services.UserService;
import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; 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.exceptions.ApiException;
import zw.qantra.tm.rest.RestService; import zw.qantra.tm.rest.RestService;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
@Service @Service("purchaseInvoicePayment")
@RequiredArgsConstructor @RequiredArgsConstructor
public class SalesInvoiceHandler implements LogicHandler<Transaction, Transaction>{ public class PurchasePaymentEntryHandler implements HandlerInterface {
Logger logger = LoggerFactory.getLogger(SalesInvoiceHandler.class); Logger logger = LoggerFactory.getLogger(PurchasePaymentEntryHandler.class);
private final RestService restService; private final RestService restService;
private final UserService userService; private final UserService userService;
private final TransactionEventService transactionEventService; private final TransactionEventService transactionEventService;
private final TransactionRepository transactionRepository; private final TransactionRepository transactionRepository;
private final ProviderService providerService;
@Value("${erpnext.url}") @Value("${erpnext.url}")
private String erpnextUrl; private String erpnextUrl;
@@ -50,20 +52,26 @@ public class SalesInvoiceHandler implements LogicHandler<Transaction, Transactio
private String erpnextPassword; private String erpnextPassword;
@Value("${erpnext.company}") @Value("${erpnext.company}")
private String erpnextCompany; private String erpnextCompany;
@Value("${erpnext.accounts.accountsreceivable}")
private String accountsReceivableAccount;
@Value("${erpnext.accounts.cash}")
private String cashAccount;
@Value("${erpnext.accounts.payable}")
private String payableAccount;
@Override @Override
public Transaction process(Transaction transaction) { public Transaction process(Transaction transaction) {
logger.info("Processing sales invoice for transaction: {}", transaction.getReference()); logger.info("Processing purchase payment entry for transaction: {}", transaction.getReference());
TransactionEvent transactionEvent = TransactionEvent.builder() TransactionEvent transactionEvent = TransactionEvent.builder()
.transactionId(transaction.getId().toString()) .transactionId(transaction.getId().toString())
.event(RequestType.SALES_INVOICE.name()) .event(RequestType.PURCHASE_PAYMENT.name())
.status(Status.PROCESSING) .status(Status.PROCESSING)
.build(); .build();
transactionEventService.save(transactionEvent); transactionEventService.save(transactionEvent);
if(transaction.getStatus() == Status.PENDING){ if(transaction.getStatus() == Status.SUCCESS){
try { try {
logger.info("Creating sales invoice in ERPNext for successful transaction"); logger.info("Creating purchase payment entry in ERPNext for successful transaction");
// login to erp // login to erp
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
@@ -92,49 +100,63 @@ public class SalesInvoiceHandler implements LogicHandler<Transaction, Transactio
headers.add("Authorization", basicAuth); headers.add("Authorization", basicAuth);
headers.add("Cookie", sid); headers.add("Cookie", sid);
User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null); Provider provider = providerService.getProviderRepository()
if(user == null) .findByClientId(transaction.getBillClientId());
throw new ApiException("Could not find user"); BigDecimal commissionPercentage = BigDecimal.valueOf(provider.getPercentageCommission())
.divide(BigDecimal.valueOf(100), 2, RoundingMode.CEILING);
BigDecimal commission = transaction.getAmount().multiply(commissionPercentage)
.setScale(2, RoundingMode.CEILING);
BigDecimal amount = transaction.getAmount().subtract(commission);
SalesInvoiceDto salesInvoiceDto = SalesInvoiceDto.builder() PaymentReferenceDto paymentReferenceDto = PaymentReferenceDto.builder()
.doctype("Sales Invoice") .referenceDoctype("Purchase Invoice")
.customer(user.getErpCustomerRef()) .referenceName(transaction.getErpPurchaseRef())
.postingDate(transaction.getCreatedAt().toLocalDate().toString()) .allocatedAmount(amount)
.dueDate(transaction.getCreatedAt().toLocalDate().toString()) .build();
String refNo = transaction.getCreditRef().isBlank() ? transaction.getId().toString() : transaction.getCreditRef();
PaymentEntryDto paymentEntryDto = PaymentEntryDto.builder()
.doctype("Payment Entry")
.company(erpnextCompany) .company(erpnextCompany)
.docstatus(1) .docstatus(1)
.items(Arrays.asList(SalesInvoiceItemDto.builder() .paymentType("Pay")
.itemCode(transaction.getProviderLabel()) .party("Steward Bank")
.qty(1) .partyType("Supplier")
.rate(transaction.getAmount().add(transaction.getGatewayCharge())) .paidFrom(cashAccount)
.amount(transaction.getAmount().add(transaction.getGatewayCharge())) .paidTo(payableAccount)
.build())) .paidAmount(amount)
.receivedAmount(amount)
.referenceNo(refNo)
.referenceDate(transaction.getCreatedAt().toLocalDate().toString())
.postingDate(transaction.getCreatedAt().toLocalDate().toString())
.references(Arrays.asList(paymentReferenceDto))
.build(); .build();
try { try {
LinkedHashMap response = restService.postAs( LinkedHashMap response = restService.postAs(
erpnextUrl + "/v2/document/Sales Invoice", salesInvoiceDto, headers, LinkedHashMap.class); erpnextUrl + "/v2/document/Payment Entry", paymentEntryDto, headers, LinkedHashMap.class);
LinkedHashMap data = (LinkedHashMap) response.get("data"); LinkedHashMap data = (LinkedHashMap) response.get("data");
String transactionId = (String) data.get("name"); String transactionId = (String) data.get("name");
transaction.setErpSalesRef(transactionId); transaction.setErpPurchasePaymentRef(transactionId);
transactionEvent.setStatus(Status.SUCCESS); transactionEvent.setStatus(Status.SUCCESS);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
throw new ApiException("Failed to create sales invoice in ERPNext"); throw new ApiException("Failed to create purchase payment entry in ERPNext");
} }
logger.info("Sales invoice created successfully in ERPNext"); logger.info("purchase payment entry created successfully in ERPNext");
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed to create sales invoice in ERPNext: {}", e.getMessage(), e); logger.error("Failed to create purchase payment entry in ERPNext: {}", e.getMessage(), e);
// Don't fail the transaction, just log the error // Don't fail the transaction, just log the error
// The ERP invoice creation is a side effect, not critical to the payment flow // The ERP invoice creation is a side effect, not critical to the payment flow
transactionEvent.setStatus(Status.FAILED); transactionEvent.setStatus(Status.FAILED);
transactionEvent.setMessage(e.getMessage()); transactionEvent.setMessage(e.getMessage());
} }
} else { } else {
logger.info("Transaction status is not SUCCESS ({}), skipping sales invoice creation", transaction.getStatus()); logger.info("Transaction status is not SUCCESS ({}), skipping purchase payment entry creation", transaction.getStatus());
} }
transactionEventService.save(transactionEvent); transactionEventService.save(transactionEvent);

View File

@@ -1,12 +1,15 @@
package zw.qantra.tm.domain.workflows.handlers.erp; package zw.qantra.tm.domain.workflows.handlers.erp;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; 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.SalesInvoiceDto;
import zw.qantra.tm.domain.dtos.erp.SalesInvoiceItemDto; import zw.qantra.tm.domain.dtos.erp.SalesInvoiceItemDto;
import zw.qantra.tm.domain.enums.RequestType; import zw.qantra.tm.domain.enums.RequestType;
@@ -16,6 +19,7 @@ import zw.qantra.tm.domain.models.TransactionEvent;
import zw.qantra.tm.domain.models.User; import zw.qantra.tm.domain.models.User;
import zw.qantra.tm.domain.repositories.TransactionRepository; import zw.qantra.tm.domain.repositories.TransactionRepository;
import zw.qantra.tm.domain.services.TransactionEventService; 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.UserService;
import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; import zw.qantra.tm.domain.services.processors.handlers.LogicHandler;
import zw.qantra.tm.domain.workflows.handlers.HandlerInterface; import zw.qantra.tm.domain.workflows.handlers.HandlerInterface;
@@ -33,7 +37,7 @@ public class SalesInvoiceHandler implements HandlerInterface {
private final RestService restService; private final RestService restService;
private final UserService userService; private final UserService userService;
private final TransactionEventService transactionEventService; private final TransactionEventService transactionEventService;
private final TransactionRepository transactionRepository; private final TransactionService transactionService;
@Value("${erpnext.url}") @Value("${erpnext.url}")
private String erpnextUrl; private String erpnextUrl;
@@ -49,6 +53,7 @@ public class SalesInvoiceHandler implements HandlerInterface {
private String erpnextCompany; private String erpnextCompany;
@Override @Override
@Transactional
public Transaction process(Transaction transaction) { public Transaction process(Transaction transaction) {
logger.info("Processing sales invoice for transaction: {}", transaction.getReference()); logger.info("Processing sales invoice for transaction: {}", transaction.getReference());
TransactionEvent transactionEvent = TransactionEvent.builder() TransactionEvent transactionEvent = TransactionEvent.builder()
@@ -58,7 +63,6 @@ public class SalesInvoiceHandler implements HandlerInterface {
.build(); .build();
transactionEventService.save(transactionEvent); transactionEventService.save(transactionEvent);
if(transaction.getStatus() == Status.PENDING){
try { try {
logger.info("Creating sales invoice in ERPNext for successful transaction"); logger.info("Creating sales invoice in ERPNext for successful transaction");
@@ -89,15 +93,15 @@ public class SalesInvoiceHandler implements HandlerInterface {
headers.add("Authorization", basicAuth); headers.add("Authorization", basicAuth);
headers.add("Cookie", sid); headers.add("Cookie", sid);
String erpCustomerRef = "";
User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null); User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null);
if(user == null) erpCustomerRef = (user == null) ? "Anonymous" : user.getErpCustomerRef();
throw new ApiException("Could not find user");
SalesInvoiceDto salesInvoiceDto = SalesInvoiceDto.builder() SalesInvoiceDto salesInvoiceDto = SalesInvoiceDto.builder()
.doctype("Sales Invoice") .doctype("Sales Invoice")
.customer(user.getErpCustomerRef()) .customer(erpCustomerRef)
.postingDate(transaction.getCreatedAt().toLocalDate().toString()) .postingDate(new LocalDateTime().toLocalDate().toString())
.dueDate(transaction.getCreatedAt().toLocalDate().toString()) .dueDate(new LocalDateTime().toLocalDate().toString())
.company(erpnextCompany) .company(erpnextCompany)
.docstatus(1) .docstatus(1)
.items(Arrays.asList(SalesInvoiceItemDto.builder() .items(Arrays.asList(SalesInvoiceItemDto.builder()
@@ -130,12 +134,9 @@ public class SalesInvoiceHandler implements HandlerInterface {
transactionEvent.setStatus(Status.FAILED); transactionEvent.setStatus(Status.FAILED);
transactionEvent.setMessage(e.getMessage()); transactionEvent.setMessage(e.getMessage());
} }
} else {
logger.info("Transaction status is not SUCCESS ({}), skipping sales invoice creation", transaction.getStatus());
}
transactionEventService.save(transactionEvent); transactionEventService.save(transactionEvent);
return transactionRepository.save(transaction); return transactionService.save(transaction);
} }
} }