fixing bug on transaction instances

This commit is contained in:
2025-10-23 09:10:54 +02:00
parent ed1b05f9b8
commit 1188b5361d
10 changed files with 81 additions and 268 deletions

View File

@@ -40,5 +40,5 @@ public class BaseEntity {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
private LocalDateTime updatedAt; // Or Date, Timestamp private LocalDateTime updatedAt; // Or Date, Timestamp
private Boolean deleted; private Boolean deleted = false;
} }

View File

@@ -66,7 +66,6 @@ public class EconetConfirmationProcessor implements TransactionProcessorInterfac
transaction.setStatus(Status.SUCCESS); transaction.setStatus(Status.SUCCESS);
transaction.setResponseCode((String) body.get("responseCode")); transaction.setResponseCode((String) body.get("responseCode"));
transaction.setAdditionalData(body.get("additionalData")); transaction.setAdditionalData(body.get("additionalData"));
transactionService.save(transaction);
AdditionalData additionalData = AdditionalData.builder() AdditionalData additionalData = AdditionalData.builder()
.transactionId(transaction.getId().toString()) .transactionId(transaction.getId().toString())

View File

@@ -1,148 +0,0 @@
package zw.qantra.tm.domain.services.processors.handlers;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.ChargeService;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CalculateChargesHandler implements LogicHandler<Transaction, Transaction> {
Logger logger = LoggerFactory.getLogger(CalculateChargesHandler.class);
private final ChargeService chargeService;
@Override
public Transaction process(Transaction transaction) {
logger.info("processing charges");
transaction.setCharge(BigDecimal.ZERO);
transaction.setTax(BigDecimal.ZERO);
transaction.setGatewayCharge(BigDecimal.ZERO);
BigDecimal amountToCharge = transaction.getAmount();
BigDecimal chargeAmount = BigDecimal.ZERO;
List<Charge> chargeList = chargeService.getChargeRepository().findAllByCurrency(transaction.getDebitCurrency());
List<Charge> appropriateCharges = new ArrayList<>();
for (Charge charge : chargeList) {
boolean isPresent = false;
for (ChargeCondition chargeCondition: charge.getIncludes()){
List<String> conditionTypes = Arrays.asList(chargeCondition.getCodes().split(","));
switch (chargeCondition.getType()){
case GATEWAY:
isPresent = conditionTypes.contains(transaction.getPaymentProcessorLabel());
break;
case PROVIDER:
isPresent = conditionTypes.contains(transaction.getIntegrationProcessorLabel());
break;
}
if(isPresent) break; // we've found a matching condition, no need to check rest of conditions
}
// if we did not find a match, means this charge does not apply to this transaction
// also implies there is no need to check exemptions
if(!isPresent) continue;
boolean isExempt = false;
for (ChargeCondition chargeCondition: charge.getExcludes()){
List<String> conditionTypes = Arrays.asList(chargeCondition.getCodes().split(","));
switch (chargeCondition.getType()){
case GATEWAY:
isExempt = conditionTypes.contains(transaction.getPaymentProcessorLabel());
break;
case PROVIDER:
isExempt = conditionTypes.contains(transaction.getIntegrationProcessorLabel());
break;
}
if(isExempt) break; // we've found a matching condition, no need to check rest of conditions
}
if(isExempt) continue; // if we found a match, means this transaction is exempt from this charge
appropriateCharges.add(charge);
}
for (Charge charge : appropriateCharges) {
// if minimum amount chargeable is lower than the amount to charge, apply the min charge
if(charge.getMinimumAmount() != null) {
if(charge.getMinimumAmount().compareTo(amountToCharge) > 0) {
applyCharge(transaction, charge, charge.getMin());
continue;
}
}
// if maximum amount chargeable is higher than the amount to charge, apply the max charge
if(charge.getMaximumAmount() != null) {
if(charge.getMaximumAmount().compareTo(amountToCharge) < 0) {
applyCharge(transaction, charge, charge.getMax());
continue;
}
}
// calculate unrestricted charge amount
// if percentage rate is not present then it's a flat charge, else it's a percentage
if(charge.getPercentageRate() == null) {
if(charge.getFlat() == null)
continue;
chargeAmount = charge.getFlat();
}else {
BigDecimal fraction = charge.getPercentageRate().divide(
new BigDecimal(100), 4, RoundingMode.CEILING);
chargeAmount = amountToCharge.multiply(fraction)
.setScale(2, RoundingMode.CEILING);
// if it's a composite charge then add the flat component as well
if(charge.isComposite()) {
chargeAmount = chargeAmount.add(charge.getFlat())
.setScale(2, RoundingMode.CEILING);
}
}
// if minimum charge is set, check if the charge amount is less than the minimum
if(charge.getMin() != null) {
if(charge.getMin().compareTo(chargeAmount) > 0){
chargeAmount = charge.getMin();
}
}
// if maximum charge is set, check if the charge amount is more than the maximum
if(charge.getMax() != null) {
if(charge.getMax().compareTo(chargeAmount) < 0) {
chargeAmount = charge.getMax();
}
}
applyCharge(transaction, charge, chargeAmount);
}
// calculate total amount for ease of use later
transaction.setTotalAmount(
transaction.getAmount().add(
transaction.getCharge().add(
transaction.getTax().add(
transaction.getGatewayCharge()))));
return transaction;
}
void applyCharge(Transaction transaction, Charge charge, BigDecimal chargeAmount) {
if(chargeAmount == null) return;
switch (charge.getChargeLabel()) {
case FEE -> transaction.setCharge(chargeAmount);
case TAX -> transaction.setTax(chargeAmount);
case GATEWAY_FEE -> transaction.setGatewayCharge(chargeAmount);
}
}
}

View File

@@ -1,33 +0,0 @@
package zw.qantra.tm.domain.services.processors.handlers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import zw.qantra.tm.domain.models.Provider;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.ProviderService;
import zw.qantra.tm.exceptions.ApiException;
@Service
@RequiredArgsConstructor
public class ConfirmationLabelHandler implements LogicHandler<Transaction, Transaction> {
private final Logger logger = LoggerFactory.getLogger(ConfirmationLabelHandler.class);
private final ProviderService providerService;
@Override
public Transaction process(Transaction transaction) {
Provider provider = providerService.getProviderRepository()
.findByClientId(transaction.getBillClientId());
if(provider == null){
throw new ApiException("Provider not found for billClientId: " + transaction.getBillClientId());
}
String label = transaction.getType() + "_" + provider.getLabel();
transaction.setConfirmationProcessorLabel(label);
logger.info("label: {}", label);
return transaction;
}
}

View File

@@ -1,18 +0,0 @@
package zw.qantra.tm.domain.services.processors.handlers;
import java.util.UUID;
import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.utils.Utils;
public class ConfirmationLogicHandler implements LogicHandler<Transaction, Transaction> {
@Override
public Transaction process(Transaction transaction) {
if(transaction.getType().equals(RequestType.CONFIRM)) {
transaction.setTrace(Utils.getUid());
transaction.setReference(UUID.randomUUID().toString());
}
return transaction;
}
}

View File

@@ -1,59 +0,0 @@
package zw.qantra.tm.domain.services.processors.handlers;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import zw.qantra.tm.domain.models.Transaction;
public class FormattingHandler implements LogicHandler<Transaction, Transaction> {
@Override
public Transaction process(Transaction transaction) {
System.out.println("processing formatting");
// strip credit account and make sure there's nothing funny
String creditAccount = transaction.getCreditAccount();
if (creditAccount != null && !creditAccount.isEmpty()) {
// Remove all non-hexadecimal characters (keep only 0-9, a-f, A-F)
String strippedAccount = creditAccount.replaceAll("[^0-9a-fA-F]", "");
// Only set if the stripped result is not empty
if (!strippedAccount.isEmpty()) {
transaction.setCreditAccount(strippedAccount);
} else {
// If stripping results in empty string, set to null
transaction.setCreditAccount(null);
}
}
// sanitize phone number formatting
String debitPhoneNumber = transaction.getDebitPhone();
String creditPhoneNumber = transaction.getCreditPhone();
PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
if(debitPhoneNumber != null && !debitPhoneNumber.isEmpty()){
try {
// phone must begin with '+'
PhoneNumber zimPhoneNumber = phoneUtil.parse(debitPhoneNumber, "");
String formattedPhoneNumber = phoneUtil.format(zimPhoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
transaction.setDebitPhone(formattedPhoneNumber);
} catch (NumberParseException e) {
System.err.println("NumberParseException was thrown: " + e.toString());
}
}
if(creditPhoneNumber != null && !creditPhoneNumber.isEmpty()){
try {
PhoneNumber zimPhoneNumber = phoneUtil.parse(creditPhoneNumber, "");
String formattedPhoneNumber = phoneUtil.format(zimPhoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
transaction.setCreditPhone(formattedPhoneNumber);
} catch (NumberParseException e) {
System.err.println("NumberParseException was thrown: " + e.toString());
}
}
return transaction;
}
}

View File

@@ -1,5 +1,6 @@
package zw.qantra.tm.domain.workflows; package zw.qantra.tm.domain.workflows;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.nflow.engine.workflow.curated.State; import io.nflow.engine.workflow.curated.State;
import io.nflow.engine.workflow.definition.NextAction; import io.nflow.engine.workflow.definition.NextAction;
@@ -17,6 +18,7 @@ import zw.qantra.tm.domain.workflows.handlers.HandlerInterface;
import zw.qantra.tm.domain.workflows.handlers.WorkflowHandlerFactory; import zw.qantra.tm.domain.workflows.handlers.WorkflowHandlerFactory;
import zw.qantra.tm.utils.Utils; import zw.qantra.tm.utils.Utils;
import java.io.IOException;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@Component @Component
@@ -35,6 +37,7 @@ public class TransactionWorkflow extends WorkflowDefinition {
public static final State CALCULATE_CHARGES = new State("calculateCharges", WorkflowStateType.start); public static final State CALCULATE_CHARGES = new State("calculateCharges", WorkflowStateType.start);
public static final State CLEANUP = new State("cleanup"); public static final State CLEANUP = new State("cleanup");
public static final State CONFIRM = new State("confirm"); public static final State CONFIRM = new State("confirm");
public static final State RECIPIENTS_UPDATE = new State("recipientsUpdate");
public static final State CONFIRM_SUCCESS = new State("confirmSuccess", WorkflowStateType.manual); public static final State CONFIRM_SUCCESS = new State("confirmSuccess", WorkflowStateType.manual);
public static final State SALES_INVOICE = new State("salesInvoice"); public static final State SALES_INVOICE = new State("salesInvoice");
public static final State GATEWAY_PAYMENT = new State("gatewayPayment"); public static final State GATEWAY_PAYMENT = new State("gatewayPayment");
@@ -54,7 +57,8 @@ public class TransactionWorkflow extends WorkflowDefinition {
permit(CALCULATE_CHARGES, CLEANUP); permit(CALCULATE_CHARGES, CLEANUP);
permit(CLEANUP, CONFIRM); permit(CLEANUP, CONFIRM);
permit(CONFIRM, CONFIRM_SUCCESS, FAILED); permit(CONFIRM, RECIPIENTS_UPDATE, FAILED);
permit(RECIPIENTS_UPDATE, CONFIRM_SUCCESS);
permit(CONFIRM_SUCCESS, SALES_INVOICE); permit(CONFIRM_SUCCESS, SALES_INVOICE);
permit(SALES_INVOICE, GATEWAY_PAYMENT); permit(SALES_INVOICE, GATEWAY_PAYMENT);
permit(GATEWAY_PAYMENT, GATEWAY_PAYMENT_SUCCESS, FAILED); permit(GATEWAY_PAYMENT, GATEWAY_PAYMENT_SUCCESS, FAILED);
@@ -100,13 +104,24 @@ public class TransactionWorkflow extends WorkflowDefinition {
transaction.setWorkflowId(execution.getWorkflowInstanceId()); transaction.setWorkflowId(execution.getWorkflowInstanceId());
transaction = process(transaction, "confirm"); transaction = process(transaction, "confirm");
execution.setVariable("body", Utils.toJson(transaction)); execution.setVariable("body", Utils.toJson(transaction));
return NextAction.moveToState(CONFIRM_SUCCESS, "Transaction confirmed successfully"); return NextAction.moveToState(RECIPIENTS_UPDATE, "Recipients updated successfully");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
return NextAction.moveToState(FAILED, e.getMessage()); return NextAction.moveToState(FAILED, e.getMessage());
} }
} }
public NextAction recipientsUpdate(StateExecution execution) throws IOException {
LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class);
byte[] json = objectMapper.writeValueAsBytes(map);
Transaction transaction = objectMapper.readValue(json, Transaction.class);
transaction.setWorkflowId(execution.getWorkflowInstanceId());
transaction = process(transaction, "recipientsUpdate");
execution.setVariable("body", Utils.toJson(transaction));
return NextAction.moveToState(CONFIRM_SUCCESS, "Transaction confirmed successfully");
}
public void confirmSuccess(StateExecution execution) { public void confirmSuccess(StateExecution execution) {
log.info("Confirm complete"); log.info("Confirm complete");
} }

View File

@@ -13,7 +13,6 @@ import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Provider; import zw.qantra.tm.domain.models.Provider;
import zw.qantra.tm.domain.models.Transaction; import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.ProviderService; import zw.qantra.tm.domain.services.ProviderService;
import zw.qantra.tm.domain.services.processors.handlers.ConfirmationLabelHandler;
import zw.qantra.tm.exceptions.ApiException; import zw.qantra.tm.exceptions.ApiException;
import zw.qantra.tm.utils.Utils; import zw.qantra.tm.utils.Utils;
@@ -22,7 +21,7 @@ import java.util.UUID;
@Service("cleanup") @Service("cleanup")
@RequiredArgsConstructor @RequiredArgsConstructor
public class CleanupHandler implements HandlerInterface { public class CleanupHandler implements HandlerInterface {
private final Logger logger = LoggerFactory.getLogger(ConfirmationLabelHandler.class); private final Logger logger = LoggerFactory.getLogger(CleanupHandler.class);
private final ProviderService providerService; private final ProviderService providerService;

View File

@@ -20,7 +20,7 @@ public class ConfirmHandler implements HandlerInterface {
@Override @Override
public Object process(Transaction transaction) throws Exception { public Object process(Transaction transaction) throws Exception {
transactionService.save(transaction); transaction = transactionService.save(transaction);
TransactionEvent transactionEvent = TransactionEvent.builder() TransactionEvent transactionEvent = TransactionEvent.builder()
.transactionId(transaction.getId().toString()) .transactionId(transaction.getId().toString())
@@ -46,14 +46,12 @@ public class ConfirmHandler implements HandlerInterface {
transaction.setIntegrationStatus(Status.FAILED); transaction.setIntegrationStatus(Status.FAILED);
transaction.setResponseCode("01"); transaction.setResponseCode("01");
transaction.setErrorMessage(e.getMessage()); transaction.setErrorMessage(e.getMessage());
transactionService.save(transaction);
return transaction;
} }
transactionEvent.setStatus(transaction.getStatus()); transactionEvent.setStatus(transaction.getStatus());
transactionEvent.setMessage(transaction.getErrorMessage()); transactionEvent.setMessage(transaction.getErrorMessage());
transactionEventService.save(transactionEvent); transactionEventService.save(transactionEvent);
return transaction; return transactionService.save(transaction);
} }
} }

View File

@@ -0,0 +1,60 @@
package zw.qantra.tm.domain.workflows.handlers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Recipient;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.RecipientService;
import zw.qantra.tm.domain.services.processors.handlers.LogicHandler;
import java.util.List;
@Service("recipientsUpdate")
@RequiredArgsConstructor
public class RecipientsUpdateHandler implements HandlerInterface {
private final RecipientService recipientService;
@Override
public Transaction process(Transaction transaction) {
System.out.println("processing recipients");
try {
if(transaction.getStatus() == Status.SUCCESS){
List<Recipient> recipients = recipientService.getRecipient(transaction.getCreditAccount(), transaction.getUserId());
Recipient recipient = null;
if(recipients.size() > 0){
recipient = recipients.get(0);
}else{
recipient = new Recipient();
recipient.setAccount(transaction.getCreditAccount());
recipient.setUserId(transaction.getUserId());
}
recipient.setLatestProviderLabel(transaction.getProviderLabel());
if(transaction.getCreditName() != null && !transaction.getCreditName().isEmpty()){
recipient.setName(transaction.getCreditName());
}
if(transaction.getCreditEmail() != null && !transaction.getCreditEmail().isEmpty()){
recipient.setEmail(transaction.getCreditEmail());
}
if(transaction.getCreditPhone() != null && !transaction.getCreditPhone().isEmpty()){
recipient.setPhoneNumber(transaction.getCreditPhone());
}
String stringToInitial = transaction.getCreditName() != null && !transaction.getCreditName().isEmpty()
? transaction.getCreditName() : transaction.getBillName();
recipient.setInitials(stringToInitial.substring(0, 2).toUpperCase());
recipientService.save(recipient);
}
}catch (Exception e) {
e.printStackTrace();
}
return transaction;
}
}