added version control & cloudflare cache clearing
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package zw.qantra.tm.domain.controllers;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import zw.qantra.tm.domain.models.AppVersion;
|
||||
import zw.qantra.tm.domain.services.AppVersionService;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/public/app-version")
|
||||
@RequiredArgsConstructor
|
||||
public class AppVersionController {
|
||||
|
||||
private final AppVersionService appVersionService;
|
||||
|
||||
/**
|
||||
* Client checks if a newer app build is available.
|
||||
* Pass the current build number as a query param.
|
||||
*/
|
||||
@GetMapping("/check/app")
|
||||
public ResponseEntity<AppVersion> checkAppUpdate(@RequestParam Integer currentBuildNumber) {
|
||||
Optional<AppVersion> update = appVersionService.checkForAppUpdate(currentBuildNumber);
|
||||
return update.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Client checks if a newer web build is available.
|
||||
* Pass the current build number as a query param.
|
||||
*/
|
||||
@GetMapping("/check/web")
|
||||
public ResponseEntity<AppVersion> checkWebUpdate(@RequestParam Integer currentBuildNumber) {
|
||||
Optional<AppVersion> update = appVersionService.checkForWebUpdate(currentBuildNumber);
|
||||
return update.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest version record.
|
||||
*/
|
||||
@GetMapping("/latest")
|
||||
public ResponseEntity<AppVersion> getLatestVersion() {
|
||||
Optional<AppVersion> latest = appVersionService.getLatestVersion();
|
||||
return latest.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a new version record (internal/admin use).
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<AppVersion> save(@RequestBody AppVersion appVersion) {
|
||||
return ResponseEntity.ok(appVersionService.save(appVersion));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package zw.qantra.tm.domain.controllers;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import zw.qantra.tm.domain.dtos.CloudflarePurgeResponse;
|
||||
import zw.qantra.tm.domain.services.CloudflareCacheService;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/public/cache/cloudflare")
|
||||
@RequiredArgsConstructor
|
||||
public class CloudflareCacheController {
|
||||
|
||||
private final CloudflareCacheService cloudflareCacheService;
|
||||
|
||||
@PostMapping("/clear")
|
||||
public ResponseEntity<CloudflarePurgeResponse> clearCache() {
|
||||
log.info("Received request to clear Cloudflare cache");
|
||||
CloudflarePurgeResponse response = cloudflareCacheService.purgeEverything();
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package zw.qantra.tm.domain.dtos;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CloudflarePurgeResponse {
|
||||
|
||||
private boolean success;
|
||||
private List<CloudflareError> errors;
|
||||
private List<String> messages;
|
||||
private CloudflareResult result;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CloudflareError {
|
||||
private int code;
|
||||
private String message;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CloudflareResult {
|
||||
private String id;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ public class GroupBatchItemUpdateRequest {
|
||||
private BigDecimal amount;
|
||||
private String recipientName;
|
||||
private String recipientPhone;
|
||||
private String recipientAccount;
|
||||
private String billClientId;
|
||||
private String billName;
|
||||
private String billProductName;
|
||||
|
||||
19
src/main/java/zw/qantra/tm/domain/models/AppVersion.java
Normal file
19
src/main/java/zw/qantra/tm/domain/models/AppVersion.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package zw.qantra.tm.domain.models;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import lombok.*;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AppVersion extends BaseEntity{
|
||||
private String description;
|
||||
private Integer appBuildNumber;
|
||||
private Integer webBuildNumber;
|
||||
private Timestamp timestamp;
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public class GroupBatchItem extends BaseEntity {
|
||||
private String recipientName;
|
||||
@Column(name = "recipient_phone")
|
||||
private String recipientPhone;
|
||||
private String recipientAccount;
|
||||
private BigDecimal amount;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package zw.qantra.tm.domain.repositories;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import zw.qantra.tm.domain.models.AppVersion;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface AppVersionRepository extends JpaRepository<AppVersion, UUID> {
|
||||
|
||||
Optional<AppVersion> findTopByOrderByAppBuildNumberDesc();
|
||||
|
||||
Optional<AppVersion> findTopByOrderByWebBuildNumberDesc();
|
||||
|
||||
Optional<AppVersion> findTopByOrderByTimestampDesc();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package zw.qantra.tm.domain.services;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import zw.qantra.tm.domain.models.AppVersion;
|
||||
import zw.qantra.tm.domain.repositories.AppVersionRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AppVersionService {
|
||||
|
||||
private final AppVersionRepository appVersionRepository;
|
||||
|
||||
/**
|
||||
* Checks if a newer app build number exists than the one provided by the client.
|
||||
*
|
||||
* @param currentBuildNumber the build number the client currently has
|
||||
* @return the latest AppVersion if a newer one exists, empty otherwise
|
||||
*/
|
||||
public Optional<AppVersion> checkForAppUpdate(Integer currentBuildNumber) {
|
||||
Optional<AppVersion> latest = appVersionRepository.findTopByOrderByAppBuildNumberDesc();
|
||||
if (latest.isPresent() && latest.get().getAppBuildNumber() > currentBuildNumber) {
|
||||
return latest;
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a newer web build number exists than the one provided by the client.
|
||||
*
|
||||
* @param currentBuildNumber the build number the client currently has
|
||||
* @return the latest AppVersion if a newer one exists, empty otherwise
|
||||
*/
|
||||
public Optional<AppVersion> checkForWebUpdate(Integer currentBuildNumber) {
|
||||
Optional<AppVersion> latest = appVersionRepository.findTopByOrderByWebBuildNumberDesc();
|
||||
if (latest.isPresent() && latest.get().getWebBuildNumber() > currentBuildNumber) {
|
||||
return latest;
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest AppVersion record regardless of build number.
|
||||
*
|
||||
* @return the latest AppVersion by timestamp
|
||||
*/
|
||||
public Optional<AppVersion> getLatestVersion() {
|
||||
return appVersionRepository.findTopByOrderByTimestampDesc();
|
||||
}
|
||||
|
||||
public AppVersion save(AppVersion appVersion) {
|
||||
return appVersionRepository.save(appVersion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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.stereotype.Service;
|
||||
import zw.qantra.tm.domain.dtos.CloudflarePurgeResponse;
|
||||
import zw.qantra.tm.rest.RestService;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CloudflareCacheService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CloudflareCacheService.class);
|
||||
|
||||
private static final String CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
|
||||
|
||||
private final RestService restService;
|
||||
|
||||
@Value("${cloudflare-token}")
|
||||
private String cloudflareToken;
|
||||
|
||||
@Value("${cloudflare-zone-id}")
|
||||
private String cloudflareZoneId;
|
||||
|
||||
/**
|
||||
* Purges the entire Cloudflare cache for the configured zone.
|
||||
*
|
||||
* @return CloudflarePurgeResponse containing success status and any errors
|
||||
*/
|
||||
public CloudflarePurgeResponse purgeEverything() {
|
||||
logger.info("Initiating Cloudflare purge_everything for zone: {}", cloudflareZoneId);
|
||||
|
||||
String path = CLOUDFLARE_API_BASE + "/zones/" + cloudflareZoneId + "/purge_cache";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(cloudflareToken);
|
||||
|
||||
Map<String, Boolean> payload = Map.of("purge_everything", true);
|
||||
|
||||
CloudflarePurgeResponse response = restService.postWithExternalAuth(
|
||||
path, payload, headers, CloudflarePurgeResponse.class
|
||||
);
|
||||
|
||||
if (response != null && response.isSuccess()) {
|
||||
logger.info("Cloudflare cache purged successfully for zone: {}", cloudflareZoneId);
|
||||
} else if (response != null) {
|
||||
logger.error("Cloudflare cache purge failed for zone: {}. Errors: {}",
|
||||
cloudflareZoneId, response.getErrors());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,7 @@ public class GroupBatchService {
|
||||
.recipientId(recipient.getId())
|
||||
.recipientName(recipient.getName())
|
||||
.recipientPhone(recipient.getPhoneNumber())
|
||||
.recipientAccount(recipient.getAccount())
|
||||
.amount(defaultAmount)
|
||||
.status(GroupBatchItemStatus.NEW)
|
||||
.confirmStatus(GroupBatchItemConfirmStatus.PENDING)
|
||||
@@ -385,6 +386,9 @@ public class GroupBatchService {
|
||||
if (update.getRecipientPhone() != null) {
|
||||
item.setRecipientPhone(update.getRecipientPhone());
|
||||
}
|
||||
if (update.getRecipientAccount() != null) {
|
||||
item.setRecipientAccount(update.getRecipientAccount());
|
||||
}
|
||||
if (update.getBillClientId() != null) {
|
||||
item.setBillClientId(update.getBillClientId());
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Service;
|
||||
import zw.qantra.tm.rest.RestService;
|
||||
import zw.qantra.tm.utils.Utils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.LinkedList;
|
||||
@@ -21,6 +22,7 @@ public class HotRechargeBalanceService {
|
||||
Logger logger = LoggerFactory.getLogger(HotRechargeBalanceService.class);
|
||||
|
||||
private final RestService restService;
|
||||
private final SettingService settingService;
|
||||
|
||||
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
||||
private String hotApiUrl;
|
||||
@@ -28,6 +30,10 @@ public class HotRechargeBalanceService {
|
||||
@Cacheable(value = "hotrechargeBalance", key = "#token + '_' + #amount.toString()")
|
||||
public boolean checkBalance(String token, BigDecimal amount, String accId) {
|
||||
try {
|
||||
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||
return true;
|
||||
}
|
||||
|
||||
String balanceUrl = hotApiUrl + "/account/balance/" + accId;
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(token);
|
||||
|
||||
@@ -178,6 +178,7 @@ public class RecipientGroupService {
|
||||
.recipientId(recipient.getId())
|
||||
.recipientName(recipient.getName())
|
||||
.recipientPhone(recipient.getPhoneNumber())
|
||||
.recipientAccount(recipient.getAccount())
|
||||
.status(GroupBatchItemStatus.NEW)
|
||||
.confirmStatus(GroupBatchItemConfirmStatus.PENDING)
|
||||
.integrationStatus(GroupBatchItemIntegrationStatus.PENDING)
|
||||
@@ -217,6 +218,7 @@ public class RecipientGroupService {
|
||||
try {
|
||||
item.setRecipientName(update.row.name);
|
||||
item.setRecipientPhone(update.row.phone);
|
||||
item.setRecipientAccount(update.row.account);
|
||||
item.setBillName(update.provider.getName());
|
||||
item.setBillProductName(update.row.billProductName);
|
||||
item.setProviderLabel(update.row.billLabel);
|
||||
@@ -474,7 +476,10 @@ public class RecipientGroupService {
|
||||
|
||||
List<Recipient> existing = recipientRepository.findByAccountAndUserId(incomingRecipient.getAccount(), recipientUserId);
|
||||
if (!existing.isEmpty()) {
|
||||
return existing.get(0);
|
||||
Recipient recipient = existing.get(0);
|
||||
recipient.setName(incomingRecipient.getName());
|
||||
recipient.setPhoneNumber(incomingRecipient.getPhoneNumber());
|
||||
return recipientRepository.save(recipient);
|
||||
}
|
||||
|
||||
incomingRecipient.setId(null);
|
||||
|
||||
@@ -39,27 +39,14 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
||||
private String hotApiUrl;
|
||||
|
||||
String token;
|
||||
|
||||
@Override
|
||||
public Object process(Transaction transaction) throws Exception {
|
||||
logger.info("Processing ZESA confirmation from HotRecharge");
|
||||
|
||||
try {
|
||||
// do this if we not simulating success, if simulating success, we skip balance check and just return success
|
||||
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("02");
|
||||
return transaction;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Map<String, Object> customerData = checkCustomer(token, transaction);
|
||||
Map<String, Object> customerData = checkCustomer(transaction);
|
||||
if (customerData == null) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("90");
|
||||
@@ -84,6 +71,7 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
|
||||
transaction.setCreditAmount(transactionService.calculateCreditAmount(transaction));
|
||||
|
||||
// token should've been updated by checkCustomer
|
||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getCreditAmount(), accId)) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("92");
|
||||
@@ -161,7 +149,25 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
return currency == null ? "" : currency.toString().trim();
|
||||
}
|
||||
|
||||
private Map<String, Object> checkCustomer(String token, Transaction transaction) {
|
||||
private Map<String, Object> checkCustomer(Transaction transaction) {
|
||||
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||
logger.info("Simulating HotRecharge success");
|
||||
return Utils.fromJson("{\n" +
|
||||
" \"accountNumber\": \"07088597534\",\n" +
|
||||
" \"details\": {\n" +
|
||||
" \"AccountName\": \"CHIRINDO LETWINA\\n2144 MARLBOROUGH\",\n" +
|
||||
" \"Status\": \"Active\",\n" +
|
||||
" \"Currency\": \"ZWG\"\n" +
|
||||
" }\n" +
|
||||
"}", LinkedHashMap.class);
|
||||
}
|
||||
|
||||
|
||||
token = hotRechargeTokenService.getToken();
|
||||
if (token == null || token.isEmpty()) {
|
||||
throw new ApiException("Failed to authenticate with HotRecharge");
|
||||
}
|
||||
|
||||
try {
|
||||
String hotProductId = providerService.getProviderId(transaction.getBillClientId());
|
||||
|
||||
|
||||
@@ -55,29 +55,12 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
||||
return transaction;
|
||||
}
|
||||
|
||||
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("02");
|
||||
transaction.setCreditRef("Simulated Integration Success");
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
LinkedHashMap<String, Object> rechargeResponse = performRecharge(transaction);
|
||||
if (rechargeResponse == null) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("99");
|
||||
@@ -137,8 +120,40 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
||||
|
||||
}
|
||||
|
||||
private LinkedHashMap<String, Object> performRecharge(String token, Transaction transaction) {
|
||||
private LinkedHashMap<String, Object> performRecharge(Transaction transaction) {
|
||||
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||
logger.info("Simulating HotRecharge success");
|
||||
return Utils.fromJson("{\n" +
|
||||
" \"successful\": true,\n" +
|
||||
" \"rechargeId\": 1.9080854E8,\n" +
|
||||
" \"amount\": 265.0,\n" +
|
||||
" \"discount\": 1.2,\n" +
|
||||
" \"balance\": {\n" +
|
||||
" \"accountTypeId\": 2.0,\n" +
|
||||
" \"name\": \"Utility ZWG\",\n" +
|
||||
" \"balance\": 3306.32\n" +
|
||||
" },\n" +
|
||||
" \"message\": \"Transaction processed successfully\",\n" +
|
||||
" \"rechargeData\": {\n" +
|
||||
" \"Network\": \"ZESA\",\n" +
|
||||
" \"Target\": \"07088597534\",\n" +
|
||||
" \"Cost\": \"261.8200\",\n" +
|
||||
" \"Token\": \"6616 0517 2063 6675 8254\",\n" +
|
||||
" \"Units\": \"40.92\",\n" +
|
||||
" \"NetAmount\": \"250\",\n" +
|
||||
" \"TaxAmount\": \"0\",\n" +
|
||||
" \"Levy\": \"15\",\n" +
|
||||
" \"Arrears\": \"0\"\n" +
|
||||
" }\n" +
|
||||
"}", LinkedHashMap.class);
|
||||
}
|
||||
|
||||
try {
|
||||
String token = hotRechargeTokenService.getToken();
|
||||
|
||||
if (token == null || token.isEmpty()) {
|
||||
throw new Exception("Failed to authenticate with HotRecharge");
|
||||
}
|
||||
String rechargeUrl = hotApiUrl + "/products/recharge";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(token);
|
||||
|
||||
Reference in New Issue
Block a user