diff --git a/HotRecharge.postman_collection.json b/HotRecharge.postman_collection.json new file mode 100644 index 0000000..057f43f --- /dev/null +++ b/HotRecharge.postman_collection.json @@ -0,0 +1,525 @@ +{ + "info": { + "_postman_id": "b240d6e9-e5d3-43a1-8d73-170edd02d714", + "name": "HotRecharge", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "569823" + }, + "item": [ + { + "name": "Auth", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Parse the response JSON and extract the token", + "const jsonData = pm.response.json();", + "const token = jsonData.token;", + "", + "// Store the token in a collection variable", + "pm.collectionVariables.set(\"token\", token);" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"AccessCode\": \"vusumuzi@qantra.co.zw\",\n \"Password\": \"Stikbanch@50\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://ssl.hot.co.zw/api/v3/identity/login", + "protocol": "https", + "host": [ + "ssl", + "hot", + "co", + "zw" + ], + "path": [ + "api", + "v3", + "identity", + "login" + ] + } + }, + "response": [] + }, + { + "name": "Get Balance Zesa", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://ssl.hot.co.zw/api/v3/account/balance/4", + "protocol": "https", + "host": [ + "ssl", + "hot", + "co", + "zw" + ], + "path": [ + "api", + "v3", + "account", + "balance", + "4" + ] + } + }, + "response": [] + }, + { + "name": "Get Balance Airtime", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://ssl.hot.co.zw/api/v3/account/balance/3", + "protocol": "https", + "host": [ + "ssl", + "hot", + "co", + "zw" + ], + "path": [ + "api", + "v3", + "account", + "balance", + "3" + ] + } + }, + "response": [] + }, + { + "name": "Get Products", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://ssl.hot.co.zw/api/v3/products/", + "protocol": "https", + "host": [ + "ssl", + "hot", + "co", + "zw" + ], + "path": [ + "api", + "v3", + "products", + "" + ] + } + }, + "response": [] + }, + { + "name": "Check Available Stock", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://ssl.hot.co.zw/api/v3/query/stock/41", + "protocol": "https", + "host": [ + "ssl", + "hot", + "co", + "zw" + ], + "path": [ + "api", + "v3", + "query", + "stock", + "41" + ] + } + }, + "response": [] + }, + { + "name": "Check Customer", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "https://ssl.hot.co.zw/api/v3/query/customer/41/07088597534", + "protocol": "https", + "host": [ + "ssl", + "hot", + "co", + "zw" + ], + "path": [ + "api", + "v3", + "query", + "customer", + "41", + "07088597534" + ] + } + }, + "response": [ + { + "name": "Check Customer", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "https://ssl.hot.co.zw/api/v3/query/customer/41/07088597534", + "protocol": "https", + "host": [ + "ssl", + "hot", + "co", + "zw" + ], + "path": [ + "api", + "v3", + "query", + "customer", + "41", + "07088597534" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": null, + "header": [ + { + "key": ":status", + "value": 200 + }, + { + "key": "content-length", + "value": "129" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "" + }, + { + "key": "x-powered-by", + "value": "ASP.NET" + }, + { + "key": "strict-transport-security", + "value": "max-age=0; includeSubDomains" + }, + { + "key": "date", + "value": "Sat, 02 May 2026 08:29:13 GMT" + } + ], + "cookie": [], + "body": "{\n \"accountNumber\": \"07088597534\",\n \"details\": {\n \"AccountName\": \"CHIRINDO LETWINA\\n2144 MARLBOROUGH\",\n \"Status\": \"Active\",\n \"Currency\": \"ZWG\"\n }\n}" + } + ] + }, + { + "name": "Recharge Airtime", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"AgentReference\": \"278917b4-4180-413d-9a31-59fcd4b9b654\",\n \"ProductId\": 101,\n \"Target\": \"0773591219\",\n \"Amount\": 0.1\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://ssl.hot.co.zw/api/v3/products/recharge", + "protocol": "https", + "host": [ + "ssl", + "hot", + "co", + "zw" + ], + "path": [ + "api", + "v3", + "products", + "recharge" + ] + } + }, + "response": [ + { + "name": "Recharge Airtime", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"AgentReference\": \"278917b4-4180-413d-9a31-59fcd4b9b654\",\n \"ProductId\": 101,\n \"Target\": \"0773591219\",\n \"Amount\": 0.1\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://ssl.hot.co.zw/api/v3/products/recharge", + "protocol": "https", + "host": [ + "ssl", + "hot", + "co", + "zw" + ], + "path": [ + "api", + "v3", + "products", + "recharge" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": null, + "header": [ + { + "key": ":status", + "value": 200 + }, + { + "key": "content-length", + "value": "218" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "" + }, + { + "key": "x-powered-by", + "value": "ASP.NET" + }, + { + "key": "strict-transport-security", + "value": "max-age=0; includeSubDomains" + }, + { + "key": "date", + "value": "Sat, 02 May 2026 08:37:11 GMT" + } + ], + "cookie": [], + "body": "{\n \"successful\": true,\n \"rechargeId\": 190356544,\n \"amount\": 0.1,\n \"discount\": 5,\n \"balance\": {\n \"accountTypeId\": 3,\n \"name\": \"USD\",\n \"balance\": 49.715\n },\n \"message\": \"OK\",\n \"rechargeData\": {\n \"Network\": \"Econet\",\n \"Target\": \"0773591219\",\n \"Cost\": \"0.09500\"\n }\n}" + } + ] + }, + { + "name": "Recharge ZESA", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"AgentReference\": \"{{$guid}}\",\n \"ProductId\": 41,\n \"Target\": \"07088597534\",\n \"Amount\": 5,\n \"RechargeOptions\": [\n {\n \"Name\": \"NotifyNumber\",\n \"ParameterType\": \"String\",\n \"Value\": \"0773591219\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://ssl.hot.co.zw/api/v3/products/recharge", + "protocol": "https", + "host": [ + "ssl", + "hot", + "co", + "zw" + ], + "path": [ + "api", + "v3", + "products", + "recharge" + ] + } + }, + "response": [ + { + "name": "Recharge ZESA", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"AgentReference\": \"{{$guid}}\",\n \"ProductId\": 41,\n \"Target\": \"07088597534\",\n \"Amount\": 5,\n \"RechargeOptions\": [\n {\n \"Name\": \"NotifyNumber\",\n \"ParameterType\": \"String\",\n \"Value\": \"0773591219\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://ssl.hot.co.zw/api/v3/products/recharge", + "protocol": "https", + "host": [ + "ssl", + "hot", + "co", + "zw" + ], + "path": [ + "api", + "v3", + "products", + "recharge" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": null, + "header": [ + { + "key": ":status", + "value": 200 + }, + { + "key": "content-length", + "value": "371" + }, + { + "key": "content-type", + "value": "application/json; charset=utf-8" + }, + { + "key": "server", + "value": "" + }, + { + "key": "x-powered-by", + "value": "ASP.NET" + }, + { + "key": "strict-transport-security", + "value": "max-age=0; includeSubDomains" + }, + { + "key": "date", + "value": "Sat, 02 May 2026 08:34:46 GMT" + } + ], + "cookie": [], + "body": "{\n \"successful\": true,\n \"rechargeId\": 190356513,\n \"amount\": 5,\n \"discount\": 1.2,\n \"balance\": {\n \"accountTypeId\": 4,\n \"name\": \"Utility USD\",\n \"balance\": 45.06\n },\n \"message\": \"Transaction processed successfully\",\n \"rechargeData\": {\n \"Network\": \"ZESA\",\n \"Target\": \"07088597534\",\n \"Cost\": \"4.9400\",\n \"Token\": \"0099 4439 8936 4293 1759\",\n \"Units\": \"57.86\",\n \"NetAmount\": \"125.29\",\n \"TaxAmount\": \"0\",\n \"Levy\": \"7.52\",\n \"Arrears\": \"0\"\n }\n}" + } + ] + } + ], + "variable": [ + { + "key": "token", + "value": "" + } + ] +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/models/Provider.java b/src/main/java/zw/qantra/tm/domain/models/Provider.java index 86b7ab4..22d66dc 100644 --- a/src/main/java/zw/qantra/tm/domain/models/Provider.java +++ b/src/main/java/zw/qantra/tm/domain/models/Provider.java @@ -1,5 +1,6 @@ package zw.qantra.tm.domain.models; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import lombok.*; import org.hibernate.annotations.SQLRestriction; @@ -22,4 +23,6 @@ public class Provider extends BaseEntity { private String accountFieldName; private String integrationProcessorLabel; private double percentageCommission; + @Column(columnDefinition = "TEXT") + private String meta; } diff --git a/src/main/java/zw/qantra/tm/domain/services/HotRechargeTokenService.java b/src/main/java/zw/qantra/tm/domain/services/HotRechargeTokenService.java new file mode 100644 index 0000000..bd3dcd2 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/HotRechargeTokenService.java @@ -0,0 +1,82 @@ +package zw.qantra.tm.domain.services; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import zw.qantra.tm.rest.RestService; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class HotRechargeTokenService { + Logger logger = LoggerFactory.getLogger(HotRechargeTokenService.class); + + private final RestService restService; + + @Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}") + private String hotAuthUrl; + @Value("${hot.access.code}") + private String hotAccessCode; + @Value("${hot.password}") + private String hotPassword; + @Value("${hot.token.expiry.minutes:30}") + private int tokenExpiryMinutes; + + private String cachedToken; + private long tokenExpiryTime; + + public synchronized String getToken() { + if (isTokenValid()) { + logger.info("Returning cached HotRecharge token"); + return cachedToken; + } + return fetchNewToken(); + } + + private boolean isTokenValid() { + return cachedToken != null && System.currentTimeMillis() < tokenExpiryTime; + } + + private synchronized String fetchNewToken() { + if (isTokenValid()) { + return cachedToken; + } + + try { + logger.info("Fetching new HotRecharge token"); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + + Map payload = Map.of( + "AccessCode", hotAccessCode, + "Password", hotPassword + ); + + LinkedHashMap response = restService.postAs(hotAuthUrl + "/identity/login", payload, headers, LinkedHashMap.class); + cachedToken = (String) response.get("token"); + tokenExpiryTime = System.currentTimeMillis() + (tokenExpiryMinutes * 60 * 1000L); + + logger.info("New HotRecharge token cached, expires in {} minutes", tokenExpiryMinutes); + return cachedToken; + } catch (Exception e) { + logger.error("Failed to fetch HotRecharge token: " + e.getMessage(), e); + cachedToken = null; + return null; + } + } + + public synchronized void invalidateToken() { + logger.info("Invalidating cached HotRecharge token"); + cachedToken = null; + tokenExpiryTime = 0; + } +} + diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/EconetBroadbandConfirmationProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/EconetBroadbandConfirmationProcessor.java deleted file mode 100644 index 30112b5..0000000 --- a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/EconetBroadbandConfirmationProcessor.java +++ /dev/null @@ -1,26 +0,0 @@ -package zw.qantra.tm.domain.services.processors.confirmations; - -import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import zw.qantra.tm.domain.dtos.BillPaymentDto; -import zw.qantra.tm.domain.enums.RequestType; -import zw.qantra.tm.domain.enums.Status; -import zw.qantra.tm.domain.models.AdditionalData; -import zw.qantra.tm.domain.models.Transaction; -import zw.qantra.tm.domain.services.AdditionalDataService; -import zw.qantra.tm.domain.services.TransactionService; -import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; -import zw.qantra.tm.rest.RestService; -import zw.qantra.tm.utils.Utils; - -import java.util.LinkedHashMap; - -@Service("CONFIRM_ECONET_BROADBAND") -public class EconetBroadbandConfirmationProcessor extends EconetConfirmationProcessor{ - public EconetBroadbandConfirmationProcessor(RestService restService, TransactionService transactionService, AdditionalDataService additionalDataService) { - super(restService, transactionService, additionalDataService); - } -} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/NetoneConfirmationProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/NetoneConfirmationProcessor.java deleted file mode 100644 index c0bd121..0000000 --- a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/NetoneConfirmationProcessor.java +++ /dev/null @@ -1,13 +0,0 @@ -package zw.qantra.tm.domain.services.processors.confirmations; - -import org.springframework.stereotype.Service; -import zw.qantra.tm.domain.services.AdditionalDataService; -import zw.qantra.tm.domain.services.TransactionService; -import zw.qantra.tm.rest.RestService; - -@Service("CONFIRM_NETONE") -public class NetoneConfirmationProcessor extends EconetConfirmationProcessor{ - public NetoneConfirmationProcessor(RestService restService, TransactionService transactionService, AdditionalDataService additionalDataService) { - super(restService, transactionService, additionalDataService); - } -} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/EconetBundlesConfirmationHotProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/EconetBundlesConfirmationHotProcessor.java new file mode 100644 index 0000000..50b7597 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/EconetBundlesConfirmationHotProcessor.java @@ -0,0 +1,29 @@ +package zw.qantra.tm.domain.services.processors.confirmations.hotrecharge; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.enums.Status; +import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; + + +@Service("CONFIRM_ECONET_BUNDLES_HOT") +@RequiredArgsConstructor +public class EconetBundlesConfirmationHotProcessor implements TransactionProcessorInterface { + @Override + public Object process(Transaction transaction) throws Exception { + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("00"); + return transaction; + } + + @Override + public Object reverse(Transaction transaction) { + return transaction; + } + + @Override + public Object poll(Transaction transaction) throws Exception { + return transaction; + } +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/EconetConfirmationHotProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/EconetConfirmationHotProcessor.java new file mode 100644 index 0000000..59c5d13 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/EconetConfirmationHotProcessor.java @@ -0,0 +1,29 @@ +package zw.qantra.tm.domain.services.processors.confirmations.hotrecharge; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.enums.Status; +import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; + + +@Service("CONFIRM_ECONET_HOT") +@RequiredArgsConstructor +public class EconetConfirmationHotProcessor implements TransactionProcessorInterface { + @Override + public Object process(Transaction transaction) throws Exception { + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("00"); + return transaction; + } + + @Override + public Object reverse(Transaction transaction) { + return transaction; + } + + @Override + public Object poll(Transaction transaction) throws Exception { + return transaction; + } +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/NetoneConfirmationHotProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/NetoneConfirmationHotProcessor.java new file mode 100644 index 0000000..b43347f --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/NetoneConfirmationHotProcessor.java @@ -0,0 +1,29 @@ +package zw.qantra.tm.domain.services.processors.confirmations.hotrecharge; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.enums.Status; +import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; + + +@Service("CONFIRM_NETONE_HOT") +@RequiredArgsConstructor +public class NetoneConfirmationHotProcessor implements TransactionProcessorInterface { + @Override + public Object process(Transaction transaction) throws Exception { + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("00"); + return transaction; + } + + @Override + public Object reverse(Transaction transaction) { + return transaction; + } + + @Override + public Object poll(Transaction transaction) throws Exception { + return transaction; + } +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/ZesaConfirmationHotProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/ZesaConfirmationHotProcessor.java new file mode 100644 index 0000000..7d4f7ed --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/hotrecharge/ZesaConfirmationHotProcessor.java @@ -0,0 +1,163 @@ +package zw.qantra.tm.domain.services.processors.confirmations.hotrecharge; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +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.RequestType; +import zw.qantra.tm.domain.enums.Status; +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.services.AdditionalDataService; +import zw.qantra.tm.domain.services.HotRechargeTokenService; +import zw.qantra.tm.domain.services.ProviderService; +import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; +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.*; + +@Service("CONFIRM_ZESA_HOT") +@RequiredArgsConstructor +public class ZesaConfirmationHotProcessor implements TransactionProcessorInterface { + Logger logger = LoggerFactory.getLogger(ZesaConfirmationHotProcessor.class); + + private final RestService restService; + private final AdditionalDataService additionalDataService; + private final ProviderService providerService; + private final HotRechargeTokenService hotRechargeTokenService; + + @Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}") + private String hotApiUrl; + + @Override + public Object process(Transaction transaction) throws Exception { + logger.info("Processing ZESA confirmation from HotRecharge"); + + try { + String token = hotRechargeTokenService.getToken(); + if (token == null || token.isEmpty()) { + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("91"); + transaction.setErrorMessage("Failed to authenticate with HotRecharge"); + return transaction; + } + + if (!checkBalance(token, transaction.getAmount())) { + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("91"); + transaction.setErrorMessage("Insufficient balance"); + return transaction; + } + + Map customerData = checkCustomer(token, transaction); + if (customerData == null) { + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("91"); + transaction.setErrorMessage("Customer not found or invalid"); + return transaction; + } + + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("00"); + + List> additionalDataList = buildAdditionalData(customerData); + transaction.setAdditionalData(additionalDataList); + + AdditionalData additionalData = AdditionalData.builder() + .transactionId(transaction.getId().toString()) + .requestType(RequestType.CONFIRM) + .jsonString(Utils.toJson(additionalDataList)) + .build(); + additionalDataService.getAdditionalDataRepository().save(additionalData); + + } catch (Exception e) { + logger.error("Error processing ZESA confirmation: " + e.getMessage(), e); + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("99"); + transaction.setErrorMessage("Processing error: " + e.getMessage()); + } + + return transaction; + } + + private List> buildAdditionalData(Map customerData) { + List> additionalDataList = new ArrayList<>(); + if (customerData != null) { + additionalDataList.add(Map.of("key", "accountNumber", + "value", customerData.getOrDefault("accountNumber", "").toString())); + Map details = (Map) customerData.get("details"); + if (details != null) { + for (Map.Entry entry : details.entrySet()) { + additionalDataList.add(Map.of("key", entry.getKey(), "value", entry.getValue().toString())); + } + } + } + return additionalDataList; + } + + private boolean checkBalance(String token, BigDecimal amount) { + try { + String balanceUrl = hotApiUrl + "/account/balance/4"; + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(token); + + LinkedList responseEntity = restService.getAs(balanceUrl, headers, LinkedList.class); + LinkedHashMap response = (LinkedHashMap) responseEntity.get(0); + logger.info("Response: " + response); + + if (response != null && response.containsKey("balance")) { + Object balance = response.get("balance"); + logger.info("Current balance: " + balance); + return (new BigDecimal(balance.toString())).compareTo(amount) > 0; + } + return false; + } catch (Exception e) { + logger.error("Failed to check balance: " + e.getMessage(), e); + return false; + } + } + + private Map 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 customerUrl = hotApiUrl + "/query/customer/" + hotProductId + "/" + transaction.getCreditAccount(); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(token); + + LinkedHashMap response = restService.getAs(customerUrl, headers, LinkedHashMap.class); + if (response != null && response.containsKey("details")) { + logger.info("Customer found: " + response.get("accountNumber")); + return response; + } + return null; + } catch (Exception e) { + logger.error("Failed to check customer: " + e.getMessage(), e); + return null; + } + } + + @Override + public Object reverse(Transaction transaction) { + return transaction; + } + + @Override + public Object poll(Transaction transaction) throws Exception { + return transaction; + } +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/EconetBroadbandConfirmationStewardProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/EconetBroadbandConfirmationStewardProcessor.java new file mode 100644 index 0000000..703898b --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/EconetBroadbandConfirmationStewardProcessor.java @@ -0,0 +1,13 @@ +package zw.qantra.tm.domain.services.processors.confirmations.steward; + +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.services.AdditionalDataService; +import zw.qantra.tm.domain.services.TransactionService; +import zw.qantra.tm.rest.RestService; + +@Service("CONFIRM_ECONET_BUNDLES_STEWARD") +public class EconetBroadbandConfirmationStewardProcessor extends EconetConfirmationStewardProcessor { + public EconetBroadbandConfirmationStewardProcessor(RestService restService, TransactionService transactionService, AdditionalDataService additionalDataService) { + super(restService, transactionService, additionalDataService); + } +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/EconetConfirmationProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/EconetConfirmationStewardProcessor.java similarity index 92% rename from src/main/java/zw/qantra/tm/domain/services/processors/confirmations/EconetConfirmationProcessor.java rename to src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/EconetConfirmationStewardProcessor.java index e2c8b24..831f06e 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/EconetConfirmationProcessor.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/EconetConfirmationStewardProcessor.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.services.processors.confirmations; +package zw.qantra.tm.domain.services.processors.confirmations.steward; import lombok.RequiredArgsConstructor; @@ -18,12 +18,11 @@ import zw.qantra.tm.rest.RestService; import zw.qantra.tm.utils.Utils; import java.util.LinkedHashMap; -import java.util.UUID; -@Service("CONFIRM_ECONET") +@Service("CONFIRM_ECONET_STEWARD") @RequiredArgsConstructor -public class EconetConfirmationProcessor implements TransactionProcessorInterface { - Logger logger = LoggerFactory.getLogger(EconetConfirmationProcessor.class); +public class EconetConfirmationStewardProcessor implements TransactionProcessorInterface { + Logger logger = LoggerFactory.getLogger(EconetConfirmationStewardProcessor.class); private final RestService restService; private final TransactionService transactionService; diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/NetoneConfirmationStewardStewardProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/NetoneConfirmationStewardStewardProcessor.java new file mode 100644 index 0000000..1a5a38c --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/NetoneConfirmationStewardStewardProcessor.java @@ -0,0 +1,13 @@ +package zw.qantra.tm.domain.services.processors.confirmations.steward; + +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.services.AdditionalDataService; +import zw.qantra.tm.domain.services.TransactionService; +import zw.qantra.tm.rest.RestService; + +@Service("CONFIRM_NETONE_STEWARD") +public class NetoneConfirmationStewardStewardProcessor extends EconetConfirmationStewardProcessor { + public NetoneConfirmationStewardStewardProcessor(RestService restService, TransactionService transactionService, AdditionalDataService additionalDataService) { + super(restService, transactionService, additionalDataService); + } +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/ZesaConfirmationProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/ZesaConfirmationStewardProcessor.java similarity index 92% rename from src/main/java/zw/qantra/tm/domain/services/processors/confirmations/ZesaConfirmationProcessor.java rename to src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/ZesaConfirmationStewardProcessor.java index 2fdce1d..08953e5 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/ZesaConfirmationProcessor.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/steward/ZesaConfirmationStewardProcessor.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.services.processors.confirmations; +package zw.qantra.tm.domain.services.processors.confirmations.steward; import lombok.RequiredArgsConstructor; @@ -19,10 +19,10 @@ import zw.qantra.tm.utils.Utils; import java.util.LinkedHashMap; -@Service("CONFIRM_ZESA") +@Service("CONFIRM_ZESA_STEWARD") @RequiredArgsConstructor -public class ZesaConfirmationProcessor implements TransactionProcessorInterface { - Logger logger = LoggerFactory.getLogger(ZesaConfirmationProcessor.class); +public class ZesaConfirmationStewardProcessor implements TransactionProcessorInterface { + Logger logger = LoggerFactory.getLogger(ZesaConfirmationStewardProcessor.class); private final RestService restService; private final TransactionService transactionService; diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/integrations/HotRechargeIntegrationProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/integrations/HotRechargeIntegrationProcessor.java new file mode 100644 index 0000000..ed14fd9 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/processors/integrations/HotRechargeIntegrationProcessor.java @@ -0,0 +1,245 @@ +package zw.qantra.tm.domain.services.processors.integrations; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +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.RequestType; +import zw.qantra.tm.domain.enums.Status; +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.services.*; +import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; +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.*; + +@Service("INTEGRATION_HOT") +@RequiredArgsConstructor +public class HotRechargeIntegrationProcessor implements TransactionProcessorInterface { + Logger logger = LoggerFactory.getLogger(HotRechargeIntegrationProcessor.class); + private final RestService restService; + private final AdditionalDataService additionalDataService; + private final SettingService settingService; + private final HotRechargeTokenService hotRechargeTokenService; + private final ProviderService providerService; + + @Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}") + private String hotApiUrl; + + @Override + @Transactional + public Object process(Transaction transaction) throws Exception { + + if(settingService.getBooleanSetting("SIMULATE_INTEGRATION_DELAY")){ + Thread.sleep(5000); + } + + if(settingService.getBooleanSetting("SIMULATE_INTEGRATION_FAILURE")){ + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("92"); + transaction.setErrorMessage("Simulated failure"); + return transaction; + } + + if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){ + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("responseCode"); + transaction.setCreditRef("creditRef"); + transaction.setErrorMessage(null); + return transaction; + } + + try { + logger.info("Processing HotRecharge integration"); + String token = hotRechargeTokenService.getToken(); + + if (token == null || token.isEmpty()) { + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("91"); + transaction.setErrorMessage("Failed to authenticate with HotRecharge"); + return transaction; + } + + if (!checkBalance(token, transaction.getAmount())) { + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("91"); + transaction.setErrorMessage("Insufficient balance"); + return transaction; + } + + LinkedHashMap rechargeResponse = performRecharge(token, transaction); + if (rechargeResponse == null) { + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("99"); + transaction.setErrorMessage("Recharge request failed"); + return transaction; + } + + Boolean successful = (Boolean) rechargeResponse.get("successful"); + if (successful != null && successful) { + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("00"); + transaction.setCreditRef(rechargeResponse.get("rechargeId").toString()); + transaction.setErrorMessage(null); + + @SuppressWarnings("unchecked") + Map rechargeData = (Map) rechargeResponse.get("rechargeData"); + if (rechargeData != null) { + List> additionalDataList = buildAdditionalDataList(rechargeData); + transaction.setAdditionalData(additionalDataList); + + AdditionalData additionalData = AdditionalData.builder() + .transactionId(transaction.getId().toString()) + .requestType(RequestType.INTEGRATION) + .jsonString(Utils.toJson(additionalDataList)) + .build(); + additionalDataService.getAdditionalDataRepository().save(additionalData); + } + } else { + transaction.setStatus(Status.FAILED); + transaction.setResponseCode((String) rechargeResponse.getOrDefault("responseCode", "99")); + transaction.setErrorMessage((String) rechargeResponse.getOrDefault("message", "Recharge failed")); + } + + } catch (Exception e) { + logger.error("Error processing HotRecharge integration: " + e.getMessage(), e); + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("99"); + transaction.setErrorMessage("Processing error: " + e.getMessage()); + } + + return transaction; + + } + + private boolean checkBalance(String token, BigDecimal amount) { + try { + String balanceUrl = hotApiUrl + "/account/balance/4"; + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(token); + + @SuppressWarnings("unchecked") + LinkedList> responseEntity = restService.getAs(balanceUrl, headers, LinkedList.class); + LinkedHashMap response = responseEntity.get(0); + logger.info("Balance response: {}", response); + + if (response != null && response.containsKey("balance")) { + BigDecimal balance = new BigDecimal(response.get("balance").toString()); + logger.info("Current balance: {}", balance); + return balance.compareTo(amount) > 0; + } + return false; + } catch (Exception e) { + logger.error("Failed to check balance: {}", e.getMessage(), e); + return false; + } + } + + private LinkedHashMap performRecharge(String token, Transaction transaction) { + try { + String rechargeUrl = hotApiUrl + "/products/recharge"; + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(token); + headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON); + + Map request = new LinkedHashMap<>(); + request.put("AgentReference", UUID.randomUUID().toString()); + request.put("ProductId", getProductId(transaction.getBillClientId())); + request.put("Target", transaction.getCreditAccount()); + request.put("Amount", transaction.getAmount()); + + if(getProductId(transaction.getBillClientId()).equals("41")) { + List> rechargeOptions = new ArrayList<>(); + if (transaction.getCreditPhone() != null) { + Map option = new LinkedHashMap<>(); + option.put("Name", "NotifyNumber"); + option.put("ParameterType", "String"); + option.put("Value", transaction.getCreditPhone()); + rechargeOptions.add(option); + } + if (!rechargeOptions.isEmpty()) { + request.put("RechargeOptions", rechargeOptions); + } + } + + logger.info("Sending recharge request: {}", request); + ResponseEntity response = restService.postAsEntity(rechargeUrl, request, headers, LinkedHashMap.class); + logger.info("Recharge response: {}", response.getBody()); + + LinkedHashMap successResponse = (LinkedHashMap) response.getBody(); + return successResponse; + } catch (HttpClientErrorException e) { + logger.info("HTTP error response received: {}", e.getStatusCode()); + try { + String responseBody = e.getResponseBodyAsString(); + logger.info("Error response body: {}", responseBody); + LinkedHashMap errorResponseMap = Utils.fromJson(responseBody, LinkedHashMap.class); + + List> errors = (List>) errorResponseMap.get("errors"); + if(errors != null && !errors.isEmpty()){ + String errorMessage = errors.get(0).get("message"); + errorResponseMap.put("message", errorMessage); + logger.info("Extracted error message: {}", errorMessage); + } + return errorResponseMap; + } catch (Exception parseException) { + logger.error("Failed to parse error response: {}", parseException.getMessage()); + LinkedHashMap errorMap = new LinkedHashMap<>(); + errorMap.put("message", e.getMessage()); + return errorMap; + } + } catch (ResourceAccessException e) { + logger.error("Network error during recharge: {}", e.getMessage(), e); + LinkedHashMap errorMap = new LinkedHashMap<>(); + errorMap.put("message", "Network error: " + e.getMessage()); + return errorMap; + } catch (Exception e) { + logger.error("Failed to perform recharge: {}", e.getMessage(), e); + return null; + } + } + + private List> buildAdditionalDataList(Map rechargeData) { + List> additionalDataList = new ArrayList<>(); + for (Map.Entry entry : rechargeData.entrySet()) { + Map dataItem = new LinkedHashMap<>(); + dataItem.put("key", entry.getKey()); + dataItem.put("value", entry.getValue().toString()); + additionalDataList.add(dataItem); + } + return additionalDataList; + } + + private String getProductId(String billClientId) { + Provider provider = providerService.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"); + } + + @Override + public Object reverse(Transaction transaction) { + return transaction; + } + + @Override + public Object poll(Transaction transaction) { + return transaction; + } + +} diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/payments/EcocashRemotePaymentProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/payments/EcocashRemotePaymentProcessor.java index ab92493..0df5449 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/payments/EcocashRemotePaymentProcessor.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/payments/EcocashRemotePaymentProcessor.java @@ -140,6 +140,14 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf @Override public Object poll(Transaction transaction) throws Exception { logger.info("polling transaction"); + + if(settingService.getBooleanSetting("SIMULATE_ECOCASH_SUCCESS")) { + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("00"); + transaction.setTargetUrl("/home"); + return transaction; + } + String token = restService.getMerchantToken(); HttpHeaders headers = new HttpHeaders(); diff --git a/src/main/java/zw/qantra/tm/domain/workflows/RegistrationWorkflow.java b/src/main/java/zw/qantra/tm/domain/workflows/RegistrationWorkflow.java index 79ed96c..74dce35 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/RegistrationWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/workflows/RegistrationWorkflow.java @@ -114,8 +114,8 @@ public class RegistrationWorkflow extends WorkflowDefinition { try { String phone = payload.getPhone(); Phonenumber.PhoneNumber zimProto = phoneUtil.parse(phone, "ZW"); - phone = String.valueOf(zimProto.getCountryCode()) + String.valueOf(zimProto.getNationalNumber()); - payload.setPhone(phone); + phone = zimProto.getCountryCode() + String.valueOf(zimProto.getNationalNumber()); + payload.setPhone(phone.replaceAll("[^0-9]", "")); } catch (NumberParseException e) { System.err.println("NumberParseException was thrown: " + e.toString()); return NextAction.moveToState(FAILED, "Invalid phone number"); diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java index b7929c7..c82bdf0 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java +++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java @@ -44,7 +44,10 @@ public class CleanupHandler implements HandlerInterface { if(provider == null){ throw new ApiException("Provider not found for billClientId: " + transaction.getBillClientId()); } - String label = transaction.getType() + "_" + provider.getLabel(); + String label = + transaction.getType() + "_" + + provider.getLabel() + "_" + + provider.getIntegrationProcessorLabel(); transaction.setConfirmationProcessorLabel(label); transaction.setProviderLabel(provider.getLabel()); @@ -79,7 +82,7 @@ public class CleanupHandler implements HandlerInterface { // phone must begin with '+' Phonenumber.PhoneNumber zimPhoneNumber = phoneUtil.parse(debitPhoneNumber, ""); String formattedPhoneNumber = phoneUtil.format(zimPhoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164); - transaction.setDebitPhone(formattedPhoneNumber); + transaction.setDebitPhone(formattedPhoneNumber.replaceAll("[^0-9]", "")); } catch (NumberParseException e) { System.err.println("NumberParseException was thrown: " + e.toString()); } @@ -89,7 +92,7 @@ public class CleanupHandler implements HandlerInterface { try { Phonenumber.PhoneNumber zimPhoneNumber = phoneUtil.parse(creditPhoneNumber, "ZW"); String formattedPhoneNumber = phoneUtil.format(zimPhoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164); - transaction.setCreditPhone(formattedPhoneNumber); + transaction.setCreditPhone(formattedPhoneNumber.replaceAll("[^0-9]", "")); } catch (NumberParseException e) { System.err.println("NumberParseException was thrown: " + e.toString()); } diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/ConfirmHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/ConfirmHandler.java index cb39a77..5ccd841 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/ConfirmHandler.java +++ b/src/main/java/zw/qantra/tm/domain/workflows/handlers/ConfirmHandler.java @@ -38,6 +38,11 @@ public class ConfirmHandler implements HandlerInterface { .build(); transactionEventService.save(transactionEvent); + // String confirmationProcessorLabel = + // transaction.getType() + "_" + + // provider.getLabel() + "_" + + // provider.getIntegrationProcessorLabel(); + TransactionProcessorInterface transactionProcessorInterface = (TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor( transaction.getConfirmationProcessorLabel()); diff --git a/src/main/java/zw/qantra/tm/rest/RestService.java b/src/main/java/zw/qantra/tm/rest/RestService.java index 8bcfd06..c3963d4 100755 --- a/src/main/java/zw/qantra/tm/rest/RestService.java +++ b/src/main/java/zw/qantra/tm/rest/RestService.java @@ -1,6 +1,8 @@ package zw.qantra.tm.rest; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,7 +10,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -17,7 +21,9 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import zw.qantra.tm.exceptions.RestTemplateResponseErrorHandler; +import java.io.IOException; import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -53,16 +59,15 @@ public class RestService { ) ); - this.restTemplate.setErrorHandler(new RestTemplateResponseErrorHandler()); - // bootstrap rest template List interceptors = this.restTemplate.getInterceptors(); if (CollectionUtils.isEmpty(interceptors)) { interceptors = new ArrayList<>(); } - interceptors.add(new RestTemplateInterceptor()); + interceptors.add(new LoggingInterceptor(this)); this.restTemplate.setInterceptors(interceptors); + } public String getUserToken(){ @@ -230,4 +235,52 @@ public class RestService { public ResponseEntity exchangeGetNoAuth(String path, HttpEntity httpEntity, Class clazz, String pathVariable) { return this.restTemplate.exchange(path, HttpMethod.GET, httpEntity, clazz, pathVariable); } + + private String prettyPrintJson(String json) { + try { + Object jsonObject = gson.fromJson(json, Object.class); + return new GsonBuilder().setPrettyPrinting().create().toJson(jsonObject); + } catch (JsonSyntaxException e) { + return json; + } + } + + private static class LoggingInterceptor implements ClientHttpRequestInterceptor { + Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class); + private final RestService restService; + + public LoggingInterceptor(RestService restService) { + this.restService = restService; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + logRequest(request, body); + ClientHttpResponse response = execution.execute(request, body); + logResponse(response); + return response; + } + + private void logRequest(HttpRequest request, byte[] body) { + logger.info("=== REST REQUEST ==="); + logger.info("Method: {} | URI: {}", request.getMethod(), request.getURI()); + logger.info("Headers: {}", request.getHeaders()); + if (body.length > 0) { + String bodyStr = new String(body, StandardCharsets.UTF_8); + logger.info("Body: {}", restService.prettyPrintJson(bodyStr)); + } + } + + private void logResponse(ClientHttpResponse response) throws IOException { + logger.info("=== REST RESPONSE ==="); + logger.info("Status: {}", response.getStatusCode()); + logger.info("Headers: {}", response.getHeaders()); + byte[] responseBody = response.getBody().readAllBytes(); + if (responseBody.length > 0) { + String bodyStr = new String(responseBody, StandardCharsets.UTF_8); + logger.info("Body: {}", restService.prettyPrintJson(bodyStr)); + } + } + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ccd49df..58cf9a5 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -77,3 +77,12 @@ management.tracing.enabled=true management.tracing.sampling.probability=1.0 management.otlp.tracing.endpoint=http://62.171.179.108:10428/insert/opentelemetry/v1/traces management.otlp.logging.endpoint=http://62.171.179.108:9428/insert/opentelemetry/v1/logs + +hot-recharge.url=https://ssl.hot.co.zw/api/v3 +hot-recharge.username=vusumuzi@qantra.co.zw +hot-recharge.password=Stikbanch@50 +hot.api.url=https://ssl.hot.co.zw/api/v3 +hot.auth.url=https://ssl.hot.co.zw/api/v3/identity/login +hot.access.code=vusumuzi@qantra.co.zw +hot.password=Stikbanch@50 +hot.token.expiry.minutes=30 \ No newline at end of file