completed zesa journey

This commit is contained in:
2025-06-27 11:33:49 +02:00
parent fdd3a224ed
commit 6ad1f55d4b
25 changed files with 762 additions and 56 deletions

View File

@@ -0,0 +1,46 @@
package zw.qantra.tm.domain.services;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.models.AdditionalData;
import zw.qantra.tm.domain.repositories.AdditionalDataRepository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Getter
@Service
@RequiredArgsConstructor
public class AdditionalDataService {
private final AdditionalDataRepository additionalDataRepository;
public List<AdditionalData> getAllAdditionalData() {
return additionalDataRepository.findAll();
}
public Optional<AdditionalData> getAdditionalData(UUID id) {
return additionalDataRepository.findById(id);
}
public AdditionalData createAdditionalData(AdditionalData additionalData) {
return additionalDataRepository.save(additionalData);
}
public AdditionalData updateAdditionalData(UUID id, AdditionalData additionalData) {
if (additionalDataRepository.existsById(id)) {
additionalData.setId(id);
return additionalDataRepository.save(additionalData);
}
return null;
}
public boolean deleteAdditionalData(UUID id) {
if (additionalDataRepository.existsById(id)) {
additionalDataRepository.deleteById(id);
return true;
}
return false;
}
}

View File

@@ -3,6 +3,8 @@ package zw.qantra.tm.domain.services;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.WordUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -64,7 +66,7 @@ public class ProviderService {
headers.setBearerAuth(token);
List<Provider> existingProviders;
if (category != null) {
if (category != null && !category.isEmpty()) {
existingProviders = getProviderRepository().findByCategory(category);
} else {
existingProviders = getProviderRepository().findAll();
@@ -84,6 +86,24 @@ public class ProviderService {
LinkedHashMap body = (LinkedHashMap) response.get("body");
List<LinkedHashMap> content = (List<LinkedHashMap>) body.get("content");
for (LinkedHashMap provider : content) {
String clientId = (String) provider.get("clientId");
if (existingProviderIds.contains(clientId)) {
// Update existing provider
Provider existingProvider = existingProviders.stream()
.filter(p -> p.getClientId().equals(clientId))
.findFirst()
.orElse(null);
if (existingProvider != null) {
provider.put("name", WordUtils.capitalizeFully((String)provider.get("name")));
provider.put("description", existingProvider.getDescription());
provider.put("image", existingProvider.getImage());
provider.put("label", existingProvider.getLabel());
provider.put("category", existingProvider.getCategory());
}
}
}
return content;
}

View File

@@ -1,8 +1,17 @@
package zw.qantra.tm.domain.services;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.UUID;
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.repositories.TransactionRepository;
import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory;
@@ -10,27 +19,54 @@ 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.LogicPipeline;
import zw.qantra.tm.domain.services.processors.handlers.ValidationHandler;
import zw.qantra.tm.exceptions.ApiException;
@Service
@RequiredArgsConstructor
public class TransactionService {
Logger logger = LoggerFactory.getLogger(TransactionService.class);
@Getter
private final TransactionRepository transactionRepository;
private final PaymentProcessorFactory paymentProcessorFactory;
private final CalculateChargesHandler calculateChargesHandler;
private final ProviderService providerService;
public TransactionRepository getTransactionRepository() {
return transactionRepository;
public Transaction poll(UUID uuid){
if(!transactionRepository.existsById(uuid)){
throw new ApiException("Transaction not found for ID: " + uuid);
}
Transaction transaction = transactionRepository.findById(uuid).get();
// no persistence happens during polling unless the transaction status changes
String label = getLabel(transaction);
logger.info("label: {}", label);
TransactionProcessorInterface transactionProcessorInterface =
(TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor(label);
try {
transactionProcessorInterface.poll(transaction);
} catch (Exception e) {
e.printStackTrace();
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("01");
transaction.setErrorMessage(e.getMessage());
}
return transaction;
}
public Transaction execute(Transaction transaction) {
transaction.setReference(UUID.randomUUID().toString());
LogicPipeline<Transaction, Transaction> transactionLogic =
new LogicPipeline<>(calculateChargesHandler)
.addHandler(new ValidationHandler());
transaction = transactionLogic.execute(transaction);
String label = transaction.getType() + "_" + transaction.getIntegrationProcessorLabel();
String label = getLabel(transaction);
logger.info("label: {}", label);
TransactionProcessorInterface transactionProcessorInterface =
(TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor(label);
@@ -46,4 +82,27 @@ public class TransactionService {
}
return transaction;
}
private String getLabel(Transaction transaction){
String label = "";
if(transaction.getType() == RequestType.CONFIRM){
// get processor label from billClientId
Provider provider = providerService.getProviderRepository()
.findByClientId(transaction.getBillClientId());
if(provider == null){
throw new ApiException("Provider not found for billClientId: " + transaction.getBillClientId());
}
label = transaction.getType() + "_" +
provider.getLabel();
}else{
label = transaction.getType() + "_" +
transaction.getPaymentProcessorLabel() + "_" +
transaction.getAuthType();
}
return label;
}
public Transaction save(Transaction transaction){
return transactionRepository.save(transaction);
}
}

View File

@@ -5,4 +5,5 @@ import zw.qantra.tm.domain.models.Transaction;
public interface TransactionProcessorInterface {
Object process(Transaction transaction) throws Exception;
Object reverse(Transaction transaction);
Object poll(Transaction transaction) throws Exception;
}

View File

@@ -1,27 +1,45 @@
package zw.qantra.tm.domain.services.processors.confirmations;
import lombok.RequiredArgsConstructor;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.dtos.BillPaymentDto;
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.Transaction;
import zw.qantra.tm.domain.services.AdditionalDataService;
import zw.qantra.tm.domain.services.TransactionService;
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
import zw.qantra.tm.rest.RestService;
import zw.qantra.tm.utils.Utils;
import java.util.Map;
import java.util.LinkedHashMap;
@Service("CONFIRM_ZESA")
@RequiredArgsConstructor
public class ZesaConfirmationProcessor implements TransactionProcessorInterface {
Logger logger = LoggerFactory.getLogger(ZesaConfirmationProcessor.class);
private final RestService restService;
private final TransactionService transactionService;
private final AdditionalDataService additionalDataService;
@Value("${sbz.aggregator.url}")
private String aggregatorUrl;
@Value("${sbz.aggregator.client-id}")
private String aggregatorClientId;
@Value("${sbz.aggregator.phone}")
private String aggregatorPhone;
@Value("${sbz.aggregator.account}")
private String aggregatorAccount;
@Override
public Object process(Transaction transaction) throws Exception {
System.out.println("processing transaction");
logger.info("processing transaction");
String token = restService.getMerchantToken();
BillPaymentDto request = BillPaymentDto.builder()
@@ -30,25 +48,50 @@ public class ZesaConfirmationProcessor implements TransactionProcessorInterface
.debitRef(transaction.getDebitRef())
.debitCurrency(transaction.getDebitCurrency())
.amount(transaction.getAmount())
.aggregatorId(transaction.getAggregatorId())
.debitPhone(transaction.getDebitPhone())
.debitAccount(transaction.getDebitAccount())
.creditAccount(transaction.getCreditAccount())
.creditPhone(transaction.getCreditPhone())
.aggregatorId(aggregatorClientId)
.debitPhone(aggregatorPhone) // qantra mobile number
.debitAccount(aggregatorAccount) // qantra corporate account
.creditAccount(transaction.getCreditAccount()) // customer's zesa meter number
.creditPhone(transaction.getCreditPhone()) // customer's contact phone number
.billName(transaction.getBillName())
.build();
String url = aggregatorUrl + "/v1/merchant/aggregation/transaction";
String url = aggregatorUrl + "/merchant/aggregation/transaction";
Map response = restService.postWithToken(token, url, request, Map.class);
LinkedHashMap response = restService.postWithToken(token, url, request, LinkedHashMap.class);
System.out.println(response);
logger.info("response: {}", response.toString());
return transaction;
LinkedHashMap body = (LinkedHashMap) response.get("body");
if (body.get("status").equals("SUCCESS")) {
transaction.setStatus(Status.SUCCESS);
transaction.setResponseCode((String) body.get("responseCode"));
transaction.setAdditionalData(body.get("additionalData"));
transactionService.save(transaction);
AdditionalData additionalData = AdditionalData.builder()
.transaction(transaction)
.jsonString(Utils.toJson(body.get("additionalData")))
.build();
additionalDataService.getAdditionalDataRepository().save(additionalData);
} else {
transaction.setStatus(Status.FAILED);
transaction.setErrorMessage((String) body.get("message"));
transaction.setResponseCode((String) body.get("responseCode"));
}
return transactionService.save(transaction);
}
@Override
public Object reverse(Transaction transaction) {
return transaction;
}
@Override
public Object poll(Transaction transaction) {
return transaction;
}
}

View File

@@ -1,11 +1,12 @@
package zw.qantra.tm.domain.services.processors.handlers;
import lombok.AllArgsConstructor;
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;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.ChargeConditionService;
import zw.qantra.tm.domain.services.ChargeService;
import java.math.BigDecimal;
@@ -15,15 +16,16 @@ import java.util.Arrays;
import java.util.List;
@AllArgsConstructor
@Service
public class CalculateChargesHandler implements LogicHandler<Transaction, Transaction> {
private ChargeConditionService chargeConditionService;
Logger logger = LoggerFactory.getLogger(CalculateChargesHandler.class);
@Autowired
private ChargeService chargeService;
@Override
public Transaction process(Transaction transaction) {
System.out.println("processing charges");
logger.info("processing charges");
transaction.setCharge(BigDecimal.ZERO);
transaction.setTax(BigDecimal.ZERO);
transaction.setGatewayCharge(BigDecimal.ZERO);
@@ -122,6 +124,12 @@ public class CalculateChargesHandler implements LogicHandler<Transaction, Transa
applyCharge(transaction, charge, chargeAmount);
}
// calculate total amount for ease of use later
transaction.setTotalAmount(
transaction.getCharge().add(
transaction.getTax().add(
transaction.getGatewayCharge())));
return transaction;
}

View File

@@ -25,4 +25,10 @@ public class ZesaIntegrationProcessor implements TransactionProcessorInterface {
public Object reverse(Transaction transaction) {
return transaction;
}
@Override
public Object poll(Transaction transaction) {
return transaction;
}
}

View File

@@ -0,0 +1,159 @@
package zw.qantra.tm.domain.services.processors.payments;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.dtos.SdkActionDto;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.TransactionService;
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
import zw.qantra.tm.rest.RestService;
import zw.qantra.tm.utils.Utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedHashMap;
@Service("REQUEST_MPGS_WEB")
@RequiredArgsConstructor
public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
private final Logger logger = LoggerFactory.getLogger(MPGSWebPaymentProcessor.class);
private final RestService restService;
private final TransactionService transactionService;
@Value("${sbz.aggregator.encryption-key}")
private String encryptionKey;
@Value("${sbz.aggregator.client-id}")
private String clientId;
@Value("${sbz.aggregator.client-secret}")
private String clientSecret;
@Value("${steward.payment.processor.url}")
private String url;
@Override
public Object process(Transaction transaction) throws Exception {
logger.info("processing transaction");
String token = restService.getMerchantToken();
String concat = clientId+
encryptionKey+
transaction.getCreditPhone()+
transaction.getAmount();
String hex = getHash(concat);
SdkActionDto sdkActionDto = SdkActionDto.builder()
.clientId(clientId)
.clientSecret(clientSecret)
.phone(transaction.getCreditPhone())
.amount(transaction.getAmount().toString())
.currency(transaction.getDebitCurrency().toString())
.hash(hex)
.authType("WEB")
.billerReference(transaction.getReference())
.action("PAYMENT")
.sandbox(true)
.paymentProcessorLabel("MPGS")
.responseUrl("https://www.google.com")
.build();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);
headers.set("Content-Type", "application/json");
try {
LinkedHashMap response = restService.postAs(url + "/merchant/sdk/action",
sdkActionDto, headers, LinkedHashMap.class);
logger.info("response: {}", Utils.toJson(response));
LinkedHashMap body = (LinkedHashMap) response.get("body");
transaction.setSdkActionId((String) body.get("uid"));
if(!body.get("status").equals("FAILED")){
transaction.setStatus(Status.PENDING);
transaction.setResponseCode("00");
transaction.setTargetUrl((String) body.get("targetUrl"));
}else{
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("01");
transaction.setErrorMessage((String) body.get("message"));
}
} catch (Exception e) {
logger.error("Network error during payment processing: {}", e.getMessage(), e);
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("99");
transaction.setErrorMessage("Network error: " + e.getMessage());
}
return transactionService.save(transaction);
}
@Override
public Object poll(Transaction transaction) throws Exception {
logger.info("polling transaction");
String token = restService.getMerchantToken();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);
headers.set("Content-Type", "application/json");
try {
LinkedHashMap response = restService.getAs(url + "/merchant/sdk/mpgs/poll/" + transaction.getSdkActionId(),
headers, LinkedHashMap.class);
logger.info("response: {}", Utils.toJson(response));
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"));
transactionService.save(transaction);
}
} catch (Exception e) {
logger.error("Network error during payment processing: {}", e.getMessage(), e);
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("99");
transaction.setErrorMessage("Network error: " + e.getMessage());
}
return transaction;
}
@Override
public Object reverse(Transaction transaction) {
return transaction;
}
public String getHash(String originalString){
String hex = "";
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance("SHA-256");
byte[] encodedhash = digest.digest(
originalString.getBytes(StandardCharsets.UTF_8));
hex = bytesToHex(encodedhash);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return hex;
}
private static String bytesToHex(byte[] hash) {
StringBuilder hexString = new StringBuilder(2 * hash.length);
for (int i = 0; i < hash.length; i++) {
String hex = Integer.toHexString(0xff & hash[i]);
if(hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}

View File

@@ -1,24 +1,131 @@
package zw.qantra.tm.domain.services.processors.payments;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedHashMap;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.dtos.SdkActionDto;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
import zw.qantra.tm.domain.services.TransactionService;
import zw.qantra.tm.rest.RestService;
import zw.qantra.tm.utils.Utils;
@Service("STEWARD_WEB")
@Service("REQUEST_STEWARD_WEB")
@RequiredArgsConstructor
public class StewardWebPaymentProcessor implements TransactionProcessorInterface {
private final Logger logger = LoggerFactory.getLogger(StewardWebPaymentProcessor.class);
private final RestService restService;
private final TransactionService transactionService;
@Value("${sbz.aggregator.encryption-key}")
private String encryptionKey;
@Value("${sbz.aggregator.client-id}")
private String clientId;
@Value("${sbz.aggregator.client-secret}")
private String clientSecret;
@Value("${steward.payment.processor.url}")
private String url;
@Override
public Object process(Transaction transaction) throws Exception {
return transaction;
logger.info("processing transaction");
String token = restService.getMerchantToken();
String concat = clientId+
encryptionKey+
transaction.getCreditPhone()+
transaction.getAmount();
String hex = getHash(concat);
SdkActionDto sdkActionDto = SdkActionDto.builder()
.clientId(clientId)
.clientSecret(clientSecret)
.phone(transaction.getCreditPhone())
.amount(transaction.getAmount().toString())
.currency(transaction.getDebitCurrency().toString())
.hash(hex)
.authType("WEB")
.billerReference(transaction.getReference())
.action("PAYMENT")
.sandbox(true)
.paymentProcessorLabel("STEWARD")
.responseUrl("https://www.google.com")
.build();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);
headers.set("Content-Type", "application/json");
try {
LinkedHashMap response = restService.postAs(url + "/merchant/sdk/action", sdkActionDto, headers, LinkedHashMap.class);
logger.info("response: {}", Utils.toJson(response));
LinkedHashMap body = (LinkedHashMap) response.get("body");
transaction.setSdkActionId((String) body.get("uid"));
if(!body.get("status").equals("FAILED")){
transaction.setStatus(Status.SUCCESS);
transaction.setResponseCode("00");
transaction.setTargetUrl((String) body.get("targetUrl"));
}else{
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("01");
transaction.setErrorMessage((String) body.get("message"));
}
} catch (Exception e) {
logger.error("Network error during payment processing: {}", e.getMessage(), e);
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("99");
transaction.setErrorMessage("Network error: " + e.getMessage());
}
return transactionService.save(transaction);
}
@Override
public Object reverse(Transaction transaction) {
return transaction;
}
@Override
public Object poll(Transaction transaction) {
return transaction;
}
public String getHash(String originalString){
String hex = "";
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance("SHA-256");
byte[] encodedhash = digest.digest(
originalString.getBytes(StandardCharsets.UTF_8));
hex = bytesToHex(encodedhash);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return hex;
}
private static String bytesToHex(byte[] hash) {
StringBuilder hexString = new StringBuilder(2 * hash.length);
for (int i = 0; i < hash.length; i++) {
String hex = Integer.toHexString(0xff & hash[i]);
if(hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}