completed initial setup

This commit is contained in:
2025-06-16 18:13:20 +02:00
parent b435c2e5f5
commit eee746a5d8
34 changed files with 817 additions and 3 deletions

View File

@@ -0,0 +1,18 @@
package zw.qantra.tm.domain.controllers;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/confirm/transaction")
@RequiredArgsConstructor
public class ConfirmationController {
@PostMapping()
public ResponseEntity post(@RequestBody Object transactionRequest) {
return ResponseEntity.ok("Transaction endpoint is working");
}
}

View File

@@ -0,0 +1,18 @@
package zw.qantra.tm.domain.controllers;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/integration/transaction")
@RequiredArgsConstructor
public class IntegrationController {
@PostMapping()
public ResponseEntity post(@RequestBody Object transactionRequest) {
return ResponseEntity.ok("Transaction endpoint is working");
}
}

View File

@@ -0,0 +1,18 @@
package zw.qantra.tm.domain.controllers;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/transaction")
@RequiredArgsConstructor
public class TransactonController {
@PostMapping()
public ResponseEntity post(@RequestBody Object transactionRequest) {
return ResponseEntity.ok("Transaction endpoint is working");
}
}

View File

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

View File

@@ -0,0 +1,52 @@
package zw.qantra.tm.domain.models;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;
import zw.qantra.tm.domain.enums.RequestType;
import java.math.BigDecimal;
import java.util.UUID;
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ConfirmTransaction {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String trace;
private BigDecimal amount;
private String reference;
private String providerUid;
private String productUid;
private String paymentProcessorLabel;
private String integrationProcessorLabel;
private String rrn;
private String channelName;
private String channel;
private String debitPhone;
private String debitAccount;
private String debitCurrency;
private String debitName;
private String debitCard;
private String debitRef;
private String creditPhone;
private String creditAccount;
private String creditCurrency;
private String creditName;
private String creditCard;
private String creditRef;
private String billClientId;
private String clientSecret;
private String aggregatorId;
private String billName;
private String billProductName;
private RequestType type;
}

View File

@@ -0,0 +1,25 @@
package zw.qantra.tm.domain.models;
import java.util.UUID;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Currency {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String isoCode;
private String name;
private String displayName;
}

View File

@@ -0,0 +1,25 @@
package zw.qantra.tm.domain.models;
import java.util.UUID;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IntegrationProcessor {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String name;
private String description;
private String type;
}

View File

@@ -0,0 +1,26 @@
package zw.qantra.tm.domain.models;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;
import java.util.UUID;
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PaymentProcessor {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String name;
private String description;
private String label;
private String type;
}

View File

@@ -0,0 +1,25 @@
package zw.qantra.tm.domain.models;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;
import java.util.UUID;
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Provider {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String clientId;
private String name;
private String description;
}

View File

@@ -0,0 +1,28 @@
package zw.qantra.tm.domain.models;
import java.util.UUID;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Setting {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String name;
private String value;
}

View File

@@ -0,0 +1,49 @@
package zw.qantra.tm.domain.models;
import jakarta.persistence.*;
import lombok.*;
import zw.qantra.tm.domain.enums.RequestType;
import java.math.BigDecimal;
import java.util.UUID;
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Transaction {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String trace;
private BigDecimal amount;
private String reference;
private String providerUid;
private String productUid;
private String paymentProcessorLabel;
private String integrationProcessorLabel;
private String rrn;
private String channelName;
private String channel;
private String debitPhone;
private String debitAccount;
private String debitCurrency;
private String debitName;
private String debitCard;
private String debitRef;
private String creditPhone;
private String creditAccount;
private String creditCurrency;
private String creditName;
private String creditCard;
private String creditRef;
private String billClientId;
private String clientSecret;
private String aggregatorId;
private String billName;
private String billProductName;
private RequestType type;
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import zw.qantra.tm.domain.models.ConfirmTransaction;
import java.util.UUID;
@Repository
public interface ConfirmTransactionRepository extends JpaRepository<ConfirmTransaction, UUID> {
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import zw.qantra.tm.domain.models.Currency;
import java.util.UUID;
@Repository
public interface CurrencyRepository extends JpaRepository<Currency, UUID> {
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import zw.qantra.tm.domain.models.IntegrationProcessor;
import java.util.UUID;
@Repository
public interface IntegrationProcessorRepository extends JpaRepository<IntegrationProcessor, UUID> {
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import zw.qantra.tm.domain.models.PaymentProcessor;
import java.util.UUID;
@Repository
public interface PaymentProcessorRepository extends JpaRepository<PaymentProcessor, UUID> {
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import zw.qantra.tm.domain.models.Provider;
import java.util.UUID;
@Repository
public interface ProviderRepository extends JpaRepository<Provider, UUID> {
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import zw.qantra.tm.domain.models.Setting;
import java.util.UUID;
@Repository
public interface SettingRepository extends JpaRepository<Setting, UUID> {
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import zw.qantra.tm.domain.models.Transaction;
import java.util.UUID;
@Repository
public interface TransactionRepository extends JpaRepository<Transaction, UUID> {
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.repositories.ConfirmTransactionRepository;
@Service
@RequiredArgsConstructor
public class ConfirmTransactionService {
private final ConfirmTransactionRepository confirmTransactionRepository;
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.repositories.CurrencyRepository;
@Service
@RequiredArgsConstructor
public class CurrencyService {
private final CurrencyRepository currencyRepository;
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.repositories.IntegrationProcessorRepository;
@Service
@RequiredArgsConstructor
public class IntegrationProcessorService {
private final IntegrationProcessorRepository integrationProcessorRepository;
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.repositories.PaymentProcessorRepository;
@Service
@RequiredArgsConstructor
public class PaymentProcessorService {
private final PaymentProcessorRepository paymentProcessorRepository;
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.repositories.ProviderRepository;
@Service
@RequiredArgsConstructor
public class ProviderService {
private final ProviderRepository providerRepository;
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.repositories.SettingRepository;
@Service
@RequiredArgsConstructor
public class SettingService {
private final SettingRepository settingRepository;
}

View File

@@ -0,0 +1,21 @@
package zw.qantra.tm.domain.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.models.PaymentProcessor;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.repositories.TransactionRepository;
import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory;
@Service
@RequiredArgsConstructor
public class TransactionService {
private final TransactionRepository transactionRepository;
private final PaymentProcessorFactory paymentProcessorFactory;
private Transaction bootstrap(Transaction transaction) {
String label = transaction.getType() + "_" + transaction.getPaymentProcessorLabel();
// PaymentProcessor paymentProcessor = paymentProcessorFactory.getPaymentProcessor(label);
return transaction;
}
}

View File

@@ -0,0 +1,40 @@
package zw.qantra.tm.domain.services.factories;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class PaymentProcessorFactory implements ApplicationContextAware {
private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public Object getPaymentProcessor(String paymentProcessorName) {
Object paymentProcessor;
try {
paymentProcessor = applicationContext.getBean(paymentProcessorName);
} catch (NoSuchBeanDefinitionException exception) {
logger.error("Couldn't find payment processor: {}", exception.getMessage());
return null;
} catch (BeansException e) {
logger.error("Error retrieving payment processor bean: {}", e.getMessage());
return null;
}
return paymentProcessor;
}
}

View File

@@ -0,0 +1,8 @@
package zw.qantra.tm.domain.services.processors;
import zw.qantra.tm.domain.models.Transaction;
public interface TransactionProcessorInterface {
Object process(Transaction transaction);
Object reverse(Transaction transaction);
}

View File

@@ -0,0 +1,25 @@
package zw.qantra.tm.domain.services.processors.confirmations;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
import zw.qantra.tm.rest.RestService;
@Service("CONFIRM_POWERTEL_ZESA")
@RequiredArgsConstructor
public class ZesaConfirmationProcessor implements TransactionProcessorInterface {
private final RestService restService;
@Override
public Object process(Transaction transaction) {
return transaction;
}
@Override
public Object reverse(Transaction transaction) {
return transaction;
}
}

View File

@@ -0,0 +1,195 @@
package zw.qantra.tm.rest;
import com.google.gson.Gson;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import zw.qantra.tm.exceptions.RestTemplateResponseErrorHandler;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/*
* Rest service that facilitates communication between services. Authentication
* is either based on client credentials, user credentials or is open. User credentials
* are only available for registered users
* */
@Service
@RequiredArgsConstructor
public class RestService {
Logger logger = LoggerFactory.getLogger(this.getClass().toString());
@Autowired
private Gson gson;
public final RestTemplate restTemplate;
public RestService() {
this.restTemplate = new RestTemplate(
new BufferingClientHttpRequestFactory(
new SimpleClientHttpRequestFactory()
)
);
this.restTemplate.setErrorHandler(new RestTemplateResponseErrorHandler());
// bootstrap rest template
List<ClientHttpRequestInterceptor> interceptors
= this.restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new RestTemplateInterceptor(gson));
this.restTemplate.setInterceptors(interceptors);
}
public String getUserToken(){
String token = "Utils.getToken()";
logger.info(token);
return token;
}
// client token is retrieved from open ID identity provider and
// cached for 20 days for easy reuse.
// todo: definitely not the best way to do this, tried implementing caching but
// that needs a bit of time to do correctly
public String getClientToken(){
logger.info("Getting client token");
MultiValueMap<String, String> payload = new LinkedMultiValueMap<>();
payload.add("grant_type", "client_credentials");
Map tokenResponse = getToken(payload);
assert tokenResponse != null;
String clientToken = (String) tokenResponse.get("access_token");
logger.info("client access token", "log", clientToken);
return clientToken;
}
public Map getToken(MultiValueMap payload){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// build the request
HttpEntity entity = new HttpEntity<>(payload, headers);
return this.restTemplate.postForObject("tokenUrl", entity, Map.class);
}
public <T> T getAsClient(String path, Class<T> clazz){
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(getClientToken());
HttpEntity entity = new HttpEntity<>(headers);
return this.restTemplate.getForObject(path, clazz, entity);
}
public <T> ResponseEntity<T> getExchangeAsClient(String path, Class<T> clazz){
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(getClientToken());
HttpEntity entity = new HttpEntity<>(headers);
return this.restTemplate.exchange(path, HttpMethod.GET, entity, clazz);
}
public <T> ResponseEntity<T> postAsClient(String path, Object payload, Class<T> clazz){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setBearerAuth(getClientToken());
// build the request
HttpEntity entity = new HttpEntity<>(payload, headers);
ResponseEntity responseEntity = this.restTemplate.postForEntity(path, entity, clazz);
return responseEntity;
}
public <T> T postAs(String path, Object payload, HttpHeaders headers, Class<T> clazz){
// build the request
HttpEntity entity = new HttpEntity<>(payload, headers);
return this.restTemplate.postForObject(path, entity, clazz);
}
public <T> T post(String path, Object payload, Class<T> clazz){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setBearerAuth(getUserToken());
// build the request
HttpEntity entity = new HttpEntity<>(payload, headers);
T t = this.restTemplate.postForObject(path, entity, clazz);
return t;
}
public <T> T postWithToken(String token , String path, Object payload, Class<T> clazz){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setBearerAuth(token);
// build the request
HttpEntity entity = new HttpEntity<>(payload, headers);
T t = this.restTemplate.postForObject(path, entity, clazz);
return t;
}
public <T> T postFormData(String path, MultiValueMap<String, Object> payload, Class<T> clazz)
{
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(payload, headers);
return this.restTemplate.postForEntity(path, request, clazz).getBody();
}
public <T> T get(String path, Type clazz){
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(getUserToken());
HttpEntity entity = new HttpEntity<>(headers);
ResponseEntity<String> responseEntity = this.restTemplate.exchange(path, HttpMethod.GET, entity, String.class);
T response = gson.fromJson(responseEntity.getBody(), clazz);
return response;
}
// make post request to endpoint without bearer auth
public <T> T postNoAuth(String path, Object payload, Class<T> clazz){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
// build the request
HttpEntity entity = new HttpEntity<>(payload, headers);
T t = this.restTemplate.postForObject(path, entity, clazz);
return t;
}
// make post request to endpoint without bearer auth
public <T> T postWithExternalAuth(String path, Object payload, HttpHeaders headers, Class<T> clazz){
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
// build the request
HttpEntity entity = new HttpEntity<>(payload, headers);
T t = this.restTemplate.postForObject(path, entity, clazz);
return t;
}
// make get request to endpoint without bearer auth
public <T> T getNoAuth(String path, Class<T> clazz){
return this.restTemplate.getForObject(path, clazz);
}
public <T> ResponseEntity<?> exchangeGetNoAuth(String path, HttpEntity<?> httpEntity, Class<?> clazz, String pathVariable) {
return this.restTemplate.exchange(path, HttpMethod.GET, httpEntity, clazz, pathVariable);
}
}

View File

@@ -0,0 +1,72 @@
package zw.qantra.tm.rest;
import com.google.gson.Gson;
import lombok.RequiredArgsConstructor;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.stream.Collectors;
@RequiredArgsConstructor
public class RestTemplateInterceptor
implements ClientHttpRequestInterceptor {
private final Logger logger = LoggerFactory.getLogger(getClass().getName());
private final Gson gson;
@Override
public ClientHttpResponse intercept(
HttpRequest request,
byte[] reqBody,
ClientHttpRequestExecution execution) throws IOException {
String requestBody = new String(reqBody, StandardCharsets.UTF_8);
if(!isJSONValid(requestBody)) requestBody = toBasicJson(requestBody);
// we don't log file data
if(request.getHeaders().getContentLength() < 10000){
logger.info("Request: "
+ request.getMethod() + ": "
+ request.getURI(), "http", gson.fromJson(requestBody, HashMap.class));
}
ClientHttpResponse response = execution.execute(request, reqBody);
InputStreamReader isr = new InputStreamReader(response.getBody());
String body = new BufferedReader(isr).lines()
.collect(Collectors.joining("\n"));
try{
logger.info("Response body: {}", "http", gson.fromJson(body, HashMap.class));
}catch (Exception exception){
logger.info("Problem serializing resp: {}", "http", exception.getMessage());
}
return response;
}
public String toBasicJson(String string){
HashMap map = new HashMap();
map.put("log", string);
return gson.toJson(map);
}
public boolean isJSONValid(String test) {
try {
new JSONObject(test);
} catch (JSONException ex) {
try {
new JSONArray(test);
} catch (JSONException ex1) {
return false;
}
}
return true;
}
}