Compare commits

...

10 Commits

Author SHA1 Message Date
be0f0ae49c bug fix 2026-07-09 23:06:06 +02:00
ba796527ae compose fix 2026-07-09 23:03:12 +02:00
1de89364be added openwa failure notifications 2026-07-09 22:35:49 +02:00
fbc3967215 added openwa failure notifications 2026-07-09 21:19:43 +02:00
94a30633ca improving otp sec 2026-07-09 18:38:52 +02:00
5d3cfde7f6 Merge remote-tracking branch 'refs/remotes/origin/release/0.0.2-RELEASE' into release/0.0.2-RELEASE 2026-07-06 12:54:27 +02:00
root
b9efd83762 added zb keys 2026-07-06 12:50:52 +02:00
220c19123f updating sms vendor 2026-07-06 12:17:19 +02:00
a4d210393e updating credit amount functionality 2026-07-05 00:01:19 +02:00
6d48472425 updating credit amount functionality 2026-07-04 23:56:46 +02:00
25 changed files with 212 additions and 76 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

14
compose.yml Normal file
View File

@@ -0,0 +1,14 @@
services:
velocity-pay-backend:
build:
context: .
dockerfile: Dockerfile
image: velocity-pay-backend:0.0.2-RELEASE
restart: always
container_name: velocity-pay-backend
ports:
- 8058:8058
- 6950:6950
network_mode: host
environment:
- TZ=Africa/Harare

18
keys/velocityafrica.csr Normal file
View File

@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIC7TCCAdUCAQAwgacxCzAJBgNVBAYTAlpXMQ8wDQYDVQQIDAZIYXJhcmUxDzAN
BgNVBAcMBkhhcmFyZTEYMBYGA1UECgwPV2FsbGV0IEV4Y2hhbmdlMRkwFwYDVQQL
DBBEaWdpdGFsIFBheW1lbnRzMR0wGwYDVQQDDBQqLnZlbG9jaXR5YWZyaWNhLm5l
dDEiMCAGCSqGSIb3DQEJARYTdnVzYWtob3phQGdtYWlsLmNvbTCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBALl1Pvoh+NUK9A8KlwgLkAIiDs3l7Kbs2Zt5
FZvyOgpjD6Cl/IJy7DC8oJ7Ty+eJkoVKPjyX7urnf1wQgstQMgryA7YMVnQPcYJe
Zm3px+lPhbkEEeVc5veNktCqL590LLMrL6IZbGR1Xh8Phd7niLn1+o+ZebgWH6XD
nIb8JFrBTy5ZigU/dGBpII/NhciIVQDF4TPy5DwfNIBAAKZdp/CiYpTFN0iV7/UA
0g43zfS3CbuEK+Mj6BkkSquWHFd76W58BAhDa2S50sd6VF+jq4EFCdWmJXmxVmPI
rxUMFi9DbI7Cd9DB56pkfnSIYx0t60SjXKy3gkNdB6zcNcFZ860CAwEAAaAAMA0G
CSqGSIb3DQEBCwUAA4IBAQBK4YsEvIQolqsVeqUSxUforA1qo/iKhK+yetlQ8YdN
hR/v+4fIIoaIZLjdSTOLDTDIJ3AUBGlKvF7mf64X+JQpyQVqUIx58S92Zr645lfU
LADsliDXCNQp0nzWDavAC9HvbA4ibl+Z1X+z/uojf3fPZEmAli2howpWYiLIsQSj
quS5L3kkv8XhYBnE0h3L4GL3kp7bFAMRzJq3392pU3xAGiJhBS3oKyBel8mfWrTE
MQDsFgK6bjy2JWRgmb5lqKSV1KlqbG5zmBkSgFcEkrB9tm7DtQbo5oPSDKO5otWf
Wn40iNGAnSRhsc0jjXR1s1oqGQh7SrEhOvm9JI76LfGh
-----END CERTIFICATE REQUEST-----

28
keys/velocityafrica.key Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5dT76IfjVCvQP
CpcIC5ACIg7N5eym7NmbeRWb8joKYw+gpfyCcuwwvKCe08vniZKFSj48l+7q539c
EILLUDIK8gO2DFZ0D3GCXmZt6cfpT4W5BBHlXOb3jZLQqi+fdCyzKy+iGWxkdV4f
D4Xe54i59fqPmXm4Fh+lw5yG/CRawU8uWYoFP3RgaSCPzYXIiFUAxeEz8uQ8HzSA
QACmXafwomKUxTdIle/1ANION830twm7hCvjI+gZJEqrlhxXe+lufAQIQ2tkudLH
elRfo6uBBQnVpiV5sVZjyK8VDBYvQ2yOwnfQweeqZH50iGMdLetEo1yst4JDXQes
3DXBWfOtAgMBAAECggEAE8cSIUCwNaAGaPg7JQt+nwucwo5rj12g0zi1ejz5vZPU
3c/1vr+cupHTGz0A+L8uFfLV8kZNn1VyupG0MRRrUcl/ZdGpApWHCF0i1UPiItpt
K07ozan7o5jAeGo9y+ZqweywFrxZV9lAYVWZdC0C4rn9r4Sxe1U4qMfUkHyhL+BQ
o2Ot2qxfHbbV/mNZcSlQHLByRu1tuVT8TxS5ZOqdu0ar8QnyoHr9tk6+x6WgCONr
K/Hvjej9BL6KrB2/vK52A8PclPJAqb8w5W8c0jQHhS9dAJV5t1KHXAejBz396vn3
PowurUTyu7RrVVK49j9/q0utPl2tosTOhPNobzY6wQKBgQD8iCyuCLMawh84i8qq
SFqyNI0bc2nWuF74QrCc/3NScUdOM8oMzUd5vskEyAa3yl1Xf0Ffg/nMO70E/3Ra
rvLoFnfS/WXHrY8TkSpNI6TY/gtf/F8LOW2HZemWSBZ4Q/iatfTX+gL/3cZFE9F6
dAbA8wcmGNR3GBKbDorxKhSv3QKBgQC8AUKI5ijfmqCnilqNrpiegxcAdGT8au1W
uIW9CjdXS9c1D/+cPFGRc+vLIqWUE7vStkSl6rzY4Va8uqh7t8P9MksE8ru8QcbD
Sq2RwO1vBPnNmb7eS/ooyRbYFOXlrSx7mntPkXUZE/c7Lpqe7XxeC+R/wsZc8fyd
H8RXlCH+EQKBgQCJNhBYy19NpbZDCimnFkM5WrZk+PPD+E+Zwtqlo2Lso6Ijyp35
ELzKWLBuiaSkzi6jcQB7PoFGyBqvEYIOF8rh8ftPR+G6egIolpKOcVSLlpVlpQGb
Xqu1itk9j5Ot6vi0TzNm+nAQyFaHh/ok99AHnMUAnTMsNoc9esxhtwSgfQKBgAoF
MyuiwMjyL9/eIudsR9wi9oXirkM5276EhTRHKB3x/91nJeOI33wWJpokFZkn2Bnp
9wy0zy3sDLhwZmx5peuH07HG0jS9eL+N6mRbWQxSwpGvwRmOCR5FYX9Q1dQA/97y
o+HgydH1W3b/C+SumbNVhO8+TV5E04b4W3mjfpBRAoGBAJMnFV0ivN+TN4/czmAx
4JulU6mh0qCfax6tMbx5RiXmlHitAXQvPooFpzItLWlrRnXj6aHHqXOs5uH0kspj
4pTnDGnQO9VxhRziEjPIAVS6vFntxOs0zshaROreLcaVoDSkmz6YkFTeiSbtzumo
rGOjofBa6j9pYfi9YCi8nSnK
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,19 @@
package zw.qantra.tm.domain.dtos.sms;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SmsPopCampaignRequest {
private String name;
private String message;
private String sender_id;
@Builder.Default
private String contact_import_method = "manual";
private String manual_contacts;
}

View File

@@ -1,6 +1,7 @@
package zw.qantra.tm.domain.models; package zw.qantra.tm.domain.models;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
import zw.qantra.tm.domain.enums.Status; import zw.qantra.tm.domain.enums.Status;
@@ -16,6 +17,7 @@ import zw.qantra.tm.domain.enums.Status;
@NoArgsConstructor @NoArgsConstructor
public class Otp extends BaseEntity{ public class Otp extends BaseEntity{
private String username; private String username;
@JsonIgnore
private String otp; private String otp;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(name = "otp_status") @Column(name = "otp_status")

View File

@@ -72,6 +72,7 @@ public class Transaction extends BaseEntity {
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private CurrencyType creditCurrency; private CurrencyType creditCurrency;
private BigDecimal creditAmount; private BigDecimal creditAmount;
private BigDecimal creditTotalAmount;
private String creditName; private String creditName;
private String creditCard; private String creditCard;
private String creditRef; private String creditRef;

View File

@@ -22,5 +22,5 @@ public interface TransactionRepository extends JpaRepository<Transaction, UUID>,
List<Transaction> findAllByWorkspaceIdIsNullAndUserIdIsNotNull(); List<Transaction> findAllByWorkspaceIdIsNullAndUserIdIsNotNull();
List<Transaction> findAllByConfirmationStatusAndPaymentStatusAndIntegrationStatus( List<Transaction> findAllByConfirmationStatusAndPaymentStatusAndIntegrationStatus(
Status s1, Status s2, Status s3); Status s1, Status s2, Status s3);
List<Transaction> findAllByPaymentStatusAndPollingStatus(Status s1, Status s2); List<Transaction> findAllByPollingStatus(Status s2);
} }

View File

@@ -35,7 +35,7 @@ public class JobService {
public void checkOrphanedTransactions() { public void checkOrphanedTransactions() {
List<Transaction> transactions = transactionService.getTransactionRepository() List<Transaction> transactions = transactionService.getTransactionRepository()
.findAllByPaymentStatusAndPollingStatus(Status.SUCCESS, Status.PENDING); .findAllByPollingStatus(Status.PENDING);
if (!transactions.isEmpty()) if (!transactions.isEmpty())
log.info("Transaction count {}", transactions.size()); log.info("Transaction count {}", transactions.size());
@@ -47,8 +47,13 @@ public class JobService {
int maxRetries = settingService.getIntegerSetting("MAX_RETRIES"); int maxRetries = settingService.getIntegerSetting("MAX_RETRIES");
int retries = transaction.getRetries() == null ? 0 : transaction.getRetries(); int retries = transaction.getRetries() == null ? 0 : transaction.getRetries();
if (retries >= maxRetries) if (retries >= maxRetries) {
break; transaction.setPaymentStatus(Status.FAILED);
transaction.setPollingStatus(Status.FAILED);
transaction.setErrorMessage("Maximum retries reached");
transactionService.save(transaction);
continue;
}
transaction.setRetries(retries + 1); transaction.setRetries(retries + 1);
// polling logic // polling logic
@@ -106,8 +111,8 @@ public class JobService {
success++; success++;
}else { }else {
transaction.setPollingStatus(Status.FAILED); transaction.setPollingStatus(Status.PENDING);
transaction.setIntegrationStatus(Status.FAILED); transaction.setIntegrationStatus(Status.PENDING);
transactionService.save(transaction); transactionService.save(transaction);
} }

View File

@@ -12,16 +12,14 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import zw.qantra.tm.domain.dtos.sms.SmsApiResponse; import zw.qantra.tm.domain.dtos.sms.SmsPopCampaignRequest;
import zw.qantra.tm.domain.dtos.sms.SmsBatchRequest;
import zw.qantra.tm.domain.dtos.sms.SmsMessage;
import zw.qantra.tm.rest.RestService; import zw.qantra.tm.rest.RestService;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.HashMap;
import java.util.UUID; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@@ -32,13 +30,18 @@ public class NotificationService {
private final RestService restService; private final RestService restService;
private final JavaMailSender emailSender; private final JavaMailSender emailSender;
@Value("${zss.sms.api.url}") @Value("${sms-pop.url}")
private String smsApiUrl; private String smsPopUrl;
@Value("${zss.sms.api.auth}") @Value("${sms-pop.apikey}")
private String smsApiAuth; private String smsPopApiKey;
@Value("${sms.debug}") @Value("${sms.debug}")
private Boolean smsDebug; private Boolean smsDebug;
@Value("${wa.api.url}")
private String waApiUrl;
@Value("${wa.api.key}")
private String waApiKey;
@Async @Async
public void sendEmail(String to, String subject, String text) { public void sendEmail(String to, String subject, String text) {
sendEmailWithHtml(to, subject, text, null); sendEmailWithHtml(to, subject, text, null);
@@ -101,6 +104,24 @@ public class NotificationService {
} }
} }
@Async
public void sendWA(String chatId, String text) {
try {
Map<String, String> payload = new HashMap<>();
payload.put("chatId", String.format("%s@c.us", chatId));
payload.put("text", text);
HttpHeaders headers = new HttpHeaders();
headers.set("X-API-Key", waApiKey);
restService.postWithExternalAuth(waApiUrl, payload, headers, Object.class);
logger.info("WA message sent to {}: {}", chatId, text);
} catch (Exception e) {
logger.error("Failed to send WA message to {}: {}", chatId, e.getMessage(), e);
}
}
@Async @Async
public void sendSms(String to, String message) { public void sendSms(String to, String message) {
if(smsDebug) { if(smsDebug) {
@@ -108,28 +129,25 @@ public class NotificationService {
return; return;
} }
try { try {
SmsMessage smsMessage = SmsMessage.builder() String phoneNumber = to;
.reference(UUID.randomUUID().toString()) if (phoneNumber.startsWith("+")) {
.message(message) phoneNumber = phoneNumber.substring(1);
.msisdn(to) }
.build();
SmsBatchRequest batchRequest = SmsBatchRequest.builder() SmsPopCampaignRequest campaignRequest = SmsPopCampaignRequest.builder()
.batchReference(UUID.randomUUID().toString()) .name("Velocity Payments")
.smsList(Collections.singletonList(smsMessage)) .message(message)
.sender_id("Velocity")
.contact_import_method("manual")
.manual_contacts(phoneNumber)
.build(); .build();
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", smsApiAuth); headers.set("Authorization", "Bearer " + smsPopApiKey);
SmsApiResponse response = restService.postWithExternalAuth( restService.postAs(smsPopUrl, campaignRequest, headers, Object.class);
smsApiUrl,
batchRequest,
headers,
SmsApiResponse.class
);
logger.info("SMS sent to {} - Status: {}, Result: {}", to, response.getStatus(), response.getResult()); logger.info("SMS sent to {} via SMSPOP", to);
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed to send SMS to {}: {}", to, e.getMessage(), e); logger.error("Failed to send SMS to {}: {}", to, e.getMessage(), e);
} }

View File

@@ -15,6 +15,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.enums.CurrencyType;
import zw.qantra.tm.domain.enums.RequestType; import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.models.AdditionalData; import zw.qantra.tm.domain.models.AdditionalData;
import zw.qantra.tm.domain.models.Currency; import zw.qantra.tm.domain.models.Currency;
@@ -33,23 +34,20 @@ public class TransactionService {
private final AdditionalDataService additionalDataService; private final AdditionalDataService additionalDataService;
private final CurrencyService currencyService; private final CurrencyService currencyService;
public BigDecimal calculateCreditAmount(Transaction transaction) { public BigDecimal calculateCurrencyExchangeValue(CurrencyType from, CurrencyType to, BigDecimal amount) {
if (transaction.getCreditCurrency() == null) { if (to == null) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
Currency debitCurrency = currencyService.findCurrencyByIsoCode(transaction.getDebitCurrency().name()); Currency debitCurrency = currencyService.findCurrencyByIsoCode(from.name());
Currency creditCurrency = currencyService.findCurrencyByIsoCode(transaction.getCreditCurrency().name()); Currency creditCurrency = currencyService.findCurrencyByIsoCode(to.name());
BigDecimal debitBankRate = new BigDecimal(debitCurrency.getBankRate()); BigDecimal debitBankRate = new BigDecimal(debitCurrency.getBankRate());
BigDecimal creditBankRate = new BigDecimal(creditCurrency.getBankRate()); BigDecimal creditBankRate = new BigDecimal(creditCurrency.getBankRate());
BigDecimal bankRate = creditBankRate.divide(debitBankRate, 4, RoundingMode.HALF_UP); BigDecimal bankRate = creditBankRate.divide(debitBankRate, 4, RoundingMode.HALF_UP);
BigDecimal creditAmount = transaction.getAmount().multiply(bankRate).setScale(4, RoundingMode.HALF_UP); return amount.multiply(bankRate).setScale(4, RoundingMode.HALF_UP);
transaction.setCreditAmount(creditAmount);
return creditAmount;
} }
public void reassignTransactions(UUID oldWorkspaceId, UUID newWorkspaceId){ public void reassignTransactions(UUID oldWorkspaceId, UUID newWorkspaceId){

View File

@@ -181,7 +181,7 @@ public class VelocityPaymentService {
? "VMC" : transaction.getPaymentProcessorLabel(); ? "VMC" : transaction.getPaymentProcessorLabel();
VelocityTransactionDto payload = VelocityTransactionDto.builder() VelocityTransactionDto payload = VelocityTransactionDto.builder()
.salesOrderId(transaction.getOrderId()) .salesOrderId(transaction.getOrderId())
.amount(transaction.getCreditAmount()) .amount(transaction.getCreditTotalAmount())
.debitPhone(transaction.getDebitPhone()) .debitPhone(transaction.getDebitPhone())
.debitRegion("ZW") .debitRegion("ZW")
.type("REQUEST") .type("REQUEST")

View File

@@ -56,8 +56,9 @@ public class NetoneConfirmationHotProcessor implements TransactionProcessorInter
} }
} }
transaction.setCreditCurrency(CurrencyType.USD); transaction.setCreditCurrency(transaction.getDebitCurrency());
transaction.setCreditAmount(transaction.getAmount()); transaction.setCreditAmount(transaction.getAmount());
transaction.setCreditTotalAmount(transaction.getTotalAmount());
transaction.setCreditAccount(Utils.formatPhoneNumber(transaction.getCreditAccount(), "ZW")); transaction.setCreditAccount(Utils.formatPhoneNumber(transaction.getCreditAccount(), "ZW"));
transaction.setStatus(Status.SUCCESS); transaction.setStatus(Status.SUCCESS);
transaction.setResponseCode("00"); transaction.setResponseCode("00");

View File

@@ -5,14 +5,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.enums.CurrencyType; import zw.qantra.tm.domain.enums.CurrencyType;
import zw.qantra.tm.domain.enums.RequestType; import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.enums.Status; import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.AdditionalData; import zw.qantra.tm.domain.models.AdditionalData;
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.*; import zw.qantra.tm.domain.services.*;
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
@@ -20,7 +17,6 @@ import zw.qantra.tm.exceptions.ApiException;
import zw.qantra.tm.rest.RestService; import zw.qantra.tm.rest.RestService;
import zw.qantra.tm.utils.Utils; import zw.qantra.tm.utils.Utils;
import java.math.BigDecimal;
import java.util.*; import java.util.*;
@Service("CONFIRM_ZESA_HOT") @Service("CONFIRM_ZESA_HOT")
@@ -69,7 +65,14 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
transaction.setCreditCurrency(CurrencyType.USD); transaction.setCreditCurrency(CurrencyType.USD);
} }
transaction.setCreditAmount(transactionService.calculateCreditAmount(transaction)); transaction.setCreditAmount(transactionService.calculateCurrencyExchangeValue(
transaction.getDebitCurrency(),
transaction.getCreditCurrency(),
transaction.getAmount()));
transaction.setCreditTotalAmount(transactionService.calculateCurrencyExchangeValue(
transaction.getDebitCurrency(),
transaction.getCreditCurrency(),
transaction.getTotalAmount()));
// token should've been updated by checkCustomer // token should've been updated by checkCustomer
if (!hotRechargeBalanceService.checkBalance(token, transaction.getCreditAmount(), accId)) { if (!hotRechargeBalanceService.checkBalance(token, transaction.getCreditAmount(), accId)) {

View File

@@ -40,6 +40,7 @@ public class CityWalletRemotePaymentProcessor implements TransactionProcessorInt
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
logger.error("Network error during payment processing: {}", e.getMessage(), e); logger.error("Network error during payment processing: {}", e.getMessage(), e);
transaction.setPaymentStatus(Status.FAILED);
transaction.setStatus(Status.FAILED); transaction.setStatus(Status.FAILED);
transaction.setResponseCode("99"); transaction.setResponseCode("99");
transaction.setSystemErrorMessage("Network error: " + e.getMessage()); transaction.setSystemErrorMessage("Network error: " + e.getMessage());
@@ -71,6 +72,7 @@ public class CityWalletRemotePaymentProcessor implements TransactionProcessorInt
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
logger.error("Network error during payment processing: {}", e.getMessage(), e); logger.error("Network error during payment processing: {}", e.getMessage(), e);
transaction.setPollingStatus(Status.FAILED);
transaction.setStatus(Status.FAILED); transaction.setStatus(Status.FAILED);
transaction.setResponseCode("99"); transaction.setResponseCode("99");
transaction.setSystemErrorMessage("Network error: " + e.getMessage()); transaction.setSystemErrorMessage("Network error: " + e.getMessage());

View File

@@ -46,6 +46,7 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf
e.printStackTrace(); e.printStackTrace();
logger.error("Network error during payment processing: {}", e.getMessage(), e); logger.error("Network error during payment processing: {}", e.getMessage(), e);
transaction.setStatus(Status.FAILED); transaction.setStatus(Status.FAILED);
transaction.setPaymentStatus(Status.FAILED);
transaction.setResponseCode("99"); transaction.setResponseCode("99");
transaction.setSystemErrorMessage("Network error: " + e.getMessage()); transaction.setSystemErrorMessage("Network error: " + e.getMessage());
transaction.setErrorMessage("Payment failed. Please try again."); transaction.setErrorMessage("Payment failed. Please try again.");
@@ -77,6 +78,7 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf
e.printStackTrace(); e.printStackTrace();
logger.error("Network error during payment processing: {}", e.getMessage(), e); logger.error("Network error during payment processing: {}", e.getMessage(), e);
transaction.setStatus(Status.FAILED); transaction.setStatus(Status.FAILED);
transaction.setPollingStatus(Status.FAILED);
transaction.setResponseCode("99"); transaction.setResponseCode("99");
transaction.setSystemErrorMessage("Network error: " + e.getMessage()); transaction.setSystemErrorMessage("Network error: " + e.getMessage());
transaction.setErrorMessage("Payment failed. Please try again."); transaction.setErrorMessage("Payment failed. Please try again.");

View File

@@ -48,6 +48,7 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
e.printStackTrace(); e.printStackTrace();
logger.error("Network error during payment processing: {}", e.getMessage(), e); logger.error("Network error during payment processing: {}", e.getMessage(), e);
transaction.setStatus(Status.FAILED); transaction.setStatus(Status.FAILED);
transaction.setPaymentStatus(Status.FAILED);
transaction.setResponseCode("99"); transaction.setResponseCode("99");
transaction.setSystemErrorMessage("Network error: " + e.getMessage()); transaction.setSystemErrorMessage("Network error: " + e.getMessage());
transaction.setErrorMessage("Payment failed. Please try again"); transaction.setErrorMessage("Payment failed. Please try again");
@@ -62,6 +63,7 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
if(settingService.getBooleanSetting("SIMULATE_MPGS_SUCCESS")) { if(settingService.getBooleanSetting("SIMULATE_MPGS_SUCCESS")) {
transaction.setStatus(Status.SUCCESS); transaction.setStatus(Status.SUCCESS);
transaction.setPollingStatus(Status.SUCCESS);
transaction.setResponseCode("02"); transaction.setResponseCode("02");
transaction.setTargetUrl("/home"); transaction.setTargetUrl("/home");
return transaction; return transaction;
@@ -74,6 +76,7 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
e.printStackTrace(); e.printStackTrace();
logger.error("Network error during payment processing: {}", e.getMessage(), e); logger.error("Network error during payment processing: {}", e.getMessage(), e);
transaction.setStatus(Status.FAILED); transaction.setStatus(Status.FAILED);
transaction.setPollingStatus(Status.FAILED);
transaction.setResponseCode("99"); transaction.setResponseCode("99");
transaction.setSystemErrorMessage("Network error: " + e.getMessage()); transaction.setSystemErrorMessage("Network error: " + e.getMessage());
transaction.setErrorMessage("Payment failed. Please try again"); transaction.setErrorMessage("Payment failed. Please try again");

View File

@@ -120,8 +120,8 @@ public class RegistrationWorkflow extends WorkflowDefinition {
Otp otp = otpService.generateOtp(username, "REGISTRATION"); Otp otp = otpService.generateOtp(username, "REGISTRATION");
notificationService.sendOtpEmail(email, username, otp.getOtp()); notificationService.sendOtpEmail(email, username, otp.getOtp());
notificationService.sendSms(registerRequest.getPhone(), String message = "Your Velocity Pay verification code is: " + otp.getOtp();
"Your Velocity Pay verification code is: " + otp.getOtp()); notificationService.sendSms(registerRequest.getPhone(), message);
log.info("otp generated successfully: {}", otp.getOtp()); log.info("otp generated successfully: {}", otp.getOtp());
} catch (Exception e) { } catch (Exception e) {
@@ -146,8 +146,8 @@ public class RegistrationWorkflow extends WorkflowDefinition {
Otp otp = otpService.generateOtp(otpRequest.getUsername(), "REGISTRATION"); Otp otp = otpService.generateOtp(otpRequest.getUsername(), "REGISTRATION");
// The username field in OtpRequest may actually contain the email; use it directly // The username field in OtpRequest may actually contain the email; use it directly
notificationService.sendOtpEmail(otpRequest.getUsername(), otpRequest.getUsername(), otp.getOtp()); notificationService.sendOtpEmail(otpRequest.getUsername(), otpRequest.getUsername(), otp.getOtp());
notificationService.sendSms(otpRequest.getPhone(), String message = "Your Velocity Pay verification code is: " + otp.getOtp();
"Your Velocity Pay verification code is: " + otp.getOtp()); notificationService.sendSms(otpRequest.getPhone(), message);
log.info("otp resent successfully: {}", otp.getOtp()); log.info("otp resent successfully: {}", otp.getOtp());
} }

View File

@@ -3,6 +3,7 @@ package zw.qantra.tm.domain.services.processors.workflows.handlers;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.enums.Status; import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Transaction; import zw.qantra.tm.domain.models.Transaction;
@@ -22,6 +23,10 @@ public class GatewayPaymentHandler implements HandlerInterface {
private final RecipientService recipientService; private final RecipientService recipientService;
private final TransactionEventService transactionEventService; private final TransactionEventService transactionEventService;
private final TransactionService transactionService; private final TransactionService transactionService;
private final NotificationService notificationService;
@Value("${wa.api.phone}")
private String waPhone;
@Override @Override
public Object process(Transaction incomingTransaction) throws Exception { public Object process(Transaction incomingTransaction) throws Exception {
@@ -73,7 +78,12 @@ public class GatewayPaymentHandler implements HandlerInterface {
transaction.setResponseCode("01"); transaction.setResponseCode("01");
transaction.setErrorMessage(e.getMessage()); transaction.setErrorMessage(e.getMessage());
transactionService.save(transaction); transactionService.save(transaction);
return transaction; }
if(Status.FAILED.equals(transaction.getPaymentStatus())) {
notificationService.sendWA(waPhone, String.format(
"Payment failed: Transaction: %s, Customer message: %s, System message: %s",
transaction.getId(), transaction.getErrorMessage(), transaction.getSystemErrorMessage()));
} }
transactionEvent.setStatus(transaction.getStatus()); transactionEvent.setStatus(transaction.getStatus());

View File

@@ -3,6 +3,7 @@ package zw.qantra.tm.domain.services.processors.workflows.handlers;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.enums.RequestType; import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.enums.Status; import zw.qantra.tm.domain.enums.Status;
@@ -28,6 +29,10 @@ public class IntegrationHandler implements HandlerInterface {
private final RecipientService recipientService; private final RecipientService recipientService;
private final TransactionEventService transactionEventService; private final TransactionEventService transactionEventService;
private final TransactionService transactionService; private final TransactionService transactionService;
private final NotificationService notificationService;
@Value("${wa.api.phone}")
private String waPhone;
List<String> lock = new ArrayList<>(); List<String> lock = new ArrayList<>();
@@ -86,6 +91,12 @@ public class IntegrationHandler implements HandlerInterface {
transaction.setIntegrationStatus(transaction.getStatus()); transaction.setIntegrationStatus(transaction.getStatus());
if(!Status.SUCCESS.equals(transaction.getIntegrationStatus())) {
notificationService.sendWA(waPhone, String.format(
"Integration failed: Transaction %s, Customer message: %s, System message: %s",
transaction.getId(), transaction.getErrorMessage(), transaction.getSystemErrorMessage()));
}
removeLock(transaction.getId().toString()); removeLock(transaction.getId().toString());
if(transaction.getStatus() != Status.SUCCESS){ if(transaction.getStatus() != Status.SUCCESS){
transactionService.save(transaction); transactionService.save(transaction);

View File

@@ -84,9 +84,14 @@ jobrunr.datasource.password=
app.reports.storage-path=/srv/data/reports app.reports.storage-path=/srv/data/reports
app.base-url=https://payapi.velocityafrica.net app.base-url=https://payapi.velocityafrica.net
zss.sms.api.url=https://secure.zss.co.zw/vportal/cnm/vsms/json/batch
zss.sms.api.auth=Basic UWFudHJhdGVjaDpVbUk3OUpwUDh3QTY=
sms.debug=false sms.debug=false
sms-pop.url=https://smspop.co.zw/api/campaigns
sms-pop.apikey=108|ypBpUtJGlPzelxJl2aySp672I9KKbyaQPtcxMPBrfb70be27
cloudflare-token=cfut_U0qhvhzAATLqHQa9lad7oRmBmPoJ6n9uIoCBbNOWc9884f18 cloudflare-token=cfut_U0qhvhzAATLqHQa9lad7oRmBmPoJ6n9uIoCBbNOWc9884f18
cloudflare-zone-id=378cea8e50c0d9a158816109b3b1fd8d cloudflare-zone-id=378cea8e50c0d9a158816109b3b1fd8d
wa.api.url=http://173.212.247.232:2785/api/sessions/70eae8dc-2be8-465a-a54b-b6c1d3313699/messages/send-text
wa.api.key=owa_k1_3d2198d81573303f0b48c3c4a6981fd13261b1d0099758eabf4efb53beaba77d
wa.api.phone=263773591219

View File

@@ -1,23 +1,8 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?> <?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet author="khoza (generated)" id="1782340507017-3"> <changeSet author="khoza (generated)" id="1783202068979-3">
<addColumn tableName="transaction"> <addColumn tableName="transaction">
<column name="batch_description" type="varchar(255)"/> <column name="credit_total_amount" type="numeric(38, 2)"/>
</addColumn>
</changeSet>
<changeSet author="khoza (generated)" id="1782340507017-4">
<addColumn tableName="group_batch_item">
<column name="credit_address" type="varchar(255)"/>
</addColumn>
</changeSet>
<changeSet author="khoza (generated)" id="1782340507017-5">
<addColumn tableName="payment_processor">
<column name="currency" type="varchar(255)"/>
</addColumn>
</changeSet>
<changeSet author="khoza (generated)" id="1782340507017-6">
<addColumn tableName="transaction">
<column name="username" type="varchar(255)"/>
</addColumn> </addColumn>
</changeSet> </changeSet>
</databaseChangeLog> </databaseChangeLog>

View File

@@ -121,7 +121,7 @@ class TransactionServiceTest {
@Test @Test
@DisplayName("Should calculate credit amount using bank rate") @DisplayName("Should calculate credit amount using bank rate")
void shouldCalculateCreditAmount() { void shouldCalculateCurrencyExchangeValue() {
Currency currencyUsd = new Currency(); Currency currencyUsd = new Currency();
currencyUsd.setBankRate("1.0"); currencyUsd.setBankRate("1.0");
Currency currencyZwl = new Currency(); Currency currencyZwl = new Currency();
@@ -130,10 +130,12 @@ class TransactionServiceTest {
when(currencyService.findCurrencyByIsoCode("USD")).thenReturn(currencyUsd); when(currencyService.findCurrencyByIsoCode("USD")).thenReturn(currencyUsd);
when(currencyService.findCurrencyByIsoCode("ZWL")).thenReturn(currencyZwl); when(currencyService.findCurrencyByIsoCode("ZWL")).thenReturn(currencyZwl);
BigDecimal creditAmount = transactionService.calculateCreditAmount(transaction); BigDecimal creditAmount = transactionService.calculateCurrencyExchangeValue(
transaction.getDebitCurrency(),
transaction.getCreditCurrency(),
transaction.getAmount());
assertEquals(new BigDecimal("1500.0000"), creditAmount); assertEquals(new BigDecimal("1500.0000"), creditAmount);
assertEquals(new BigDecimal("1500.0000"), transaction.getCreditAmount());
} }
@Test @Test
@@ -141,7 +143,10 @@ class TransactionServiceTest {
void shouldReturnZeroWhenCreditCurrencyNull() { void shouldReturnZeroWhenCreditCurrencyNull() {
transaction.setCreditCurrency(null); transaction.setCreditCurrency(null);
BigDecimal result = transactionService.calculateCreditAmount(transaction); BigDecimal result = transactionService.calculateCurrencyExchangeValue(
transaction.getDebitCurrency(),
transaction.getCreditCurrency(),
transaction.getAmount());
assertEquals(BigDecimal.ZERO, result); assertEquals(BigDecimal.ZERO, result);
} }

View File

@@ -49,6 +49,9 @@ class GatewayPaymentHandlerTest {
@Mock @Mock
private TransactionProcessorInterface transactionProcessor; private TransactionProcessorInterface transactionProcessor;
@Mock
private NotificationService notificationService;
@InjectMocks @InjectMocks
private GatewayPaymentHandler handler; private GatewayPaymentHandler handler;
@@ -63,7 +66,7 @@ class GatewayPaymentHandlerTest {
transaction.setType(RequestType.REQUEST); transaction.setType(RequestType.REQUEST);
transaction.setAuthType(AuthType.MOBILE); transaction.setAuthType(AuthType.MOBILE);
transaction.setPaymentProcessorLabel("ecocash"); transaction.setPaymentProcessorLabel("ecocash");
transaction.setPaymentStatus(null); transaction.setPaymentStatus(Status.PENDING);
lenient().when(transactionService.getTransactionRepository()).thenReturn(transactionRepository); lenient().when(transactionService.getTransactionRepository()).thenReturn(transactionRepository);
} }

View File

@@ -48,6 +48,9 @@ class IntegrationHandlerTest {
@Mock @Mock
private TransactionProcessorInterface transactionProcessor; private TransactionProcessorInterface transactionProcessor;
@Mock
private NotificationService notificationService;
@InjectMocks @InjectMocks
private IntegrationHandler handler; private IntegrationHandler handler;