Merge branch 'feature/adding-transaction-workflows' into release/0.0.1-RELEASE

# Conflicts:
#	src/main/java/zw/qantra/tm/domain/services/processors/handlers/erp/SalesInvoiceHandler.java
#	src/main/java/zw/qantra/tm/domain/services/processors/payments/MPGSWebPaymentProcessor.java
#	src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PaymentEntryHandler.java
This commit is contained in:
2025-10-22 00:22:10 +02:00
25 changed files with 1205 additions and 436 deletions

View File

@@ -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<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) {
Transaction transaction = transactionRepository.findById(id).orElseThrow(() -> new ApiException("Transaction not found for ID: " + id));

View File

@@ -1,153 +0,0 @@
package zw.qantra.tm.domain.services.processors.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;
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 JournalEntryHandler implements LogicHandler<Transaction, Transaction>{
Logger logger = LoggerFactory.getLogger(JournalEntryHandler.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;
@Value("${erpnext.accounts.gatewayexpense}")
private String gatewayExpenseAccount;
@Value("${erpnext.accounts.accountsreceivable}")
private String accountsReceivableAccount;
@Value("${erpnext.accounts.cash}")
private String cashAccount;
@Value("${erpnext.cost.center}")
private String costCenter;
@Override
public Transaction process(Transaction transaction) {
logger.info("Processing journal entry for transaction: {}", transaction.getReference());
TransactionEvent transactionEvent = TransactionEvent.builder()
.transactionId(transaction.getId().toString())
.event(RequestType.JOURNAL_ENTRY.name())
.status(Status.PROCESSING)
.build();
transactionEventService.save(transactionEvent);
if(transaction.getStatus() == Status.SUCCESS){
try {
logger.info("Creating journal entry in ERPNext for successful transaction");
// login to erp
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
headers.add("Accept", "application/json");
Map<String, String> loginRequest = Map.of(
"usr", erpnextUsername,
"pwd", erpnextPassword);
String sid = "";
try {
ResponseEntity response = restService.postAsEntity(
erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class);
sid = response.getHeaders().getFirst("Set-Cookie");
} catch (Exception e) {
e.printStackTrace();
throw new ApiException("Failed to login to ERPNext");
}
// create customer in erp
String basicAuth = "Basic " + Base64.getEncoder().encodeToString(
(erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8));
headers.add("Authorization", basicAuth);
headers.add("Cookie", sid);
JournalEntryAccountDto expenseEntry = JournalEntryAccountDto.builder()
.doctype("Journal Entry Account")
.account(gatewayExpenseAccount)
.debitInAccountCurrency(transaction.getGatewayCharge())
.costCenter(costCenter)
.build();
JournalEntryAccountDto cashAccEntry = JournalEntryAccountDto.builder()
.doctype("Journal Entry Account")
.account(cashAccount)
.creditInAccountCurrency(transaction.getGatewayCharge())
.build();
JournalEntryDto journalEntryDto = JournalEntryDto.builder()
.doctype("Journal Entry")
.company(erpnextCompany)
.postingDate(transaction.getCreatedAt().toLocalDate().toString())
.docstatus(1)
.accounts(Arrays.asList(cashAccEntry, expenseEntry))
.build();
try {
LinkedHashMap response = restService.postAs(
erpnextUrl + "/v2/document/Journal Entry", journalEntryDto, headers, LinkedHashMap.class);
LinkedHashMap data = (LinkedHashMap) response.get("data");
String transactionId = (String) data.get("name");
transaction.setErpJournalRef(transactionId);
transactionEvent.setStatus(Status.SUCCESS);
} catch (Exception e) {
e.printStackTrace();
throw new ApiException("Failed to create journal entry in ERPNext");
}
logger.info("journal entry created successfully in ERPNext");
} catch (Exception e) {
logger.error("Failed to create journal entry 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 journal entry creation", transaction.getStatus());
}
transactionEventService.save(transactionEvent);
return transactionRepository.save(transaction);
}
}

View File

@@ -1,159 +0,0 @@
package zw.qantra.tm.domain.services.processors.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.PaymentEntryDto;
import zw.qantra.tm.domain.dtos.erp.PaymentReferenceDto;
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.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Service
@RequiredArgsConstructor
public class PaymentEntryHandler implements LogicHandler<Transaction, Transaction>{
Logger logger = LoggerFactory.getLogger(PaymentEntryHandler.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;
@Value("${erpnext.accounts.accountsreceivable}")
private String accountsReceivableAccount;
@Value("${erpnext.accounts.cash}")
private String cashAccount;
@Override
public Transaction process(Transaction transaction) {
logger.info("Processing payment entry for transaction: {}", transaction.getReference());
TransactionEvent transactionEvent = TransactionEvent.builder()
.transactionId(transaction.getId().toString())
.event(RequestType.SALES_PAYMENT.name())
.status(Status.PROCESSING)
.build();
transactionEventService.save(transactionEvent);
if(transaction.getStatus() == Status.SUCCESS){
try {
logger.info("Creating payment entry in ERPNext for successful transaction");
// login to erp
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
headers.add("Accept", "application/json");
Map<String, String> loginRequest = Map.of(
"usr", erpnextUsername,
"pwd", erpnextPassword);
String sid = "";
try {
ResponseEntity response = restService.postAsEntity(
erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class);
sid = response.getHeaders().getFirst("Set-Cookie");
} catch (Exception e) {
e.printStackTrace();
throw new ApiException("Failed to login to ERPNext");
}
// create customer in erp
String basicAuth = "Basic " + Base64.getEncoder().encodeToString(
(erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8));
headers.add("Authorization", basicAuth);
headers.add("Cookie", sid);
String erpCustomerRef = "";
User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null);
if(user == null){
erpCustomerRef = "Anonymous";
}
BigDecimal amount = transaction.getAmount().add(transaction.getGatewayCharge());
PaymentReferenceDto paymentReferenceDto = PaymentReferenceDto.builder()
.referenceDoctype("Sales Invoice")
.referenceName(transaction.getErpSalesRef())
.allocatedAmount(amount)
.build();
PaymentEntryDto paymentEntryDto = PaymentEntryDto.builder()
.doctype("Payment Entry")
.company(erpnextCompany)
.docstatus(1)
.paymentType("Receive")
.party(erpCustomerRef)
.partyType("Customer")
.paidFrom(accountsReceivableAccount)
.paidTo(cashAccount)
.paidAmount(amount)
.receivedAmount(amount)
.referenceNo(transaction.getDebitRef())
.referenceDate(transaction.getCreatedAt().toLocalDate().toString())
.postingDate(transaction.getCreatedAt().toLocalDate().toString())
.references(Arrays.asList(paymentReferenceDto))
.build();
try {
LinkedHashMap response = restService.postAs(
erpnextUrl + "/v2/document/Payment Entry", paymentEntryDto, headers, LinkedHashMap.class);
LinkedHashMap data = (LinkedHashMap) response.get("data");
String transactionId = (String) data.get("name");
transaction.setErpSalesPaymentRef(transactionId);
transactionEvent.setStatus(Status.SUCCESS);
} catch (Exception e) {
e.printStackTrace();
throw new ApiException("Failed to create payment entry in ERPNext");
}
logger.info("payment entry created successfully in ERPNext");
} catch (Exception e) {
logger.error("Failed to create payment entry 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 payment entry creation", transaction.getStatus());
}
transactionEventService.save(transactionEvent);
return transactionRepository.save(transaction);
}
}

View File

@@ -1,177 +0,0 @@
package zw.qantra.tm.domain.services.processors.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.*;
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.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.exceptions.ApiException;
import zw.qantra.tm.rest.RestService;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Service
@RequiredArgsConstructor
public class PurchaseInvoiceHandler implements LogicHandler<Transaction, Transaction>{
Logger logger = LoggerFactory.getLogger(PurchaseInvoiceHandler.class);
private final RestService restService;
private final UserService userService;
private final TransactionEventService transactionEventService;
private final TransactionRepository transactionRepository;
private final ProviderService providerService;
@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;
@Value("${erpnext.cost.center}")
private String erpnextCostCenter;
@Value("${erpnext.accounts.commission}")
private String erpnextCommissionAccount;
@Override
public Transaction process(Transaction transaction) {
logger.info("Processing purchase invoice for transaction: {}", transaction.getReference());
TransactionEvent transactionEvent = TransactionEvent.builder()
.transactionId(transaction.getId().toString())
.event(RequestType.PURCHASE_INVOICE.name())
.status(Status.PROCESSING)
.build();
transactionEventService.save(transactionEvent);
if(transaction.getStatus() == Status.SUCCESS){
try {
logger.info("Creating purchase invoice in ERPNext for successful transaction");
// login to erp
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
headers.add("Accept", "application/json");
Map<String, String> loginRequest = Map.of(
"usr", erpnextUsername,
"pwd", erpnextPassword);
String sid = "";
try {
ResponseEntity response = restService.postAsEntity(
erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class);
sid = response.getHeaders().getFirst("Set-Cookie");
} catch (Exception e) {
e.printStackTrace();
throw new ApiException("Failed to login to ERPNext");
}
// create customer in erp
String basicAuth = "Basic " + Base64.getEncoder().encodeToString(
(erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8));
headers.add("Authorization", basicAuth);
headers.add("Cookie", sid);
PurchaseInvoiceItemDto purchaseInvoiceItemDto = PurchaseInvoiceItemDto.builder()
.doctype("Purchase Invoice Item")
.uom("Nos")
.itemCode(transaction.getProviderLabel())
.itemName(transaction.getProviderLabel())
.qty(transaction.getAmount())
.rate(1)
.amount(transaction.getAmount())
.build();
Provider provider = providerService.getProviderRepository()
.findByClientId(transaction.getBillClientId());
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);
PurchaseTaxDto purchaseTaxDto = PurchaseTaxDto.builder()
.doctype("Purchase Taxes and Charges")
.chargeType("On Net Total")
.accountHead(erpnextCommissionAccount)
.rate(BigDecimal.valueOf(provider.getPercentageCommission()).negate())
.costCenter(erpnextCostCenter)
.taxAmount(commission.negate())
.total(amount)
.description("Commission")
.build();
PurchaseInvoiceDto purchaseInvoiceDto = PurchaseInvoiceDto.builder()
.doctype("Purchase Invoice")
.supplier("Steward Bank")
.company(erpnextCompany)
.postingDate(transaction.getCreatedAt().toLocalDate().toString())
.dueDate(transaction.getCreatedAt().toLocalDate().plusDays(1).toString())
.docstatus(1)
.isReturn(0)
.isPaid(0)
.currency(transaction.getDebitCurrency().toString())
.conversionRate(1)
.netTotal(amount)
.grandTotal(amount)
.items(Collections.singletonList(purchaseInvoiceItemDto))
.taxes(Collections.singletonList(purchaseTaxDto))
.build();
try {
LinkedHashMap response = restService.postAs(
erpnextUrl + "/v2/document/Purchase Invoice", purchaseInvoiceDto, headers, LinkedHashMap.class);
LinkedHashMap data = (LinkedHashMap) response.get("data");
String transactionId = (String) data.get("name");
transaction.setErpPurchaseRef(transactionId);
transactionEvent.setStatus(Status.SUCCESS);
} catch (Exception e) {
e.printStackTrace();
throw new ApiException("Failed to create purchase invoice in ERPNext");
}
logger.info("purchase invoice created successfully in ERPNext");
} catch (Exception e) {
logger.error("Failed to create purchase 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 purchase invoice creation", transaction.getStatus());
}
transactionEventService.save(transactionEvent);
return transactionRepository.save(transaction);
}
}

View File

@@ -1,167 +0,0 @@
package zw.qantra.tm.domain.services.processors.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.PaymentEntryDto;
import zw.qantra.tm.domain.dtos.erp.PaymentReferenceDto;
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.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.exceptions.ApiException;
import zw.qantra.tm.rest.RestService;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Service
@RequiredArgsConstructor
public class PurchasePaymentEntryHandler implements LogicHandler<Transaction, Transaction>{
Logger logger = LoggerFactory.getLogger(PurchasePaymentEntryHandler.class);
private final RestService restService;
private final UserService userService;
private final TransactionEventService transactionEventService;
private final TransactionRepository transactionRepository;
private final ProviderService providerService;
@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;
@Value("${erpnext.accounts.accountsreceivable}")
private String accountsReceivableAccount;
@Value("${erpnext.accounts.cash}")
private String cashAccount;
@Value("${erpnext.accounts.payable}")
private String payableAccount;
@Override
public Transaction process(Transaction transaction) {
logger.info("Processing purchase payment entry for transaction: {}", transaction.getReference());
TransactionEvent transactionEvent = TransactionEvent.builder()
.transactionId(transaction.getId().toString())
.event(RequestType.PURCHASE_PAYMENT.name())
.status(Status.PROCESSING)
.build();
transactionEventService.save(transactionEvent);
if(transaction.getStatus() == Status.SUCCESS){
try {
logger.info("Creating purchase payment entry in ERPNext for successful transaction");
// login to erp
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
headers.add("Accept", "application/json");
Map<String, String> loginRequest = Map.of(
"usr", erpnextUsername,
"pwd", erpnextPassword);
String sid = "";
try {
ResponseEntity response = restService.postAsEntity(
erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class);
sid = response.getHeaders().getFirst("Set-Cookie");
} catch (Exception e) {
e.printStackTrace();
throw new ApiException("Failed to login to ERPNext");
}
// create customer in erp
String basicAuth = "Basic " + Base64.getEncoder().encodeToString(
(erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8));
headers.add("Authorization", basicAuth);
headers.add("Cookie", sid);
Provider provider = providerService.getProviderRepository()
.findByClientId(transaction.getBillClientId());
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);
PaymentReferenceDto paymentReferenceDto = PaymentReferenceDto.builder()
.referenceDoctype("Purchase Invoice")
.referenceName(transaction.getErpPurchaseRef())
.allocatedAmount(amount)
.build();
String refNo = transaction.getCreditRef().isBlank() ? transaction.getId().toString() : transaction.getCreditRef();
PaymentEntryDto paymentEntryDto = PaymentEntryDto.builder()
.doctype("Payment Entry")
.company(erpnextCompany)
.docstatus(1)
.paymentType("Pay")
.party("Steward Bank")
.partyType("Supplier")
.paidFrom(cashAccount)
.paidTo(payableAccount)
.paidAmount(amount)
.receivedAmount(amount)
.referenceNo(refNo)
.referenceDate(transaction.getCreatedAt().toLocalDate().toString())
.postingDate(transaction.getCreatedAt().toLocalDate().toString())
.references(Arrays.asList(paymentReferenceDto))
.build();
try {
LinkedHashMap response = restService.postAs(
erpnextUrl + "/v2/document/Payment Entry", paymentEntryDto, headers, LinkedHashMap.class);
LinkedHashMap data = (LinkedHashMap) response.get("data");
String transactionId = (String) data.get("name");
transaction.setErpPurchasePaymentRef(transactionId);
transactionEvent.setStatus(Status.SUCCESS);
} catch (Exception e) {
e.printStackTrace();
throw new ApiException("Failed to create purchase payment entry in ERPNext");
}
logger.info("purchase payment entry created successfully in ERPNext");
} catch (Exception e) {
logger.error("Failed to create purchase payment entry 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 purchase payment entry creation", transaction.getStatus());
}
transactionEventService.save(transactionEvent);
return transactionRepository.save(transaction);
}
}

View File

@@ -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<Transaction, Transaction>{
Logger logger = LoggerFactory.getLogger(SalesInvoiceHandler.class);
private final RestService restService;
private final UserService userService;
private final TransactionEventService transactionEventService;
private final TransactionRepository transactionRepository;
@Value("${erpnext.url}")
private String erpnextUrl;
@Value("${erpnext.api.key}")
private String erpnextApiKey;
@Value("${erpnext.api.secret}")
private String erpnextApiSecret;
@Value("${erpnext.username}")
private String erpnextUsername;
@Value("${erpnext.password}")
private String erpnextPassword;
@Value("${erpnext.company}")
private String erpnextCompany;
@Override
public Transaction process(Transaction transaction) {
logger.info("Processing sales invoice for transaction: {}", transaction.getReference());
TransactionEvent transactionEvent = TransactionEvent.builder()
.transactionId(transaction.getId().toString())
.event(RequestType.SALES_INVOICE.name())
.status(Status.PROCESSING)
.build();
transactionEventService.save(transactionEvent);
if(transaction.getStatus() == Status.PENDING){
try {
logger.info("Creating sales invoice in ERPNext for successful transaction");
// login to erp
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
headers.add("Accept", "application/json");
Map<String, String> loginRequest = Map.of(
"usr", erpnextUsername,
"pwd", erpnextPassword);
String sid = "";
try {
ResponseEntity response = restService.postAsEntity(
erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class);
sid = response.getHeaders().getFirst("Set-Cookie");
} catch (Exception e) {
e.printStackTrace();
throw new ApiException("Failed to login to ERPNext");
}
// create customer in erp
String basicAuth = "Basic " + Base64.getEncoder().encodeToString(
(erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8));
headers.add("Authorization", basicAuth);
headers.add("Cookie", sid);
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);
}
}