package zw.qantra.tm.domain.services; import lombok.Getter; 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; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import com.fasterxml.jackson.databind.ObjectMapper; import zw.qantra.tm.domain.dtos.SBZProductDto; import zw.qantra.tm.domain.models.Provider; import zw.qantra.tm.domain.repositories.ProviderRepository; import zw.qantra.tm.exceptions.ApiException; import zw.qantra.tm.rest.RestService; import zw.qantra.tm.utils.Utils; import java.math.BigDecimal; import java.util.*; import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Slf4j public class ProviderService { @Getter private final ProviderRepository providerRepository; private final RestService restService; private final HotRechargeTokenService hotRechargeTokenService; @Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}") private String hotApiUrl; @Value("${sbz.aggregator.url}") private String aggregatorUrl; public SBZProductDto getProviderProduct(UUID providerId, UUID productId) { return getProviderProducts(providerId.toString()).stream() .filter(product -> product.getUid().equals(productId)) .findFirst() .orElse(null); } @Deprecated public List getProviderProducts(UUID providerId) { String token = restService.getMerchantToken(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); headers.setBearerAuth(token); LinkedHashMap response; try { response = this.restService.getAs( aggregatorUrl + "/merchant/aggregation/products?size=20&clientUid=" + providerId.toString(), headers, LinkedHashMap.class ); } catch (Exception e) { log.error("Error fetching products for provider " + providerId, e); return Collections.emptyList(); } LinkedHashMap body = (LinkedHashMap) response.get("body"); List content = (List) body.get("content"); List products = new ArrayList<>(); for (LinkedHashMap product : content) { ObjectMapper mapper = new ObjectMapper(); SBZProductDto productDto = mapper.convertValue(product, SBZProductDto.class); products.add(productDto); } return products; } // todo: figure out caching for different providers // @Cacheable(value = "providerProducts", key = "#providerUid") public List getProviderProducts(String providerUid) { try { Provider provider = providerRepository.findById(UUID.fromString(providerUid)).orElse(null); if (provider == null) { return Collections.emptyList(); } if(provider.getRequiresProducts().equals(false)){ return Collections.emptyList(); } Map meta = Utils.fromJson(provider.getMeta(), Map.class); String providerId = meta.get("hotProductId"); String stockUrl = hotApiUrl + "/query/stock/" + providerId; HttpHeaders headers = new HttpHeaders(); String token = hotRechargeTokenService.getToken(); headers.setBearerAuth(token); LinkedHashMap response; try { response = restService.getAs(stockUrl, headers, LinkedHashMap.class); } catch (Exception e) { log.error("Error fetching products for provider " + providerId); return Collections.emptyList(); } List stock = (List) response.get("stock"); List products = new ArrayList<>(); for (Object item : stock) { LinkedHashMap stockItem = (LinkedHashMap) item; int stockProviderId = (int) stockItem.get("productId"); // providerId is referred to as productId in the stock response if (String.valueOf(stockProviderId).equals(providerId)) { log.info("Found matching provider in stock: " + stockItem); SBZProductDto sbzProductDto = new SBZProductDto(); sbzProductDto.setName((String) stockItem.get("name")); sbzProductDto.setDescription((String) stockItem.get("description")); sbzProductDto.setDisplayName((String) stockItem.get("name")); sbzProductDto.setDefaultAmount(new BigDecimal(stockItem.get("amount").toString())); sbzProductDto.setRequiresAmount(true); // always true for now sbzProductDto.setUid(UUID.fromString(Utils.getUid())); sbzProductDto.setExternalId((String) stockItem.get("productCode")); products.add(sbzProductDto); } } return products; } catch (IllegalArgumentException e) { log.error("Error fetching products: {}", providerUid); return Collections.emptyList(); } } public String getProviderId(String billClientId) { Provider provider = getProviderRepository() .findByClientId(billClientId); if(provider == null){ throw new ApiException("Provider not found for billClientId: " + billClientId); } @SuppressWarnings("unchecked") Map meta = Utils.fromJson(provider.getMeta(), Map.class); return meta.get("hotProductId"); } public String getExternalId(String providerId, String productName) { try { List products = getProviderProducts(providerId); return products.stream() .filter(product -> product.getName().equals(productName)) .map(SBZProductDto::getExternalId) .findFirst() .orElse(null); } catch (Exception e) { log.error("Failed to fetch external Id: " + productName, e); return null; } } public Object getProviderByClientId(String clientId) { return providerRepository.findByClientId(clientId); } public Object findAll(Specification specification, Pageable pageable) { return getProviderRepository().findAll(specification, pageable); } public Object getProviders(List existingProviders) { String token = restService.getMerchantToken(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); headers.setBearerAuth(token); List existingProviderIds = existingProviders.stream() .map(Provider::getClientId) .collect(Collectors.toList()); String existingProviderIdsString = String.join(",", existingProviderIds); LinkedHashMap response = this.restService.getAs( aggregatorUrl + "/merchant/aggregation/providers?supportedCurrencies=USD&clientIdIn=" + existingProviderIdsString, headers, LinkedHashMap.class ); LinkedHashMap body = (LinkedHashMap) response.get("body"); List content = (List) body.get("content"); for (LinkedHashMap provider : content) { String clientId = (String) provider.get("clientId"); if (existingProviderIds.contains(clientId)) { // Update existing provider Provider existingProvider = existingProviders.stream() .filter(p -> p.getClientId().equals(clientId)) .findFirst() .orElse(null); if (existingProvider != null) { provider.put("name", existingProvider.getDescription()); provider.put("description", existingProvider.getDescription()); provider.put("image", existingProvider.getImage()); provider.put("label", existingProvider.getLabel()); provider.put("category", existingProvider.getCategory()); provider.put("accountFieldName", existingProvider.getAccountFieldName()); } } } return content; } public Optional getProvider(UUID id) { return providerRepository.findById(id); } public Provider createProvider(Provider provider) { return providerRepository.save(provider); } public Provider updateProvider(UUID id, Provider provider) { if (providerRepository.existsById(id)) { provider.setId(id); return providerRepository.save(provider); } return null; } public boolean deleteProvider(UUID id) { if (providerRepository.existsById(id)) { providerRepository.deleteById(id); return true; } 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 } }