updating mpgs related changes

This commit is contained in:
2025-08-26 00:12:11 +02:00
parent be01f7c199
commit 96d75c3b94
14 changed files with 273 additions and 8466 deletions

View File

@@ -0,0 +1,107 @@
package zw.qantra.tm.domain.dtos;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErpNextSalesInvoiceDto {
@JsonProperty("doctype")
private String doctype = "Sales Invoice";
@JsonProperty("company")
private String company;
@JsonProperty("customer")
private String customer;
@JsonProperty("posting_date")
private String postingDate;
@JsonProperty("due_date")
private String dueDate;
@JsonProperty("reference_no")
private String referenceNo;
@JsonProperty("remarks")
private String remarks;
@JsonProperty("currency")
private String currency;
@JsonProperty("conversion_rate")
private Double conversionRate = 1.0;
@JsonProperty("items")
private List<ErpNextInvoiceItemDto> items;
@JsonProperty("taxes")
private List<ErpNextTaxDto> taxes;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ErpNextInvoiceItemDto {
@JsonProperty("doctype")
private String doctype = "Sales Invoice Item";
@JsonProperty("item_code")
private String itemCode;
@JsonProperty("item_name")
private String itemName;
@JsonProperty("description")
private String description;
@JsonProperty("qty")
private Double qty = 1.0;
@JsonProperty("rate")
private Double rate;
@JsonProperty("amount")
private Double amount;
@JsonProperty("income_account")
private String incomeAccount;
@JsonProperty("cost_center")
private String costCenter;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ErpNextTaxDto {
@JsonProperty("doctype")
private String doctype = "Sales Taxes and Charges";
@JsonProperty("charge_type")
private String chargeType = "On Net Total";
@JsonProperty("account_head")
private String accountHead;
@JsonProperty("description")
private String description;
@JsonProperty("rate")
private Double rate;
@JsonProperty("tax_amount")
private Double taxAmount;
}
}

View File

@@ -5,14 +5,12 @@ import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
@@ -20,7 +18,6 @@ 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.Recipient;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.repositories.TransactionRepository;
import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory;
@@ -31,6 +28,8 @@ 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.PurchaseInvoiceHandler;
import zw.qantra.tm.domain.services.processors.handlers.erp.SalesInvoiceHandler;
import zw.qantra.tm.exceptions.ApiException;
import zw.qantra.tm.utils.Utils;
@@ -76,6 +75,12 @@ public class TransactionService {
transactionRepository.save(transaction);
return transaction;
}
// run post integration logic
LogicPipeline<Transaction, Transaction> postTransactionLogic =
new LogicPipeline<>(new PurchaseInvoiceHandler());
transaction = postTransactionLogic.execute(transaction);
return transaction;
}
@@ -85,6 +90,10 @@ public class TransactionService {
}
Transaction transaction = transactionRepository.findById(uuid).get();
// if the transaction is already successful, return it
if(transaction.getStatus() == Status.SUCCESS){
return transaction;
}
// no persistence happens during polling unless the transaction status changes
String label = getLabel(transaction);
@@ -97,11 +106,12 @@ public class TransactionService {
transactionProcessorInterface.poll(transaction);
// if tran was successful deep copy transaction & push integration to the aggregator
Transaction transactionCopy = Utils.deepCopy(transaction, Transaction.class);
transactionCopy.setType(RequestType.INTEGRATION);
transactionCopy.setId(null);
integration(transactionCopy);
if(transaction.getStatus() == Status.SUCCESS){
Transaction transactionCopy = Utils.deepCopy(transaction, Transaction.class);
transactionCopy.setType(RequestType.INTEGRATION);
transactionCopy.setId(null);
integration(transactionCopy);
}
} catch (Exception e) {
e.printStackTrace();
transaction.setStatus(Status.FAILED);
@@ -143,7 +153,8 @@ public class TransactionService {
// run post transaction logic
LogicPipeline<Transaction, Transaction> postTransactionLogic =
new LogicPipeline<>(new RecipientsUpdateHandler(recipientService));
new LogicPipeline<>(new RecipientsUpdateHandler(recipientService))
.addHandler(new SalesInvoiceHandler());
transaction = postTransactionLogic.execute(transaction);
return transaction;

View File

@@ -3,7 +3,6 @@ package zw.qantra.tm.domain.services.processors.handlers;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.models.Charge;
import zw.qantra.tm.domain.models.ChargeCondition;

View File

@@ -1,5 +1,5 @@
package zw.qantra.tm.domain.services.processors.handlers;
interface LogicHandler<I, O> {
public interface LogicHandler<I, O> {
O process(I input);
}

View File

@@ -1,7 +1,5 @@
package zw.qantra.tm.domain.services.processors.handlers;
import org.springframework.stereotype.Service;
public class LogicPipeline<I, O> {
private final LogicHandler<I, O> currentHandler;

View File

@@ -0,0 +1,21 @@
package zw.qantra.tm.domain.services.processors.handlers.erp;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import zw.qantra.tm.domain.services.processors.handlers.LogicHandler;
import zw.qantra.tm.domain.models.Transaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
@RequiredArgsConstructor
public class PurchaseInvoiceHandler implements LogicHandler<Transaction, Transaction>{
Logger logger = LoggerFactory.getLogger(PurchaseInvoiceHandler.class);
@Override
public Transaction process(Transaction transaction) {
logger.info("processing purchase invoice");
return transaction;
}
}

View File

@@ -0,0 +1,40 @@
package zw.qantra.tm.domain.services.processors.handlers.erp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.processors.handlers.LogicHandler;
@Service
@RequiredArgsConstructor
public class SalesInvoiceHandler implements LogicHandler<Transaction, Transaction>{
Logger logger = LoggerFactory.getLogger(SalesInvoiceHandler.class);
@Override
public Transaction process(Transaction transaction) {
logger.info("Processing sales invoice for transaction: {}", transaction.getReference());
if(transaction.getStatus() == Status.SUCCESS){
try {
logger.info("Creating sales invoice in ERPNext for successful transaction");
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
}
} else {
logger.info("Transaction status is not SUCCESS ({}), skipping sales invoice creation", transaction.getStatus());
}
return transaction;
}
}

View File

@@ -5,6 +5,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import jakarta.transaction.TransactionScoped;
import jakarta.transaction.Transactional;
import zw.qantra.tm.domain.dtos.BillPaymentDto;
import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.enums.Status;
@@ -16,6 +19,7 @@ import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
import zw.qantra.tm.rest.RestService;
import zw.qantra.tm.utils.Utils;
import java.beans.Transient;
import java.util.LinkedHashMap;
@Service("INTEGRATION_STEWARD")
@@ -36,6 +40,7 @@ public class StewardIntegrationProcessor implements TransactionProcessorInterfac
private String aggregatorAccount;
@Override
@Transactional
public Object process(Transaction transaction) throws Exception {
logger.info("processing integration");
String token = restService.getMerchantToken();
@@ -69,12 +74,14 @@ public class StewardIntegrationProcessor implements TransactionProcessorInterfac
transaction.setResponseCode((String) body.get("responseCode"));
transaction.setCreditRef((String) body.get("creditReference"));
transaction.setAdditionalData(body.get("additionalData"));
transaction.setErrorMessage(null); // clear any previous error message
transactionService.save(transaction);
AdditionalData additionalData = AdditionalData.builder()
.transaction(transaction)
.jsonString(Utils.toJson(body.get("additionalData")))
.build();
AdditionalData additionalData = additionalDataService.getAdditionalDataRepository()
.findByTransactionId(transaction.getId()).orElse(new AdditionalData());
additionalData.setTransaction(transaction);
additionalData.setJsonString(Utils.toJson(body.get("additionalData")));
additionalDataService.getAdditionalDataRepository().save(additionalData);
// fetch the corresponding request transaction if it exists
@@ -82,16 +89,15 @@ public class StewardIntegrationProcessor implements TransactionProcessorInterfac
.findByTraceAndType(transaction.getTrace(), RequestType.REQUEST);
if (requestTransaction != null) {
// create a new additional data for the request transaction
AdditionalData requestAdditionalData = AdditionalData.builder()
.transaction(requestTransaction)
.jsonString(Utils.toJson(body.get("additionalData")))
.build();
AdditionalData requestAdditionalData = additionalDataService.getAdditionalDataRepository()
.findByTransactionId(requestTransaction.getId()).orElse(new AdditionalData());
requestAdditionalData.setTransaction(requestTransaction);
requestAdditionalData.setJsonString(Utils.toJson(body.get("additionalData")));
additionalDataService.getAdditionalDataRepository().save(requestAdditionalData);
}
} else {
transaction.setStatus(Status.FAILED);
transaction.setErrorMessage((String) body.get("message"));
transaction.setResponseCode((String) body.get("responseCode"));
transaction.setErrorMessage((String) body.get("errorMessage"));
}

View File

@@ -88,7 +88,11 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("01");
LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction");
transaction.setErrorMessage((String) transactionData.get("errorMessage"));
if (transactionData != null) {
transaction.setErrorMessage((String) transactionData.get("errorMessage"));
} else {
transaction.setErrorMessage(body.get("errorMessage").toString());
}
}
} catch (Exception e) {
logger.error("Network error during payment processing: {}", e.getMessage(), e);
@@ -117,16 +121,24 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
LinkedHashMap body = (LinkedHashMap) response.get("body");
// only update tran status if polling returns a success
if(body.get("status").equals("SUCCESS")){
transaction.setStatus(Status.SUCCESS);
transaction.setResponseCode("00");
transaction.setTargetUrl((String) body.get("targetUrl"));
LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction");
transaction.setDebitRef((String) transactionData.get("debitReference"));
if(!body.get("status").equals("SUCCESS")){
logger.error(body.get("errorMessage").toString());
transaction.setStatus(Status.FAILED);
transaction.setResponseCode(body.get("responseCode").toString());
transaction.setErrorMessage(body.get("errorMessage").toString());
transactionService.save(transaction);
return transaction;
}
transaction.setStatus(Status.SUCCESS);
transaction.setResponseCode("00");
transaction.setTargetUrl((String) body.get("targetUrl"));
LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction");
transaction.setDebitRef((String) transactionData.get("debitReference"));
transactionService.save(transaction);
} catch (Exception e) {
logger.error("Network error during payment processing: {}", e.getMessage(), e);
transaction.setStatus(Status.FAILED);

View File

@@ -5,6 +5,8 @@ logging.level.org.springframework.boot.autoconfigure=INFO
server.port=6950
server.servlet.context-path=/api
spring.jpa.hibernate.ddl-auto=update
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
@@ -16,13 +18,26 @@ spring.datasource.password=zdDZMzq6F4B4L1IUl
# sbz.merchant.url=http://192.168.16.29:24050/v1
# sbz.merchant.url=https://localhost:24000/v1
sbz.merchant.url=https://api.stewardpay.co.zw/lab/v2
sbz.merchant.url=http://localhost:24000/v1
# sbz.merchant.url=https://api.stewardpay.co.zw/lab/v2
sbz.aggregator.url=${sbz.merchant.url}
sbz.aggregator.client-id=steward_pay
sbz.aggregator.client-secret=XjkbBAJeJCnAplYDTbp5xOjeyySVnYGp
sbz.aggregator.phone=263773591219
sbz.aggregator.account=1046737845
sbz.aggregator.client-id=steward_pay
sbz.aggregator.client-secret=XjkbBAJeJCnAplYDTbp5xOjeyySVnYGp
sbz.aggregator.encryption-key=735470ce-8efb-4c0f-ba3f-7298f805ab33
steward.payment.processor.url=${sbz.merchant.url}
# ERPNext Configuration
erpnext.url=http://localhost:8000
erpnext.api.key=your_api_key_here
erpnext.api.secret=your_api_secret_here
erpnext.company=Qantra Payments
erpnext.customer=Default Customer
erpnext.item=Payment Service
erpnext.income.account=5111 - Sales Income - QPAY
erpnext.cost.center=Main - QPAY
erpnext.tax.account=VAT - QPAY
erpnext.tax.rate=15.0

View File

@@ -1,21 +1,43 @@
spring.application.name=tm
debug=true
server.servlet.context-path=/api
logging.level.root=INFO
logging.level.org.springframework.boot.autoconfigure=INFO
server.port=6950
server.servlet.context-path=/api
spring.jpa.hibernate.ddl-auto=update
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.datasource.url=jdbc:postgresql://localhost:5432/qpay?useLegacyDatetimeCode=false
spring.datasource.url=jdbc:postgresql://173.212.247.232:5532/qpay?useLegacyDatetimeCode=false
spring.datasource.username=postgres
spring.datasource.password=password
spring.datasource.password=zdDZMzq6F4B4L1IUl
sbz.aggregator.url=http://192.168.16.29:24050/v1
# sbz.merchant.url=http://192.168.16.29:24050/v1
# sbz.merchant.url=https://localhost:24000/v1
sbz.merchant.url=https://api.stewardpay.co.zw/v1
sbz.aggregator.url=${sbz.merchant.url}
sbz.aggregator.phone=263773591219
sbz.aggregator.account=1041248676
sbz.aggregator.client-id=seasons_international_p_l
sbz.aggregator.client-secret=7765fcfc-7c46-4694-b0bb-40c470482d7b
sbz.aggregator.encryption-key=f29b64d6-e44b-42ea-a0ea-9d9f6a806dbe
steward.payment.processor.url=${sbz.merchant.url}
# ERPNext Configuration
erpnext.url=http://localhost:8000
erpnext.api.key=your_api_key_here
erpnext.api.secret=your_api_secret_here
erpnext.company=Qantra Payments
erpnext.customer=Default Customer
erpnext.item=Payment Service
erpnext.income.account=5111 - Sales Income - QPAY
erpnext.cost.center=Main - QPAY
erpnext.tax.account=VAT - QPAY
erpnext.tax.rate=15.0