diff --git a/src/main/java/zw/qantra/tm/TmApplication.java b/src/main/java/zw/qantra/tm/TmApplication.java index c60ab3c..08381c4 100644 --- a/src/main/java/zw/qantra/tm/TmApplication.java +++ b/src/main/java/zw/qantra/tm/TmApplication.java @@ -8,10 +8,12 @@ import org.springframework.context.annotation.Bean; import com.google.gson.Gson; import org.springframework.context.annotation.Import; +import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication @Import(RestConfiguration.class) @EnableCaching +@EnableAsync public class TmApplication { public static void main(String[] args) { diff --git a/src/main/java/zw/qantra/tm/configs/CacheConfig.java b/src/main/java/zw/qantra/tm/configs/CacheConfig.java index 9358261..ff23988 100644 --- a/src/main/java/zw/qantra/tm/configs/CacheConfig.java +++ b/src/main/java/zw/qantra/tm/configs/CacheConfig.java @@ -15,7 +15,7 @@ public class CacheConfig { @Bean public CacheManager cacheManager() { - CaffeineCacheManager cacheManager = new CaffeineCacheManager("providerProducts"); + CaffeineCacheManager cacheManager = new CaffeineCacheManager("providerProducts", "hotrechargeBalance"); cacheManager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(24, TimeUnit.HOURS) .maximumSize(10000)); diff --git a/src/main/java/zw/qantra/tm/configs/VelocityApiProperties.java b/src/main/java/zw/qantra/tm/configs/VelocityApiProperties.java new file mode 100644 index 0000000..3132f62 --- /dev/null +++ b/src/main/java/zw/qantra/tm/configs/VelocityApiProperties.java @@ -0,0 +1,21 @@ +package zw.qantra.tm.configs; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "velocity.api") +public class VelocityApiProperties { + private String baseUrl; + private String apiKey; + private String cancelUrl; + private String successUrl; + private String creditPhone; + private String creditAccount; + private String defaultAuthType; + private long pollIntervalMs; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/controllers/ConfigController.java b/src/main/java/zw/qantra/tm/domain/controllers/ConfigController.java index b45b8d0..45d55c0 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/ConfigController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/ConfigController.java @@ -1,20 +1,12 @@ package zw.qantra.tm.domain.controllers; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.cache.CacheManager; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import zw.qantra.tm.domain.enums.ChargeConditionType; -import zw.qantra.tm.domain.enums.ChargeLabelEnum; -import zw.qantra.tm.domain.enums.CurrencyType; -import zw.qantra.tm.domain.models.Category; -import zw.qantra.tm.domain.models.Charge; -import zw.qantra.tm.domain.models.ChargeCondition; -import zw.qantra.tm.domain.models.IntegrationProcessor; -import zw.qantra.tm.domain.models.PaymentProcessor; -import zw.qantra.tm.domain.models.Provider; import zw.qantra.tm.domain.services.ChargeConditionService; import zw.qantra.tm.domain.services.ChargeService; import zw.qantra.tm.domain.services.IntegrationProcessorService; @@ -22,11 +14,7 @@ import zw.qantra.tm.domain.services.CategoryService; import zw.qantra.tm.domain.services.PaymentProcessorService; import zw.qantra.tm.domain.services.ProviderService; -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; - +@Slf4j @RestController @RequestMapping("/configs") @RequiredArgsConstructor @@ -45,333 +33,4 @@ public class ConfigController { return ResponseEntity.ok("Cache cleared"); } - @GetMapping("/seed/live") - public ResponseEntity seedLiveConfig(){ - - // clear existing payment processors before seeding - paymentProcessorService.getPaymentProcessorRepository().deleteAll(); - paymentProcessorService.getPaymentProcessorRepository().saveAll(Arrays.asList( - PaymentProcessor.builder() - .name("Ecocash") - .label("ECOCASH") - .type("GATEWAY") - .image("ecocash.png") - .accountFieldName("phoneNumber") - .accountFieldLabel("EcoCash Phone Number") - .description("Pay using your mobile wallet") - .authType("REMOTE") - .build(), - PaymentProcessor.builder() - .name("Visa/MasterCard") - .label("MPGS") - .type("GATEWAY") - .image("visa-mastercard.png") - .accountFieldName("cardNumber") - .accountFieldLabel("Card Number") - .description("Pay using your international card") - .authType("WEB") - .build() - )); - - - - integrationProcessorService.getIntegrationProcessorRepository().deleteAll(); - integrationProcessorService.getIntegrationProcessorRepository().saveAll(Arrays.asList( - IntegrationProcessor.builder() - .name("STEWARD") - .label("STEWARD") - .description("Steward Integration Processor") - .build() - )); - - // clear existing categories before seeding - categoryService.getCategoryRepository().deleteAll(); - // seed categories - List categories = Arrays.asList( - Category.builder() - .name("Airtime") - .description("Airtime for local mobile networks") - .label("AIRTIME") - .build(), - Category.builder() - .name("Utilities") - .description("Council bills & rates") - .label("UTILITIES") - .build() - ); - categoryService.getCategoryRepository().saveAll(categories); - - // clear existing providers before seeding - providerService.getProviderRepository().deleteAll(); - - // seed providers - List providers = Arrays.asList( - Provider.builder() - .description("Econet Airtime") - .clientId("econet_airtime") - .label("ECONET") - .category("AIRTIME") - .image("econet.png") - .externalId("78f1f497-c9eb-401c-b22c-884756e68e40") - .integrationProcessorLabel("STEWARD") - .accountFieldName("Phone Number") - .percentageCommission(6) - .build(), - Provider.builder() - .description("NetOne Airtime") - .clientId("netone_usd_airtime") - .label("NETONE") - .category("AIRTIME") - .image("netone.png") - .externalId("95d485a7-39c9-4559-8a27-6521969abe8f") - .integrationProcessorLabel("STEWARD") - .accountFieldName("Phone Number") - .percentageCommission(5.5) - .build(), - Provider.builder() - .description("Prepaid Zesa") - .clientId("zesa_prepaid_usd") - .label("ZESA") - .category("UTILITIES") - .image("zesa.png") - .externalId("0f67f7cc-b62b-4fb5-915f-6740b2afdba6") - .integrationProcessorLabel("STEWARD") - .accountFieldName("Meter Number") - .percentageCommission(1.5) - .build(), - Provider.builder() - .description("Econet Broadband") - .clientId("econet_broadband_usd") - .label("ECONET_BROADBAND") - .category("AIRTIME") - .image("econet.png") - .externalId("287863e2-a791-4106-b629-79dadc9a6779") - .integrationProcessorLabel("STEWARD") - .accountFieldName("Phone Number") - .percentageCommission(6) - .build() - ); - providerService.getProviderRepository().saveAll(providers); - - // Clear existing charges and charge conditions before seeding - List chargeConditions = Arrays.asList( - ChargeCondition.builder() - .name("Ecocash Gateway") - .type(ChargeConditionType.GATEWAY) - .codes("ECOCASH") - .build(), - ChargeCondition.builder() - .name("Steward Gateway") - .type(ChargeConditionType.GATEWAY) - .codes("STEWARD") - .build(), - ChargeCondition.builder() - .name("Mastercard Gateway") - .type(ChargeConditionType.GATEWAY) - .codes("MPGS") - .build() - ); - chargeService.getChargeRepository().deleteAll(); - chargeConditionService.getChargeConditionRepository().deleteAll(); - chargeConditionService.getChargeConditionRepository().saveAll(chargeConditions); - - List charges = Arrays.asList( - Charge.builder() - .description("Gateway fee") - .chargeLabel(ChargeLabelEnum.GATEWAY_FEE) - .currency(CurrencyType.USD) - .percentageRate(new BigDecimal("1")) - .includes(new HashSet<>() {{ - add(chargeConditions.get(0)); - }}) - .build(), - Charge.builder() - .description("Gateway fee") - .chargeLabel(ChargeLabelEnum.GATEWAY_FEE) - .currency(CurrencyType.USD) - .percentageRate(new BigDecimal("2")) - .min(new BigDecimal("0.55")) - .includes(new HashSet<>() {{ - add(chargeConditions.get(2)); - }}) - .build() -// Charge.builder() -// .description("Tax") -// .chargeLabel(ChargeLabelEnum.TAX) -// .currency(CurrencyType.USD) -// .percentageRate(new BigDecimal("2")) -// .max(new BigDecimal("10150")) -// .minimumAmount(new BigDecimal("5")) -// .maximumAmount(new BigDecimal("500000")) -// .includes(new HashSet<>() {{ -// add(chargeConditions.get(0)); -// add(chargeConditions.get(1)); -// add(chargeConditions.get(2)); -// }}) -// .build() - ); - - chargeService.getChargeRepository().saveAll(charges); - - return ResponseEntity.ok("Project configuration seeding complete"); - } - - @GetMapping("/seed") - public ResponseEntity seedConfig(){ - - // clear existing payment processors before seeding - paymentProcessorService.getPaymentProcessorRepository().deleteAll(); - paymentProcessorService.getPaymentProcessorRepository().saveAll(Arrays.asList( - PaymentProcessor.builder() - .name("Ecocash") - .label("ECOCASH") - .type("GATEWAY") - .image("ecocash.png") - .accountFieldName("phoneNumber") - .accountFieldLabel("EcoCash Phone Number") - .description("Pay using your mobile wallet") - .authType("REMOTE") - .build(), - PaymentProcessor.builder() - .name("Visa/MasterCard") - .label("MPGS") - .type("GATEWAY") - .image("visa-mastercard.png") - .accountFieldName("cardNumber") - .accountFieldLabel("Card Number") - .description("Pay using your international card") - .authType("WEB") - .build() - )); - - - - integrationProcessorService.getIntegrationProcessorRepository().deleteAll(); - integrationProcessorService.getIntegrationProcessorRepository().saveAll(Arrays.asList( - IntegrationProcessor.builder() - .name("STEWARD") - .label("STEWARD") - .description("Steward Integration Processor") - .build() - )); - - // clear existing categories before seeding - categoryService.getCategoryRepository().deleteAll(); - // seed categories - List categories = Arrays.asList( - Category.builder() - .name("Airtime") - .description("Airtime for local mobile networks") - .label("AIRTIME") - .build(), - Category.builder() - .name("Utilities") - .description("Council bills & rates") - .label("UTILITIES") - .build() - ); - categoryService.getCategoryRepository().saveAll(categories); - - // clear existing providers before seeding - providerService.getProviderRepository().deleteAll(); - - // seed providers - List providers = Arrays.asList( - Provider.builder() - .description("Econet Airtime") - .clientId("econet_airtime") - .label("ECONET") - .category("AIRTIME") - .image("econet.png") - .externalId("78f1f497-c9eb-401c-b22c-884756e68e40") - .integrationProcessorLabel("STEWARD") - .accountFieldName("Phone Number") - .build(), - Provider.builder() - .description("NetOne Airtime") - .clientId("netone_usd_airtime") - .label("NETONE") - .category("AIRTIME") - .image("netone.png") - .externalId("95d485a7-39c9-4559-8a27-6521969abe8f") - .integrationProcessorLabel("STEWARD") - .accountFieldName("Phone Number") - .build(), - Provider.builder() - .description("Prepaid Zesa") - .clientId("powertel_zesa") - .label("ZESA") - .category("UTILITIES") - .image("zesa.png") - .externalId("0f67f7cc-b62b-4fb5-915f-6740b2afdba6") - .integrationProcessorLabel("STEWARD") - .accountFieldName("Meter Number") - .build(), - Provider.builder() - .description("Econet Broadband") - .clientId("econet_broadband_usd") - .label("ECONET_BROADBAND") - .category("AIRTIME") - .image("econet.png") - .externalId("287863e2-a791-4106-b629-79dadc9a6779") - .integrationProcessorLabel("STEWARD") - .accountFieldName("Phone Number") - .build() - ); - providerService.getProviderRepository().saveAll(providers); - - // Clear existing charges and charge conditions before seeding - List chargeConditions = Arrays.asList( - ChargeCondition.builder() - .name("Ecocash Gateway") - .type(ChargeConditionType.GATEWAY) - .codes("ECOCASH") - .build(), - ChargeCondition.builder() - .name("Steward Gateway") - .type(ChargeConditionType.GATEWAY) - .codes("STEWARD") - .build(), - ChargeCondition.builder() - .name("Mastercard Gateway") - .type(ChargeConditionType.GATEWAY) - .codes("MPGS") - .build() - ); - chargeService.getChargeRepository().deleteAll(); - chargeConditionService.getChargeConditionRepository().deleteAll(); - chargeConditionService.getChargeConditionRepository().saveAll(chargeConditions); - - List charges = Arrays.asList( - Charge.builder() - .description("Gateway fee") - .chargeLabel(ChargeLabelEnum.GATEWAY_FEE) - .currency(CurrencyType.USD) - .percentageRate(new BigDecimal("1")) - .includes(new HashSet<>() {{ - add(chargeConditions.get(0)); - add(chargeConditions.get(1)); - add(chargeConditions.get(2)); - }}) - .build(), - Charge.builder() - .description("Tax") - .chargeLabel(ChargeLabelEnum.TAX) - .currency(CurrencyType.USD) - .percentageRate(new BigDecimal("2")) - .max(new BigDecimal("10150")) - .minimumAmount(new BigDecimal("5")) - .maximumAmount(new BigDecimal("500000")) - .includes(new HashSet<>() {{ - add(chargeConditions.get(0)); - add(chargeConditions.get(1)); - add(chargeConditions.get(2)); - }}) - .build() - ); - - chargeService.getChargeRepository().saveAll(charges); - - return ResponseEntity.ok("Project configuration seeding complete"); - } -} +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/controllers/OnboardingController.java b/src/main/java/zw/qantra/tm/domain/controllers/OnboardingController.java index 268c216..7cca4d8 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/OnboardingController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/OnboardingController.java @@ -1,6 +1,5 @@ package zw.qantra.tm.domain.controllers; -import io.nflow.engine.internal.dao.WorkflowInstanceDao; import io.nflow.engine.service.WorkflowInstanceService; import io.nflow.engine.workflow.instance.QueryWorkflowInstances; import io.nflow.engine.workflow.instance.WorkflowInstance; @@ -15,8 +14,8 @@ import org.springframework.web.bind.annotation.*; import zw.qantra.tm.domain.dtos.*; import zw.qantra.tm.domain.services.OtpService; import zw.qantra.tm.domain.services.UserService; -import zw.qantra.tm.domain.workflows.RegistrationWorkflow; -import zw.qantra.tm.domain.workflows.WorkflowUtils; +import zw.qantra.tm.domain.services.processors.workflows.RegistrationWorkflow; +import zw.qantra.tm.domain.services.processors.workflows.WorkflowUtils; import zw.qantra.tm.utils.Utils; import java.util.*; 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 35347b5..144ced9 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java @@ -1,6 +1,5 @@ package zw.qantra.tm.domain.controllers; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; @@ -28,8 +27,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import zw.qantra.tm.domain.workflows.TransactionWorkflow; -import zw.qantra.tm.domain.workflows.WorkflowUtils; +import zw.qantra.tm.domain.services.processors.workflows.TransactionWorkflow; +import zw.qantra.tm.domain.services.processors.workflows.WorkflowUtils; import zw.qantra.tm.utils.LogUtils; import zw.qantra.tm.utils.Utils; @@ -189,16 +188,10 @@ public class TransactonController { throws ExecutionException, InterruptedException, NumberParseException { LogUtils.logPrettyJson(logger, "incoming transaction", transaction); - // phone must begin with '+' - // need to format the debit phone number since it's the business key - PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); - Phonenumber.PhoneNumber number = phoneUtil.parse(transaction.getDebitPhone(), ""); - String formattedPhoneNumber = phoneUtil.format(number, PhoneNumberUtil.PhoneNumberFormat.E164); - var instance = workflowInstanceFactory.newWorkflowInstanceBuilder() .setType(TransactionWorkflow.TYPE) .setExternalId(transaction.getUserId() + "-" + new Instant().getMillis()) - .setBusinessKey(formattedPhoneNumber) + .setBusinessKey(transaction.getUserId()) .putStateVariable("confirmation", Utils.toJson(transaction)) .setNextActivation(DateTime.now()) .build(); diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/CustomerDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/CustomerDto.java deleted file mode 100644 index 76446bd..0000000 --- a/src/main/java/zw/qantra/tm/domain/dtos/erp/CustomerDto.java +++ /dev/null @@ -1,38 +0,0 @@ -package zw.qantra.tm.domain.dtos.erp; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; - -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class CustomerDto { - @JsonProperty("doctype") - private String doctype; - - @JsonProperty("customer_name") - private String customerName; - - @JsonProperty("customer_group") - private String customerGroup; - - @JsonProperty("territory") - private String territory; - - @JsonProperty("customer_type") - private String customerType; - - @JsonProperty("language") - private String language; - - @JsonProperty("mobile_no") - private String mobileNo; - - @JsonProperty("email_id") - private String emailId; - - @JsonProperty("docstatus") - private Integer docstatus; -} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryAccountDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryAccountDto.java deleted file mode 100644 index 2dd9c0a..0000000 --- a/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryAccountDto.java +++ /dev/null @@ -1,27 +0,0 @@ -package zw.qantra.tm.domain.dtos.erp; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class JournalEntryAccountDto { - private String doctype; - private String account; - @JsonProperty("debit_in_account_currency") - private BigDecimal debitInAccountCurrency; - @JsonProperty("credit_in_account_currency") - private BigDecimal creditInAccountCurrency; - @JsonProperty("cost_center") - private String costCenter; - @JsonProperty("party_type") - private String partyType; - private String party; -} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryDto.java deleted file mode 100644 index 1feb682..0000000 --- a/src/main/java/zw/qantra/tm/domain/dtos/erp/JournalEntryDto.java +++ /dev/null @@ -1,22 +0,0 @@ -package zw.qantra.tm.domain.dtos.erp; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class JournalEntryDto { - private String doctype; - private String company; - @JsonProperty("posting_date") - private String postingDate; - private int docstatus; - private List accounts; -} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentEntryDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentEntryDto.java deleted file mode 100644 index df13ee3..0000000 --- a/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentEntryDto.java +++ /dev/null @@ -1,40 +0,0 @@ -package zw.qantra.tm.domain.dtos.erp; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class PaymentEntryDto { - private String doctype; - private String company; - @JsonProperty("payment_type") - private String paymentType; - @JsonProperty("party_type") - private String partyType; - private String party; - @JsonProperty("paid_from") - private String paidFrom; - @JsonProperty("paid_to") - private String paidTo; - @JsonProperty("paid_amount") - private BigDecimal paidAmount; - @JsonProperty("received_amount") - private BigDecimal receivedAmount; - @JsonProperty("posting_date") - private String postingDate; - @JsonProperty("reference_no") - private String referenceNo; - @JsonProperty("reference_date") - private String referenceDate; - private List references; - private int docstatus; -} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentReferenceDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentReferenceDto.java deleted file mode 100644 index 9ec07ff..0000000 --- a/src/main/java/zw/qantra/tm/domain/dtos/erp/PaymentReferenceDto.java +++ /dev/null @@ -1,22 +0,0 @@ -package zw.qantra.tm.domain.dtos.erp; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class PaymentReferenceDto { - @JsonProperty("reference_doctype") - private String referenceDoctype; - @JsonProperty("reference_name") - private String referenceName; - @JsonProperty("allocated_amount") - private BigDecimal allocatedAmount; -} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceDto.java deleted file mode 100644 index 9609b1b..0000000 --- a/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceDto.java +++ /dev/null @@ -1,39 +0,0 @@ -package zw.qantra.tm.domain.dtos.erp; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class PurchaseInvoiceDto { - - private String doctype; - private String supplier; - @JsonProperty("posting_date") - private String postingDate; - @JsonProperty("due_date") - private String dueDate; - private String company; - @JsonProperty("is_return") - private int isReturn; - private String currency; - @JsonProperty("conversion_rate") - private int conversionRate; - @JsonProperty("net_total") - private BigDecimal netTotal; - @JsonProperty("grand_total") - private BigDecimal grandTotal; - @JsonProperty("is_paid") - private int isPaid; - private int docstatus; - private List items; - private List taxes; -} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceItemDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceItemDto.java deleted file mode 100644 index f708df7..0000000 --- a/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseInvoiceItemDto.java +++ /dev/null @@ -1,26 +0,0 @@ -package zw.qantra.tm.domain.dtos.erp; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class PurchaseInvoiceItemDto { - - private String doctype; - @JsonProperty("item_code") - private String itemCode; - @JsonProperty("item_name") - private String itemName; - private BigDecimal qty; - private int rate; - private BigDecimal amount; - private String uom; -} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseTaxDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseTaxDto.java deleted file mode 100644 index efd2b7c..0000000 --- a/src/main/java/zw/qantra/tm/domain/dtos/erp/PurchaseTaxDto.java +++ /dev/null @@ -1,29 +0,0 @@ -package zw.qantra.tm.domain.dtos.erp; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class PurchaseTaxDto { - - private String doctype; - @JsonProperty("charge_type") - private String chargeType; - @JsonProperty("account_head") - private String accountHead; - private BigDecimal rate; - @JsonProperty("cost_center") - private String costCenter; - @JsonProperty("tax_amount") - private BigDecimal taxAmount; - private BigDecimal total; - private String description; -} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceDto.java deleted file mode 100644 index 5fe49a2..0000000 --- a/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceDto.java +++ /dev/null @@ -1,25 +0,0 @@ -package zw.qantra.tm.domain.dtos.erp; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SalesInvoiceDto { - private String doctype; - private String customer; - @JsonProperty("posting_date") - private String postingDate; - @JsonProperty("due_date") - private String dueDate; - private String company; - private int docstatus; - private List items; -} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceItemDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceItemDto.java deleted file mode 100644 index 710249f..0000000 --- a/src/main/java/zw/qantra/tm/domain/dtos/erp/SalesInvoiceItemDto.java +++ /dev/null @@ -1,21 +0,0 @@ -package zw.qantra.tm.domain.dtos.erp; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SalesInvoiceItemDto { - @JsonProperty("item_code") - private String itemCode; - private int qty; - private BigDecimal rate; - private BigDecimal amount; -} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityCustomerDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityCustomerDto.java new file mode 100644 index 0000000..c6de1bd --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityCustomerDto.java @@ -0,0 +1,18 @@ +package zw.qantra.tm.domain.dtos.erp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VelocityCustomerDto { + private String name; + private String phone; + private String email; + private String billingCurrencyString; + private String companyIdString; +} diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityCustomerResponseDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityCustomerResponseDto.java new file mode 100644 index 0000000..9264901 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityCustomerResponseDto.java @@ -0,0 +1,21 @@ +package zw.qantra.tm.domain.dtos.erp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VelocityCustomerResponseDto { + private String id; + private String createdAt; + private String name; + private String phone; + private String email; + private String status; + private String billingCurrencyString; + private String companyIdString; +} diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocitySalesOrderDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocitySalesOrderDto.java new file mode 100644 index 0000000..885a3f2 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocitySalesOrderDto.java @@ -0,0 +1,43 @@ +package zw.qantra.tm.domain.dtos.erp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VelocitySalesOrderDto { + private String currencyCodeString; + private String orderDate; + private String dueDate; + private String notes; + private Boolean authorized; + private List items; + private List charges; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ItemDto { + private String name; + private String itemCode; + private BigDecimal qty; + private BigDecimal unitPrice; + private BigDecimal amount; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ChargeDto { + private BigDecimal amount; + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocitySalesOrderResponseDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocitySalesOrderResponseDto.java new file mode 100644 index 0000000..09419fd --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocitySalesOrderResponseDto.java @@ -0,0 +1,67 @@ +package zw.qantra.tm.domain.dtos.erp; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class VelocitySalesOrderResponseDto { + private String state; + private String status; + private BodyDto body; + private String message; + private String workflowId; + private String externalId; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class BodyDto { + private String id; + private String createdAt; + private Long orderDate; + private Long dueDate; + private BigDecimal paidAmount; + private BigDecimal changeAmount; + private BigDecimal outstandingAmount; + private String trace; + private Boolean authorized; + private String name; + private BigDecimal grandTotal; + private BigDecimal taxAmount; + private BigDecimal netTotal; + private String notes; + private BigDecimal totalQty; + private String status; + private String createdBy; + private String companyIdString; + private String customerIdString; + private CurrencyDto currency; + private String currencyCodeString; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class CurrencyDto { + private String id; + private String createdAt; + private String name; + private String symbol; + private String code; + private BigDecimal rate; + private Boolean defaultCurrency; + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityTransactionDto.java b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityTransactionDto.java new file mode 100644 index 0000000..27219f8 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/erp/VelocityTransactionDto.java @@ -0,0 +1,29 @@ +package zw.qantra.tm.domain.dtos.erp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VelocityTransactionDto { + private BigDecimal amount; + private String paymentProcessorLabel; + private String debitPhone; + private String debitRegion; + private String debitCurrency; + private String debitRef; + private String creditPhone; + private String creditRegion; + private String creditAccount; + private String type; + private String authType; + private String salesOrderId; + private String cancelUrl; + private String successUrl; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/models/Charge.java b/src/main/java/zw/qantra/tm/domain/models/Charge.java index a0f30a8..a8af206 100644 --- a/src/main/java/zw/qantra/tm/domain/models/Charge.java +++ b/src/main/java/zw/qantra/tm/domain/models/Charge.java @@ -42,7 +42,7 @@ public class Charge extends BaseEntity { private TargetEvent targetEvent; // include / exclude @JsonIgnore - @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "charge_include_products", joinColumns = @JoinColumn(name = "charge_id"), @@ -51,7 +51,7 @@ public class Charge extends BaseEntity { private Set includes = new HashSet<>(); @JsonIgnore - @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "charge_exclude_products", joinColumns = @JoinColumn(name = "charge_id"), diff --git a/src/main/java/zw/qantra/tm/domain/models/ChargeCondition.java b/src/main/java/zw/qantra/tm/domain/models/ChargeCondition.java index e7d2f3b..637cc7b 100644 --- a/src/main/java/zw/qantra/tm/domain/models/ChargeCondition.java +++ b/src/main/java/zw/qantra/tm/domain/models/ChargeCondition.java @@ -12,7 +12,7 @@ import java.util.List; @Builder @AllArgsConstructor @NoArgsConstructor -public class ChargeCondition extends BaseEntity { +public class ChargeCondition extends BaseEntity { private String name; @Enumerated(EnumType.STRING) private ChargeConditionType type; 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 ee5cb3d..fa08359 100644 --- a/src/main/java/zw/qantra/tm/domain/models/Transaction.java +++ b/src/main/java/zw/qantra/tm/domain/models/Transaction.java @@ -77,6 +77,11 @@ public class Transaction extends BaseEntity { @Enumerated(EnumType.STRING) private Status pollingStatus; + + private String orderId; + private String orderTrace; + private String orderTransactionTrace; + private String erpSalesRef; private String erpJournalRef; private String erpSalesPaymentRef; diff --git a/src/main/java/zw/qantra/tm/domain/repositories/CategoryRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/CategoryRepository.java index d1f3612..0b68c2a 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/CategoryRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/CategoryRepository.java @@ -8,4 +8,5 @@ import java.util.UUID; @Repository public interface CategoryRepository extends JpaRepository { -} \ No newline at end of file + boolean existsByName(String name); +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/repositories/ChargeConditionRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/ChargeConditionRepository.java index 62e36d1..b7d91fc 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/ChargeConditionRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/ChargeConditionRepository.java @@ -4,8 +4,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import zw.qantra.tm.domain.models.ChargeCondition; +import java.util.Optional; import java.util.UUID; @Repository public interface ChargeConditionRepository extends JpaRepository { -} \ No newline at end of file + boolean existsByCodes(String codes); + Optional findByCodes(String codes); +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/repositories/ChargeRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/ChargeRepository.java index 7652933..b00b120 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/ChargeRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/ChargeRepository.java @@ -2,13 +2,17 @@ package zw.qantra.tm.domain.repositories; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import zw.qantra.tm.domain.enums.ChargeLabelEnum; import zw.qantra.tm.domain.enums.CurrencyType; import zw.qantra.tm.domain.models.Charge; +import java.math.BigDecimal; import java.util.List; import java.util.UUID; @Repository public interface ChargeRepository extends JpaRepository { List findAllByCurrency(CurrencyType currency); -} \ No newline at end of file + boolean existsByChargeLabelAndCurrency(ChargeLabelEnum chargeLabel, CurrencyType currency); + boolean existsByChargeLabelAndCurrencyAndPercentageRate(ChargeLabelEnum chargeLabel, CurrencyType currency, BigDecimal percentageRate); +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/repositories/IntegrationProcessorRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/IntegrationProcessorRepository.java index bef1e51..8c12c34 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/IntegrationProcessorRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/IntegrationProcessorRepository.java @@ -8,4 +8,5 @@ import java.util.UUID; @Repository public interface IntegrationProcessorRepository extends JpaRepository { -} \ No newline at end of file + boolean existsByName(String name); +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/repositories/PaymentProcessorRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/PaymentProcessorRepository.java index a5e9cc0..521eb50 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/PaymentProcessorRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/PaymentProcessorRepository.java @@ -8,4 +8,5 @@ import java.util.UUID; @Repository public interface PaymentProcessorRepository extends JpaRepository { -} \ No newline at end of file + boolean existsByLabel(String label); +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/repositories/ProviderRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/ProviderRepository.java index 352aad2..6f23b27 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/ProviderRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/ProviderRepository.java @@ -11,4 +11,5 @@ import java.util.UUID; public interface ProviderRepository extends JpaRepository { List findByCategory(String category); Provider findByClientId(String clientId); -} \ No newline at end of file + boolean existsByClientId(String clientId); +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/EmailService.java b/src/main/java/zw/qantra/tm/domain/services/EmailService.java index aa47ba8..e0784f3 100644 --- a/src/main/java/zw/qantra/tm/domain/services/EmailService.java +++ b/src/main/java/zw/qantra/tm/domain/services/EmailService.java @@ -3,6 +3,7 @@ package zw.qantra.tm.domain.services; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @Component @@ -10,6 +11,7 @@ public class EmailService { @Autowired private JavaMailSender emailSender; + @Async public void sendSimpleMessage( String to, String subject, String text) { SimpleMailMessage message = new SimpleMailMessage(); diff --git a/src/main/java/zw/qantra/tm/domain/services/HotRechargeBalanceService.java b/src/main/java/zw/qantra/tm/domain/services/HotRechargeBalanceService.java new file mode 100644 index 0000000..0a3d0ec --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/HotRechargeBalanceService.java @@ -0,0 +1,56 @@ +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.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; +import zw.qantra.tm.rest.RestService; + +import java.math.BigDecimal; +import java.util.LinkedList; +import java.util.LinkedHashMap; + +@Service +@RequiredArgsConstructor +public class HotRechargeBalanceService { + + Logger logger = LoggerFactory.getLogger(HotRechargeBalanceService.class); + + private final RestService restService; + + @Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}") + private String hotApiUrl; + + @Cacheable(value = "hotrechargeBalance", key = "#token + '_' + #amount.toString()") + public 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; + } + } + + @CacheEvict(value = "hotrechargeBalance", allEntries = true) + public void evictCache() { + logger.info("Evicting HotRecharge balance cache after successful transaction"); + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/HotRechargeTokenService.java b/src/main/java/zw/qantra/tm/domain/services/HotRechargeTokenService.java index bd3dcd2..44362a3 100644 --- a/src/main/java/zw/qantra/tm/domain/services/HotRechargeTokenService.java +++ b/src/main/java/zw/qantra/tm/domain/services/HotRechargeTokenService.java @@ -26,7 +26,7 @@ public class HotRechargeTokenService { private String hotAccessCode; @Value("${hot.password}") private String hotPassword; - @Value("${hot.token.expiry.minutes:30}") + @Value("${hot.token.expiry.minutes:25}") private int tokenExpiryMinutes; private String cachedToken; 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 7e6ca50..b4f4107 100644 --- a/src/main/java/zw/qantra/tm/domain/services/SettingService.java +++ b/src/main/java/zw/qantra/tm/domain/services/SettingService.java @@ -26,4 +26,17 @@ public class SettingService { return false; } } + + public String getStringSetting(String key) { + try { + Setting setting = settingRepository.findBySettingName(key); + if (setting != null) { + return setting.getSettingValue(); + } + return ""; + } catch (Exception exception) { + exception.printStackTrace(); + return ""; + } + } } \ 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 new file mode 100644 index 0000000..c8ae07c --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/VelocityPaymentService.java @@ -0,0 +1,313 @@ +package zw.qantra.tm.domain.services; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.util.UriComponentsBuilder; +import zw.qantra.tm.domain.dtos.RegisterRequest; +import zw.qantra.tm.domain.dtos.erp.*; +import zw.qantra.tm.domain.enums.CurrencyType; +import zw.qantra.tm.domain.enums.Status; +import zw.qantra.tm.domain.models.Transaction; +import zw.qantra.tm.domain.repositories.TransactionRepository; +import zw.qantra.tm.configs.VelocityApiProperties; +import zw.qantra.tm.exceptions.AlreadyExistsException; +import zw.qantra.tm.rest.RestService; +import zw.qantra.tm.utils.Utils; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +@Service +@Slf4j +@RequiredArgsConstructor +public class VelocityPaymentService { + private final TransactionRepository transactionRepository; + private final ObjectMapper objectMapper; + private final RestService restService; + private final VelocityApiProperties velocityApiProperties; + private final SettingService settingService; + + + private static final String VELOCITY_API_CREATE_ORDER_PATH = "/sales-orders"; + private static final String VELOCITY_API_INITIATE_PAYMENT_PATH = "/transactions"; + 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"; + + + public VelocityCustomerResponseDto createCustomer(RegisterRequest registerRequest) { + VelocityCustomerDto payload = VelocityCustomerDto.builder() + .name(registerRequest.getFirstName() + " " + registerRequest.getLastName()) + .email(registerRequest.getEmail()) + .phone(registerRequest.getPhone()) + .billingCurrencyString(CurrencyType.USD.name()) + .companyIdString(settingService.getStringSetting("VELOCITY_COMPANY_ID")) + .build(); + + try { + LinkedHashMap responseMap = makeRequest(HttpMethod.POST, VELOCITY_API_CUSTOMERS, payload); + String status = (String) responseMap.get("status"); + String id = (String) responseMap.get("id"); + log.info("Customer created with status: {}, id: {}", status, id); + + if("PENDING".equalsIgnoreCase(status) && id != null) { + LinkedHashMap authResponse = makeRequest(HttpMethod.PUT, + VELOCITY_API_CUSTOMERS + "/authorize/" + id, null); + String workflowStatus = (String) authResponse.get("status"); + + if("finished".equalsIgnoreCase(workflowStatus)) { + return Utils.deepCopy( + authResponse.get("body"), VelocityCustomerResponseDto.class); + } + }else { + List errors = (List) responseMap.get("errors"); + if(errors != null && !errors.isEmpty()){ + String errorMessage = errors.get(0); + + if(errorMessage.contains("already exists")) { + log.warn("Customer already exists in Velocity: {}", errorMessage); + throw new AlreadyExistsException(errorMessage); // or handle as needed + } + log.error("Error creating customer: {}", errorMessage); + throw new IllegalStateException("Error creating customer: " + errorMessage); + } + } + return null; + } catch (Exception e) { + log.error("Error creating customer: {}", e.getMessage(), e); + throw new IllegalStateException("Error creating customer: " + e.getMessage(), e); + } + + } + + public Transaction processPayment(Transaction transaction) { + Transaction orderTransaction = createSalesOrder(transaction); + + return initiatePayment(orderTransaction); + } + + public Transaction createSalesOrder(Transaction transaction) { + BigDecimal amount = transaction.getTotalAmount(); + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + VelocitySalesOrderDto payload = VelocitySalesOrderDto.builder() + .currencyCodeString(transaction.getDebitCurrency().name()) + .orderDate(LocalDate.now().format(fmt)) + .dueDate(LocalDate.now().plusDays(7).format(fmt)) + .notes(transaction.getReference()) + .authorized(Boolean.TRUE) + .items(Collections.singletonList(VelocitySalesOrderDto.ItemDto.builder() + .name(transaction.getBillName()) + .itemCode("VLT-" + transaction.getProviderLabel()) + .qty(BigDecimal.ONE) + .unitPrice(amount) + .amount(amount) + .build())) + .charges(Collections.emptyList()) + .build(); + + try { + LinkedHashMap responseMap = makeRequest(HttpMethod.POST, VELOCITY_API_CREATE_ORDER_PATH, payload); + VelocitySalesOrderResponseDto response = Utils.deepCopy( + responseMap, VelocitySalesOrderResponseDto.class); + + String workflowStatus = response.getStatus(); + VelocitySalesOrderResponseDto.BodyDto body = response.getBody(); + String orderStatus = body.getStatus(); + String velocityOrderId = body.getId(); + String velocityOrderTrace = body.getTrace(); + String velocityOrderName = body.getName(); + if ("manual".equalsIgnoreCase(workflowStatus) && "UNPAID".equalsIgnoreCase(orderStatus)) { + transaction.setOrderId(velocityOrderId); + transaction.setOrderTrace(velocityOrderTrace); + transaction.setErpSalesRef(velocityOrderName); + return transaction; + } + + log.error("Unexpected sales order response from Velocity: {}", response); + throw new IllegalStateException("Unexpected sales order response from Velocity"); + } catch (Exception e) { + log.error("Error creating sales order: {}", e.getMessage(), e); + throw new IllegalStateException("Error creating sales order: " + e.getMessage(), e); + } + + } + + private Transaction initiatePayment(Transaction transaction) { + String paymentProcessorLabel = Objects.equals(transaction.getPaymentProcessorLabel(), "MPGS") + ? "VMC" : transaction.getPaymentProcessorLabel(); + VelocityTransactionDto payload = VelocityTransactionDto.builder() + .salesOrderId(transaction.getOrderId()) + .amount(transaction.getTotalAmount()) + .debitPhone(transaction.getDebitPhone()) + .debitRegion("ZW") + .type("REQUEST") + .paymentProcessorLabel(paymentProcessorLabel) + .debitCurrency(transaction.getDebitCurrency().name()) + .debitRef(transaction.getErpSalesRef()) + .creditPhone(velocityApiProperties.getCreditPhone()) + .creditRegion("ZW") + .creditAccount(velocityApiProperties.getCreditAccount()) + .authType(velocityApiProperties.getDefaultAuthType()) + .build(); + + if ("MPGS".equalsIgnoreCase(transaction.getPaymentProcessorLabel())) { + payload.setCancelUrl(velocityApiProperties.getCancelUrl() + "/" + transaction.getOrderId()); + payload.setSuccessUrl(velocityApiProperties.getSuccessUrl() + "/" + transaction.getOrderId()); + } + + try { + LinkedHashMap response = makeRequest( + HttpMethod.POST, VELOCITY_API_INITIATE_PAYMENT_PATH, payload); + LinkedHashMap body = (LinkedHashMap) response.get("body"); + String transactionStatus = (String) body.get("status"); + String tranTrace = (String) body.get("trace"); + 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.setTargetUrl(redirectUrl); + return transaction; + } + return null; + + } catch (Exception e) { + log.error("Error initiating payment: {}", e.getMessage(), e); + throw new IllegalStateException("Error initiating payment: " + e.getMessage(), e); + } + } + + public Transaction pollPaymentStatus(Transaction transaction) { + LinkedHashMap response = makeRequest( + HttpMethod.PUT, + VELOCITY_API_POLL_PAYMENT_PATH.replace("{traceId}", transaction.getOrderTransactionTrace()), + null + ); + + LinkedHashMap body = (LinkedHashMap) response.get("body"); + 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")); + + return transaction; + } + + @Async + public void updateSalesOrderWorkflow(Transaction transaction) { + LinkedHashMap response = makeRequest( + HttpMethod.PUT, + VELOCITY_API_UPDATE_WORKFLOW_PATH.replace("{traceId}", transaction.getOrderTrace()), + null + ); + + LinkedHashMap body = (LinkedHashMap) response.get("body"); + LinkedHashMap salesOrderNode = (LinkedHashMap) body.get("salesOrder"); + String status = (String) salesOrderNode.get("status"); + + if (!"PAID".equals(status)) { + log.info("Sales order status is not PAID. Status: {}", status); + } + } + + + private LinkedHashMap makeRequest(HttpMethod method, String path, Object payload) { + long startNs = System.nanoTime(); + ResponseEntity response; + try { + response = restService.exchange( + buildUrl(path), + method, + new HttpEntity<>(payload, headers()), + LinkedHashMap.class + ); + } catch (Exception ex) { + log.error("Velocity API request failed. method={}, path={}", method, path, ex); + throw new IllegalStateException("Velocity API request failed: " + ex.getMessage(), ex); + } + + LinkedHashMap body = response.getBody(); + if (!response.getStatusCode().is2xxSuccessful()) { + throw new IllegalStateException("Velocity API request failed with status: " + response.getStatusCode()); + } + + return body; + } + + private JsonNode resolveBody(JsonNode response) { + JsonNode body = response.path("body"); + return body.isMissingNode() || body.isNull() ? response : body; + } + + private String normalize(String value) { + return value == null ? null : value.toUpperCase(Locale.ROOT); + } + + private Double numberValue(JsonNode node, String key, Double fallback) { + JsonNode value = node.get(key); + if (value == null || value.isNull()) { + return fallback; + } + return value.asDouble(); + } + + private HttpHeaders headers() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + if (velocityApiProperties.getApiKey() != null && !velocityApiProperties.getApiKey().isBlank()) { + headers.set("X-API-Key", velocityApiProperties.getApiKey()); + } + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + return headers; + } + + private String buildUrl(String path) { + return UriComponentsBuilder.fromUriString(velocityApiProperties.getBaseUrl()) + .path(path) + .build() + .toUriString(); + } + + private String text(JsonNode node, String... keys) { + for (String key : keys) { + JsonNode value = node.get(key); + if (value != null && !value.isNull()) { + return value.asText(); + } + } + return null; + } + + private boolean isTerminal(String status) { + if (status == null) { + return false; + } + String normalized = status.toUpperCase(); + return normalized.equals("SUCCESS") + || normalized.equals("PAID") + || normalized.equals("FAILED") + || normalized.equals("DECLINED") + || normalized.equals("CANCELLED"); + } + + private void sleep() { + try { + Thread.sleep(Math.max(200L, velocityApiProperties.getPollIntervalMs())); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while polling Velocity payment status", e); + } + } +} \ No newline at end of file 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 index 6688de3..873e9ec 100644 --- 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 @@ -2,6 +2,7 @@ package zw.qantra.tm.domain.services.processors.confirmations.hotrecharge; import org.springframework.stereotype.Service; import zw.qantra.tm.domain.services.AdditionalDataService; +import zw.qantra.tm.domain.services.HotRechargeBalanceService; import zw.qantra.tm.domain.services.HotRechargeTokenService; import zw.qantra.tm.rest.RestService; @@ -11,7 +12,8 @@ public class EconetBundlesConfirmationHotProcessor extends NetoneConfirmationHot public EconetBundlesConfirmationHotProcessor( AdditionalDataService additionalDataService, HotRechargeTokenService hotRechargeTokenService, + HotRechargeBalanceService hotRechargeBalanceService, RestService restService) { - super(additionalDataService, hotRechargeTokenService, restService); + super(additionalDataService, hotRechargeTokenService, hotRechargeBalanceService, restService); } -} +} \ No newline at end of file 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 index 09378dc..985101f 100644 --- 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 @@ -2,6 +2,7 @@ package zw.qantra.tm.domain.services.processors.confirmations.hotrecharge; import org.springframework.stereotype.Service; import zw.qantra.tm.domain.services.AdditionalDataService; +import zw.qantra.tm.domain.services.HotRechargeBalanceService; import zw.qantra.tm.domain.services.HotRechargeTokenService; import zw.qantra.tm.rest.RestService; @@ -11,7 +12,8 @@ public class EconetConfirmationHotProcessor extends NetoneConfirmationHotProcess public EconetConfirmationHotProcessor( AdditionalDataService additionalDataService, HotRechargeTokenService hotRechargeTokenService, + HotRechargeBalanceService hotRechargeBalanceService, RestService restService) { - super(additionalDataService, hotRechargeTokenService, restService); + super(additionalDataService, hotRechargeTokenService, hotRechargeBalanceService, restService); } -} +} \ No newline at end of file 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 index 55cfbcd..7990761 100644 --- 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 @@ -10,6 +10,7 @@ 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.HotRechargeBalanceService; import zw.qantra.tm.domain.services.HotRechargeTokenService; import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; import zw.qantra.tm.rest.RestService; @@ -23,9 +24,10 @@ import java.util.*; @RequiredArgsConstructor @Slf4j public class NetoneConfirmationHotProcessor implements TransactionProcessorInterface { - private final AdditionalDataService additionalDataService; - private final HotRechargeTokenService hotRechargeTokenService; - private final RestService restService; + protected final AdditionalDataService additionalDataService; + protected final HotRechargeTokenService hotRechargeTokenService; + protected final HotRechargeBalanceService hotRechargeBalanceService; + protected final RestService restService; @Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}") private String hotApiUrl; @@ -35,7 +37,7 @@ public class NetoneConfirmationHotProcessor implements TransactionProcessorInter public Object process(Transaction transaction) throws Exception { String token = hotRechargeTokenService.getToken(); - if (!checkBalance(token, transaction.getAmount())) { + if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount())) { transaction.setStatus(Status.FAILED); transaction.setResponseCode("92"); transaction.setErrorMessage("There's a technical issue on our end. Please try again later."); @@ -82,26 +84,4 @@ public class NetoneConfirmationHotProcessor implements TransactionProcessorInter return transaction; } - 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); - log.info("Response: {}", response); - - if (response != null && response.containsKey("balance")) { - Object balance = response.get("balance"); - log.info("Current balance: {}", balance); - return (new BigDecimal(balance.toString())).compareTo(amount) > 0; - } - return false; - } catch (Exception e) { - log.error("Failed to check balance: {}", e.getMessage(), e); - return false; - } - } - -} +} \ No newline at end of file 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 index 81d9a2f..3eb5197 100644 --- 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 @@ -14,6 +14,7 @@ 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.HotRechargeBalanceService; import zw.qantra.tm.domain.services.HotRechargeTokenService; import zw.qantra.tm.domain.services.ProviderService; import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; @@ -33,6 +34,7 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa private final AdditionalDataService additionalDataService; private final ProviderService providerService; private final HotRechargeTokenService hotRechargeTokenService; + private final HotRechargeBalanceService hotRechargeBalanceService; @Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}") private String hotApiUrl; @@ -50,7 +52,7 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa return transaction; } - if (!checkBalance(token, transaction.getAmount())) { + if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount())) { transaction.setStatus(Status.FAILED); transaction.setResponseCode("92"); transaction.setErrorMessage("There's a technical issue on our end. Please try again later."); @@ -103,28 +105,6 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa 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() @@ -160,4 +140,4 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa public Object poll(Transaction transaction) throws Exception { return transaction; } -} +} \ No newline at end of file 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 index 2240baa..b110d12 100644 --- 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 @@ -31,6 +31,7 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte private final SettingService settingService; private final HotRechargeTokenService hotRechargeTokenService; private final ProviderService providerService; + private final HotRechargeBalanceService hotRechargeBalanceService; @Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}") private String hotApiUrl; @@ -53,7 +54,7 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){ transaction.setStatus(Status.SUCCESS); transaction.setResponseCode("responseCode"); - transaction.setCreditRef("creditRef"); + transaction.setCreditRef("Simulated Integration Success"); transaction.setErrorMessage(null); return transaction; } @@ -69,7 +70,7 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte return transaction; } - if (!checkBalance(token, transaction.getAmount())) { + if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount())) { transaction.setStatus(Status.FAILED); transaction.setResponseCode("92"); transaction.setErrorMessage("There's a technical issue on our end. Please try again later."); @@ -86,6 +87,7 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte Boolean successful = (Boolean) rechargeResponse.get("successful"); if (successful != null && successful) { + hotRechargeBalanceService.evictCache(); transaction.setStatus(Status.SUCCESS); transaction.setResponseCode("00"); transaction.setCreditRef(rechargeResponse.get("rechargeId").toString()); @@ -123,29 +125,6 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte } - 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"; @@ -159,7 +138,12 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte request.put("Target", transaction.getCreditAccount()); request.put("Amount", transaction.getAmount()); - // do this for zesa + // for zesa skip this + if(!providerService.getProviderId(transaction.getBillClientId()).equals("41")) { + request.put("CustomerSMS", "%COMPANYNAME% topped up your account with $%AMOUNT%."); + } + + // do this for zesa only if(providerService.getProviderId(transaction.getBillClientId()).equals("41")) { List> rechargeOptions = new ArrayList<>(); if (transaction.getDebitPhone() != null) { @@ -246,4 +230,4 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte return transaction; } -} +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/integrations/StewardIntegrationProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/integrations/StewardIntegrationProcessor.java index 9058e21..8884987 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/integrations/StewardIntegrationProcessor.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/integrations/StewardIntegrationProcessor.java @@ -57,6 +57,14 @@ public class StewardIntegrationProcessor implements TransactionProcessorInterfac return transaction; } + if(settingService.getBooleanSetting("SIMULATE_STEWARD_SUCCESS")){ + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("responseCode"); + transaction.setCreditRef("Simulated Integration Success"); + transaction.setErrorMessage(null); + return transaction; + } + logger.info("processing integration"); String token = restService.getMerchantToken(); 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 676bf33..dd60ec8 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 @@ -19,6 +19,7 @@ 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; @@ -29,6 +30,7 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf private final RestService restService; private final TransactionService transactionService; private final SettingService settingService; + private final VelocityPaymentService velocityPaymentService; @Value("${sbz.aggregator.encryption-key}") private String encryptionKey; @@ -51,78 +53,10 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf return transaction; } - String token = restService.getMerchantToken(); - - // send total less gateway charges - BigDecimal totalAmount = transaction.getAmount() - .add(transaction.getTax()) - .add(transaction.getCharge()) - .add(transaction.getGatewayCharge()); - - String concat = clientId+ - encryptionKey+ - transaction.getDebitPhone()+ - totalAmount; - - String hex = getHash(concat); - - SdkActionDto sdkActionDto = SdkActionDto.builder() - .clientId(clientId) - .clientSecret(clientSecret) - .phone(transaction.getDebitPhone()) - .amount(totalAmount.toString()) - .currency(transaction.getDebitCurrency().toString()) - .hash(hex) - .authType("REMOTE") - .billerReference(transaction.getReference()) - .action("PAYMENT") - .sandbox(true) - .paymentProcessorLabel("ECOCASH") - .responseUrl("https://peakapi.qantra.co.zw") - .build(); - - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + token); - headers.set("Content-Type", "application/json"); - try { - LinkedHashMap response = restService.postAs(url + "/merchant/sdk/action", - sdkActionDto, headers, LinkedHashMap.class); - - LinkedHashMap body = (LinkedHashMap) response.get("body"); - transaction.setSdkActionId((String) body.get("uid")); - - if(body.get("status").equals("SUCCESS")){ - transaction.setStatus(Status.SUCCESS); - transaction.setResponseCode("00"); - - LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction"); - transaction.setPaymentProcessorRef((String) transactionData.get("debitReference")); - - // getting omni channel rrn from additional data - java.util.List additionalData = - (java.util.List) transactionData.get("additionalData"); - if (additionalData != null) { - for (LinkedHashMap dataItem : additionalData) { - if ("Transaction Reference".equals(dataItem.get("name"))) { - transaction.setDebitRef((String) dataItem.get("value")); - break; - } - } - } - - transactionService.save(transaction); - }else{ - transaction.setStatus(Status.FAILED); - transaction.setResponseCode("01"); - String errorMessage = "Problem processing payment. Please try again."; - if(body.get("transaction") != null) { - LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction"); - errorMessage = (String) transactionData.get("errorMessage"); - } - transaction.setErrorMessage(errorMessage); - } + 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"); @@ -149,42 +83,15 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf return transaction; } - String token = restService.getMerchantToken(); - - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + token); - headers.set("Content-Type", "application/json"); - try { - LinkedHashMap response = restService.getAs(url + "/merchant/sdk/reference/" + transaction.getReference(), - headers, LinkedHashMap.class); + transaction = velocityPaymentService.pollPaymentStatus(transaction); + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("00"); - LinkedHashMap body = (LinkedHashMap) response.get("body"); + transactionService.save(transaction); - // only update tran status if polling returns a success - if(body.get("status").equals("SUCCESS")){ - transaction.setStatus(Status.SUCCESS); - transaction.setResponseCode("00"); - transaction.setTargetUrl((String) body.get("targetUrl")); - - LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction"); - transaction.setPaymentProcessorRef((String) transactionData.get("debitReference")); - - // getting omni channel rrn from additional data - java.util.List additionalData = - (java.util.List) transactionData.get("additionalData"); - if (additionalData != null) { - for (LinkedHashMap dataItem : additionalData) { - if ("Transaction Reference".equals(dataItem.get("name"))) { - transaction.setDebitRef((String) dataItem.get("value")); - break; - } - } - } - - transactionService.save(transaction); - } } catch (Exception e) { + e.printStackTrace(); logger.error("Network error during payment processing: {}", e.getMessage(), e); transaction.setStatus(Status.FAILED); transaction.setResponseCode("99"); 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 f0e68d7..ffbf084 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 @@ -12,6 +12,7 @@ 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 zw.qantra.tm.utils.Utils; @@ -33,6 +34,7 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface { private final RestService restService; private final TransactionService transactionService; private final SettingService settingService; + private final VelocityPaymentService velocityPaymentService; @Value("${sbz.aggregator.encryption-key}") private String encryptionKey; @@ -48,70 +50,25 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface { @Override public Object process(Transaction transaction) throws Exception { logger.info("processing transaction"); - String token = restService.getMerchantToken(); - // send total less gateway charges - BigDecimal totalAmount = transaction.getAmount() - .add(transaction.getTax()) - .add(transaction.getCharge()) - .add(transaction.getGatewayCharge()); - - String concat = clientId+ - encryptionKey+ - transaction.getCreditPhone()+ - totalAmount; - - String hex = getHash(concat); - - SdkActionDto sdkActionDto = SdkActionDto.builder() - .clientId(clientId) - .clientSecret(clientSecret) - .channel("api") - .phone(transaction.getCreditPhone()) - .amount(totalAmount.toString()) - .currency(transaction.getDebitCurrency().toString()) - .hash(hex) - .authType("WEB") - .billerReference(transaction.getReference()) - .action("PAYMENT") - .sandbox(true) - .paymentProcessorLabel("MPGS") - .responseUrl("https://stewardbank.co.zw") - .returnUrl(returnUrl + "/" + transaction.getReference()) - .build(); - - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + token); - headers.set("Content-Type", "application/json"); + if(settingService.getBooleanSetting("SIMULATE_MPGS_SUCCESS")) { + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("00"); + transaction.setPaymentProcessorRef(RandomStringUtils.randomNumeric(10)); + transaction.setDebitRef(RandomStringUtils.randomNumeric(10)); + return transaction; + } try { - LinkedHashMap response = restService.postAs(url + "/merchant/sdk/action", - sdkActionDto, headers, LinkedHashMap.class); - logger.info("response: {}", Utils.toJson(response)); - - LinkedHashMap body = (LinkedHashMap) response.get("body"); - transaction.setSdkActionId((String) body.get("uid")); - - // if status is not failed, set the transaction to pending and send customer to the target url - if(!body.get("status").equals("FAILED")){ - transaction.setStatus(Status.PENDING); - transaction.setResponseCode("00"); - transaction.setTargetUrl((String) body.get("targetUrl")); - }else{ - transaction.setStatus(Status.FAILED); - transaction.setResponseCode("01"); - LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction"); - if (transactionData != null) { - transaction.setErrorMessage((String) transactionData.get("errorMessage")); - } else { - transaction.setErrorMessage(body.get("errorMessage").toString()); - } - } + // velocity expects VMC + 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.setErrorMessage("Network error: " + e.getMessage()); + transaction.setSystemErrorMessage("Network error: " + e.getMessage()); + transaction.setErrorMessage("Payment failed. Please try again"); } return transactionService.save(transaction); @@ -120,56 +77,29 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface { @Override public Object poll(Transaction transaction) throws Exception { logger.info("polling transaction"); - String token = restService.getMerchantToken(); - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + token); - headers.set("Content-Type", "application/json"); + if(settingService.getBooleanSetting("SIMULATE_MPGS_SUCCESS")) { + transaction.setStatus(Status.SUCCESS); + transaction.setResponseCode("00"); + transaction.setTargetUrl("/home"); + return transaction; + } try { - LinkedHashMap response = restService.getAs(url + "/merchant/sdk/mpgs/poll/" + transaction.getSdkActionId(), - headers, LinkedHashMap.class); - logger.info("response: {}", Utils.toJson(response)); - - LinkedHashMap body = (LinkedHashMap) response.get("body"); - - // todo: REMOVE THIS - if(settingService.getBooleanSetting("SIMULATE_MPGS_SUCCESS")){ - body.put("status", "SUCCESS"); - LinkedHashMap hashMap = new LinkedHashMap(); - hashMap.put("creditReference", RandomStringUtils.randomNumeric(10)); - body.put("transaction", hashMap); - } - - // only update tran status if polling returns a success - if(!body.get("status").equals("SUCCESS")){ - logger.error(body.get("errorMessage").toString()); - transaction.setStatus(Status.FAILED); - transaction.setResponseCode(body.get("responseCode").toString()); - transaction.setErrorMessage(body.get("errorMessage").toString()); - transactionService.save(transaction); - return transaction; - } + transaction = velocityPaymentService.pollPaymentStatus(transaction); transaction.setStatus(Status.SUCCESS); - transaction.setPaymentStatus(Status.SUCCESS); transaction.setResponseCode("00"); - transaction.setTargetUrl((String) body.get("targetUrl")); - - try { - LinkedHashMap transactionData = (LinkedHashMap) body.get("transaction"); - transaction.setDebitRef((String) transactionData.get("debitReference")); - } catch (Exception e) { - logger.info("Notification not yet posted at the bank"); - } transactionService.save(transaction); } catch (Exception e) { + e.printStackTrace(); logger.error("Network error during payment processing: {}", e.getMessage(), e); transaction.setStatus(Status.FAILED); transaction.setResponseCode("99"); - transaction.setErrorMessage("Network error: " + e.getMessage()); + 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/workflows/PollCronWorkflow.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/PollCronWorkflow.java similarity index 98% rename from src/main/java/zw/qantra/tm/domain/workflows/PollCronWorkflow.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/PollCronWorkflow.java index 61db957..0651623 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/PollCronWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/PollCronWorkflow.java @@ -1,8 +1,7 @@ -package zw.qantra.tm.domain.workflows; +package zw.qantra.tm.domain.services.processors.workflows; import io.nflow.engine.service.WorkflowInstanceService; import io.nflow.engine.workflow.curated.CronWorkflow; -import io.nflow.engine.workflow.curated.State; import io.nflow.engine.workflow.definition.*; import io.nflow.engine.workflow.instance.WorkflowInstance; import io.nflow.engine.workflow.instance.WorkflowInstanceAction; @@ -18,7 +17,6 @@ import zw.qantra.tm.domain.services.TransactionService; import zw.qantra.tm.utils.LogUtils; import zw.qantra.tm.utils.Utils; -import java.sql.Timestamp; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; diff --git a/src/main/java/zw/qantra/tm/domain/workflows/PurchaseInvoiceWorkflow.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/PurchaseInvoiceWorkflow.java similarity index 93% rename from src/main/java/zw/qantra/tm/domain/workflows/PurchaseInvoiceWorkflow.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/PurchaseInvoiceWorkflow.java index ce7066b..aa111ac 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/PurchaseInvoiceWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/PurchaseInvoiceWorkflow.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.workflows; +package zw.qantra.tm.domain.services.processors.workflows; import com.fasterxml.jackson.databind.ObjectMapper; import io.nflow.engine.workflow.curated.State; @@ -11,8 +11,8 @@ import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import zw.qantra.tm.domain.models.Transaction; -import zw.qantra.tm.domain.workflows.handlers.HandlerInterface; -import zw.qantra.tm.domain.workflows.handlers.WorkflowHandlerFactory; +import zw.qantra.tm.domain.services.processors.workflows.handlers.HandlerInterface; +import zw.qantra.tm.domain.services.processors.workflows.handlers.WorkflowHandlerFactory; import zw.qantra.tm.utils.Utils; import java.util.LinkedHashMap; diff --git a/src/main/java/zw/qantra/tm/domain/workflows/RegistrationWorkflow.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/RegistrationWorkflow.java similarity index 70% rename from src/main/java/zw/qantra/tm/domain/workflows/RegistrationWorkflow.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/RegistrationWorkflow.java index 74dce35..58e554d 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/RegistrationWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/RegistrationWorkflow.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.workflows; +package zw.qantra.tm.domain.services.processors.workflows; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; @@ -15,7 +15,7 @@ import io.nflow.engine.workflow.definition.NextAction; import io.nflow.engine.workflow.definition.StateExecution; import io.nflow.engine.workflow.definition.WorkflowDefinition; import zw.qantra.tm.domain.dtos.*; -import zw.qantra.tm.domain.dtos.erp.CustomerDto; +import zw.qantra.tm.domain.dtos.erp.VelocityCustomerResponseDto; import zw.qantra.tm.domain.models.Otp; import zw.qantra.tm.domain.models.User; import zw.qantra.tm.domain.services.EmailService; @@ -25,6 +25,8 @@ import io.nflow.engine.workflow.curated.State; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validator; +import zw.qantra.tm.domain.services.VelocityPaymentService; +import zw.qantra.tm.exceptions.AlreadyExistsException; import zw.qantra.tm.rest.RestService; import org.springframework.http.HttpHeaders; @@ -46,21 +48,8 @@ public class RegistrationWorkflow extends WorkflowDefinition { private RestService restService; @Autowired private EmailService emailService; - - @Value("${infobip.url}") - private String infobipUrl; - @Value("${infobip.apikey}") - private String infobipApiKey; - @Value("${erpnext.url}") - private String erpnextUrl; - @Value("${erpnext.api.key}") - private String erpnextApiKey; - @Value("${erpnext.api.secret}") - private String erpnextApiSecret; - @Value("${erpnext.username}") - private String erpnextUsername; - @Value("${erpnext.password}") - private String erpnextPassword; + @Autowired + private VelocityPaymentService velocityPaymentService; public static final String TYPE = "registrationWorkflow"; @@ -190,75 +179,17 @@ public class RegistrationWorkflow extends WorkflowDefinition { public NextAction updateErp(StateExecution execution) { RegisterRequest registerRequest = execution.getVariable("registerRequest", RegisterRequest.class); - // login to erp - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", "application/json"); - headers.add("Accept", "application/json"); - - Map loginRequest = Map.of( - "usr", erpnextUsername, - "pwd", erpnextPassword); - - String sid = ""; - try { - ResponseEntity response = restService.postAsEntity( - erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); - - sid = response.getHeaders().getFirst("Set-Cookie"); - } catch (Exception e) { - e.printStackTrace(); - return NextAction.retryAfter(DateTime.now().plusSeconds(30), "Failed to login to ERP: " + e.getMessage()); - } - - // create customer in erp - String basicAuth = "Basic " + Base64.getEncoder().encodeToString( - (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); - headers.add("Authorization", basicAuth); - headers.add("Cookie", sid); - - // check if customer exists first - String customerName = registerRequest.getFirstName() + " " + registerRequest.getLastName(); - try { - LinkedHashMap checkResponse = restService.getAs(erpnextUrl + "/v2/document/Customer" + - "?fields=[\"name\"]&filters=[[\"name\", \"=\", " + - "\"" + customerName + "\"]]", headers, LinkedHashMap.class); - List dataList = (List) checkResponse.get("data"); - - if(!dataList.isEmpty()){ - User user = userService.fetchUserByUsername(registerRequest.getUsername()); - user.setErpCustomerRef(customerName); - userService.getUserRepository().save(user); - - return NextAction.stopInState(DONE, "Customer already exists in ERP. Ignoring update: " + - registerRequest.getUsername()); - } - - // if not proceed - CustomerDto customerDto = CustomerDto.builder() - .doctype("Customer") - .customerName(customerName) - .customerGroup("Commercial") - .territory("All Territories") - .customerType("Individual") - .language("en") - .mobileNo(registerRequest.getPhone()) - .emailId(registerRequest.getEmail()) - .docstatus(1) - .build(); - - LinkedHashMap response = restService.postAs( - erpnextUrl + "/v2/document/Customer", customerDto, headers, LinkedHashMap.class); - - LinkedHashMap data = (LinkedHashMap) response.get("data"); - String customerId = (String) data.get("name"); + VelocityCustomerResponseDto customer = velocityPaymentService.createCustomer(registerRequest); User user = userService.fetchUserByUsername(registerRequest.getUsername()); - user.setErpCustomerRef(customerId); + user.setErpCustomerRef(customer.getId()); userService.getUserRepository().save(user); + } catch (AlreadyExistsException e){ + log.error("Customer already exists in ERP: {}", e.getMessage(), e); + return NextAction.stopInState(DONE, "ERP updated successfully for: " + registerRequest.getUsername()); } catch (Exception e) { e.printStackTrace(); - return NextAction.retryAfter(DateTime.now().plusMinutes(5), "Failed to update ERP: " + e.getMessage()); } diff --git a/src/main/java/zw/qantra/tm/domain/workflows/SalesInvoiceWorkflow.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/SalesInvoiceWorkflow.java similarity index 95% rename from src/main/java/zw/qantra/tm/domain/workflows/SalesInvoiceWorkflow.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/SalesInvoiceWorkflow.java index 3ff494e..6e82751 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/SalesInvoiceWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/SalesInvoiceWorkflow.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.workflows; +package zw.qantra.tm.domain.services.processors.workflows; import com.fasterxml.jackson.databind.ObjectMapper; import io.nflow.engine.workflow.curated.State; @@ -12,8 +12,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import zw.qantra.tm.domain.models.Transaction; import zw.qantra.tm.domain.services.TransactionService; -import zw.qantra.tm.domain.workflows.handlers.HandlerInterface; -import zw.qantra.tm.domain.workflows.handlers.WorkflowHandlerFactory; +import zw.qantra.tm.domain.services.processors.workflows.handlers.HandlerInterface; +import zw.qantra.tm.domain.services.processors.workflows.handlers.WorkflowHandlerFactory; import zw.qantra.tm.utils.Utils; import java.util.LinkedHashMap; diff --git a/src/main/java/zw/qantra/tm/domain/workflows/StartupApplicationListener.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/StartupApplicationListener.java similarity index 92% rename from src/main/java/zw/qantra/tm/domain/workflows/StartupApplicationListener.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/StartupApplicationListener.java index 473c091..0fa8848 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/StartupApplicationListener.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/StartupApplicationListener.java @@ -1,16 +1,14 @@ -package zw.qantra.tm.domain.workflows; +package zw.qantra.tm.domain.services.processors.workflows; import io.nflow.engine.service.WorkflowInstanceService; import io.nflow.engine.workflow.instance.WorkflowInstanceFactory; import lombok.RequiredArgsConstructor; import org.joda.time.DateTime; -import org.joda.time.Instant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; -import zw.qantra.tm.utils.Utils; @Component @RequiredArgsConstructor diff --git a/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/TransactionWorkflow.java similarity index 68% rename from src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/TransactionWorkflow.java index 4bfee5b..3acb7f6 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/TransactionWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/TransactionWorkflow.java @@ -1,25 +1,17 @@ -package zw.qantra.tm.domain.workflows; +package zw.qantra.tm.domain.services.processors.workflows; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.nflow.engine.service.WorkflowInstanceService; import io.nflow.engine.workflow.curated.State; import io.nflow.engine.workflow.definition.NextAction; import io.nflow.engine.workflow.definition.StateExecution; import io.nflow.engine.workflow.definition.WorkflowDefinition; import io.nflow.engine.workflow.definition.WorkflowStateType; -import io.nflow.engine.workflow.instance.WorkflowInstanceFactory; -import org.joda.time.DateTime; -import org.joda.time.Instant; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import zw.qantra.tm.domain.enums.Status; import zw.qantra.tm.domain.models.Transaction; -import zw.qantra.tm.domain.services.TransactionService; -import zw.qantra.tm.domain.workflows.handlers.HandlerInterface; -import zw.qantra.tm.domain.workflows.handlers.WorkflowHandlerFactory; -import zw.qantra.tm.exceptions.ApiException; +import zw.qantra.tm.domain.services.VelocityPaymentService; +import zw.qantra.tm.domain.services.processors.workflows.handlers.*; import zw.qantra.tm.utils.Utils; import java.io.IOException; @@ -32,13 +24,23 @@ public class TransactionWorkflow extends WorkflowDefinition { @Autowired private WorkflowHandlerFactory workflowHandlerFactory; @Autowired - private TransactionService transactionService; - @Autowired private ObjectMapper objectMapper; @Autowired - private WorkflowInstanceService workflowInstanceService; + private VelocityPaymentService velocityPaymentService; @Autowired - private WorkflowInstanceFactory workflowInstanceFactory; + private CalculateChargesHandler calculateChargesHandler; + @Autowired + private CleanupHandler cleanupHandler; + @Autowired + private RecipientsUpdateHandler recipientsUpdateHandler; + @Autowired + private GatewayPaymentHandler gatewayPaymentHandler; + @Autowired + private ConfirmHandler confirmHandler; + @Autowired + private PollStatusHandler pollStatusHandler; + @Autowired + private IntegrationHandler integrationHandler; public static final String TYPE = "transactionWorkflow"; @@ -79,9 +81,7 @@ public class TransactionWorkflow extends WorkflowDefinition { byte[] json = objectMapper.writeValueAsBytes(map); Transaction transaction = objectMapper.readValue(json, Transaction.class); - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("calculateCharges"); - transaction = (Transaction) handlerInterface.process(transaction); + transaction = calculateChargesHandler.process(transaction); execution.setVariable("body", Utils.toJson(transaction)); @@ -98,9 +98,7 @@ public class TransactionWorkflow extends WorkflowDefinition { byte[] json = objectMapper.writeValueAsBytes(map); Transaction transaction = objectMapper.readValue(json, Transaction.class); - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("cleanup"); - transaction = (Transaction) handlerInterface.process(transaction); + transaction = (Transaction) cleanupHandler.process(transaction); execution.setVariable("body", Utils.toJson(transaction)); @@ -118,9 +116,7 @@ public class TransactionWorkflow extends WorkflowDefinition { Transaction transaction = objectMapper.readValue(json, Transaction.class); transaction.setWorkflowId(execution.getWorkflowInstanceId()); - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("confirm"); - transaction = (Transaction) handlerInterface.process(transaction); + transaction = (Transaction) confirmHandler.process(transaction); execution.setVariable("body", Utils.toJson(transaction)); return NextAction.moveToState(RECIPIENTS_UPDATE, "Recipients updated successfully"); } catch (Exception e) { @@ -134,9 +130,7 @@ public class TransactionWorkflow extends WorkflowDefinition { byte[] json = objectMapper.writeValueAsBytes(map); Transaction transaction = objectMapper.readValue(json, Transaction.class); - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("recipientsUpdate"); - handlerInterface.process(transaction); + recipientsUpdateHandler.process(transaction); return NextAction.moveToState(CONFIRM_SUCCESS, "Transaction confirmed successfully"); } @@ -152,9 +146,7 @@ public class TransactionWorkflow extends WorkflowDefinition { byte[] json = objectMapper.writeValueAsBytes(map); Transaction transaction = objectMapper.readValue(json, Transaction.class); - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("gatewayPayment"); - transaction = (Transaction) handlerInterface.process(transaction); + transaction = (Transaction) gatewayPaymentHandler.process(transaction); execution.setVariable("body", Utils.toJson(transaction)); return NextAction.moveToState(GATEWAY_PAYMENT_SUCCESS, "Gateway payment initiated"); @@ -165,25 +157,7 @@ public class TransactionWorkflow extends WorkflowDefinition { } public void gatewayPaymentSuccess(StateExecution execution) { - try { - LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class); - byte[] json = objectMapper.writeValueAsBytes(map); - Transaction transaction = objectMapper.readValue(json, Transaction.class); - - var instance = workflowInstanceFactory.newWorkflowInstanceBuilder() - .setType(SalesInvoiceWorkflow.TYPE) - .setExternalId(transaction.getUserId() + "-" + new Instant().getMillis()) - .setBusinessKey(execution.getBusinessKey()) - .putStateVariable("body", Utils.toJson(transaction)) - .setNextActivation(DateTime.now()) - .build(); - - long workflowId = workflowInstanceService.insertWorkflowInstance(instance); - - log.info("Gateway payment initiation complete"); - }catch (Exception e) { - e.printStackTrace(); - } + log.info("Gateway payment initiation complete"); } public NextAction pollStatus(StateExecution execution) throws IOException { @@ -193,9 +167,7 @@ public class TransactionWorkflow extends WorkflowDefinition { Transaction transaction = objectMapper.readValue(json, Transaction.class); try { - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("pollStatus"); - transaction = (Transaction) handlerInterface.process(transaction); + transaction = (Transaction) pollStatusHandler.process(transaction); execution.setVariable("body", Utils.toJson(transaction)); return NextAction.moveToState(POLL_STATUS_SUCCESS, "Polling complete"); @@ -216,9 +188,7 @@ public class TransactionWorkflow extends WorkflowDefinition { Transaction transaction = objectMapper.readValue(json, Transaction.class); try { - HandlerInterface handlerInterface = - (HandlerInterface) workflowHandlerFactory.getWorkflowHandler("integration"); - transaction = (Transaction) handlerInterface.process(transaction); + transaction = (Transaction) integrationHandler.process(transaction); execution.setVariable("body", Utils.toJson(transaction)); return NextAction.moveToState(INTEGRATION_STATUS_SUCCESS, "Integration request successful"); @@ -235,15 +205,7 @@ public class TransactionWorkflow extends WorkflowDefinition { byte[] json = objectMapper.writeValueAsBytes(map); Transaction transaction = objectMapper.readValue(json, Transaction.class); - var instance = workflowInstanceFactory.newWorkflowInstanceBuilder() - .setType(PurchaseInvoiceWorkflow.TYPE) - .setExternalId(transaction.getUserId() + "-" + new Instant().getMillis()) - .setBusinessKey(execution.getBusinessKey()) - .putStateVariable("body", Utils.toJson(transaction)) - .setNextActivation(DateTime.now()) - .build(); - - long workflowId = workflowInstanceService.insertWorkflowInstance(instance); + velocityPaymentService.updateSalesOrderWorkflow(transaction); log.info("Transaction workflow complete"); }catch (Exception e) { diff --git a/src/main/java/zw/qantra/tm/domain/workflows/WorkflowUtils.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/WorkflowUtils.java similarity index 95% rename from src/main/java/zw/qantra/tm/domain/workflows/WorkflowUtils.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/WorkflowUtils.java index 4618510..33be8eb 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/WorkflowUtils.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/WorkflowUtils.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.workflows; +package zw.qantra.tm.domain.services.processors.workflows; import java.util.Arrays; import java.util.EnumSet; @@ -6,8 +6,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import io.nflow.engine.model.ModelObject; -import io.nflow.engine.service.WorkflowInstanceInclude.*; import io.nflow.engine.workflow.instance.WorkflowInstanceAction; import org.springframework.stereotype.Component; diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/CalculateChargesHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/CalculateChargesHandler.java similarity index 98% rename from src/main/java/zw/qantra/tm/domain/workflows/handlers/CalculateChargesHandler.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/CalculateChargesHandler.java index 41f2520..5a23194 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/CalculateChargesHandler.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/CalculateChargesHandler.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.workflows.handlers; +package zw.qantra.tm.domain.services.processors.workflows.handlers; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; @@ -8,7 +8,6 @@ import zw.qantra.tm.domain.models.Charge; import zw.qantra.tm.domain.models.ChargeCondition; import zw.qantra.tm.domain.models.Transaction; import zw.qantra.tm.domain.services.ChargeService; -import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/CleanupHandler.java similarity index 97% rename from src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/CleanupHandler.java index c82bdf0..5920826 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/CleanupHandler.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/CleanupHandler.java @@ -1,10 +1,9 @@ -package zw.qantra.tm.domain.workflows.handlers; +package zw.qantra.tm.domain.services.processors.workflows.handlers; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; import lombok.RequiredArgsConstructor; -import org.aspectj.apache.bcel.generic.RET; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/ConfirmHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/ConfirmHandler.java similarity index 83% rename from src/main/java/zw/qantra/tm/domain/workflows/handlers/ConfirmHandler.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/ConfirmHandler.java index 5ccd841..017cb55 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/ConfirmHandler.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/ConfirmHandler.java @@ -1,10 +1,8 @@ -package zw.qantra.tm.domain.workflows.handlers; +package zw.qantra.tm.domain.services.processors.workflows.handlers; import lombok.RequiredArgsConstructor; 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.Transaction; import zw.qantra.tm.domain.models.TransactionEvent; import zw.qantra.tm.domain.services.AdditionalDataService; @@ -12,13 +10,8 @@ import zw.qantra.tm.domain.services.TransactionEventService; import zw.qantra.tm.domain.services.TransactionService; import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory; import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface; -import zw.qantra.tm.domain.services.processors.handlers.*; import zw.qantra.tm.utils.Utils; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.UUID; - @Service("confirm") @RequiredArgsConstructor public class ConfirmHandler implements HandlerInterface { @@ -43,11 +36,11 @@ public class ConfirmHandler implements HandlerInterface { // provider.getLabel() + "_" + // provider.getIntegrationProcessorLabel(); - TransactionProcessorInterface transactionProcessorInterface = - (TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor( - transaction.getConfirmationProcessorLabel()); - try { + TransactionProcessorInterface transactionProcessorInterface = + (TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor( + transaction.getConfirmationProcessorLabel()); + // MAIN TRANSACTION LOGIC! transaction = (Transaction) transactionProcessorInterface.process(transaction); transaction.setConfirmationStatus(transaction.getStatus()); diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/PaymentHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/GatewayPaymentHandler.java similarity index 91% rename from src/main/java/zw/qantra/tm/domain/workflows/handlers/PaymentHandler.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/GatewayPaymentHandler.java index edfed92..6b64e3e 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/PaymentHandler.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/GatewayPaymentHandler.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.workflows.handlers; +package zw.qantra.tm.domain.services.processors.workflows.handlers; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; @@ -14,7 +14,7 @@ import zw.qantra.tm.exceptions.ApiException; @Service("gatewayPayment") @RequiredArgsConstructor -public class PaymentHandler implements HandlerInterface { +public class GatewayPaymentHandler implements HandlerInterface { Logger logger = LoggerFactory.getLogger(TransactionService.class); private final PaymentProcessorFactory paymentProcessorFactory; @@ -26,6 +26,10 @@ public class PaymentHandler implements HandlerInterface { @Override public Object process(Transaction incomingTransaction) throws Exception { Transaction transaction = incomingTransaction; + + if(incomingTransaction == null) { + throw new ApiException("Transaction not found"); + } if(incomingTransaction.getId() != null){ transaction = transactionService.getTransactionRepository().findById(incomingTransaction.getId()).orElseThrow(() -> new ApiException("Transaction not found for ID: " + incomingTransaction.getId())); @@ -56,6 +60,7 @@ public class PaymentHandler implements HandlerInterface { // MAIN TRANSACTION LOGIC! transactionProcessorInterface.process(transaction); transaction.setPaymentStatus(transaction.getStatus()); + transaction.setPollingStatus(Status.PENDING); }catch (Exception e){ e.printStackTrace(); diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/HandlerInterface.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/HandlerInterface.java similarity index 68% rename from src/main/java/zw/qantra/tm/domain/workflows/handlers/HandlerInterface.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/HandlerInterface.java index 9bc52e4..ba1db33 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/HandlerInterface.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/HandlerInterface.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.workflows.handlers; +package zw.qantra.tm.domain.services.processors.workflows.handlers; import zw.qantra.tm.domain.models.Transaction; diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/IntegrationHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/IntegrationHandler.java similarity index 98% rename from src/main/java/zw/qantra/tm/domain/workflows/handlers/IntegrationHandler.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/IntegrationHandler.java index 54515c3..acfd303 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/IntegrationHandler.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/IntegrationHandler.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.workflows.handlers; +package zw.qantra.tm.domain.services.processors.workflows.handlers; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/PollStatusHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/PollStatusHandler.java similarity index 97% rename from src/main/java/zw/qantra/tm/domain/workflows/handlers/PollStatusHandler.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/PollStatusHandler.java index d208c00..c7e1055 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/PollStatusHandler.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/PollStatusHandler.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.workflows.handlers; +package zw.qantra.tm.domain.services.processors.workflows.handlers; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/RecipientsUpdateHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/RecipientsUpdateHandler.java similarity index 95% rename from src/main/java/zw/qantra/tm/domain/workflows/handlers/RecipientsUpdateHandler.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/RecipientsUpdateHandler.java index bc34949..cbbad77 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/RecipientsUpdateHandler.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/RecipientsUpdateHandler.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.workflows.handlers; +package zw.qantra.tm.domain.services.processors.workflows.handlers; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -6,7 +6,6 @@ import zw.qantra.tm.domain.enums.Status; import zw.qantra.tm.domain.models.Recipient; import zw.qantra.tm.domain.models.Transaction; import zw.qantra.tm.domain.services.RecipientService; -import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; import java.util.List; diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/WorkflowHandlerFactory.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/WorkflowHandlerFactory.java similarity index 95% rename from src/main/java/zw/qantra/tm/domain/workflows/handlers/WorkflowHandlerFactory.java rename to src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/WorkflowHandlerFactory.java index 492e078..d92e5e8 100644 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/WorkflowHandlerFactory.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/WorkflowHandlerFactory.java @@ -1,4 +1,4 @@ -package zw.qantra.tm.domain.workflows.handlers; +package zw.qantra.tm.domain.services.processors.workflows.handlers; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/JournalEntryHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/JournalEntryHandler.java deleted file mode 100644 index ce06c59..0000000 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/JournalEntryHandler.java +++ /dev/null @@ -1,154 +0,0 @@ -package zw.qantra.tm.domain.workflows.handlers.erp; - -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 zw.qantra.tm.domain.dtos.erp.JournalEntryAccountDto; -import zw.qantra.tm.domain.dtos.erp.JournalEntryDto; -import zw.qantra.tm.domain.enums.RequestType; -import zw.qantra.tm.domain.enums.Status; -import zw.qantra.tm.domain.models.Transaction; -import zw.qantra.tm.domain.models.TransactionEvent; -import zw.qantra.tm.domain.models.User; -import zw.qantra.tm.domain.repositories.TransactionRepository; -import zw.qantra.tm.domain.services.TransactionEventService; -import zw.qantra.tm.domain.services.UserService; -import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; -import zw.qantra.tm.domain.workflows.handlers.HandlerInterface; -import zw.qantra.tm.exceptions.ApiException; -import zw.qantra.tm.rest.RestService; - -import java.nio.charset.StandardCharsets; -import java.util.*; - -@Service("journalEntry") -@RequiredArgsConstructor -public class JournalEntryHandler implements HandlerInterface { - Logger logger = LoggerFactory.getLogger(JournalEntryHandler.class); - - private final RestService restService; - private final UserService userService; - private final TransactionEventService transactionEventService; - private final TransactionRepository transactionRepository; - - @Value("${erpnext.url}") - private String erpnextUrl; - @Value("${erpnext.api.key}") - private String erpnextApiKey; - @Value("${erpnext.api.secret}") - private String erpnextApiSecret; - @Value("${erpnext.username}") - private String erpnextUsername; - @Value("${erpnext.password}") - private String erpnextPassword; - @Value("${erpnext.company}") - private String erpnextCompany; - @Value("${erpnext.accounts.gatewayexpense}") - private String gatewayExpenseAccount; - @Value("${erpnext.accounts.accountsreceivable}") - private String accountsReceivableAccount; - @Value("${erpnext.accounts.cash}") - private String cashAccount; - @Value("${erpnext.cost.center}") - private String costCenter; - - @Override - public Transaction process(Transaction transaction) { - logger.info("Processing journal entry for transaction: {}", transaction.getReference()); - - if(transaction.getErpJournalRef() != null) - return transaction; - - TransactionEvent transactionEvent = TransactionEvent.builder() - .transactionId(transaction.getId().toString()) - .event(RequestType.JOURNAL_ENTRY.name()) - .status(Status.PROCESSING) - .build(); - transactionEventService.save(transactionEvent); - - if(transaction.getStatus() == Status.SUCCESS){ - try { - logger.info("Creating journal entry in ERPNext for successful transaction"); - - // login to erp - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", "application/json"); - headers.add("Accept", "application/json"); - - Map loginRequest = Map.of( - "usr", erpnextUsername, - "pwd", erpnextPassword); - - String sid = ""; - - try { - ResponseEntity response = restService.postAsEntity( - erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); - - sid = response.getHeaders().getFirst("Set-Cookie"); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to login to ERPNext"); - } - - // create customer in erp - String basicAuth = "Basic " + Base64.getEncoder().encodeToString( - (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); - headers.add("Authorization", basicAuth); - headers.add("Cookie", sid); - - JournalEntryAccountDto expenseEntry = JournalEntryAccountDto.builder() - .doctype("Journal Entry Account") - .account(gatewayExpenseAccount) - .debitInAccountCurrency(transaction.getGatewayCharge()) - .costCenter(costCenter) - .build(); - JournalEntryAccountDto cashAccEntry = JournalEntryAccountDto.builder() - .doctype("Journal Entry Account") - .account(cashAccount) - .creditInAccountCurrency(transaction.getGatewayCharge()) - .build(); - - JournalEntryDto journalEntryDto = JournalEntryDto.builder() - .doctype("Journal Entry") - .company(erpnextCompany) - .postingDate(transaction.getCreatedAt().toLocalDate().toString()) - .docstatus(1) - .accounts(Arrays.asList(cashAccEntry, expenseEntry)) - .build(); - - try { - LinkedHashMap response = restService.postAs( - erpnextUrl + "/v2/document/Journal Entry", journalEntryDto, headers, LinkedHashMap.class); - - LinkedHashMap data = (LinkedHashMap) response.get("data"); - String transactionId = (String) data.get("name"); - - transaction.setErpJournalRef(transactionId); - transactionEvent.setStatus(Status.SUCCESS); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to create journal entry in ERPNext"); - } - - logger.info("journal entry created successfully in ERPNext"); - } catch (Exception e) { - logger.error("Failed to create journal entry in ERPNext: {}", e.getMessage(), e); - // Don't fail the transaction, just log the error - // The ERP invoice creation is a side effect, not critical to the payment flow - transactionEvent.setStatus(Status.FAILED); - transactionEvent.setMessage(e.getMessage()); - } - } else { - logger.info("Transaction status is not SUCCESS ({}), skipping journal entry creation", transaction.getStatus()); - } - - transactionEventService.save(transactionEvent); - - return transactionRepository.save(transaction); - } -} diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PaymentEntryHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PaymentEntryHandler.java deleted file mode 100644 index b72661a..0000000 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PaymentEntryHandler.java +++ /dev/null @@ -1,160 +0,0 @@ -package zw.qantra.tm.domain.workflows.handlers.erp; - -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 zw.qantra.tm.domain.dtos.erp.PaymentEntryDto; -import zw.qantra.tm.domain.dtos.erp.PaymentReferenceDto; -import zw.qantra.tm.domain.enums.RequestType; -import zw.qantra.tm.domain.enums.Status; -import zw.qantra.tm.domain.models.Transaction; -import zw.qantra.tm.domain.models.TransactionEvent; -import zw.qantra.tm.domain.models.User; -import zw.qantra.tm.domain.repositories.TransactionRepository; -import zw.qantra.tm.domain.services.TransactionEventService; -import zw.qantra.tm.domain.services.UserService; -import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; -import zw.qantra.tm.domain.workflows.handlers.HandlerInterface; -import zw.qantra.tm.exceptions.ApiException; -import zw.qantra.tm.rest.RestService; - -import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; -import java.util.*; - -@Service("salesPayment") -@RequiredArgsConstructor -public class PaymentEntryHandler implements HandlerInterface { - Logger logger = LoggerFactory.getLogger(PaymentEntryHandler.class); - - private final RestService restService; - private final UserService userService; - private final TransactionEventService transactionEventService; - private final TransactionRepository transactionRepository; - - @Value("${erpnext.url}") - private String erpnextUrl; - @Value("${erpnext.api.key}") - private String erpnextApiKey; - @Value("${erpnext.api.secret}") - private String erpnextApiSecret; - @Value("${erpnext.username}") - private String erpnextUsername; - @Value("${erpnext.password}") - private String erpnextPassword; - @Value("${erpnext.company}") - private String erpnextCompany; - @Value("${erpnext.accounts.accountsreceivable}") - private String accountsReceivableAccount; - @Value("${erpnext.accounts.cash}") - private String cashAccount; - - @Override - public Transaction process(Transaction transaction) { - logger.info("Processing payment entry for transaction: {}", transaction.getReference()); - - if(transaction.getErpSalesPaymentRef() != null) - return transaction; - - TransactionEvent transactionEvent = TransactionEvent.builder() - .transactionId(transaction.getId().toString()) - .event(RequestType.SALES_PAYMENT.name()) - .status(Status.PROCESSING) - .build(); - transactionEventService.save(transactionEvent); - - if(transaction.getPaymentStatus() == Status.SUCCESS){ - try { - logger.info("Creating payment entry in ERPNext for successful transaction"); - - // login to erp - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", "application/json"); - headers.add("Accept", "application/json"); - - Map loginRequest = Map.of( - "usr", erpnextUsername, - "pwd", erpnextPassword); - - String sid = ""; - - try { - ResponseEntity response = restService.postAsEntity( - erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); - - sid = response.getHeaders().getFirst("Set-Cookie"); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to login to ERPNext"); - } - - // create customer in erp - String basicAuth = "Basic " + Base64.getEncoder().encodeToString( - (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); - headers.add("Authorization", basicAuth); - headers.add("Cookie", sid); - - String erpCustomerRef = ""; - User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null); - erpCustomerRef = (user == null) ? "Anonymous" : user.getErpCustomerRef(); - - BigDecimal amount = transaction.getAmount().add(transaction.getGatewayCharge()); - - PaymentReferenceDto paymentReferenceDto = PaymentReferenceDto.builder() - .referenceDoctype("Sales Invoice") - .referenceName(transaction.getErpSalesRef()) - .allocatedAmount(amount) - .build(); - - PaymentEntryDto paymentEntryDto = PaymentEntryDto.builder() - .doctype("Payment Entry") - .company(erpnextCompany) - .docstatus(1) - .paymentType("Receive") - .party(erpCustomerRef) - .partyType("Customer") - .paidFrom(accountsReceivableAccount) - .paidTo(cashAccount) - .paidAmount(amount) - .receivedAmount(amount) - .referenceNo(transaction.getId().toString()) - .referenceDate(transaction.getCreatedAt().toLocalDate().toString()) - .postingDate(transaction.getCreatedAt().toLocalDate().toString()) - .references(Arrays.asList(paymentReferenceDto)) - .build(); - - try { - LinkedHashMap response = restService.postAs( - erpnextUrl + "/v2/document/Payment Entry", paymentEntryDto, headers, LinkedHashMap.class); - - LinkedHashMap data = (LinkedHashMap) response.get("data"); - String transactionId = (String) data.get("name"); - - transaction.setErpSalesPaymentRef(transactionId); - transactionEvent.setStatus(Status.SUCCESS); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to create payment entry in ERPNext"); - } - - logger.info("payment entry created successfully in ERPNext"); - } catch (Exception e) { - logger.error("Failed to create payment entry in ERPNext: {}", e.getMessage(), e); - // Don't fail the transaction, just log the error - // The ERP invoice creation is a side effect, not critical to the payment flow - transactionEvent.setStatus(Status.FAILED); - transactionEvent.setMessage(e.getMessage()); - } - } else { - logger.info("Transaction status is not SUCCESS ({}), skipping payment entry creation", transaction.getStatus()); - } - - transactionEventService.save(transactionEvent); - - return transactionRepository.save(transaction); - } -} diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PurchaseInvoiceHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PurchaseInvoiceHandler.java deleted file mode 100644 index 6952e2f..0000000 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PurchaseInvoiceHandler.java +++ /dev/null @@ -1,187 +0,0 @@ -package zw.qantra.tm.domain.workflows.handlers.erp; - -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 zw.qantra.tm.domain.dtos.erp.PurchaseInvoiceDto; -import zw.qantra.tm.domain.dtos.erp.PurchaseInvoiceItemDto; -import zw.qantra.tm.domain.dtos.erp.PurchaseTaxDto; -import zw.qantra.tm.domain.enums.RequestType; -import zw.qantra.tm.domain.enums.Status; -import zw.qantra.tm.domain.models.Provider; -import zw.qantra.tm.domain.models.Transaction; -import zw.qantra.tm.domain.models.TransactionEvent; -import zw.qantra.tm.domain.models.User; -import zw.qantra.tm.domain.repositories.TransactionRepository; -import zw.qantra.tm.domain.services.ProviderService; -import zw.qantra.tm.domain.services.TransactionEventService; -import zw.qantra.tm.domain.services.UserService; -import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; -import zw.qantra.tm.domain.workflows.handlers.HandlerInterface; -import zw.qantra.tm.exceptions.ApiException; -import zw.qantra.tm.rest.RestService; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.nio.charset.StandardCharsets; -import java.util.*; - -import static java.time.OffsetDateTime.now; - -@Service("purchaseInvoice") -@RequiredArgsConstructor -public class PurchaseInvoiceHandler implements HandlerInterface { - Logger logger = LoggerFactory.getLogger(PurchaseInvoiceHandler.class); - - private final RestService restService; - private final UserService userService; - private final TransactionEventService transactionEventService; - private final TransactionRepository transactionRepository; - private final ProviderService providerService; - - @Value("${erpnext.url}") - private String erpnextUrl; - @Value("${erpnext.api.key}") - private String erpnextApiKey; - @Value("${erpnext.api.secret}") - private String erpnextApiSecret; - @Value("${erpnext.username}") - private String erpnextUsername; - @Value("${erpnext.password}") - private String erpnextPassword; - @Value("${erpnext.company}") - private String erpnextCompany; - @Value("${erpnext.cost.center}") - private String erpnextCostCenter; - @Value("${erpnext.accounts.commission}") - private String erpnextCommissionAccount; - - @Override - public Transaction process(Transaction transaction) { - logger.info("Processing purchase invoice for transaction: {}", transaction.getReference()); - - if(transaction.getErpPurchaseRef() != null) - return transaction; - - TransactionEvent transactionEvent = TransactionEvent.builder() - .transactionId(transaction.getId().toString()) - .event(RequestType.PURCHASE_INVOICE.name()) - .status(Status.PROCESSING) - .build(); - transactionEventService.save(transactionEvent); - - if(transaction.getErpPurchaseRef() != null) - return transaction; - - if(transaction.getStatus() == Status.SUCCESS){ - try { - logger.info("Creating purchase invoice in ERPNext for successful transaction"); - - // login to erp - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", "application/json"); - headers.add("Accept", "application/json"); - - Map loginRequest = Map.of( - "usr", erpnextUsername, - "pwd", erpnextPassword); - - String sid = ""; - - try { - ResponseEntity response = restService.postAsEntity( - erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); - - sid = response.getHeaders().getFirst("Set-Cookie"); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to login to ERPNext"); - } - - // create customer in erp - String basicAuth = "Basic " + Base64.getEncoder().encodeToString( - (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); - headers.add("Authorization", basicAuth); - headers.add("Cookie", sid); - - PurchaseInvoiceItemDto purchaseInvoiceItemDto = PurchaseInvoiceItemDto.builder() - .doctype("Purchase Invoice Item") - .uom("Nos") - .itemCode(transaction.getProviderLabel()) - .itemName(transaction.getProviderLabel()) - .qty(transaction.getAmount()) - .rate(1) - .amount(transaction.getAmount()) - .build(); - - Provider provider = providerService.getProviderRepository() - .findByClientId(transaction.getBillClientId()); - BigDecimal commissionPercentage = BigDecimal.valueOf(provider.getPercentageCommission()) - .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_EVEN); - BigDecimal commission = transaction.getAmount().multiply(commissionPercentage) - .setScale(2, RoundingMode.HALF_EVEN); - BigDecimal amount = transaction.getAmount().subtract(commission); - - PurchaseTaxDto purchaseTaxDto = PurchaseTaxDto.builder() - .doctype("Purchase Taxes and Charges") - .chargeType("On Net Total") - .accountHead(erpnextCommissionAccount) - .rate(BigDecimal.valueOf(provider.getPercentageCommission()).negate()) - .costCenter(erpnextCostCenter) - .taxAmount(commission.negate()) - .total(amount) - .description("Commission") - .build(); - - PurchaseInvoiceDto purchaseInvoiceDto = PurchaseInvoiceDto.builder() - .doctype("Purchase Invoice") - .supplier("Steward Bank") - .company(erpnextCompany) - .postingDate(now().toLocalDate().toString()) - .dueDate(now().toLocalDate().plusDays(1).toString()) - .docstatus(1) - .isReturn(0) - .isPaid(0) - .currency(transaction.getDebitCurrency().toString()) - .conversionRate(1) - .netTotal(amount) - .grandTotal(amount) - .items(Collections.singletonList(purchaseInvoiceItemDto)) - .taxes(Collections.singletonList(purchaseTaxDto)) - .build(); - - try { - LinkedHashMap response = restService.postAs( - erpnextUrl + "/v2/document/Purchase Invoice", purchaseInvoiceDto, headers, LinkedHashMap.class); - - LinkedHashMap data = (LinkedHashMap) response.get("data"); - String transactionId = (String) data.get("name"); - - transaction.setErpPurchaseRef(transactionId); - transactionEvent.setStatus(Status.SUCCESS); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to create purchase invoice in ERPNext"); - } - - logger.info("purchase invoice created successfully in ERPNext"); - } catch (Exception e) { - logger.error("Failed to create purchase invoice in ERPNext: {}", e.getMessage(), e); - // Don't fail the transaction, just log the error - // The ERP invoice creation is a side effect, not critical to the payment flow - transactionEvent.setStatus(Status.FAILED); - transactionEvent.setMessage(e.getMessage()); - } - } else { - logger.info("Transaction status is not SUCCESS ({}), skipping purchase invoice creation", transaction.getStatus()); - } - - transactionEventService.save(transactionEvent); - - return transactionRepository.save(transaction); - } -} diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PurchasePaymentEntryHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PurchasePaymentEntryHandler.java deleted file mode 100644 index e9266e8..0000000 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/PurchasePaymentEntryHandler.java +++ /dev/null @@ -1,173 +0,0 @@ -package zw.qantra.tm.domain.workflows.handlers.erp; - -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 zw.qantra.tm.domain.dtos.erp.PaymentEntryDto; -import zw.qantra.tm.domain.dtos.erp.PaymentReferenceDto; -import zw.qantra.tm.domain.enums.RequestType; -import zw.qantra.tm.domain.enums.Status; -import zw.qantra.tm.domain.models.Provider; -import zw.qantra.tm.domain.models.Transaction; -import zw.qantra.tm.domain.models.TransactionEvent; -import zw.qantra.tm.domain.models.User; -import zw.qantra.tm.domain.repositories.TransactionRepository; -import zw.qantra.tm.domain.services.ProviderService; -import zw.qantra.tm.domain.services.TransactionEventService; -import zw.qantra.tm.domain.services.UserService; -import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; -import zw.qantra.tm.domain.workflows.handlers.HandlerInterface; -import zw.qantra.tm.exceptions.ApiException; -import zw.qantra.tm.rest.RestService; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.nio.charset.StandardCharsets; -import java.util.*; - -@Service("purchaseInvoicePayment") -@RequiredArgsConstructor -public class PurchasePaymentEntryHandler implements HandlerInterface { - Logger logger = LoggerFactory.getLogger(PurchasePaymentEntryHandler.class); - - private final RestService restService; - private final UserService userService; - private final TransactionEventService transactionEventService; - private final TransactionRepository transactionRepository; - private final ProviderService providerService; - - @Value("${erpnext.url}") - private String erpnextUrl; - @Value("${erpnext.api.key}") - private String erpnextApiKey; - @Value("${erpnext.api.secret}") - private String erpnextApiSecret; - @Value("${erpnext.username}") - private String erpnextUsername; - @Value("${erpnext.password}") - private String erpnextPassword; - @Value("${erpnext.company}") - private String erpnextCompany; - @Value("${erpnext.accounts.accountsreceivable}") - private String accountsReceivableAccount; - @Value("${erpnext.accounts.cash}") - private String cashAccount; - @Value("${erpnext.accounts.payable}") - private String payableAccount; - - @Override - public Transaction process(Transaction transaction) { - logger.info("Processing purchase payment entry for transaction: {}", transaction.getReference()); - - if(transaction.getErpPurchasePaymentRef() != null) - return transaction; - - TransactionEvent transactionEvent = TransactionEvent.builder() - .transactionId(transaction.getId().toString()) - .event(RequestType.PURCHASE_PAYMENT.name()) - .status(Status.PROCESSING) - .build(); - transactionEventService.save(transactionEvent); - - if(transaction.getErpPurchasePaymentRef() != null) - return transaction; - - if(transaction.getStatus() == Status.SUCCESS){ - try { - logger.info("Creating purchase payment entry in ERPNext for successful transaction"); - - // login to erp - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", "application/json"); - headers.add("Accept", "application/json"); - - Map loginRequest = Map.of( - "usr", erpnextUsername, - "pwd", erpnextPassword); - - String sid = ""; - - try { - ResponseEntity response = restService.postAsEntity( - erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); - - sid = response.getHeaders().getFirst("Set-Cookie"); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to login to ERPNext"); - } - - // create customer in erp - String basicAuth = "Basic " + Base64.getEncoder().encodeToString( - (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); - headers.add("Authorization", basicAuth); - headers.add("Cookie", sid); - - Provider provider = providerService.getProviderRepository() - .findByClientId(transaction.getBillClientId()); - BigDecimal commissionPercentage = BigDecimal.valueOf(provider.getPercentageCommission()) - .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_EVEN); - BigDecimal commission = transaction.getAmount().multiply(commissionPercentage) - .setScale(2, RoundingMode.HALF_EVEN); - BigDecimal amount = transaction.getAmount().subtract(commission); - - PaymentReferenceDto paymentReferenceDto = PaymentReferenceDto.builder() - .referenceDoctype("Purchase Invoice") - .referenceName(transaction.getErpPurchaseRef()) - .allocatedAmount(amount) - .build(); - - String refNo = transaction.getCreditRef().isBlank() ? transaction.getId().toString() : transaction.getCreditRef(); - - PaymentEntryDto paymentEntryDto = PaymentEntryDto.builder() - .doctype("Payment Entry") - .company(erpnextCompany) - .docstatus(1) - .paymentType("Pay") - .party("Steward Bank") - .partyType("Supplier") - .paidFrom(cashAccount) - .paidTo(payableAccount) - .paidAmount(amount) - .receivedAmount(amount) - .referenceNo(refNo) - .referenceDate(transaction.getCreatedAt().toLocalDate().toString()) - .postingDate(transaction.getCreatedAt().toLocalDate().toString()) - .references(Arrays.asList(paymentReferenceDto)) - .build(); - - try { - LinkedHashMap response = restService.postAs( - erpnextUrl + "/v2/document/Payment Entry", paymentEntryDto, headers, LinkedHashMap.class); - - LinkedHashMap data = (LinkedHashMap) response.get("data"); - String transactionId = (String) data.get("name"); - - transaction.setErpPurchasePaymentRef(transactionId); - transactionEvent.setStatus(Status.SUCCESS); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to create purchase payment entry in ERPNext"); - } - - logger.info("purchase payment entry created successfully in ERPNext"); - } catch (Exception e) { - logger.error("Failed to create purchase payment entry in ERPNext: {}", e.getMessage(), e); - // Don't fail the transaction, just log the error - // The ERP invoice creation is a side effect, not critical to the payment flow - transactionEvent.setStatus(Status.FAILED); - transactionEvent.setMessage(e.getMessage()); - } - } else { - logger.info("Transaction status is not SUCCESS ({}), skipping purchase payment entry creation", transaction.getStatus()); - } - - transactionEventService.save(transactionEvent); - - return transactionRepository.save(transaction); - } -} diff --git a/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/SalesInvoiceHandler.java b/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/SalesInvoiceHandler.java deleted file mode 100644 index f929127..0000000 --- a/src/main/java/zw/qantra/tm/domain/workflows/handlers/erp/SalesInvoiceHandler.java +++ /dev/null @@ -1,146 +0,0 @@ -package zw.qantra.tm.domain.workflows.handlers.erp; - -import lombok.RequiredArgsConstructor; -import org.joda.time.LocalDate; -import org.joda.time.LocalDateTime; -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.transaction.annotation.Transactional; -import zw.qantra.tm.domain.dtos.erp.SalesInvoiceDto; -import zw.qantra.tm.domain.dtos.erp.SalesInvoiceItemDto; -import zw.qantra.tm.domain.enums.RequestType; -import zw.qantra.tm.domain.enums.Status; -import zw.qantra.tm.domain.models.Transaction; -import zw.qantra.tm.domain.models.TransactionEvent; -import zw.qantra.tm.domain.models.User; -import zw.qantra.tm.domain.repositories.TransactionRepository; -import zw.qantra.tm.domain.services.TransactionEventService; -import zw.qantra.tm.domain.services.TransactionService; -import zw.qantra.tm.domain.services.UserService; -import zw.qantra.tm.domain.services.processors.handlers.LogicHandler; -import zw.qantra.tm.domain.workflows.handlers.HandlerInterface; -import zw.qantra.tm.exceptions.ApiException; -import zw.qantra.tm.rest.RestService; - -import java.nio.charset.StandardCharsets; -import java.util.*; - -@Service("salesInvoice") -@RequiredArgsConstructor -public class SalesInvoiceHandler implements HandlerInterface { - Logger logger = LoggerFactory.getLogger(SalesInvoiceHandler.class); - - private final RestService restService; - private final UserService userService; - private final TransactionEventService transactionEventService; - private final TransactionService transactionService; - - @Value("${erpnext.url}") - private String erpnextUrl; - @Value("${erpnext.api.key}") - private String erpnextApiKey; - @Value("${erpnext.api.secret}") - private String erpnextApiSecret; - @Value("${erpnext.username}") - private String erpnextUsername; - @Value("${erpnext.password}") - private String erpnextPassword; - @Value("${erpnext.company}") - private String erpnextCompany; - - @Override - @Transactional - public Transaction process(Transaction transaction) { - logger.info("Processing sales invoice for transaction: {}", transaction.getReference()); - - if(transaction.getErpSalesRef() != null) - return transaction; - - TransactionEvent transactionEvent = TransactionEvent.builder() - .transactionId(transaction.getId().toString()) - .event(RequestType.SALES_INVOICE.name()) - .status(Status.PROCESSING) - .build(); - transactionEventService.save(transactionEvent); - - try { - logger.info("Creating sales invoice in ERPNext for successful transaction"); - - // login to erp - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", "application/json"); - headers.add("Accept", "application/json"); - - Map loginRequest = Map.of( - "usr", erpnextUsername, - "pwd", erpnextPassword); - - String sid = ""; - - try { - ResponseEntity response = restService.postAsEntity( - erpnextUrl + "/method/login", loginRequest, headers, LinkedHashMap.class); - - sid = response.getHeaders().getFirst("Set-Cookie"); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to login to ERPNext"); - } - - // create customer in erp - String basicAuth = "Basic " + Base64.getEncoder().encodeToString( - (erpnextApiKey + ":" + erpnextApiSecret).getBytes(StandardCharsets.UTF_8)); - headers.add("Authorization", basicAuth); - headers.add("Cookie", sid); - - String erpCustomerRef = ""; - User user = userService.getUserRepository().findById(UUID.fromString(transaction.getUserId())).orElse(null); - erpCustomerRef = (user == null) ? "Anonymous" : user.getErpCustomerRef(); - - SalesInvoiceDto salesInvoiceDto = SalesInvoiceDto.builder() - .doctype("Sales Invoice") - .customer(erpCustomerRef) - .postingDate(new LocalDateTime().toLocalDate().toString()) - .dueDate(new LocalDateTime().plusDays(1).toLocalDate().toString()) - .company(erpnextCompany) - .docstatus(1) - .items(Arrays.asList(SalesInvoiceItemDto.builder() - .itemCode(transaction.getProviderLabel()) - .qty(1) - .rate(transaction.getAmount().add(transaction.getGatewayCharge())) - .amount(transaction.getAmount().add(transaction.getGatewayCharge())) - .build())) - .build(); - - try { - LinkedHashMap response = restService.postAs( - erpnextUrl + "/v2/document/Sales Invoice", salesInvoiceDto, headers, LinkedHashMap.class); - - LinkedHashMap data = (LinkedHashMap) response.get("data"); - String transactionId = (String) data.get("name"); - - transaction.setErpSalesRef(transactionId); - transactionEvent.setStatus(Status.SUCCESS); - } catch (Exception e) { - e.printStackTrace(); - throw new ApiException("Failed to create sales invoice in ERPNext"); - } - - logger.info("Sales invoice created successfully in ERPNext"); - } catch (Exception e) { - logger.error("Failed to create sales invoice in ERPNext: {}", e.getMessage(), e); - // Don't fail the transaction, just log the error - // The ERP invoice creation is a side effect, not critical to the payment flow - transactionEvent.setStatus(Status.FAILED); - transactionEvent.setMessage(e.getMessage()); - } - - transactionEventService.save(transactionEvent); - - return transactionService.save(transaction); - } -} diff --git a/src/main/java/zw/qantra/tm/exceptions/AlreadyExistsException.java b/src/main/java/zw/qantra/tm/exceptions/AlreadyExistsException.java new file mode 100755 index 0000000..d01ffad --- /dev/null +++ b/src/main/java/zw/qantra/tm/exceptions/AlreadyExistsException.java @@ -0,0 +1,15 @@ +package zw.qantra.tm.exceptions; + +public class AlreadyExistsException extends RuntimeException { + + private String message; + + public AlreadyExistsException(String message) { + this.message = message; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/zw/qantra/tm/rest/RestService.java b/src/main/java/zw/qantra/tm/rest/RestService.java index c3963d4..752628f 100755 --- a/src/main/java/zw/qantra/tm/rest/RestService.java +++ b/src/main/java/zw/qantra/tm/rest/RestService.java @@ -232,8 +232,8 @@ public class RestService { return this.restTemplate.getForObject(path, clazz); } - public ResponseEntity exchangeGetNoAuth(String path, HttpEntity httpEntity, Class clazz, String pathVariable) { - return this.restTemplate.exchange(path, HttpMethod.GET, httpEntity, clazz, pathVariable); + public ResponseEntity exchange(String path, HttpMethod method, HttpEntity objectHttpEntity, Class jsonNodeClass) { + return this.restTemplate.exchange(path, method, objectHttpEntity, jsonNodeClass); } private String prettyPrintJson(String json) { diff --git a/src/main/java/zw/qantra/tm/seeders/CategorySeeder.java b/src/main/java/zw/qantra/tm/seeders/CategorySeeder.java new file mode 100644 index 0000000..892c730 --- /dev/null +++ b/src/main/java/zw/qantra/tm/seeders/CategorySeeder.java @@ -0,0 +1,48 @@ +package zw.qantra.tm.seeders; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import zw.qantra.tm.domain.models.Category; +import zw.qantra.tm.domain.repositories.CategoryRepository; + +import java.util.Arrays; +import java.util.List; + +/** + * Seeds Category entities. + * Priority: 3rd - Must run after IntegrationProcessorSeeder and before ProviderSeeder. + * Only seeds entities that don't already exist (checked by name). + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class CategorySeeder implements Seeder { + + private final CategoryRepository categoryRepository; + + @Override + public void seed() { + List categories = Arrays.asList( + Category.builder() + .name("Airtime") + .description("Airtime for local mobile networks") + .label("AIRTIME") + .build(), + Category.builder() + .name("Utilities") + .description("Council bills & rates") + .label("UTILITIES") + .build() + ); + + for (Category category : categories) { + if (categoryRepository.existsByName(category.getName())) { + log.info("Category '{}' already exists, skipping.", category.getName()); + } else { + categoryRepository.save(category); + log.info("Category '{}' seeded.", category.getName()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/seeders/ChargeConditionSeeder.java b/src/main/java/zw/qantra/tm/seeders/ChargeConditionSeeder.java new file mode 100644 index 0000000..2a95158 --- /dev/null +++ b/src/main/java/zw/qantra/tm/seeders/ChargeConditionSeeder.java @@ -0,0 +1,54 @@ +package zw.qantra.tm.seeders; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import zw.qantra.tm.domain.enums.ChargeConditionType; +import zw.qantra.tm.domain.models.ChargeCondition; +import zw.qantra.tm.domain.repositories.ChargeConditionRepository; + +import java.util.Arrays; +import java.util.List; + +/** + * Seeds ChargeCondition entities. + * Priority: 5th - Must run after ProviderSeeder and before ChargeSeeder. + * Only seeds entities that don't already exist (checked by codes). + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ChargeConditionSeeder implements Seeder { + + private final ChargeConditionRepository chargeConditionRepository; + + @Override + public void seed() { + List chargeConditions = Arrays.asList( + ChargeCondition.builder() + .name("Ecocash Gateway") + .type(ChargeConditionType.GATEWAY) + .codes("ECOCASH") + .build(), + ChargeCondition.builder() + .name("Steward Gateway") + .type(ChargeConditionType.GATEWAY) + .codes("STEWARD") + .build(), + ChargeCondition.builder() + .name("Mastercard Gateway") + .type(ChargeConditionType.GATEWAY) + .codes("MPGS") + .build() + ); + + for (ChargeCondition condition : chargeConditions) { + if (chargeConditionRepository.existsByCodes(condition.getCodes())) { + log.info("ChargeCondition '{}' already exists, skipping.", condition.getCodes()); + } else { + chargeConditionRepository.save(condition); + log.info("ChargeCondition '{}' seeded.", condition.getCodes()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/seeders/ChargeSeeder.java b/src/main/java/zw/qantra/tm/seeders/ChargeSeeder.java new file mode 100644 index 0000000..7e7fa61 --- /dev/null +++ b/src/main/java/zw/qantra/tm/seeders/ChargeSeeder.java @@ -0,0 +1,72 @@ +package zw.qantra.tm.seeders; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import zw.qantra.tm.domain.enums.ChargeLabelEnum; +import zw.qantra.tm.domain.enums.CurrencyType; +import zw.qantra.tm.domain.models.Charge; +import zw.qantra.tm.domain.models.ChargeCondition; +import zw.qantra.tm.domain.repositories.ChargeConditionRepository; +import zw.qantra.tm.domain.repositories.ChargeRepository; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashSet; + +/** + * Seeds Charge entities. + * Priority: 6th - Must run last, after ChargeConditionSeeder. + * Only seeds entities that don't already exist (checked by chargeLabel, currency, and percentageRate). + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ChargeSeeder implements Seeder { + + private final ChargeRepository chargeRepository; + private final ChargeConditionRepository chargeConditionRepository; + + @Override + public void seed() { + // Look up persisted ChargeConditions by their codes + ChargeCondition ecocashCondition = chargeConditionRepository.findByCodes("ECOCASH").orElse(null); + ChargeCondition mpgsCondition = chargeConditionRepository.findByCodes("MPGS").orElse(null); + + if (ecocashCondition == null || mpgsCondition == null) { + log.warn("Required ChargeConditions not found. Skipping Charge seeding."); + return; + } + + // Ecocash Gateway Fee (1%) + if (chargeRepository.existsByChargeLabelAndCurrencyAndPercentageRate( + ChargeLabelEnum.GATEWAY_FEE, CurrencyType.USD, new BigDecimal("1"))) { + log.info("Charge 'Gateway fee (1%)' already exists, skipping."); + } else { + chargeRepository.save(Charge.builder() + .description("Gateway fee") + .chargeLabel(ChargeLabelEnum.GATEWAY_FEE) + .currency(CurrencyType.USD) + .percentageRate(new BigDecimal("1")) + .includes(new HashSet<>(Arrays.asList(ecocashCondition))) + .build()); + log.info("Charge 'Gateway fee (1%)' seeded."); + } + + // MPGS Gateway Fee (2%, min 0.55) + if (chargeRepository.existsByChargeLabelAndCurrencyAndPercentageRate( + ChargeLabelEnum.GATEWAY_FEE, CurrencyType.USD, new BigDecimal("2"))) { + log.info("Charge 'Gateway fee (2%)' already exists, skipping."); + } else { + chargeRepository.save(Charge.builder() + .description("Gateway fee") + .chargeLabel(ChargeLabelEnum.GATEWAY_FEE) + .currency(CurrencyType.USD) + .percentageRate(new BigDecimal("1")) + .min(new BigDecimal("0.20")) + .includes(new HashSet<>(Arrays.asList(mpgsCondition))) + .build()); + log.info("Charge 'Gateway fee (2%)' seeded."); + } + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/seeders/IntegrationProcessorSeeder.java b/src/main/java/zw/qantra/tm/seeders/IntegrationProcessorSeeder.java new file mode 100644 index 0000000..71de139 --- /dev/null +++ b/src/main/java/zw/qantra/tm/seeders/IntegrationProcessorSeeder.java @@ -0,0 +1,48 @@ +package zw.qantra.tm.seeders; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import zw.qantra.tm.domain.models.IntegrationProcessor; +import zw.qantra.tm.domain.repositories.IntegrationProcessorRepository; + +import java.util.Arrays; +import java.util.List; + +/** + * Seeds IntegrationProcessor entities. + * Priority: 2nd - Must run after PaymentProcessorSeeder. + * Only seeds entities that don't already exist (checked by name). + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class IntegrationProcessorSeeder implements Seeder { + + private final IntegrationProcessorRepository integrationProcessorRepository; + + @Override + public void seed() { + List processors = Arrays.asList( + IntegrationProcessor.builder() + .name("HOT") + .label("HOT") + .description("Hot Recharge Integration Processor") + .build(), + IntegrationProcessor.builder() + .name("STEWARD") + .label("STEWARD") + .description("Steward Integration Processor") + .build() + ); + + for (IntegrationProcessor processor : processors) { + if (integrationProcessorRepository.existsByName(processor.getName())) { + log.info("IntegrationProcessor '{}' already exists, skipping.", processor.getName()); + } else { + integrationProcessorRepository.save(processor); + log.info("IntegrationProcessor '{}' seeded.", processor.getName()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/seeders/LiveConfigSeeder.java b/src/main/java/zw/qantra/tm/seeders/LiveConfigSeeder.java new file mode 100644 index 0000000..14b9454 --- /dev/null +++ b/src/main/java/zw/qantra/tm/seeders/LiveConfigSeeder.java @@ -0,0 +1,65 @@ +package zw.qantra.tm.seeders; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Configuration; + +/** + * Orchestrates all seeders in the correct priority order matching + * ConfigController.seedLiveConfig() execution sequence: + *

+ * 1. PaymentProcessorSeeder + * 2. IntegrationProcessorSeeder + * 3. CategorySeeder + * 4. ProviderSeeder + * 5. ChargeConditionSeeder + * 6. ChargeSeeder + * 7. SettingSeeder + */ +@Slf4j +@Configuration +@RequiredArgsConstructor +public class LiveConfigSeeder { + + private final PaymentProcessorSeeder paymentProcessorSeeder; + private final IntegrationProcessorSeeder integrationProcessorSeeder; + private final CategorySeeder categorySeeder; + private final ProviderSeeder providerSeeder; + private final ChargeConditionSeeder chargeConditionSeeder; + private final ChargeSeeder chargeSeeder; + private final SettingSeeder settingSeeder; + + /** + * Runs all seeders in the correct priority order. + * This method is idempotent and can be called multiple times. + */ + @PostConstruct + public void seedAll() { + log.info("Starting live configuration seeding..."); + + log.info("Seeding Payment Processors..."); + paymentProcessorSeeder.seed(); + + log.info("Seeding Integration Processors..."); + integrationProcessorSeeder.seed(); + + log.info("Seeding Categories..."); + categorySeeder.seed(); + + log.info("Seeding Providers..."); + providerSeeder.seed(); + + log.info("Seeding Charge Conditions..."); + chargeConditionSeeder.seed(); + + log.info("Seeding Charges..."); + chargeSeeder.seed(); + + log.info("Seeding Settings..."); + settingSeeder.seed(); + + log.info("Live configuration seeding complete."); + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/seeders/PaymentProcessorSeeder.java b/src/main/java/zw/qantra/tm/seeders/PaymentProcessorSeeder.java new file mode 100644 index 0000000..1a5447e --- /dev/null +++ b/src/main/java/zw/qantra/tm/seeders/PaymentProcessorSeeder.java @@ -0,0 +1,58 @@ +package zw.qantra.tm.seeders; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import zw.qantra.tm.domain.models.PaymentProcessor; +import zw.qantra.tm.domain.repositories.PaymentProcessorRepository; + +import java.util.Arrays; +import java.util.List; + +/** + * Seeds PaymentProcessor entities. + * Priority: 1st - Must run before any other seeder. + * Only seeds entities that don't already exist (checked by label). + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class PaymentProcessorSeeder implements Seeder { + + private final PaymentProcessorRepository paymentProcessorRepository; + + @Override + public void seed() { + List processors = Arrays.asList( + PaymentProcessor.builder() + .name("Ecocash") + .label("ECOCASH") + .type("GATEWAY") + .image("ecocash.png") + .accountFieldName("phoneNumber") + .accountFieldLabel("EcoCash Phone Number") + .description("Pay using your mobile wallet") + .authType("REMOTE") + .build(), + PaymentProcessor.builder() + .name("Visa/MasterCard") + .label("MPGS") + .type("GATEWAY") + .image("visa-mastercard.png") + .accountFieldName("cardNumber") + .accountFieldLabel("Card Number") + .description("Pay using your international card") + .authType("WEB") + .build() + ); + + for (PaymentProcessor processor : processors) { + if (paymentProcessorRepository.existsByLabel(processor.getLabel())) { + log.info("PaymentProcessor '{}' already exists, skipping.", processor.getLabel()); + } else { + paymentProcessorRepository.save(processor); + log.info("PaymentProcessor '{}' seeded.", processor.getLabel()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/seeders/ProviderSeeder.java b/src/main/java/zw/qantra/tm/seeders/ProviderSeeder.java new file mode 100644 index 0000000..d36b63e --- /dev/null +++ b/src/main/java/zw/qantra/tm/seeders/ProviderSeeder.java @@ -0,0 +1,103 @@ +package zw.qantra.tm.seeders; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import zw.qantra.tm.domain.models.Provider; +import zw.qantra.tm.domain.repositories.ProviderRepository; +import zw.qantra.tm.utils.Utils; + +import java.util.Arrays; +import java.util.List; + +/** + * Seeds Provider entities. + * Priority: 4th - Must run after CategorySeeder (providers reference categories). + * Only seeds entities that don't already exist (checked by clientId). + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ProviderSeeder implements Seeder { + + private final ProviderRepository providerRepository; + + @Override + public void seed() { + List providers = Arrays.asList( + Provider.builder() + .name("Econet Airtime") + .description("Econet Airtime") + .clientId("econet_airtime") + .label("ECONET") + .category("AIRTIME") + .image("econet.png") + .externalId("78f1f497-c9eb-401c-b22c-884756e68e40") + .integrationProcessorLabel("HOT") + .accountFieldName("Phone Number") + .percentageCommission(5) + .requiresAccount(true) + .requiresAmount(true) + .requiresPhone(true) + .uid(Utils.getUid()) + .build(), + Provider.builder() + .name("NetOne Airtime") + .description("NetOne Airtime") + .clientId("netone_usd_airtime") + .label("NETONE") + .category("AIRTIME") + .image("netone.png") + .externalId("95d485a7-39c9-4559-8a27-6521969abe8f") + .integrationProcessorLabel("HOT") + .accountFieldName("Phone Number") + .percentageCommission(5) + .requiresAccount(true) + .requiresAmount(true) + .requiresPhone(true) + .uid(Utils.getUid()) + .build(), + Provider.builder() + .name("Zesa Prepaid") + .description("Prepaid Zesa") + .clientId("zesa_prepaid_usd") + .label("ZESA") + .category("UTILITIES") + .image("zesa.png") + .externalId("0f67f7cc-b62b-4fb5-915f-6740b2afdba6") + .integrationProcessorLabel("HOT") + .accountFieldName("Meter Number") + .percentageCommission(1.5) + .requiresAccount(true) + .requiresAmount(true) + .requiresPhone(true) + .uid(Utils.getUid()) + .build(), + Provider.builder() + .name("Econet Bundles") + .description("Econet Bundles") + .clientId("econet_bundles_usd") + .label("ECONET_BUNDLES") + .category("AIRTIME") + .image("econet.png") + .externalId("287863e2-a791-4106-b629-79dadc9a6779") + .integrationProcessorLabel("HOT") + .accountFieldName("Phone Number") + .percentageCommission(5) + .requiresAccount(true) + .requiresAmount(false) + .requiresPhone(true) + .uid(Utils.getUid()) + .build() + ); + + for (Provider provider : providers) { + if (providerRepository.existsByClientId(provider.getClientId())) { + log.info("Provider '{}' already exists, skipping.", provider.getClientId()); + } else { + providerRepository.save(provider); + log.info("Provider '{}' seeded.", provider.getClientId()); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/seeders/Seeder.java b/src/main/java/zw/qantra/tm/seeders/Seeder.java new file mode 100644 index 0000000..f0d078e --- /dev/null +++ b/src/main/java/zw/qantra/tm/seeders/Seeder.java @@ -0,0 +1,10 @@ +package zw.qantra.tm.seeders; + +/** + * Base interface for all seeders. + * Each seeder encapsulates the seeding logic for a specific entity + * following the order defined in the original seedLiveConfig method. + */ +public interface Seeder { + void seed(); +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/seeders/SettingSeeder.java b/src/main/java/zw/qantra/tm/seeders/SettingSeeder.java new file mode 100644 index 0000000..4799c3b --- /dev/null +++ b/src/main/java/zw/qantra/tm/seeders/SettingSeeder.java @@ -0,0 +1,61 @@ +package zw.qantra.tm.seeders; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import zw.qantra.tm.domain.models.Setting; +import zw.qantra.tm.domain.repositories.SettingRepository; + +import java.util.Arrays; +import java.util.List; + +/** + * Seeds Setting entities for simulation flags. + * Uses findBySettingName to check for existing records before seeding. + * When the "lab" Spring profile is active, simulation flags default to "true". + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class SettingSeeder implements Seeder { + + private final SettingRepository settingRepository; + private final Environment environment; + + @Override + public void seed() { + boolean isLab = environment.matchesProfiles("lab"); + String defaultValue = isLab ? "true" : "false"; + + log.info("Lab profile active: {}. Setting simulation defaults to '{}'.", isLab, defaultValue); + + List settings = Arrays.asList( + Setting.builder() + .settingName("SIMULATE_MPGS_SUCCESS") + .settingValue(defaultValue) + .build(), + Setting.builder() + .settingName("SIMULATE_ECOCASH_SUCCESS") + .settingValue(defaultValue) + .build(), + Setting.builder() + .settingName("SIMULATE_HOT_SUCCESS") + .settingValue(defaultValue) + .build(), + Setting.builder() + .settingName("VELOCITY_COMPANY_ID") + .settingValue("d5d381af-f632-464b-a97b-048a8b17f99d") + .build() + ); + + for (Setting setting : settings) { + if (settingRepository.findBySettingName(setting.getSettingName()) != null) { + log.info("Setting '{}' already exists, skipping.", setting.getSettingName()); + } else { + settingRepository.save(setting); + log.info("Setting '{}' seeded with value '{}'.", setting.getSettingName(), setting.getSettingValue()); + } + } + } +} diff --git a/src/main/java/zw/qantra/tm/utils/Utils.java b/src/main/java/zw/qantra/tm/utils/Utils.java index d357ebd..e3d96dc 100644 --- a/src/main/java/zw/qantra/tm/utils/Utils.java +++ b/src/main/java/zw/qantra/tm/utils/Utils.java @@ -2,6 +2,7 @@ package zw.qantra.tm.utils; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.util.StdDateFormat; @@ -47,6 +48,7 @@ public class Utils { public static T fromJson(String json, Class clazz){ ObjectMapper mapper = new ObjectMapper(); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.setDateFormat(new StdDateFormat().withColonInTimeZone(true)); mapper.registerModule(new JavaTimeModule()); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); diff --git a/src/main/resources/application-lab.properties b/src/main/resources/application-lab.properties index 43c40f0..a79b94b 100644 --- a/src/main/resources/application-lab.properties +++ b/src/main/resources/application-lab.properties @@ -10,9 +10,9 @@ server.servlet.context-path=/api spring.jpa.hibernate.ddl-auto=update spring.datasource.driver-class-name=org.postgresql.Driver -spring.datasource.url=jdbc:postgresql://173.212.247.232:5532/qpay?useLegacyDatetimeCode=false +spring.datasource.url=jdbc:postgresql://localhost:5432/qpay?useLegacyDatetimeCode=false spring.datasource.username=postgres -spring.datasource.password=zdDZMzq6F4B4L1IUl +spring.datasource.password=example # sbz.merchant.url=http://192.168.16.29:24050/v1 @@ -28,29 +28,19 @@ sbz.aggregator.encryption-key=735470ce-8efb-4c0f-ba3f-7298f805ab33 steward.payment.processor.url=${sbz.merchant.url} -# ERPNext Configuration -erpnext.url=http://173.212.247.232:8085/api -erpnext.api.key=1102c6da0312f75 -erpnext.api.secret=e3d95692bcf16c7 -erpnext.username=Administrator -erpnext.password=admin -erpnext.company=Qantra -erpnext.accounts.cash=Bank Account - Q -erpnext.accounts.gatewayexpense=Gateway Expenses - Q -erpnext.accounts.accountsreceivable=Debtors - Q -erpnext.accounts.commission=Discounts Received - Q -erpnext.accounts.payable=Creditors - Q -erpnext.income.account=Sales Income - Q -erpnext.cost.center=Main - Q -erpnext.tax.account=VAT - Q -erpnext.tax.rate=15.0 - logging.level.root=WARN -spring.datasource.nflow.url=jdbc:postgresql://173.212.247.232:5532/nflow +spring.datasource.nflow.url=jdbc:postgresql://localhost:5432/qpay-nflow spring.datasource.nflow.username=postgres -spring.datasource.nflow.password=zdDZMzq6F4B4L1IUl +spring.datasource.nflow.password=example powertel.clientid=powertel_zesa ecocash.apikey=OUzGezwTgktLg_9v0I48I6WUM4LVLGFl ecocash.url=https://developers.ecocash.co.zw/api/ecocash_pay/api/v2/payment/instant/c2b/sandbox + +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.cancel-url=http://localhost:9005/poll +velocity.api.success-url=http://localhost:9005/poll \ No newline at end of file diff --git a/src/main/resources/application-staging.properties b/src/main/resources/application-staging.properties new file mode 100644 index 0000000..410b1b8 --- /dev/null +++ b/src/main/resources/application-staging.properties @@ -0,0 +1,42 @@ +spring.application.name=tm +debug=false + +logging.level.org.springframework.boot.autoconfigure=INFO + +server.port=6950 + +server.servlet.context-path=/api + +spring.jpa.hibernate.ddl-auto=update +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.datasource.url=jdbc:postgresql://173.212.247.232:5532/qpay?useLegacyDatetimeCode=false +spring.datasource.username=postgres +spring.datasource.password=zdDZMzq6F4B4L1IUl + + +# sbz.merchant.url=http://192.168.16.29:24050/v1 +# sbz.merchant.url=http://localhost:24000/v1 +sbz.merchant.url=https://api.stewardpay.co.zw/lab/v2 +sbz.aggregator.url=${sbz.merchant.url} +sbz.aggregator.phone=263773591219 +sbz.aggregator.account=1046737845 + +sbz.aggregator.client-id=steward_pay +sbz.aggregator.client-secret=XjkbBAJeJCnAplYDTbp5xOjeyySVnYGp +sbz.aggregator.encryption-key=735470ce-8efb-4c0f-ba3f-7298f805ab33 + +steward.payment.processor.url=${sbz.merchant.url} + +logging.level.root=WARN +spring.datasource.nflow.url=jdbc:postgresql://173.212.247.232:5532/qpay-nflow +spring.datasource.nflow.username=postgres +spring.datasource.nflow.password=zdDZMzq6F4B4L1IUl + +powertel.clientid=powertel_zesa + +ecocash.apikey=OUzGezwTgktLg_9v0I48I6WUM4LVLGFl +ecocash.url=https://developers.ecocash.co.zw/api/ecocash_pay/api/v2/payment/instant/c2b/sandbox + +velocity.api.cancel-url=http://localhost:9005/poll +velocity.api.success-url=http://localhost:9005/poll \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 58cf9a5..249f9cc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -30,23 +30,6 @@ steward.payment.processor.url=${sbz.merchant.url} mpgs.return-url=https://vdt.co.zw/#/return -# ERPNext Configuration -erpnext.url=http://62.171.179.108:8080/api -erpnext.api.key=dff94c65b52ecbd -erpnext.api.secret=49218fe805c9fb4 -erpnext.username=Administrator -erpnext.password=admin -erpnext.company=QantraTech -erpnext.accounts.cash=Steward Bank Account - Q -erpnext.accounts.gatewayexpense=Gateway Expenses - Q -erpnext.accounts.accountsreceivable=Debtors - Q -erpnext.accounts.commission=Discounts Received - Q -erpnext.accounts.payable=Creditors - Q -erpnext.income.account=Sales Income - Q -erpnext.cost.center=Main - Q -erpnext.tax.account=VAT - Q -erpnext.tax.rate=15.0 - # JWT Configuration jwt.secret=your-super-secret-jwt-key-here-make-it-very-long-and-secure-in-production jwt.expiration=86400000 @@ -57,10 +40,6 @@ spring.datasource.nflow.username=postgres spring.datasource.nflow.password=zdDZMzq6F4B4L1IUla spring.main.allow-bean-definition-overriding=true -# Email Configuration -infobip.url=https://z3m696.api.infobip.com/sms/2/text/advanced -infobip.apikey=0ede918b7c1110465a0518021521638c-ebc4fdb7-547d-413b-bbca-781949ac6597 - spring.mail.host=mail.zimscape.com spring.mail.port=587 spring.mail.username=notifications@qantra.co.zw @@ -85,4 +64,9 @@ 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 +hot.token.expiry.minutes=25 + +velocity.api.base-url=https://api.velocityafrica.net +velocity.api.api-key=z5WdSlItIhYL6nd7rxzf-jFdKN3jJyL1LubhDFBvXmc +velocity.api.cancel-url=https://pay.velocityafrica/poll +velocity.api.success-url=https://pay.velocityafrica/poll \ No newline at end of file diff --git a/src/main/resources/liquibase-lab.properties b/src/main/resources/liquibase-lab.properties index 40cdf32..f4128ab 100644 --- a/src/main/resources/liquibase-lab.properties +++ b/src/main/resources/liquibase-lab.properties @@ -1,5 +1,5 @@ changeLogFile=src/main/resources/liquibase-diff-changeLog.xml -url=jdbc:postgresql://173.212.247.232:5532/qpay?serverTimezone=Africa/Harare&useLegacyDatetimeCode=false +url=jdbc:postgresql://localhost:5432/qpay?serverTimezone=Africa/Harare&useLegacyDatetimeCode=false username=postgres password=zdDZMzq6F4B4L1IUl