completed initial scaffolding for services

This commit is contained in:
2025-06-16 23:12:38 +02:00
parent eee746a5d8
commit b214d42d2f
24 changed files with 305 additions and 88 deletions

View File

@@ -6,13 +6,17 @@ 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;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.TransactionService;
@RestController
@RequestMapping("/confirm/transaction")
@RequiredArgsConstructor
public class ConfirmationController {
private final TransactionService transactionService;
@PostMapping()
public ResponseEntity post(@RequestBody Object transactionRequest) {
return ResponseEntity.ok("Transaction endpoint is working");
public ResponseEntity post(@RequestBody Transaction transaction) {
return ResponseEntity.ok(transactionService.execute(transaction));
}
}

View File

@@ -0,0 +1,7 @@
package zw.qantra.tm.domain.enums;
public enum ChargeConditionType {
PROVIDER, // bill provider conditions
GATEWAY, // payment gateway conditions
TRANSACTION // qantra conditions
}

View File

@@ -0,0 +1,7 @@
package zw.qantra.tm.domain.enums;
public enum ChargeLabelEnum {
FEE,
TAX,
GATEWAY_FEE
}

View File

@@ -0,0 +1,9 @@
package zw.qantra.tm.domain.enums;
public enum CurrencyType {
ZWL,
ZWG,
USD,
ZiG,
ZAR
}

View File

@@ -0,0 +1,9 @@
package zw.qantra.tm.domain.enums;
public enum Status {
SUCCESS,
PENDING,
FAILED,
CANCELLED,
REVERSED
}

View File

@@ -0,0 +1,6 @@
package zw.qantra.tm.domain.enums;
public enum TargetEvent {
INCLUDE,
EXCLUDE
}

View File

@@ -0,0 +1,67 @@
package zw.qantra.tm.domain.models;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
import zw.qantra.tm.domain.enums.ChargeLabelEnum;
import zw.qantra.tm.domain.enums.CurrencyType;
import zw.qantra.tm.domain.enums.TargetEvent;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Charge {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String description;
// % rate to charge
private BigDecimal percentageRate;
private BigDecimal flat;
private boolean composite; // composite is % plus flat
private boolean global;
private BigDecimal min;
private BigDecimal max;
private BigDecimal minimumAmount;
private BigDecimal maximumAmount;
@Enumerated(EnumType.STRING)
private ChargeLabelEnum chargeLabel;
@Enumerated(EnumType.STRING)
private CurrencyType currency;
private String account;
private String target;
private String targetValue;
@Enumerated(EnumType.STRING)
private TargetEvent targetEvent; // include / exclude
@JsonIgnore
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(
name = "charge_include_products",
joinColumns = @JoinColumn(name = "charge_id"),
inverseJoinColumns = @JoinColumn(name = "charge_condition_id")
)
private Set<ChargeCondition> includes = new HashSet<>();
@JsonIgnore
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(
name = "charge_exclude_products",
joinColumns = @JoinColumn(name = "charge_id"),
inverseJoinColumns = @JoinColumn(name = "charge_condition_id")
)
private Set<ChargeCondition> excludes = new HashSet<>();
}

View File

@@ -0,0 +1,31 @@
package zw.qantra.tm.domain.models;
import jakarta.persistence.*;
import lombok.*;
import zw.qantra.tm.domain.enums.ChargeConditionType;
import java.util.List;
import java.util.UUID;
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChargeCondition {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String name;
@Enumerated(EnumType.STRING)
private ChargeConditionType type;
@Column(columnDefinition = "longtext")
private String codes;
@ManyToMany(mappedBy = "includes")
List<Charge> includeCharges;
@ManyToMany(mappedBy = "excludes")
List<Charge> excludeCharges;
}

View File

@@ -1,52 +0,0 @@
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

@@ -1,10 +1,14 @@
package zw.qantra.tm.domain.models;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.persistence.*;
import lombok.*;
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;
import java.util.UUID;
@Entity
@@ -13,6 +17,7 @@ import java.util.UUID;
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Transaction {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@@ -20,6 +25,10 @@ public class Transaction {
private String trace;
private BigDecimal amount;
private BigDecimal charge;
private BigDecimal gatewayCharge;
private BigDecimal tax;
private BigDecimal totalAmount;
private String reference;
private String providerUid;
private String productUid;
@@ -30,13 +39,13 @@ public class Transaction {
private String channel;
private String debitPhone;
private String debitAccount;
private String debitCurrency;
private CurrencyType debitCurrency;
private String debitName;
private String debitCard;
private String debitRef;
private String creditPhone;
private String creditAccount;
private String creditCurrency;
private CurrencyType creditCurrency;
private String creditName;
private String creditCard;
private String creditRef;
@@ -46,4 +55,11 @@ public class Transaction {
private String billName;
private String billProductName;
private RequestType type;
private String errorMessage;
private String responseCode;
private Status status;
@Transient
private List<Object> additionalData;
}

View File

@@ -1,11 +0,0 @@
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

@@ -1,11 +0,0 @@
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

@@ -2,10 +2,15 @@ package zw.qantra.tm.domain.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.enums.Status;
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;
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;
@Service
@RequiredArgsConstructor
@@ -13,9 +18,26 @@ 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);
public Transaction execute(Transaction transaction) {
LogicPipeline<Transaction, Transaction> transactionLogic =
new LogicPipeline<>(new CalculateChargesHandler())
.addHandler(new ValidationHandler());
transaction = transactionLogic.execute(transaction);
String label = transaction.getType() + "_" + transaction.getIntegrationProcessorLabel();
TransactionProcessorInterface transactionProcessorInterface =
(TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor(label);
try {
transactionProcessorInterface.process(transaction);
}catch (Exception e){
e.printStackTrace();
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("01");
transaction.setErrorMessage(e.getMessage());
transactionRepository.save(transaction);
return transaction;
}
return transaction;
}
}

View File

@@ -9,6 +9,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import zw.qantra.tm.exceptions.ApiException;
@Component
@RequiredArgsConstructor
@@ -28,11 +29,13 @@ public class PaymentProcessorFactory implements ApplicationContextAware {
try {
paymentProcessor = applicationContext.getBean(paymentProcessorName);
} catch (NoSuchBeanDefinitionException exception) {
exception.printStackTrace();
logger.error("Couldn't find payment processor: {}", exception.getMessage());
return null;
throw new ApiException("Couldn't find payment processor");
} catch (BeansException e) {
e.printStackTrace();
logger.error("Error retrieving payment processor bean: {}", e.getMessage());
return null;
throw new ApiException("Error retrieving payment processor bean");
}
return paymentProcessor;

View File

@@ -3,6 +3,6 @@ package zw.qantra.tm.domain.services.processors;
import zw.qantra.tm.domain.models.Transaction;
public interface TransactionProcessorInterface {
Object process(Transaction transaction);
Object process(Transaction transaction) throws Exception;
Object reverse(Transaction transaction);
}

View File

@@ -1,21 +1,25 @@
package zw.qantra.tm.domain.services.processors.confirmations;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
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")
@Service("CONFIRM_ZESA")
@RequiredArgsConstructor
public class ZesaConfirmationProcessor implements TransactionProcessorInterface {
private final RestService restService;
@Value("${sbz.aggregator.url}")
private String aggregatorUrl;
@Override
public Object process(Transaction transaction) {
return transaction;
public Object process(Transaction transaction) throws Exception {
System.out.println("processing transaction");
throw new Exception("Challenge encountered, please contact support.");
// return transaction;
}
@Override

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.services.processors.handlers;
import zw.qantra.tm.domain.models.Transaction;
public class CalculateChargesHandler implements LogicHandler<Transaction, Transaction> {
@Override
public Transaction process(Transaction transaction) {
System.out.println("processing charges");
return transaction;
}
}

View File

@@ -0,0 +1,5 @@
package zw.qantra.tm.domain.services.processors.handlers;
interface LogicHandler<I, O> {
O process(I input);
}

View File

@@ -0,0 +1,20 @@
package zw.qantra.tm.domain.services.processors.handlers;
import org.springframework.stereotype.Service;
public class LogicPipeline<I, O> {
private final LogicHandler<I, O> currentHandler;
public LogicPipeline(LogicHandler<I, O> currentHandler) {
this.currentHandler = currentHandler;
}
public <K> LogicPipeline<I, K> addHandler(LogicHandler<O, K> newHandler) {
return new LogicPipeline<>(input -> newHandler.process(currentHandler.process(input)));
}
public O execute(I input) {
return currentHandler.process(input);
}
}

View File

@@ -0,0 +1,11 @@
package zw.qantra.tm.domain.services.processors.handlers;
import zw.qantra.tm.domain.models.Transaction;
public class ValidationHandler implements LogicHandler<Transaction, Transaction> {
@Override
public Transaction process(Transaction transaction) {
System.out.println("processing validation");
return transaction;
}
}

View File

@@ -0,0 +1,28 @@
package zw.qantra.tm.domain.services.processors.integrations;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
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("REQUEST_ZESA")
@RequiredArgsConstructor
public class ZesaIntegrationProcessor implements TransactionProcessorInterface {
private final RestService restService;
@Value("${sbz.aggregator.url}")
private String aggregatorUrl;
@Override
public Object process(Transaction transaction) throws Exception {
return transaction;
}
@Override
public Object reverse(Transaction transaction) {
return transaction;
}
}

View File

@@ -0,0 +1,24 @@
package zw.qantra.tm.domain.services.processors.payments;
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("STEWARD_WEB")
@RequiredArgsConstructor
public class StewardWebPaymentProcessor implements TransactionProcessorInterface {
private final RestService restService;
@Override
public Object process(Transaction transaction) throws Exception {
return transaction;
}
@Override
public Object reverse(Transaction transaction) {
return transaction;
}
}

View File

@@ -12,3 +12,5 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.datasource.url=jdbc:mysql://localhost:3306/qpay?useLegacyDatetimeCode=false
spring.datasource.username=root
spring.datasource.password=password
sbz.aggregator.url=http://192.168.16.29:24050/v1

View File

@@ -1,5 +1,8 @@
spring.application.name=tm
debug=true
server.servlet.context-path=/api
logging.level.root=INFO
logging.level.org.springframework.boot.autoconfigure=INFO
@@ -12,3 +15,5 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.datasource.url=jdbc:mysql://localhost:3306/qpay?useLegacyDatetimeCode=false
spring.datasource.username=root
spring.datasource.password=password
sbz.aggregator.url=http://192.168.16.29:24050/v1