diff --git a/data/jobrunr.mv.db b/data/jobrunr.mv.db index dc305ea..fa54cde 100644 Binary files a/data/jobrunr.mv.db and b/data/jobrunr.mv.db differ diff --git a/data/jobrunr.trace.db b/data/jobrunr.trace.db new file mode 100644 index 0000000..9c3c351 --- /dev/null +++ b/data/jobrunr.trace.db @@ -0,0 +1,64 @@ +2026-06-11 11:30:43.191867+02:00 database: flush +org.h2.message.DbException: General error: "org.h2.mvstore.MVStoreException: The file is locked: /Users/khoza/dev/qantra/qpay-transaction-manager-spring-boot/data/jobrunr.mv.db [2.2.224/7]" [50000-224] + at org.h2.message.DbException.get(DbException.java:212) + at org.h2.message.DbException.convert(DbException.java:407) + at org.h2.mvstore.db.Store.lambda$new$0(Store.java:122) + at org.h2.mvstore.MVStore.handleException(MVStore.java:1546) + at org.h2.mvstore.MVStore.panic(MVStore.java:371) + at org.h2.mvstore.MVStore.(MVStore.java:291) + at org.h2.mvstore.MVStore$Builder.open(MVStore.java:2035) + at org.h2.mvstore.db.Store.(Store.java:133) + at org.h2.engine.Database.(Database.java:326) + at org.h2.engine.Engine.openSession(Engine.java:92) + at org.h2.engine.Engine.openSession(Engine.java:222) + at org.h2.engine.Engine.createSession(Engine.java:201) + at org.h2.engine.SessionRemote.connectEmbeddedOrServer(SessionRemote.java:343) + at org.h2.jdbc.JdbcConnection.(JdbcConnection.java:125) + at org.h2.Driver.connect(Driver.java:59) + at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:137) + at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:360) + at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:202) + at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:461) + at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:550) + at com.zaxxer.hikari.pool.HikariPool.(HikariPool.java:98) + at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:111) + at org.jobrunr.storage.sql.common.SqlStorageProviderFactory.getStorageProviderUsingDataSource(SqlStorageProviderFactory.java:40) + at org.jobrunr.storage.sql.common.SqlStorageProviderFactory.using(SqlStorageProviderFactory.java:36) + at org.jobrunr.storage.sql.common.SqlStorageProviderFactory.using(SqlStorageProviderFactory.java:31) + at org.jobrunr.spring.autoconfigure.storage.JobRunrSqlStorageAutoConfiguration.sqlStorageProvider(JobRunrSqlStorageAutoConfiguration.java:33) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) + at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.base/java.lang.reflect.Method.invoke(Method.java:569) + at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:146) + at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:644) + at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:636) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1355) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1185) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) + at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) + at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:971) + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:625) + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) + at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363) + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352) + at zw.qantra.tm.TmApplication.main(TmApplication.java:20) +Caused by: org.h2.jdbc.JdbcSQLNonTransientException: General error: "org.h2.mvstore.MVStoreException: The file is locked: /Users/khoza/dev/qantra/qpay-transaction-manager-spring-boot/data/jobrunr.mv.db [2.2.224/7]" [50000-224] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:566) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + ... 51 more +Caused by: org.h2.mvstore.MVStoreException: The file is locked: /Users/khoza/dev/qantra/qpay-transaction-manager-spring-boot/data/jobrunr.mv.db [2.2.224/7] + at org.h2.mvstore.DataUtils.newMVStoreException(DataUtils.java:996) + at org.h2.mvstore.SingleFileStore.lockFileChannel(SingleFileStore.java:143) + at org.h2.mvstore.SingleFileStore.open(SingleFileStore.java:117) + at org.h2.mvstore.SingleFileStore.open(SingleFileStore.java:81) + at org.h2.mvstore.MVStore.(MVStore.java:286) + ... 45 more diff --git a/src/main/java/zw/qantra/tm/domain/controllers/AccountController.java b/src/main/java/zw/qantra/tm/domain/controllers/AccountController.java new file mode 100644 index 0000000..e1a3591 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/controllers/AccountController.java @@ -0,0 +1,47 @@ +package zw.qantra.tm.domain.controllers; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import zw.qantra.tm.domain.dtos.erp.VelocityAccountDto; +import zw.qantra.tm.domain.dtos.erp.VelocityStatementDto; +import zw.qantra.tm.domain.services.VelocityAccountService; + +import java.util.List; + +@RestController +@RequestMapping("/accounts") +@RequiredArgsConstructor +public class AccountController { + + private final VelocityAccountService velocityAccountService; + private static final List SUPPORTED_CURRENCIES = List.of("USD", "ZWG"); + + @GetMapping("/phone/{currencyPhoneConcat}") + @PreAuthorize("hasRole('ACCOUNT_READ')") + public ResponseEntity getAccountByPhone( + @PathVariable String currencyPhoneConcat) { + try { + VelocityAccountDto account = velocityAccountService.getAccountByPhone(currencyPhoneConcat); + return ResponseEntity.ok(account); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + @GetMapping("/phone/{currencyPhoneConcat}/statement") + @PreAuthorize("hasRole('ACCOUNT_READ')") + public ResponseEntity getStatementByPhone( + @PathVariable String currencyPhoneConcat, + @RequestParam String startDate, + @RequestParam String endDate) { + try { + VelocityStatementDto statement = velocityAccountService.getStatementByPhone( + currencyPhoneConcat, startDate, endDate); + return ResponseEntity.ok(statement); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java b/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java index 8316df5..f585c9c 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java @@ -27,6 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import zw.qantra.tm.domain.services.processors.payments.CityWalletRemotePaymentProcessor; import zw.qantra.tm.domain.services.processors.workflows.TransactionWorkflow; import zw.qantra.tm.domain.services.processors.workflows.WorkflowUtils; import zw.qantra.tm.utils.LogUtils; @@ -48,6 +49,7 @@ public class TransactonController { private final WorkflowInstanceFactory workflowInstanceFactory; private final WorkflowUtils workflowUtils; private final ObjectStringMapper objectStringMapper; + private final CityWalletRemotePaymentProcessor cityWalletRemotePaymentProcessor; @GetMapping public ResponseEntity> getAll( @@ -206,6 +208,16 @@ public class TransactonController { return ResponseEntity.ok(response); } + @PutMapping("/{id}/velocity-otp") + public ResponseEntity updateOtp(@PathVariable UUID id, @RequestBody Transaction transaction) { + Transaction fetchTransaction = transactionService.findById(id); + if (fetchTransaction == null) { + return ResponseEntity.notFound().build(); + } + fetchTransaction.setVerificationCode(transaction.getVerificationCode()); + return ResponseEntity.ok(cityWalletRemotePaymentProcessor.updateVelocityTransactionOtp(fetchTransaction)); + } + @PutMapping("/{id}") public ResponseEntity update(@PathVariable UUID id, @RequestBody Transaction transaction) { if (!transactionService.getTransactionRepository().existsById(id)) { diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityAccountDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityAccountDto.java new file mode 100644 index 0000000..3157680 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityAccountDto.java @@ -0,0 +1,33 @@ +package zw.qantra.tm.domain.dtos.erp; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.sql.Timestamp; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class VelocityAccountDto { + private String name; + private String description; + private String accountNumber; + private String alternateAccountNumber; + private String parentAccountId; + private String type; + private String category; + private String ledger; + private Timestamp deactivatedAt; + private BigDecimal balance; + private String partyType; + private String party; + private String currency; + private String customerStringId; + private String companyStringId; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityStatementDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityStatementDto.java new file mode 100644 index 0000000..63364d7 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityStatementDto.java @@ -0,0 +1,51 @@ +package zw.qantra.tm.domain.dtos.erp; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class VelocityStatementDto { + private BigDecimal startBalance; + private BigDecimal endBalance; + private List transactions; + private String accountName; + private String accountNumber; + private String startDate; + private String endDate; + + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public static class StatementItem { + private String name; + private String narration; + private String party; + private String company; + private String notes; + private Timestamp postingDate; + private BigDecimal amount; + private String account; + private String accountName; + private String currency; + private String reference; + private TransactionSide type; + private BigDecimal balance; + } + + public enum TransactionSide { + DEBIT, CREDIT + } +} diff --git a/src/main/java/zw/qantra/tm/domain/models/Transaction.java b/src/main/java/zw/qantra/tm/domain/models/Transaction.java index d655e3a..18e1aa0 100644 --- a/src/main/java/zw/qantra/tm/domain/models/Transaction.java +++ b/src/main/java/zw/qantra/tm/domain/models/Transaction.java @@ -1,5 +1,6 @@ package zw.qantra.tm.domain.models; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import jakarta.persistence.*; import lombok.*; @@ -17,6 +18,7 @@ import java.util.UUID; @AllArgsConstructor @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) public class Transaction extends BaseEntity { private String userId; private String trace; @@ -81,6 +83,7 @@ public class Transaction extends BaseEntity { private String orderId; private String orderTrace; private String orderTransactionTrace; + private String orderTransactionId; private String erpSalesRef; private String erpJournalRef; @@ -107,6 +110,8 @@ public class Transaction extends BaseEntity { @Column(columnDefinition = "TEXT") private String targetUrl; private String sdkActionId; + private String verificationCode; + private Integer retries; @Transient private Object additionalData; diff --git a/src/main/java/zw/qantra/tm/domain/services/JobService.java b/src/main/java/zw/qantra/tm/domain/services/JobService.java index a8c1380..c770c91 100644 --- a/src/main/java/zw/qantra/tm/domain/services/JobService.java +++ b/src/main/java/zw/qantra/tm/domain/services/JobService.java @@ -30,6 +30,7 @@ public class JobService { private final WorkflowInstanceService workflowInstanceService; private final WorkflowUtils workflowUtils; private final TransactionService transactionService; + private final SettingService settingService; public void checkOrphanedTransactions() { List transactions = transactionService.getTransactionRepository() @@ -42,6 +43,13 @@ public class JobService { for (Transaction transaction: transactions) { attempts++; try { + int maxRetries = settingService.getIntegerSetting("MAX_RETRIES"); + int retries = transaction.getRetries() == null ? 0 : transaction.getRetries(); + + if (retries >= maxRetries) + break; + + transaction.setRetries(retries + 1); // polling logic WorkflowInstance update = new WorkflowInstance.Builder() .setId(transaction.getWorkflowId()) diff --git a/src/main/java/zw/qantra/tm/domain/services/ProviderService.java b/src/main/java/zw/qantra/tm/domain/services/ProviderService.java index 4e3dcbf..31b55e3 100644 --- a/src/main/java/zw/qantra/tm/domain/services/ProviderService.java +++ b/src/main/java/zw/qantra/tm/domain/services/ProviderService.java @@ -128,7 +128,7 @@ public class ProviderService { return products; } catch (IllegalArgumentException e) { - log.error("Error fetching products: " + providerUid, e); + log.error("Error fetching products: {}", providerUid); return Collections.emptyList(); } } diff --git a/src/main/java/zw/qantra/tm/domain/services/SettingService.java b/src/main/java/zw/qantra/tm/domain/services/SettingService.java index b4f4107..5292c6a 100644 --- a/src/main/java/zw/qantra/tm/domain/services/SettingService.java +++ b/src/main/java/zw/qantra/tm/domain/services/SettingService.java @@ -14,6 +14,19 @@ public class SettingService { return settingRepository; } + public Integer getIntegerSetting(String key) { + try { + Setting setting = settingRepository.findBySettingName(key); + if (setting != null) { + return Integer.parseInt(setting.getSettingValue()); + } + return 0; + } catch (Exception exception) { + exception.printStackTrace(); + return 0; + } + } + public Boolean getBooleanSetting(String key) { try { Setting setting = settingRepository.findBySettingName(key); diff --git a/src/main/java/zw/qantra/tm/domain/services/VelocityAccountService.java b/src/main/java/zw/qantra/tm/domain/services/VelocityAccountService.java new file mode 100644 index 0000000..1db6290 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/VelocityAccountService.java @@ -0,0 +1,77 @@ +package zw.qantra.tm.domain.services; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.dtos.erp.VelocityAccountDto; +import zw.qantra.tm.domain.dtos.erp.VelocityStatementDto; +import zw.qantra.tm.utils.Utils; + +import java.util.LinkedHashMap; + +@Service +@Slf4j +@RequiredArgsConstructor +public class VelocityAccountService { + + private final VelocityPaymentService velocityPaymentService; + + private static final String VELOCITY_API_ACCOUNT_BY_PHONE_PATH = "/accounts/phone/{currencyPhoneConcat}"; + private static final String VELOCITY_API_STATEMENT_BY_PHONE_PATH = "/statements/phone/{currencyPhoneConcat}"; + + /** + * Fetches an account by the currency-phone concatenation. + * + * @param currencyPhoneConcat The concatenation of currency and phone (e.g. "USD+263771234567"). + * @return VelocityAccountDto with account details. + */ + public VelocityAccountDto getAccountByPhone(String currencyPhoneConcat) { + try { + LinkedHashMap responseMap = velocityPaymentService.makeRequest( + HttpMethod.GET, + VELOCITY_API_ACCOUNT_BY_PHONE_PATH.replace("{currencyPhoneConcat}", currencyPhoneConcat), + null + ); + if (responseMap == null || responseMap.isEmpty()) { + log.error("Error fetching account"); + throw new IllegalStateException("Error fetching account"); + } + return Utils.deepCopy(responseMap, VelocityAccountDto.class); + } catch (Exception e) { + log.error("Error fetching account by phone {}: {}", currencyPhoneConcat, e.getMessage(), e); + throw new IllegalStateException("Error fetching account: " + e.getMessage(), e); + } + } + + /** + * Fetches an account statement by the currency-phone concatenation and date range. + * + * @param currencyPhoneConcat The concatenation of currency and phone (e.g. "USD+263771234567"). + * @param startDate The start date for the statement (e.g. "2025-01-01"). + * @param endDate The end date for the statement (e.g. "2025-12-31"). + * @return VelocityStatementDto with statement details. + */ + public VelocityStatementDto getStatementByPhone(String currencyPhoneConcat, String startDate, String endDate) { + try { + String path = VELOCITY_API_STATEMENT_BY_PHONE_PATH + .replace("{currencyPhoneConcat}", currencyPhoneConcat) + + "?startDate=" + startDate + "&endDate=" + endDate; + + LinkedHashMap responseMap = velocityPaymentService.makeRequest( + HttpMethod.GET, + path, + null + ); + if (responseMap == null || responseMap.isEmpty()) { + log.error("Error fetching statement"); + throw new IllegalStateException("Error fetching statement"); + } + return Utils.deepCopy(responseMap, VelocityStatementDto.class); + } catch (Exception e) { + log.error("Error fetching statement for phone {} ({} - {}): {}", + currencyPhoneConcat, startDate, endDate, e.getMessage(), e); + throw new IllegalStateException("Error fetching statement: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/VelocityPaymentService.java b/src/main/java/zw/qantra/tm/domain/services/VelocityPaymentService.java index 130fcde..3d361d6 100644 --- a/src/main/java/zw/qantra/tm/domain/services/VelocityPaymentService.java +++ b/src/main/java/zw/qantra/tm/domain/services/VelocityPaymentService.java @@ -44,6 +44,7 @@ public class VelocityPaymentService { private static final String VELOCITY_API_POLL_PAYMENT_PATH = "/transactions/poll/{traceId}"; private static final String VELOCITY_API_UPDATE_WORKFLOW_PATH = "/sales-orders/update-workflow/{traceId}"; private static final String VELOCITY_API_CUSTOMERS = "/customers"; + private static final String VELOCITY_API_UPDATE_TRANSACTION_PATH = "/transactions/{id}"; public VelocityCustomerResponseDto createCustomer(RegisterRequest registerRequest) { VelocityCustomerDto payload = VelocityCustomerDto.builder() @@ -170,14 +171,22 @@ public class VelocityPaymentService { LinkedHashMap response = makeRequest( HttpMethod.POST, VELOCITY_API_INITIATE_PAYMENT_PATH, payload); LinkedHashMap body = (LinkedHashMap) response.get("body"); + + if(body == null){ + String errorMessage = (String) response.get("errorMessage"); + log.error("Error initiating payment: {}", errorMessage); + throw new IllegalStateException("Error initiating payment: " + errorMessage); + } String transactionStatus = (String) body.get("status"); String tranTrace = (String) body.get("trace"); + String tranId = (String) body.get("id"); String transactionName = (String) body.get("name"); String redirectUrl = body.containsKey("redirectUrl") ? (String) body.get("redirectUrl") : null; if ("SUCCESS".equalsIgnoreCase(transactionStatus)) { transaction.setErpSalesPaymentRef(transactionName); transaction.setOrderTransactionTrace(tranTrace); + transaction.setOrderTransactionId(tranId); transaction.setTargetUrl(redirectUrl); return transaction; } @@ -193,6 +202,14 @@ public class VelocityPaymentService { } public Transaction pollPaymentStatus(Transaction transaction) { + int maxRetries = settingService.getIntegerSetting("MAX_RETRIES"); + int retries = transaction.getRetries() == null ? 0 : transaction.getRetries(); + + if (retries >= maxRetries) + return transaction; + + transaction.setRetries(retries + 1); + LinkedHashMap response = makeRequest( HttpMethod.PUT, VELOCITY_API_POLL_PAYMENT_PATH.replace("{traceId}", transaction.getOrderTransactionTrace()), @@ -200,14 +217,44 @@ public class VelocityPaymentService { ); LinkedHashMap body = (LinkedHashMap) response.get("body"); + if(body == null){ + String errorMessage = (String) response.get("errorMessage"); + log.error("Error polling payment: {}", errorMessage); + throw new IllegalStateException("Error initiating payment: " + errorMessage); + } + transaction.setStatus(Status.valueOf((String) body.get("status"))); transaction.setPaymentStatus(Status.valueOf((String) body.get("paymentStatus"))); transaction.setPollingStatus(Status.valueOf((String) body.get("pollStatus"))); transaction.setDebitRef((String) body.get("debitRef")); transaction.setPaymentProcessorRef((String) body.get("paymentProcessorRef")); + transaction.setResponseCode((String) body.get("responseCode")); + + if (!transaction.getStatus().equals(Status.SUCCESS)) { + transaction.setErrorMessage((String) body.get("errorMessage")); + } return transaction; } + /** + * Updates the OTP (additional data) on a transaction requiring manual verification. + * Equivalent to the Flutter/Dart {@code updateTransaction} method. + * + * @param id The transaction ID to update. + * @param otp The OTP value to set as additional data. + * @return A {@code LinkedHashMap} containing the Velocity API response. + */ + public LinkedHashMap updateTransaction(String id, String otp) { + Map payload = new HashMap<>(); + payload.put("additionalData", otp); + + return makeRequest( + HttpMethod.PUT, + VELOCITY_API_UPDATE_TRANSACTION_PATH.replace("{id}", id), + payload + ); + } + @Async public void updateSalesOrderWorkflow(Transaction transaction) { LinkedHashMap response = makeRequest( @@ -226,7 +273,7 @@ public class VelocityPaymentService { } - private LinkedHashMap makeRequest(HttpMethod method, String path, Object payload) { + LinkedHashMap makeRequest(HttpMethod method, String path, Object payload) { long startNs = System.nanoTime(); ResponseEntity response; try { diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/payments/CityWalletRemotePaymentProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/payments/CityWalletRemotePaymentProcessor.java new file mode 100644 index 0000000..2fc3de9 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/processors/payments/CityWalletRemotePaymentProcessor.java @@ -0,0 +1,102 @@ +package zw.qantra.tm.domain.services.processors.payments; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.RandomStringUtils; +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.enums.Status; +import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.services.SettingService; +import zw.qantra.tm.domain.services.TransactionService; +import zw.qantra.tm.domain.services.VelocityPaymentService; +import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; +import zw.qantra.tm.rest.RestService; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +@Service("REQUEST_WALLET_REMOTE") +@RequiredArgsConstructor +public class CityWalletRemotePaymentProcessor implements TransactionProcessorInterface { + private final Logger logger = LoggerFactory.getLogger(CityWalletRemotePaymentProcessor.class); + private final TransactionService transactionService; + private final SettingService settingService; + private final VelocityPaymentService velocityPaymentService; + + @Override + public Object process(Transaction transaction) throws Exception { + logger.info("processing transaction"); + + if(settingService.getBooleanSetting("SIMULATE_WALLET_SUCCESS")) { + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("02"); + transaction.setPaymentProcessorRef(RandomStringUtils.randomNumeric(10)); + transaction.setDebitRef(RandomStringUtils.randomNumeric(10)); + return transaction; + } + + try { + transaction = velocityPaymentService.processPayment(transaction); + } catch (Exception e) { + e.printStackTrace(); + logger.error("Network error during payment processing: {}", e.getMessage(), e); + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("99"); + transaction.setSystemErrorMessage("Network error: " + e.getMessage()); + transaction.setErrorMessage("Payment failed. Please try again."); + } + + return transactionService.save(transaction); + } + + @Override + public Object reverse(Transaction transaction) { + return transaction; + } + + @Override + public Object poll(Transaction transaction) throws Exception { + logger.info("polling transaction"); + + if(settingService.getBooleanSetting("SIMULATE_WALLET_SUCCESS")) { + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("02"); + transaction.setTargetUrl("/home"); + return transaction; + } + + try { + transaction = velocityPaymentService.pollPaymentStatus(transaction); + + } catch (Exception e) { + e.printStackTrace(); + logger.error("Network error during payment processing: {}", e.getMessage(), e); + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("99"); + transaction.setSystemErrorMessage("Network error: " + e.getMessage()); + transaction.setErrorMessage("Payment failed. Please try again."); + } + + return transactionService.save(transaction); + } + + public Transaction updateVelocityTransactionOtp(Transaction transaction) { + String code = transaction.getVerificationCode(); + + try { + velocityPaymentService.updateTransaction(transaction.getOrderTransactionId(), code); + } catch (Exception e) { + e.printStackTrace(); + logger.error("Network error during payment update: {}", e.getMessage(), e); + transaction.setStatus(Status.FAILED); + transaction.setResponseCode("99"); + transaction.setSystemErrorMessage("Network error: " + e.getMessage()); + transaction.setErrorMessage("Payment failed. Please try again."); + } + 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 a357c02..3eb4343 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 @@ -23,20 +23,10 @@ import zw.qantra.tm.rest.RestService; @RequiredArgsConstructor public class EcocashRemotePaymentProcessor implements TransactionProcessorInterface { private final Logger logger = LoggerFactory.getLogger(EcocashRemotePaymentProcessor.class); - private final RestService restService; private final TransactionService transactionService; private final SettingService settingService; private final VelocityPaymentService velocityPaymentService; - @Value("${sbz.aggregator.encryption-key}") - private String encryptionKey; - @Value("${sbz.aggregator.client-id}") - private String clientId; - @Value("${sbz.aggregator.client-secret}") - private String clientSecret; - @Value("${steward.payment.processor.url}") - private String url; - @Override public Object process(Transaction transaction) throws Exception { logger.info("processing transaction"); @@ -81,11 +71,6 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf try { transaction = velocityPaymentService.pollPaymentStatus(transaction); - transaction.setStatus(Status.SUCCESS); - transaction.setResponseCode("00"); - - transactionService.save(transaction); - } catch (Exception e) { e.printStackTrace(); logger.error("Network error during payment processing: {}", e.getMessage(), e); @@ -95,7 +80,7 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf transaction.setErrorMessage("Payment failed. Please try again."); } - return transaction; + return transactionService.save(transaction); } public String getHash(String originalString){ diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/payments/MPGSWebPaymentProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/payments/MPGSWebPaymentProcessor.java index dbba440..de86c05 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/payments/MPGSWebPaymentProcessor.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/payments/MPGSWebPaymentProcessor.java @@ -31,22 +31,10 @@ import java.util.Random; @RequiredArgsConstructor public class MPGSWebPaymentProcessor implements TransactionProcessorInterface { private final Logger logger = LoggerFactory.getLogger(MPGSWebPaymentProcessor.class); - private final RestService restService; private final TransactionService transactionService; private final SettingService settingService; private final VelocityPaymentService velocityPaymentService; - @Value("${sbz.aggregator.encryption-key}") - private String encryptionKey; - @Value("${sbz.aggregator.client-id}") - private String clientId; - @Value("${sbz.aggregator.client-secret}") - private String clientSecret; - @Value("${steward.payment.processor.url}") - private String url; - @Value("${mpgs.return-url}") - private String returnUrl; - @Override public Object process(Transaction transaction) throws Exception { logger.info("processing transaction"); @@ -88,11 +76,6 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface { try { transaction = velocityPaymentService.pollPaymentStatus(transaction); - transaction.setStatus(transaction.getStatus()); - transaction.setResponseCode("00"); - - transactionService.save(transaction); - } catch (Exception e) { e.printStackTrace(); logger.error("Network error during payment processing: {}", e.getMessage(), e); @@ -102,7 +85,7 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface { transaction.setErrorMessage("Payment failed. Please try again"); } - return transaction; + return transactionService.save(transaction); } @Override diff --git a/src/main/java/zw/qantra/tm/seeders/PaymentProcessorSeeder.java b/src/main/java/zw/qantra/tm/seeders/PaymentProcessorSeeder.java index 1a5447e..f2ba7b2 100644 --- a/src/main/java/zw/qantra/tm/seeders/PaymentProcessorSeeder.java +++ b/src/main/java/zw/qantra/tm/seeders/PaymentProcessorSeeder.java @@ -24,6 +24,16 @@ public class PaymentProcessorSeeder implements Seeder { @Override public void seed() { List processors = Arrays.asList( + PaymentProcessor.builder() + .name("CityWallet") + .label("WALLET") + .type("GATEWAY") + .image("city.png") + .accountFieldName("phoneNumber") + .accountFieldLabel("Wallet Phone Number") + .description("Pay using your CityWallet") + .authType("REMOTE") + .build(), PaymentProcessor.builder() .name("Ecocash") .label("ECOCASH") diff --git a/src/main/resources/application-lab.properties b/src/main/resources/application-lab.properties index bb7bb51..975ef4c 100644 --- a/src/main/resources/application-lab.properties +++ b/src/main/resources/application-lab.properties @@ -40,8 +40,8 @@ ecocash.url=https://developers.ecocash.co.zw/api/ecocash_pay/api/v2/payment/inst #velocity.api.base-url=https://api.velocityafrica.net #velocity.api.api-key=z5WdSlItIhYL6nd7rxzf-jFdKN3jJyL1LubhDFBvXmc -#velocity.api.base-url=http://localhost:9005 -#velocity.api.api-key=gH13nirZdUQdqC9impgzTzqEqv-jdbLOTx_HD5fQr_A +velocity.api.base-url=http://localhost:6975 +velocity.api.api-key=gH13nirZdUQdqC9impgzTzqEqv-jdbLOTx_HD5fQr_A velocity.api.cancel-url=http://localhost:9005/poll velocity.api.success-url=http://localhost:9005/poll diff --git a/src/main/resources/liquibase-diff-changeLog.xml b/src/main/resources/liquibase-diff-changeLog.xml index 22c9a79..c9f61e7 100644 --- a/src/main/resources/liquibase-diff-changeLog.xml +++ b/src/main/resources/liquibase-diff-changeLog.xml @@ -1,99 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - -