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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
2025-08-01 18:07:03.225 [main] INFO zw.qantra.tm.TmApplication - Starting TmApplication using Java 17.0.14 with PID 536123 (/home/vk/dev/qantra/qpay-tm/target/classes started by vk in /home/vk/dev/qantra/qpay-tm)
2025-08-01 18:07:03.227 [main] INFO zw.qantra.tm.TmApplication - The following 1 profile is active: "lab"
2025-08-01 18:07:03.228 [main] DEBUG o.s.boot.SpringApplication - Loading source class zw.qantra.tm.TmApplication
2025-08-01 18:07:03.251 [main] DEBUG o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@7502291e
2025-08-01 18:07:03.823 [main] DEBUG o.s.b.w.e.t.TomcatServletWebServerFactory - Code archive: /home/vk/.m2/repository/org/springframework/boot/spring-boot/3.5.0/spring-boot-3.5.0.jar
2025-08-01 18:07:03.824 [main] DEBUG o.s.b.w.e.t.TomcatServletWebServerFactory - Code archive: /home/vk/.m2/repository/org/springframework/boot/spring-boot/3.5.0/spring-boot-3.5.0.jar
2025-08-01 18:07:03.824 [main] DEBUG o.s.b.w.e.t.TomcatServletWebServerFactory - None of the document roots [src/main/webapp, public, static] point to a directory and will be ignored.
2025-08-01 18:07:03.834 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port 6950 (http)
2025-08-01 18:07:03.874 [main] DEBUG o.s.b.w.s.c.ServletWebServerApplicationContext - Published root WebApplicationContext as ServletContext attribute with name [org.springframework.web.context.WebApplicationContext.ROOT]
2025-08-01 18:07:03.875 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 623 ms
2025-08-01 18:07:03.885 [main] DEBUG o.s.b.w.s.ServletContextInitializerBeans - Mapping filters: customCorsFilter urls=[/*] order=-2147483648, characterEncodingFilter urls=[/*] order=-2147483648, formContentFilter urls=[/*] order=-9900, requestContextFilter urls=[/*] order=-105
2025-08-01 18:07:03.885 [main] DEBUG o.s.b.w.s.ServletContextInitializerBeans - Mapping servlets: dispatcherServlet urls=[/]
2025-08-01 18:07:03.898 [main] DEBUG o.s.b.w.s.f.OrderedRequestContextFilter - Filter 'requestContextFilter' configured for use
2025-08-01 18:07:03.900 [main] DEBUG o.s.b.w.s.f.OrderedCharacterEncodingFilter - Filter 'characterEncodingFilter' configured for use
2025-08-01 18:07:03.900 [main] DEBUG o.s.b.w.s.f.OrderedFormContentFilter - Filter 'formContentFilter' configured for use
2025-08-01 18:07:04.145 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2025-08-01 18:07:06.340 [main] INFO com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@74cd798f
2025-08-01 18:07:06.342 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2025-08-01 18:07:06.655 [main] WARN org.hibernate.orm.deprecation - HHH90000025: PostgreSQLDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default)
2025-08-01 18:07:13.413 [main] WARN o.s.b.a.o.j.JpaBaseConfiguration$JpaWebConfiguration - spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2025-08-01 18:07:13.460 [main] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping - 43 mappings in 'requestMappingHandlerMapping'
2025-08-01 18:07:13.484 [main] DEBUG o.s.w.s.h.SimpleUrlHandlerMapping - Patterns [/webjars/**, /**] in 'resourceHandlerMapping'
2025-08-01 18:07:13.502 [main] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerAdapter - ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
2025-08-01 18:07:13.526 [main] DEBUG o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver - ControllerAdvice beans: 1 @ExceptionHandler, 1 ResponseBodyAdvice
2025-08-01 18:07:13.595 [main] INFO o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port 6950 (http) with context path '/api'
2025-08-01 18:07:13.601 [main] INFO zw.qantra.tm.TmApplication - Started TmApplication in 10.655 seconds (process running for 11.159)
2025-08-01 18:07:13.602 [main] DEBUG o.s.b.a.ApplicationAvailabilityBean - Application availability state LivenessState changed to CORRECT
2025-08-01 18:07:13.603 [main] DEBUG o.s.b.a.ApplicationAvailabilityBean - Application availability state ReadinessState changed to ACCEPTING_TRAFFIC

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