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,66 @@
package zw.qantra.tm.domain.controllers;
import lombok.RequiredArgsConstructor;
import zw.qantra.tm.domain.services.AdditionalDataService;
import zw.qantra.tm.domain.models.AdditionalData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/additional-data")
@RequiredArgsConstructor
public class AdditionalDataController {
Logger logger = LoggerFactory.getLogger(AdditionalDataController.class);
private final AdditionalDataService additionalDataService;
@GetMapping
public ResponseEntity<List<AdditionalData>> getAll() {
return ResponseEntity.ok(additionalDataService.getAllAdditionalData());
}
@GetMapping("/transaction/{transactionId}")
public ResponseEntity<AdditionalData> getByTransactionId(@PathVariable UUID transactionId) {
return additionalDataService.getAdditionalDataRepository().findByTransactionId(transactionId)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/{id}")
public ResponseEntity<AdditionalData> getById(@PathVariable UUID id) {
return additionalDataService.getAdditionalData(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<AdditionalData> create(@RequestBody AdditionalData additionalData) {
logger.info("incoming additional data: {}", additionalData);
AdditionalData createdAdditionalData = additionalDataService.createAdditionalData(additionalData);
logger.info("outgoing additional data: {}", createdAdditionalData);
return ResponseEntity.ok(createdAdditionalData);
}
@PutMapping("/{id}")
public ResponseEntity<AdditionalData> update(@PathVariable UUID id, @RequestBody AdditionalData additionalData) {
AdditionalData updatedAdditionalData = additionalDataService.updateAdditionalData(id, additionalData);
if (updatedAdditionalData == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(updatedAdditionalData);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable UUID id) {
boolean deleted = additionalDataService.deleteAdditionalData(id);
if (!deleted) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build();
}
}

View File

@@ -5,7 +5,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import zw.qantra.tm.domain.models.Category;
import zw.qantra.tm.domain.services.CategoryService;
import zw.qantra.tm.rest.ApiResponse;
import java.util.UUID;
@@ -18,35 +17,35 @@ public class CategoryController {
@GetMapping
public ResponseEntity getAllCategories() {
return ApiResponse.ok(categoryService.getAllCategories());
return ResponseEntity.ok(categoryService.getAllCategories());
}
@GetMapping("/{id}")
public ResponseEntity getCategory(@PathVariable UUID id) {
return categoryService.getCategory(id)
.map(ApiResponse::ok)
.orElse(ApiResponse.notFound());
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity createCategory(@RequestBody Category category) {
return ApiResponse.ok(categoryService.createCategory(category));
return ResponseEntity.ok(categoryService.createCategory(category));
}
@PutMapping("/{id}")
public ResponseEntity updateCategory(@PathVariable UUID id, @RequestBody Category category) {
Category updatedCategory = categoryService.updateCategory(id, category);
if (updatedCategory != null) {
return ApiResponse.ok(updatedCategory);
return ResponseEntity.ok(updatedCategory);
}
return ApiResponse.notFound();
return ResponseEntity.notFound().build();
}
@DeleteMapping("/{id}")
public ResponseEntity deleteCategory(@PathVariable UUID id) {
if (categoryService.deleteCategory(id)) {
return ApiResponse.ok("Category deleted successfully");
return ResponseEntity.ok("Category deleted successfully");
}
return ApiResponse.notFound();
return ResponseEntity.notFound().build();
}
}

View File

@@ -8,10 +8,16 @@ import org.springframework.web.bind.annotation.RestController;
import zw.qantra.tm.domain.enums.ChargeConditionType;
import zw.qantra.tm.domain.enums.ChargeLabelEnum;
import zw.qantra.tm.domain.enums.CurrencyType;
import zw.qantra.tm.domain.models.Category;
import zw.qantra.tm.domain.models.Charge;
import zw.qantra.tm.domain.models.ChargeCondition;
import zw.qantra.tm.domain.models.PaymentProcessor;
import zw.qantra.tm.domain.models.Provider;
import zw.qantra.tm.domain.services.ChargeConditionService;
import zw.qantra.tm.domain.services.ChargeService;
import zw.qantra.tm.domain.services.CategoryService;
import zw.qantra.tm.domain.services.PaymentProcessorService;
import zw.qantra.tm.domain.services.ProviderService;
import java.math.BigDecimal;
import java.util.Arrays;
@@ -24,9 +30,90 @@ import java.util.List;
public class ConfigController {
private final ChargeConditionService chargeConditionService;
private final ChargeService chargeService;
private final PaymentProcessorService paymentProcessorService;
private final ProviderService providerService;
private final CategoryService categoryService;
@GetMapping("/charges")
@GetMapping("/seed")
public ResponseEntity charges(){
// clear existing categories before seeding
categoryService.getCategoryRepository().deleteAll();
// seed categories
List<Category> categories = Arrays.asList(
Category.builder()
.name("Airtime")
.description("Airtime for local mobile networks")
.label("AIRTIME")
.build(),
Category.builder()
.name("Utilities")
.description("Council bills & rates")
.label("UTILITIES")
.build()
);
categoryService.getCategoryRepository().saveAll(categories);
// clear existing providers before seeding
providerService.getProviderRepository().deleteAll();
// seed providers
List<Provider> providers = Arrays.asList(
Provider.builder()
.description("Econet Airtime")
.clientId("econet_airtime")
.label("ECONET")
.category("AIRTIME")
.image("econet.png")
.externalId("78f1f497-c9eb-401c-b22c-884756e68e40")
.build(),
Provider.builder()
.description("NetOne Airtime")
.clientId("netone_usd_airtime")
.label("NETONE")
.category("AIRTIME")
.image("netone.png")
.externalId("95d485a7-39c9-4559-8a27-6521969abe8f")
.build(),
Provider.builder()
.description("Prepaid Zesa")
.clientId("powertel_zesa")
.label("ZESA")
.category("UTILITIES")
.image("zesa.png")
.externalId("0f67f7cc-b62b-4fb5-915f-6740b2afdba6")
.build(),
Provider.builder()
.description("Econet Broadband")
.clientId("econet_broadband_usd")
.label("ECONET_BROADBAND")
.category("AIRTIME")
.image("econet.png")
.externalId("287863e2-a791-4106-b629-79dadc9a6779")
.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()
.name("Ecocash Gateway")
@@ -78,6 +165,6 @@ public class ConfigController {
chargeService.getChargeRepository().saveAll(charges);
return ResponseEntity.ok("Charges configuration complete");
return ResponseEntity.ok("Project configuration seeding complete");
}
}

View File

@@ -5,7 +5,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import zw.qantra.tm.domain.models.Provider;
import zw.qantra.tm.domain.services.ProviderService;
import zw.qantra.tm.rest.ApiResponse;
import java.util.UUID;
@@ -18,40 +17,40 @@ public class ProviderController {
@GetMapping
public ResponseEntity getProviders(@RequestParam(required = false) String category) {
return ApiResponse.ok(providerService.getProviders(category));
return ResponseEntity.ok(providerService.getProviders(category));
}
@GetMapping("/{id}/products")
public ResponseEntity getProducts(@PathVariable UUID id) {
return ApiResponse.ok(providerService.getProviderProducts(id));
return ResponseEntity.ok(providerService.getProviderProducts(id));
}
@GetMapping("/{id}")
public ResponseEntity getProvider(@PathVariable UUID id) {
return providerService.getProvider(id)
.map(ApiResponse::ok)
.orElse(ApiResponse.notFound());
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity createProvider(@RequestBody Provider provider) {
return ApiResponse.ok(providerService.createProvider(provider));
return ResponseEntity.ok(providerService.createProvider(provider));
}
@PutMapping("/{id}")
public ResponseEntity updateProvider(@PathVariable UUID id, @RequestBody Provider provider) {
Provider updatedProvider = providerService.updateProvider(id, provider);
if (updatedProvider != null) {
return ApiResponse.ok(updatedProvider);
return ResponseEntity.ok(updatedProvider);
}
return ApiResponse.notFound();
return ResponseEntity.notFound().build();
}
@DeleteMapping("/{id}")
public ResponseEntity deleteProvider(@PathVariable UUID id) {
if (providerService.deleteProvider(id)) {
return ApiResponse.ok("Provider deleted successfully");
return ResponseEntity.ok("Provider deleted successfully");
}
return ApiResponse.notFound();
return ResponseEntity.notFound().build();
}
}

View File

@@ -4,8 +4,11 @@ import lombok.RequiredArgsConstructor;
import zw.qantra.tm.domain.services.TransactionService;
import zw.qantra.tm.domain.models.Transaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@@ -13,6 +16,7 @@ import java.util.UUID;
@RequestMapping("/transaction")
@RequiredArgsConstructor
public class TransactonController {
Logger logger = LoggerFactory.getLogger(TransactonController.class);
private final TransactionService transactionService;
@@ -28,9 +32,17 @@ public class TransactonController {
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/poll/{id}")
public ResponseEntity<Transaction> poll(@PathVariable UUID id) {
return ResponseEntity.ok(transactionService.poll(id));
}
@PostMapping
public ResponseEntity<Transaction> create(@RequestBody Transaction transaction) {
return ResponseEntity.ok(transactionService.execute(transaction));
logger.info("incoming transaction: {}", transaction);
Transaction createdTransaction = transactionService.execute(transaction);
logger.info("outgoing transaction: {}", createdTransaction);
return ResponseEntity.ok(createdTransaction);
}
@PutMapping("/{id}")

View File

@@ -0,0 +1,27 @@
package zw.qantra.tm.domain.dtos;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SdkActionDto {
private String clientId;
private String clientSecret;
private String phone;
private String amount;
private String currency;
private String hash;
private String authType;
private String billerReference;
private String action;
private Boolean sandbox;
private String paymentProcessorLabel;
private String responseUrl;
}

View File

@@ -0,0 +1,9 @@
package zw.qantra.tm.domain.enums;
public enum AuthType {
WEB,
USSD,
WHATSAPP,
MOBILE,
REMOTE
}

View File

@@ -1,6 +1,7 @@
package zw.qantra.tm.domain.enums;
public enum RequestType {
CONFIRM,
REQUEST,
CONFIRM
INTEGRATION
}

View File

@@ -0,0 +1,27 @@
package zw.qantra.tm.domain.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AdditionalData extends BaseEntity {
@OneToOne
@JoinColumn(name = "transaction_id")
private Transaction transaction;
@Column(columnDefinition = "TEXT")
private String jsonString;
}

View File

@@ -1,5 +1,6 @@
package zw.qantra.tm.domain.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import lombok.*;
@@ -14,5 +15,6 @@ public class PaymentProcessor extends BaseEntity {
private String description;
private String label;
private String type;
@Column(columnDefinition = "TEXT")
private String image;
}

View File

@@ -3,12 +3,12 @@ package zw.qantra.tm.domain.models;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.persistence.*;
import lombok.*;
import zw.qantra.tm.domain.enums.AuthType;
import zw.qantra.tm.domain.enums.CurrencyType;
import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.enums.Status;
import java.math.BigDecimal;
import java.util.List;
@Entity
@Getter
@@ -35,12 +35,14 @@ public class Transaction extends BaseEntity {
private String channel;
private String debitPhone;
private String debitAccount;
@Enumerated(EnumType.STRING)
private CurrencyType debitCurrency;
private String debitName;
private String debitCard;
private String debitRef;
private String creditPhone;
private String creditAccount;
@Enumerated(EnumType.STRING)
private CurrencyType creditCurrency;
private String creditName;
private String creditCard;
@@ -50,12 +52,20 @@ public class Transaction extends BaseEntity {
private String aggregatorId;
private String billName;
private String billProductName;
@Enumerated(EnumType.STRING)
private RequestType type;
@Enumerated(EnumType.STRING)
private AuthType authType;
@Column(columnDefinition = "TEXT")
private String errorMessage;
private String responseCode;
private Status status;
@Column(columnDefinition = "TEXT")
private String targetUrl;
private String sdkActionId;
@Transient
private List<Object> additionalData;
private Object additionalData;
}

View File

@@ -0,0 +1,13 @@
package zw.qantra.tm.domain.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import zw.qantra.tm.domain.models.AdditionalData;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface AdditionalDataRepository extends JpaRepository<AdditionalData, UUID> {
Optional<AdditionalData> findByTransactionId(UUID transactionId);
}

View File

@@ -10,4 +10,5 @@ import java.util.UUID;
@Repository
public interface ProviderRepository extends JpaRepository<Provider, UUID> {
List<Provider> findByCategory(String category);
Provider findByClientId(String clientId);
}

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();
}
}

View File

@@ -35,10 +35,9 @@ public class RestTemplateInterceptor
// we don't log file data
if(request.getHeaders().getContentLength() < 10000){
logger.info("Request: {}: {}\n{}\n{}",
logger.info("Request: {}: {}\n{}",
request.getMethod(),
request.getURI(),
request.getHeaders().toSingleValueMap(),
gson.toJson(gson.fromJson(requestBody, HashMap.class)));
}
ClientHttpResponse response = execution.execute(request, reqBody);

View File

@@ -39,7 +39,7 @@ public class Utils {
public static String getUid(){
return UUID.randomUUID().toString();
}
public static <T> T getJson(String json, Class<T> clazz){
public static <T> T fromJson(String json, Class<T> clazz){
ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.setDateFormat(new StdDateFormat().withColonInTimeZone(true));
@@ -56,7 +56,7 @@ public class Utils {
return (T) obj;
}
public static String setJson(Object obj){
public static String toJson(Object obj){
ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.setDateFormat(new StdDateFormat().withColonInTimeZone(true));

View File

@@ -15,5 +15,10 @@ spring.datasource.username=postgres
spring.datasource.password=password
sbz.aggregator.url=http://192.168.16.29:24050/v1
sbz.aggregator.client-id=small_bakery
sbz.aggregator.client-secret=tmtCUeJA1dQezn405HSrYSdhV81XiBEi
sbz.aggregator.client-id=steward_pay
sbz.aggregator.client-secret=XjkbBAJeJCnAplYDTbp5xOjeyySVnYGp
sbz.aggregator.phone=263773591219
sbz.aggregator.account=1046737845
sbz.aggregator.encryption-key=735470ce-8efb-4c0f-ba3f-7298f805ab33
steward.payment.processor.url=http://192.168.16.29:24050/v1