improving ecocash payment handling

This commit is contained in:
2025-07-14 01:03:01 +02:00
parent 6b40c48e6b
commit 75b1564faa
14 changed files with 8801 additions and 37 deletions

View File

@@ -38,7 +38,34 @@ public class ConfigController {
private final IntegrationProcessorService integrationProcessorService;
@GetMapping("/seed")
public ResponseEntity charges(){
public ResponseEntity seedConfig(){
// clear existing payment processors before seeding
paymentProcessorService.getPaymentProcessorRepository().deleteAll();
paymentProcessorService.getPaymentProcessorRepository().saveAll(Arrays.asList(
PaymentProcessor.builder()
.name("Ecocash")
.label("ECOCASH")
.type("GATEWAY")
.image("ecocash.png")
.accountFieldName("phoneNumber")
.accountFieldLabel("EcoCash Phone Number")
.description("Pay using your mobile wallet")
.authType("REMOTE")
.build(),
PaymentProcessor.builder()
.name("Visa/MasterCard")
.label("MPGS")
.type("GATEWAY")
.image("visa-mastercard.png")
.accountFieldName("cardNumber")
.accountFieldLabel("Card Number")
.description("Pay using your international card")
.authType("WEB")
.build()
));
integrationProcessorService.getIntegrationProcessorRepository().deleteAll();
integrationProcessorService.getIntegrationProcessorRepository().saveAll(Arrays.asList(
@@ -79,6 +106,7 @@ public class ConfigController {
.image("econet.png")
.externalId("78f1f497-c9eb-401c-b22c-884756e68e40")
.integrationProcessorLabel("STEWARD")
.accountFieldName("Phone Number")
.build(),
Provider.builder()
.description("NetOne Airtime")
@@ -88,6 +116,7 @@ public class ConfigController {
.image("netone.png")
.externalId("95d485a7-39c9-4559-8a27-6521969abe8f")
.integrationProcessorLabel("STEWARD")
.accountFieldName("Phone Number")
.build(),
Provider.builder()
.description("Prepaid Zesa")
@@ -97,6 +126,7 @@ public class ConfigController {
.image("zesa.png")
.externalId("0f67f7cc-b62b-4fb5-915f-6740b2afdba6")
.integrationProcessorLabel("STEWARD")
.accountFieldName("Meter Number")
.build(),
Provider.builder()
.description("Econet Broadband")
@@ -106,30 +136,11 @@ public class ConfigController {
.image("econet.png")
.externalId("287863e2-a791-4106-b629-79dadc9a6779")
.integrationProcessorLabel("STEWARD")
.accountFieldName("Phone Number")
.build()
);
providerService.getProviderRepository().saveAll(providers);
// Clear existing payment processors before seeding
paymentProcessorService.getPaymentProcessorRepository().deleteAll();
// seed payment processors
List<PaymentProcessor> paymentProcessors = Arrays.asList(
PaymentProcessor.builder()
.name("Ecocash")
.label("ECOCASH")
.type("GATEWAY")
.image("https://www.ecocash.co.zw/images/logo.png")
.build(),
PaymentProcessor.builder()
.name("Mastercard")
.label("MPGS")
.type("GATEWAY")
.image("https://www.mastercard.com/content/dam/public/mastercardcom/global/en/icons/mc-symbol.svg")
.build()
);
paymentProcessorService.getPaymentProcessorRepository().saveAll(paymentProcessors);
// Clear existing charges and charge conditions before seeding
List<ChargeCondition> chargeConditions = Arrays.asList(
ChargeCondition.builder()

View File

@@ -0,0 +1,78 @@
package zw.qantra.tm.domain.controllers;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import zw.qantra.tm.domain.models.PaymentProcessor;
import zw.qantra.tm.domain.services.PaymentProcessorService;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@RestController
@RequestMapping("/payment-processors")
@RequiredArgsConstructor
public class PaymentProcessorController {
private final PaymentProcessorService paymentProcessorService;
@GetMapping
public ResponseEntity<List<PaymentProcessor>> getAllPaymentProcessors() {
return ResponseEntity.ok(paymentProcessorService.getPaymentProcessorRepository().findAll());
}
@GetMapping("/{id}")
public ResponseEntity<PaymentProcessor> getPaymentProcessorById(@PathVariable UUID id) {
Optional<PaymentProcessor> paymentProcessor = paymentProcessorService.getPaymentProcessorRepository().findById(id);
return paymentProcessor.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/type/{type}")
public ResponseEntity<List<PaymentProcessor>> getPaymentProcessorsByType(@PathVariable String type) {
// Note: This would require adding a findByType method to the repository
// For now, filtering in memory
List<PaymentProcessor> allProcessors = paymentProcessorService.getPaymentProcessorRepository().findAll();
List<PaymentProcessor> filteredProcessors = allProcessors.stream()
.filter(processor -> type.equals(processor.getType()))
.toList();
return ResponseEntity.ok(filteredProcessors);
}
@GetMapping("/label/{label}")
public ResponseEntity<PaymentProcessor> getPaymentProcessorByLabel(@PathVariable String label) {
// Note: This would require adding a findByLabel method to the repository
// For now, filtering in memory
Optional<PaymentProcessor> paymentProcessor = paymentProcessorService.getPaymentProcessorRepository().findAll().stream()
.filter(processor -> label.equals(processor.getLabel()))
.findFirst();
return paymentProcessor.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<PaymentProcessor> createPaymentProcessor(@RequestBody PaymentProcessor paymentProcessor) {
PaymentProcessor createdProcessor = paymentProcessorService.getPaymentProcessorRepository().save(paymentProcessor);
return ResponseEntity.ok(createdProcessor);
}
@PutMapping("/{id}")
public ResponseEntity<PaymentProcessor> updatePaymentProcessor(@PathVariable UUID id, @RequestBody PaymentProcessor paymentProcessor) {
if (!paymentProcessorService.getPaymentProcessorRepository().existsById(id)) {
return ResponseEntity.notFound().build();
}
paymentProcessor.setId(id);
PaymentProcessor updatedProcessor = paymentProcessorService.getPaymentProcessorRepository().save(paymentProcessor);
return ResponseEntity.ok(updatedProcessor);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletePaymentProcessor(@PathVariable UUID id) {
if (!paymentProcessorService.getPaymentProcessorRepository().existsById(id)) {
return ResponseEntity.notFound().build();
}
paymentProcessorService.getPaymentProcessorRepository().deleteById(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -14,7 +14,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import zw.qantra.tm.utils.Utils;
import zw.qantra.tm.utils.LogUtils;
import java.util.List;
import java.util.UUID;
@@ -71,9 +71,9 @@ public class TransactonController {
@PostMapping
public ResponseEntity<Transaction> create(@RequestBody Transaction transaction) {
logger.info("incoming transaction: {}", Utils.toJson(transaction));
LogUtils.logPrettyJson(logger, "incoming transaction", transaction);
Transaction createdTransaction = transactionService.execute(transaction);
logger.info("outgoing transaction: {}", Utils.toJson(createdTransaction));
LogUtils.logPrettyJson(logger, "outgoing transaction", createdTransaction);
return ResponseEntity.ok(createdTransaction);
}

View File

@@ -17,4 +17,7 @@ public class PaymentProcessor extends BaseEntity {
private String type;
@Column(columnDefinition = "TEXT")
private String image;
private String accountFieldName;
private String accountFieldLabel;
private String authType;
}

View File

@@ -170,9 +170,14 @@ public class TransactionService {
public Transaction findById(UUID id) {
Transaction transaction = transactionRepository.findById(id).orElseThrow(() -> new ApiException("Transaction not found for ID: " + id));
AdditionalData additionalData = additionalDataService.getAdditionalDataRepository()
.findByTransactionId(id).orElseThrow(() -> new ApiException("Additional data not found for transaction ID: " + id));
transaction.setAdditionalData(Utils.fromJson(additionalData.getJsonString(), List.class));
try {
AdditionalData additionalData = additionalDataService.getAdditionalDataRepository()
.findByTransactionId(id).orElseThrow(() -> new ApiException("Additional data not found for transaction ID: " + id));
transaction.setAdditionalData(Utils.fromJson(additionalData.getJsonString(), List.class));
} catch (ApiException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return transaction;
}

View File

@@ -60,8 +60,6 @@ public class EconetConfirmationProcessor implements TransactionProcessorInterfac
LinkedHashMap response = restService.postWithToken(token, url, request, LinkedHashMap.class);
logger.info("response: {}", response.toString());
LinkedHashMap body = (LinkedHashMap) response.get("body");
if (body.get("status").equals("SUCCESS")) {
@@ -78,7 +76,6 @@ public class EconetConfirmationProcessor implements TransactionProcessorInterfac
} 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

@@ -60,8 +60,6 @@ public class ZesaConfirmationProcessor implements TransactionProcessorInterface
LinkedHashMap response = restService.postWithToken(token, url, request, LinkedHashMap.class);
logger.info("response: {}", response.toString());
LinkedHashMap body = (LinkedHashMap) response.get("body");
if (body.get("status").equals("SUCCESS")) {
@@ -78,7 +76,6 @@ public class ZesaConfirmationProcessor implements TransactionProcessorInterface
} 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

@@ -67,6 +67,7 @@ public class StewardIntegrationProcessor implements TransactionProcessorInterfac
if (body.get("status").equals("SUCCESS")) {
transaction.setStatus(Status.SUCCESS);
transaction.setResponseCode((String) body.get("responseCode"));
transaction.setCreditRef((String) body.get("creditReference"));
transaction.setAdditionalData(body.get("additionalData"));
transactionService.save(transaction);

View File

@@ -1,16 +1,103 @@
package zw.qantra.tm.domain.services.processors.payments;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.http.HttpHeaders;
import lombok.RequiredArgsConstructor;
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;
@Service("REQUEST_ECOCASH_REMOTE")
@RequiredArgsConstructor
public class EcocashRemotePaymentProcessor implements TransactionProcessorInterface {
private final Logger logger = LoggerFactory.getLogger(EcocashRemotePaymentProcessor.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();
// send total less gateway charges
BigDecimal totalAmount = transaction.getAmount().add(transaction.getTax()).add(transaction.getCharge());
String concat = clientId+
encryptionKey+
transaction.getCreditPhone()+
totalAmount;
String hex = getHash(concat);
SdkActionDto sdkActionDto = SdkActionDto.builder()
.clientId(clientId)
.clientSecret(clientSecret)
.phone(transaction.getCreditPhone())
.amount(totalAmount.toString())
.currency(transaction.getDebitCurrency().toString())
.hash(hex)
.authType("REMOTE")
.billerReference(transaction.getReference())
.action("PAYMENT")
.sandbox(true)
.paymentProcessorLabel("ECOCASH")
.responseUrl("https://peakapi.stewardpay.co.zw")
.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);
LinkedHashMap body = (LinkedHashMap) response.get("body");
transaction.setSdkActionId((String) body.get("uid"));
if(body.get("status").equals("SUCCESS")){
transaction.setStatus(Status.SUCCESS);
transaction.setResponseCode("00");
LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction");
transaction.setDebitRef((String) transactionData.get("debitReference"));
transactionService.save(transaction);
}else{
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("01");
LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction");
transaction.setErrorMessage((String) transactionData.get("errorMessage"));
}
} 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
@@ -20,6 +107,64 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf
@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/reference/" + transaction.getReference(),
headers, LinkedHashMap.class);
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"));
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;
}
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

@@ -79,6 +79,7 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
LinkedHashMap body = (LinkedHashMap) response.get("body");
transaction.setSdkActionId((String) body.get("uid"));
// if status is not failed, set the transaction to pending and send customer to the target url
if(!body.get("status").equals("FAILED")){
transaction.setStatus(Status.PENDING);
transaction.setResponseCode("00");
@@ -86,7 +87,8 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
}else{
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("01");
transaction.setErrorMessage((String) body.get("message"));
LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction");
transaction.setErrorMessage((String) transactionData.get("errorMessage"));
}
} catch (Exception e) {
logger.error("Network error during payment processing: {}", e.getMessage(), e);
@@ -119,6 +121,10 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
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) {