implement zesa currency swap feature
This commit is contained in:
@@ -23,7 +23,8 @@ public class ProviderController {
|
||||
@GetMapping
|
||||
public ResponseEntity getProviders(
|
||||
@And({
|
||||
@Spec(path = "category", spec = Equal.class)
|
||||
@Spec(path = "category", spec = Equal.class),
|
||||
@Spec(path = "currency", spec = Equal.class),
|
||||
}) Specification<Provider> spec, Pageable pageable) {
|
||||
return ResponseEntity.ok(providerService.findAll(spec, pageable));
|
||||
}
|
||||
@@ -71,4 +72,10 @@ public class ProviderController {
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
public ResponseEntity evictCache() {
|
||||
providerService.evictProviderProductsCache();
|
||||
return ResponseEntity.ok("Cache evicted");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ public class Currency extends BaseEntity {
|
||||
private String isoCode;
|
||||
private String name;
|
||||
private String displayName;
|
||||
private String bankRate;
|
||||
private String forecastRate;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ package zw.qantra.tm.domain.models;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||
|
||||
|
||||
@Entity
|
||||
@@ -19,6 +22,8 @@ public class Provider extends BaseEntity {
|
||||
private String name;
|
||||
private String description;
|
||||
private String externalId;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private CurrencyType currency;
|
||||
private String image;
|
||||
private String category;
|
||||
private String accountFieldName;
|
||||
|
||||
@@ -51,6 +51,7 @@ public class Transaction extends BaseEntity {
|
||||
private String creditAccount;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private CurrencyType creditCurrency;
|
||||
private BigDecimal creditAmount;
|
||||
private String creditName;
|
||||
private String creditCard;
|
||||
private String creditRef;
|
||||
|
||||
@@ -4,8 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import zw.qantra.tm.domain.models.Currency;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface CurrencyRepository extends JpaRepository<Currency, UUID> {
|
||||
}
|
||||
Optional<Currency> findByIsoCode(String isoCode);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package zw.qantra.tm.domain.services;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import zw.qantra.tm.domain.models.Currency;
|
||||
import zw.qantra.tm.domain.repositories.CurrencyRepository;
|
||||
import zw.qantra.tm.exceptions.ApiException;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -12,4 +14,9 @@ public class CurrencyService {
|
||||
public CurrencyRepository getCurrencyRepository() {
|
||||
return currencyRepository;
|
||||
}
|
||||
}
|
||||
|
||||
public Currency findCurrencyByIsoCode(String isoCode) {
|
||||
return currencyRepository.findByIsoCode(isoCode)
|
||||
.orElseThrow(() -> new ApiException("Currency not found for ISO code: " + isoCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ public class HotRechargeBalanceService {
|
||||
private String hotApiUrl;
|
||||
|
||||
@Cacheable(value = "hotrechargeBalance", key = "#token + '_' + #amount.toString()")
|
||||
public boolean checkBalance(String token, BigDecimal amount) {
|
||||
public boolean checkBalance(String token, BigDecimal amount, String accId) {
|
||||
try {
|
||||
String balanceUrl = hotApiUrl + "/account/balance/4";
|
||||
String balanceUrl = hotApiUrl + "/account/balance/" + accId;
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(token);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
@@ -81,7 +82,8 @@ public class ProviderService {
|
||||
return products;
|
||||
}
|
||||
|
||||
@Cacheable(value = "providerProducts", key = "#providerUid")
|
||||
// todo: figure out caching for different providers
|
||||
// @Cacheable(value = "providerProducts", key = "#providerUid")
|
||||
public List<SBZProductDto> getProviderProducts(String providerUid) {
|
||||
try {
|
||||
Provider provider = providerRepository.findById(UUID.fromString(providerUid)).orElse(null);
|
||||
@@ -126,6 +128,7 @@ public class ProviderService {
|
||||
|
||||
return products;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Error fetching products: " + providerUid, e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -159,11 +162,7 @@ public class ProviderService {
|
||||
}
|
||||
|
||||
public Object getProviderByClientId(String clientId) {
|
||||
Provider provider = providerRepository.findByClientId(clientId);
|
||||
if (provider != null) {
|
||||
return ((List)getProviders(Collections.singletonList(provider))).stream().findFirst().orElse(null);
|
||||
}
|
||||
return null;
|
||||
return providerRepository.findByClientId(clientId);
|
||||
}
|
||||
|
||||
public Object findAll(Specification<Provider> specification, Pageable pageable) {
|
||||
@@ -237,4 +236,17 @@ public class ProviderService {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Evict ALL providerProducts cache entries
|
||||
@CacheEvict(value = "providerProducts", allEntries = true)
|
||||
public void evictProviderProductsCache() {
|
||||
// empty - annotation handles the eviction
|
||||
}
|
||||
|
||||
// Or evict a SINGLE entry by key
|
||||
@CacheEvict(value = "providerProducts", key = "#providerUid")
|
||||
public void evictProviderProductCache(String providerUid) {
|
||||
// only evicts cache for this specific providerUid
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package zw.qantra.tm.domain.services;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -15,6 +17,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import zw.qantra.tm.domain.enums.RequestType;
|
||||
import zw.qantra.tm.domain.models.AdditionalData;
|
||||
import zw.qantra.tm.domain.models.Currency;
|
||||
import zw.qantra.tm.domain.models.Transaction;
|
||||
import zw.qantra.tm.domain.repositories.TransactionRepository;
|
||||
import zw.qantra.tm.exceptions.ApiException;
|
||||
@@ -28,6 +31,21 @@ public class TransactionService {
|
||||
@Getter
|
||||
private final TransactionRepository transactionRepository;
|
||||
private final AdditionalDataService additionalDataService;
|
||||
private final CurrencyService currencyService;
|
||||
|
||||
public BigDecimal calculateCreditAmount(Transaction transaction) {
|
||||
if (transaction.getCreditCurrency() == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
Currency currency = currencyService.findCurrencyByIsoCode(transaction.getCreditCurrency().name());
|
||||
|
||||
BigDecimal bankRate = new BigDecimal(currency.getBankRate());
|
||||
BigDecimal creditAmount = transaction.getAmount().multiply(bankRate).setScale(4, RoundingMode.HALF_EVEN);
|
||||
|
||||
transaction.setCreditAmount(creditAmount);
|
||||
return creditAmount;
|
||||
}
|
||||
|
||||
public void reassignTransactions(String oldUid, String newUid){
|
||||
List<Transaction> transactions = transactionRepository.findAllByUserId(oldUid);
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
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.Status;
|
||||
import zw.qantra.tm.domain.models.AdditionalData;
|
||||
@@ -41,7 +42,13 @@ public class NetoneConfirmationHotProcessor implements TransactionProcessorInter
|
||||
// do this if we not simulating success, if simulating success, we skip balance check and just return success
|
||||
if(!settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||
String token = hotRechargeTokenService.getToken();
|
||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount())) {
|
||||
|
||||
// Account Type Id Account Name
|
||||
// 1 ZWG
|
||||
// 2 ZWG Non-VAT (Utilitiies)
|
||||
// 3 USD
|
||||
// 4 USD Non-VAT (Utilitiies)
|
||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount(), "3")) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("92");
|
||||
transaction.setErrorMessage("There's a technical issue on our end. Please try again later.");
|
||||
@@ -49,6 +56,8 @@ public class NetoneConfirmationHotProcessor implements TransactionProcessorInter
|
||||
}
|
||||
}
|
||||
|
||||
transaction.setCreditCurrency(CurrencyType.USD);
|
||||
transaction.setCreditAmount(transaction.getAmount());
|
||||
transaction.setCreditAccount(Utils.formatPhoneNumber(transaction.getCreditAccount(), "ZW"));
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("00");
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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.Status;
|
||||
import zw.qantra.tm.domain.models.AdditionalData;
|
||||
@@ -33,6 +34,7 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
private final HotRechargeTokenService hotRechargeTokenService;
|
||||
private final HotRechargeBalanceService hotRechargeBalanceService;
|
||||
private final SettingService settingService;
|
||||
private final TransactionService transactionService;
|
||||
|
||||
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
||||
private String hotApiUrl;
|
||||
@@ -57,13 +59,6 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
return transaction;
|
||||
}
|
||||
|
||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount())) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("92");
|
||||
transaction.setErrorMessage("There's a technical issue on our end. Please try again later.");
|
||||
return transaction;
|
||||
}
|
||||
|
||||
Map<String, Object> customerData = checkCustomer(token, transaction);
|
||||
if (customerData == null) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
@@ -72,6 +67,30 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
return transaction;
|
||||
}
|
||||
|
||||
// Account Type Id Account Name
|
||||
// 1 ZWG
|
||||
// 2 ZWG Non-VAT (Utilitiies)
|
||||
// 3 USD
|
||||
// 4 USD Non-VAT (Utilitiies)
|
||||
String accId = "4";
|
||||
String currency = extractCurrency(customerData);
|
||||
logger.info("Customer currency: {}", currency.isEmpty() ? "UNKNOWN" : currency);
|
||||
if(currency.equals("ZWG")){
|
||||
accId = "2";
|
||||
transaction.setCreditCurrency(CurrencyType.ZWG);
|
||||
}else {
|
||||
transaction.setCreditCurrency(CurrencyType.USD);
|
||||
}
|
||||
|
||||
transaction.setCreditAmount(transactionService.calculateCreditAmount(transaction));
|
||||
|
||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount(), accId)) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("92");
|
||||
transaction.setErrorMessage("There's a technical issue on our end. Please try again later.");
|
||||
return transaction;
|
||||
}
|
||||
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("00");
|
||||
|
||||
@@ -110,15 +129,27 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
return additionalDataList;
|
||||
}
|
||||
|
||||
private String extractCurrency(Map<String, Object> customerData) {
|
||||
if (customerData == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Object detailsObj = customerData.get("details");
|
||||
if (!(detailsObj instanceof Map<?, ?> details)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Object currency = details.get("Currency");
|
||||
if (currency == null) {
|
||||
currency = details.get("currency");
|
||||
}
|
||||
|
||||
return currency == null ? "" : currency.toString().trim();
|
||||
}
|
||||
|
||||
private Map<String, Object> checkCustomer(String token, Transaction transaction) {
|
||||
try {
|
||||
Provider provider = providerService.getProviderRepository()
|
||||
.findByClientId(transaction.getBillClientId());
|
||||
if(provider == null){
|
||||
throw new ApiException("Provider not found for billClientId: " + transaction.getBillClientId());
|
||||
}
|
||||
Map meta = Utils.fromJson(provider.getMeta(), Map.class);
|
||||
String hotProductId = (String) meta.get("hotProductId");
|
||||
String hotProductId = providerService.getProviderId(transaction.getBillClientId());
|
||||
|
||||
String customerUrl = hotApiUrl + "/query/customer/" + hotProductId + "/" + transaction.getCreditAccount();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||
import zw.qantra.tm.domain.enums.RequestType;
|
||||
import zw.qantra.tm.domain.enums.Status;
|
||||
import zw.qantra.tm.domain.models.AdditionalData;
|
||||
@@ -36,6 +37,8 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
||||
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
||||
private String hotApiUrl;
|
||||
|
||||
private static final String ZESA_ZWG_CLIENT = "zesa_prepaid_zwg";
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Object process(Transaction transaction) throws Exception {
|
||||
@@ -70,12 +73,8 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
||||
return transaction;
|
||||
}
|
||||
|
||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount())) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("92");
|
||||
transaction.setErrorMessage("There's a technical issue on our end. Please try again later.");
|
||||
return transaction;
|
||||
}
|
||||
// todo: a balance check might be necessary here. Removing
|
||||
// for now so not to spam hotrecharge with requests
|
||||
|
||||
LinkedHashMap<String, Object> rechargeResponse = performRecharge(token, transaction);
|
||||
if (rechargeResponse == null) {
|
||||
@@ -132,19 +131,30 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
||||
headers.setBearerAuth(token);
|
||||
headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
|
||||
|
||||
String providerId = providerService.getProviderId(transaction.getBillClientId());
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("AgentReference", UUID.randomUUID().toString());
|
||||
request.put("ProductId", providerService.getProviderId(transaction.getBillClientId()));
|
||||
request.put("ProductId", providerId);
|
||||
request.put("Target", transaction.getCreditAccount());
|
||||
request.put("Amount", transaction.getAmount());
|
||||
request.put("Amount", transaction.getCreditAmount());
|
||||
|
||||
// for zesa skip this
|
||||
if(!providerService.getProviderId(transaction.getBillClientId()).equals("41")) {
|
||||
// 41 = zesa
|
||||
if(providerId.equals("41")) {
|
||||
request.put("CustomerSMS", "A ZETDC token was purchased for " +
|
||||
"%ACOUNTNAME% (%METERNUMBER%) that resulted in %KWH% units.");
|
||||
}
|
||||
else {
|
||||
request.put("CustomerSMS", "%COMPANYNAME% topped up your account with $%AMOUNT%.");
|
||||
}
|
||||
|
||||
// do this for zesa only
|
||||
if(providerService.getProviderId(transaction.getBillClientId()).equals("41")) {
|
||||
if(providerId.equals("41")) {
|
||||
// update product to zwg product id
|
||||
if(transaction.getCreditCurrency().equals(CurrencyType.ZWG)) {
|
||||
String zwgProviderId = providerService.getProviderId(ZESA_ZWG_CLIENT);
|
||||
request.put("ProductId", zwgProviderId);
|
||||
}
|
||||
|
||||
List<Map<String, String>> rechargeOptions = new ArrayList<>();
|
||||
if (transaction.getDebitPhone() != null) {
|
||||
Map<String, String> option = new LinkedHashMap<>();
|
||||
@@ -159,9 +169,8 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
||||
}
|
||||
|
||||
// do this for Econet bundles
|
||||
if(providerService.getProviderId(transaction.getBillClientId()).equals("111")) {
|
||||
String productCode = providerService.getExternalId(
|
||||
providerService.getProviderId(transaction.getBillClientId()), transaction.getProductUid());
|
||||
if(providerId.equals("111")) {
|
||||
String productCode = providerService.getExternalId(providerId, transaction.getProductUid());
|
||||
List<Map<String, String>> rechargeOptions = new ArrayList<>();
|
||||
Map<String, String> option = new LinkedHashMap<>();
|
||||
option.put("Name", "ProductCode");
|
||||
|
||||
Reference in New Issue
Block a user