From 9ae78635c7b3162803522e83dcd53708c638e3bf Mon Sep 17 00:00:00 2001 From: Vus Khoza Date: Wed, 18 Jun 2025 09:28:01 +0200 Subject: [PATCH] completed initial charge logic --- pom.xml | 11 +- src/main/java/zw/qantra/tm/TmApplication.java | 7 + .../domain/controllers/ConfigController.java | 83 ++++++++++++ .../controllers/ProviderController.java | 57 ++++++++ .../controllers/TransactonController.java | 49 ++++++- .../qantra/tm/domain/dtos/SBZProductDto.java | 22 ++++ .../qantra/tm/domain/models/BaseEntity.java | 28 ++++ .../zw/qantra/tm/domain/models/Charge.java | 7 +- .../tm/domain/models/ChargeCondition.java | 9 +- .../zw/qantra/tm/domain/models/Currency.java | 11 +- .../domain/models/IntegrationProcessor.java | 13 +- .../tm/domain/models/PaymentProcessor.java | 6 +- .../zw/qantra/tm/domain/models/Provider.java | 13 +- .../zw/qantra/tm/domain/models/Setting.java | 11 +- .../qantra/tm/domain/models/Transaction.java | 8 +- .../ChargeConditionRepository.java | 11 ++ .../domain/repositories/ChargeRepository.java | 14 ++ .../services/ChargeConditionService.java | 20 +++ .../tm/domain/services/ChargeService.java | 20 +++ .../tm/domain/services/CurrencyService.java | 4 + .../services/IntegrationProcessorService.java | 4 + .../services/PaymentProcessorService.java | 4 + .../tm/domain/services/ProviderService.java | 76 +++++++++++ .../tm/domain/services/SettingService.java | 4 + .../domain/services/TransactionService.java | 10 +- .../ZesaConfirmationProcessor.java | 6 +- .../handlers/CalculateChargesHandler.java | 124 ++++++++++++++++++ .../RestTemplateResponseErrorHandler.java | 2 +- .../java/zw/qantra/tm/rest/RestService.java | 33 ++++- .../tm/rest/RestTemplateInterceptor.java | 16 ++- src/main/resources/application-lab.properties | 11 +- src/main/resources/application.properties | 10 +- src/main/resources/liquibase-lab.properties | 6 +- src/main/resources/liquibase.properties | 6 +- 34 files changed, 614 insertions(+), 102 deletions(-) create mode 100644 src/main/java/zw/qantra/tm/domain/controllers/ConfigController.java create mode 100644 src/main/java/zw/qantra/tm/domain/controllers/ProviderController.java create mode 100644 src/main/java/zw/qantra/tm/domain/dtos/SBZProductDto.java create mode 100644 src/main/java/zw/qantra/tm/domain/models/BaseEntity.java create mode 100644 src/main/java/zw/qantra/tm/domain/repositories/ChargeConditionRepository.java create mode 100644 src/main/java/zw/qantra/tm/domain/repositories/ChargeRepository.java create mode 100644 src/main/java/zw/qantra/tm/domain/services/ChargeConditionService.java create mode 100644 src/main/java/zw/qantra/tm/domain/services/ChargeService.java diff --git a/pom.xml b/pom.xml index 5df47dc..30ced52 100644 --- a/pom.xml +++ b/pom.xml @@ -58,11 +58,6 @@ org.springframework.boot spring-boot-starter-web - - com.mysql - mysql-connector-j - runtime - org.projectlombok lombok @@ -103,6 +98,12 @@ json 20231013 + + org.postgresql + postgresql + runtime + + diff --git a/src/main/java/zw/qantra/tm/TmApplication.java b/src/main/java/zw/qantra/tm/TmApplication.java index 3a8f8f7..ec94f6d 100644 --- a/src/main/java/zw/qantra/tm/TmApplication.java +++ b/src/main/java/zw/qantra/tm/TmApplication.java @@ -2,6 +2,9 @@ package zw.qantra.tm; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import com.google.gson.Gson; @SpringBootApplication public class TmApplication { @@ -10,4 +13,8 @@ public class TmApplication { SpringApplication.run(TmApplication.class, args); } + @Bean + public Gson gson() { + return new Gson(); + } } diff --git a/src/main/java/zw/qantra/tm/domain/controllers/ConfigController.java b/src/main/java/zw/qantra/tm/domain/controllers/ConfigController.java new file mode 100644 index 0000000..03a386f --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/controllers/ConfigController.java @@ -0,0 +1,83 @@ +package zw.qantra.tm.domain.controllers; + +import lombok.RequiredArgsConstructor; +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.Charge; +import zw.qantra.tm.domain.models.ChargeCondition; +import zw.qantra.tm.domain.services.ChargeConditionService; +import zw.qantra.tm.domain.services.ChargeService; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +@RestController +@RequestMapping("/configs") +@RequiredArgsConstructor +public class ConfigController { + private final ChargeConditionService chargeConditionService; + private final ChargeService chargeService; + + @GetMapping("/charges") + public ResponseEntity charges(){ + 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("Charges configuration complete"); + } +} diff --git a/src/main/java/zw/qantra/tm/domain/controllers/ProviderController.java b/src/main/java/zw/qantra/tm/domain/controllers/ProviderController.java new file mode 100644 index 0000000..ebca345 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/controllers/ProviderController.java @@ -0,0 +1,57 @@ +package zw.qantra.tm.domain.controllers; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import zw.qantra.tm.domain.models.Provider; +import zw.qantra.tm.domain.services.ProviderService; +import zw.qantra.tm.rest.ApiResponse; + +import java.util.UUID; + +@RestController +@RequestMapping("/providers") +@RequiredArgsConstructor +public class ProviderController { + + private final ProviderService providerService; + + @GetMapping + public ResponseEntity getProviders() { + return ApiResponse.ok(providerService.getProviders()); + } + + @GetMapping("/{id}/products") + public ResponseEntity getProducts(@PathVariable UUID id) { + return ApiResponse.ok(providerService.getProviderProducts(id)); + } + + @GetMapping("/{id}") + public ResponseEntity getProvider(@PathVariable UUID id) { + return providerService.getProvider(id) + .map(ApiResponse::ok) + .orElse(ApiResponse.notFound()); + } + + @PostMapping + public ResponseEntity createProvider(@RequestBody Provider provider) { + return ApiResponse.ok(providerService.createProvider(provider)); + } + + @PutMapping("/{id}") + public ResponseEntity updateProvider(@PathVariable UUID id, @RequestBody Provider provider) { + Provider updatedProvider = providerService.updateProvider(id, provider); + if (updatedProvider != null) { + return ApiResponse.ok(updatedProvider); + } + return ApiResponse.notFound(); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteProvider(@PathVariable UUID id) { + if (providerService.deleteProvider(id)) { + return ApiResponse.ok("Provider deleted successfully"); + } + return ApiResponse.notFound(); + } +} 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 0885d41..162c508 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java @@ -1,18 +1,53 @@ package zw.qantra.tm.domain.controllers; import lombok.RequiredArgsConstructor; +import zw.qantra.tm.domain.services.TransactionService; +import zw.qantra.tm.domain.models.Transaction; + import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.UUID; @RestController @RequestMapping("/transaction") @RequiredArgsConstructor public class TransactonController { - @PostMapping() - public ResponseEntity post(@RequestBody Object transactionRequest) { - return ResponseEntity.ok("Transaction endpoint is working"); + + private final TransactionService transactionService; + + @GetMapping + public ResponseEntity> getAll() { + return ResponseEntity.ok(transactionService.getTransactionRepository().findAll()); + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable UUID id) { + return transactionService.getTransactionRepository().findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity create(@RequestBody Transaction transaction) { + return ResponseEntity.ok(transactionService.execute(transaction)); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable UUID id, @RequestBody Transaction transaction) { + if (!transactionService.getTransactionRepository().existsById(id)) { + return ResponseEntity.notFound().build(); + } + transaction.setId(id); + return ResponseEntity.ok(transactionService.execute(transaction)); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable UUID id) { + if (!transactionService.getTransactionRepository().existsById(id)) { + return ResponseEntity.notFound().build(); + } + transactionService.getTransactionRepository().deleteById(id); + return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/zw/qantra/tm/domain/dtos/SBZProductDto.java b/src/main/java/zw/qantra/tm/domain/dtos/SBZProductDto.java new file mode 100644 index 0000000..c75b97b --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/dtos/SBZProductDto.java @@ -0,0 +1,22 @@ +package zw.qantra.tm.domain.dtos; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SBZProductDto { + private String name; + private String description; + private String displayName; + private BigDecimal defaultAmount; + private boolean requiresAmount; + private UUID uid; +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/models/BaseEntity.java b/src/main/java/zw/qantra/tm/domain/models/BaseEntity.java new file mode 100644 index 0000000..4870587 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/models/BaseEntity.java @@ -0,0 +1,28 @@ +package zw.qantra.tm.domain.models; + +import jakarta.persistence.Column; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.util.UUID; + +@Getter +@Setter +@MappedSuperclass +public class BaseEntity { + @Id + @GeneratedValue(generator = "UUID") + @GenericGenerator( + name = "UUID", + strategy = "org.hibernate.id.UUIDGenerator" + ) + @JdbcTypeCode(SqlTypes.UUID) + @Column(name = "id", updatable = false, nullable = false) + private UUID id; +} 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 3e317b5..a0f30a8 100644 --- a/src/main/java/zw/qantra/tm/domain/models/Charge.java +++ b/src/main/java/zw/qantra/tm/domain/models/Charge.java @@ -10,7 +10,6 @@ import zw.qantra.tm.domain.enums.TargetEvent; import java.math.BigDecimal; import java.util.HashSet; import java.util.Set; -import java.util.UUID; @Entity @Getter @@ -18,11 +17,7 @@ import java.util.UUID; @Builder @AllArgsConstructor @NoArgsConstructor -public class Charge { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - +public class Charge extends BaseEntity { private String description; // % rate to charge 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 80a5ee1..e7d2f3b 100644 --- a/src/main/java/zw/qantra/tm/domain/models/ChargeCondition.java +++ b/src/main/java/zw/qantra/tm/domain/models/ChargeCondition.java @@ -5,7 +5,6 @@ import lombok.*; import zw.qantra.tm.domain.enums.ChargeConditionType; import java.util.List; -import java.util.UUID; @Entity @Getter @@ -13,15 +12,11 @@ import java.util.UUID; @Builder @AllArgsConstructor @NoArgsConstructor -public class ChargeCondition { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - +public class ChargeCondition extends BaseEntity { private String name; @Enumerated(EnumType.STRING) private ChargeConditionType type; - @Column(columnDefinition = "longtext") + @Column(columnDefinition = "TEXT") private String codes; @ManyToMany(mappedBy = "includes") diff --git a/src/main/java/zw/qantra/tm/domain/models/Currency.java b/src/main/java/zw/qantra/tm/domain/models/Currency.java index 6c350b0..3d75768 100644 --- a/src/main/java/zw/qantra/tm/domain/models/Currency.java +++ b/src/main/java/zw/qantra/tm/domain/models/Currency.java @@ -1,11 +1,6 @@ package zw.qantra.tm.domain.models; -import java.util.UUID; - import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; import lombok.*; @Entity @@ -14,11 +9,7 @@ import lombok.*; @Builder @AllArgsConstructor @NoArgsConstructor -public class Currency { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - +public class Currency extends BaseEntity { private String isoCode; private String name; private String displayName; diff --git a/src/main/java/zw/qantra/tm/domain/models/IntegrationProcessor.java b/src/main/java/zw/qantra/tm/domain/models/IntegrationProcessor.java index 527132b..958aa5e 100644 --- a/src/main/java/zw/qantra/tm/domain/models/IntegrationProcessor.java +++ b/src/main/java/zw/qantra/tm/domain/models/IntegrationProcessor.java @@ -1,11 +1,6 @@ package zw.qantra.tm.domain.models; -import java.util.UUID; - import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; import lombok.*; @Entity @@ -14,12 +9,8 @@ import lombok.*; @Builder @AllArgsConstructor @NoArgsConstructor -public class IntegrationProcessor { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - +public class IntegrationProcessor extends BaseEntity { private String name; private String description; - private String type; + private String label; } diff --git a/src/main/java/zw/qantra/tm/domain/models/PaymentProcessor.java b/src/main/java/zw/qantra/tm/domain/models/PaymentProcessor.java index 49ef204..470d981 100644 --- a/src/main/java/zw/qantra/tm/domain/models/PaymentProcessor.java +++ b/src/main/java/zw/qantra/tm/domain/models/PaymentProcessor.java @@ -3,7 +3,7 @@ package zw.qantra.tm.domain.models; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.Column; import lombok.*; import java.util.UUID; @@ -14,9 +14,9 @@ import java.util.UUID; @Builder @AllArgsConstructor @NoArgsConstructor -public class PaymentProcessor { - @Id +public class PaymentProcessor extends BaseEntity { @GeneratedValue(strategy = GenerationType.UUID) + @Column(columnDefinition = "VARCHAR(36)") private UUID id; private String name; diff --git a/src/main/java/zw/qantra/tm/domain/models/Provider.java b/src/main/java/zw/qantra/tm/domain/models/Provider.java index 3df2252..b88e648 100644 --- a/src/main/java/zw/qantra/tm/domain/models/Provider.java +++ b/src/main/java/zw/qantra/tm/domain/models/Provider.java @@ -1,12 +1,8 @@ package zw.qantra.tm.domain.models; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; import lombok.*; -import java.util.UUID; @Entity @Getter @@ -14,12 +10,9 @@ import java.util.UUID; @Builder @AllArgsConstructor @NoArgsConstructor -public class Provider { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - +public class Provider extends BaseEntity { private String clientId; - private String name; + private String label; private String description; + private String externalId; } diff --git a/src/main/java/zw/qantra/tm/domain/models/Setting.java b/src/main/java/zw/qantra/tm/domain/models/Setting.java index 95ae24c..25b7aeb 100644 --- a/src/main/java/zw/qantra/tm/domain/models/Setting.java +++ b/src/main/java/zw/qantra/tm/domain/models/Setting.java @@ -1,11 +1,6 @@ package zw.qantra.tm.domain.models; -import java.util.UUID; - import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -18,11 +13,7 @@ import lombok.Setter; @Builder @AllArgsConstructor @NoArgsConstructor -public class Setting { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - +public class Setting extends BaseEntity { private String name; private String value; } \ No newline at end of file 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 c4044c9..a6228de 100644 --- a/src/main/java/zw/qantra/tm/domain/models/Transaction.java +++ b/src/main/java/zw/qantra/tm/domain/models/Transaction.java @@ -9,7 +9,6 @@ import zw.qantra.tm.domain.enums.Status; import java.math.BigDecimal; import java.util.List; -import java.util.UUID; @Entity @Getter @@ -18,11 +17,7 @@ import java.util.UUID; @AllArgsConstructor @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) -public class Transaction { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - +public class Transaction extends BaseEntity { private String trace; private BigDecimal amount; private BigDecimal charge; @@ -34,6 +29,7 @@ public class Transaction { private String productUid; private String paymentProcessorLabel; private String integrationProcessorLabel; + private String providerLabel; private String rrn; private String channelName; private String channel; diff --git a/src/main/java/zw/qantra/tm/domain/repositories/ChargeConditionRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/ChargeConditionRepository.java new file mode 100644 index 0000000..62e36d1 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/repositories/ChargeConditionRepository.java @@ -0,0 +1,11 @@ +package zw.qantra.tm.domain.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import zw.qantra.tm.domain.models.ChargeCondition; + +import java.util.UUID; + +@Repository +public interface ChargeConditionRepository extends JpaRepository { +} \ 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 new file mode 100644 index 0000000..7652933 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/repositories/ChargeRepository.java @@ -0,0 +1,14 @@ +package zw.qantra.tm.domain.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import zw.qantra.tm.domain.enums.CurrencyType; +import zw.qantra.tm.domain.models.Charge; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface ChargeRepository extends JpaRepository { + List findAllByCurrency(CurrencyType currency); +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/ChargeConditionService.java b/src/main/java/zw/qantra/tm/domain/services/ChargeConditionService.java new file mode 100644 index 0000000..edfede5 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/ChargeConditionService.java @@ -0,0 +1,20 @@ +package zw.qantra.tm.domain.services; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.models.ChargeCondition; +import zw.qantra.tm.domain.repositories.ChargeConditionRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class ChargeConditionService { + private final ChargeConditionRepository chargeConditionRepository; + + public ChargeConditionRepository getChargeConditionRepository() { + return chargeConditionRepository; + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/ChargeService.java b/src/main/java/zw/qantra/tm/domain/services/ChargeService.java new file mode 100644 index 0000000..3b7c3e7 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/ChargeService.java @@ -0,0 +1,20 @@ +package zw.qantra.tm.domain.services; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import zw.qantra.tm.domain.models.Charge; +import zw.qantra.tm.domain.repositories.ChargeRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class ChargeService { + private final ChargeRepository chargeRepository; + + public ChargeRepository getChargeRepository() { + return chargeRepository; + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/CurrencyService.java b/src/main/java/zw/qantra/tm/domain/services/CurrencyService.java index b94af52..5a1abc2 100644 --- a/src/main/java/zw/qantra/tm/domain/services/CurrencyService.java +++ b/src/main/java/zw/qantra/tm/domain/services/CurrencyService.java @@ -8,4 +8,8 @@ import zw.qantra.tm.domain.repositories.CurrencyRepository; @RequiredArgsConstructor public class CurrencyService { private final CurrencyRepository currencyRepository; + + public CurrencyRepository getCurrencyRepository() { + return currencyRepository; + } } \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/IntegrationProcessorService.java b/src/main/java/zw/qantra/tm/domain/services/IntegrationProcessorService.java index 77e82c7..e4f0440 100644 --- a/src/main/java/zw/qantra/tm/domain/services/IntegrationProcessorService.java +++ b/src/main/java/zw/qantra/tm/domain/services/IntegrationProcessorService.java @@ -8,4 +8,8 @@ import zw.qantra.tm.domain.repositories.IntegrationProcessorRepository; @RequiredArgsConstructor public class IntegrationProcessorService { private final IntegrationProcessorRepository integrationProcessorRepository; + + public IntegrationProcessorRepository getIntegrationProcessorRepository() { + return integrationProcessorRepository; + } } \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/PaymentProcessorService.java b/src/main/java/zw/qantra/tm/domain/services/PaymentProcessorService.java index 4b2bee4..0c8f473 100644 --- a/src/main/java/zw/qantra/tm/domain/services/PaymentProcessorService.java +++ b/src/main/java/zw/qantra/tm/domain/services/PaymentProcessorService.java @@ -8,4 +8,8 @@ import zw.qantra.tm.domain.repositories.PaymentProcessorRepository; @RequiredArgsConstructor public class PaymentProcessorService { private final PaymentProcessorRepository paymentProcessorRepository; + + public PaymentProcessorRepository getPaymentProcessorRepository() { + return paymentProcessorRepository; + } } \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/ProviderService.java b/src/main/java/zw/qantra/tm/domain/services/ProviderService.java index 17afbf8..2ce20ab 100644 --- a/src/main/java/zw/qantra/tm/domain/services/ProviderService.java +++ b/src/main/java/zw/qantra/tm/domain/services/ProviderService.java @@ -1,11 +1,87 @@ package zw.qantra.tm.domain.services; import lombok.RequiredArgsConstructor; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import zw.qantra.tm.domain.dtos.SBZProductDto; +import zw.qantra.tm.domain.models.Provider; +import zw.qantra.tm.domain.repositories.ChargeRepository; import zw.qantra.tm.domain.repositories.ProviderRepository; +import zw.qantra.tm.rest.RestService; + +import java.util.*; @Service @RequiredArgsConstructor public class ProviderService { private final ProviderRepository providerRepository; + private final RestService restService; + + @Value("${sbz.aggregator.url}") + private String aggregatorUrl; + + public ProviderRepository getProviderRepository() { + return providerRepository; + } + + public List getProviderProducts(UUID providerId) { + String token = restService.getMerchantToken(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + headers.setBearerAuth(token); + + LinkedHashMap response = this.restService.getAs( + aggregatorUrl + "/merchant/aggregation/products?size=100&clientUid=" + providerId.toString(), + headers, + LinkedHashMap.class + ); + + LinkedHashMap body = (LinkedHashMap) response.get("body"); + List content = (List) body.get("content"); + + List products = new ArrayList<>(); + for (LinkedHashMap product : content) { + ObjectMapper mapper = new ObjectMapper(); + SBZProductDto productDto = mapper.convertValue(product, SBZProductDto.class); + products.add(productDto); + } + + return products; + } + + public List getProviders() { + return providerRepository.findAll(); + } + + public Optional getProvider(UUID id) { + return providerRepository.findById(id); + } + + public Provider createProvider(Provider provider) { + return providerRepository.save(provider); + } + + public Provider updateProvider(UUID id, Provider provider) { + if (providerRepository.existsById(id)) { + provider.setId(id); + return providerRepository.save(provider); + } + return null; + } + + public boolean deleteProvider(UUID id) { + if (providerRepository.existsById(id)) { + providerRepository.deleteById(id); + return true; + } + return false; + } } \ No newline at end of file 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 4b36c93..7fdd001 100644 --- a/src/main/java/zw/qantra/tm/domain/services/SettingService.java +++ b/src/main/java/zw/qantra/tm/domain/services/SettingService.java @@ -8,4 +8,8 @@ import zw.qantra.tm.domain.repositories.SettingRepository; @RequiredArgsConstructor public class SettingService { private final SettingRepository settingRepository; + + public SettingRepository getSettingRepository() { + return settingRepository; + } } \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/TransactionService.java b/src/main/java/zw/qantra/tm/domain/services/TransactionService.java index e50c0dc..c116808 100644 --- a/src/main/java/zw/qantra/tm/domain/services/TransactionService.java +++ b/src/main/java/zw/qantra/tm/domain/services/TransactionService.java @@ -3,7 +3,6 @@ package zw.qantra.tm.domain.services; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import zw.qantra.tm.domain.enums.Status; -import zw.qantra.tm.domain.models.PaymentProcessor; import zw.qantra.tm.domain.models.Transaction; import zw.qantra.tm.domain.repositories.TransactionRepository; import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory; @@ -17,11 +16,18 @@ import zw.qantra.tm.domain.services.processors.handlers.ValidationHandler; public class TransactionService { private final TransactionRepository transactionRepository; private final PaymentProcessorFactory paymentProcessorFactory; + private final CalculateChargesHandler calculateChargesHandler; + + public TransactionRepository getTransactionRepository() { + return transactionRepository; + } public Transaction execute(Transaction transaction) { + LogicPipeline transactionLogic = - new LogicPipeline<>(new CalculateChargesHandler()) + new LogicPipeline<>(calculateChargesHandler) .addHandler(new ValidationHandler()); + transaction = transactionLogic.execute(transaction); String label = transaction.getType() + "_" + transaction.getIntegrationProcessorLabel(); diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/ZesaConfirmationProcessor.java b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/ZesaConfirmationProcessor.java index 7bb28e0..d4dde96 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/ZesaConfirmationProcessor.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/confirmations/ZesaConfirmationProcessor.java @@ -18,8 +18,10 @@ public class ZesaConfirmationProcessor implements TransactionProcessorInterface @Override public Object process(Transaction transaction) throws Exception { System.out.println("processing transaction"); - throw new Exception("Challenge encountered, please contact support."); -// return transaction; + + + + return transaction; } @Override diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/CalculateChargesHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/CalculateChargesHandler.java index b3afe66..420056d 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/handlers/CalculateChargesHandler.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/handlers/CalculateChargesHandler.java @@ -1,11 +1,135 @@ package zw.qantra.tm.domain.services.processors.handlers; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +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.ChargeConditionService; +import zw.qantra.tm.domain.services.ChargeService; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + + +@AllArgsConstructor +@Service public class CalculateChargesHandler implements LogicHandler { + private ChargeConditionService chargeConditionService; + private ChargeService chargeService; + @Override public Transaction process(Transaction transaction) { System.out.println("processing charges"); + transaction.setCharge(BigDecimal.ZERO); + transaction.setTax(BigDecimal.ZERO); + transaction.setGatewayCharge(BigDecimal.ZERO); + + BigDecimal amountToCharge = transaction.getAmount(); + BigDecimal chargeAmount = BigDecimal.ZERO; + + List chargeList = chargeService.getChargeRepository().findAllByCurrency(transaction.getDebitCurrency()); + + List appropriateCharges = new ArrayList<>(); + for (Charge charge : chargeList) { + boolean isPresent = false; + for (ChargeCondition chargeCondition: charge.getIncludes()){ + List conditionTypes = Arrays.asList(chargeCondition.getCodes().split(",")); + switch (chargeCondition.getType()){ + case GATEWAY: + isPresent = conditionTypes.contains(transaction.getPaymentProcessorLabel()); + break; + case PROVIDER: + isPresent = conditionTypes.contains(transaction.getIntegrationProcessorLabel()); + break; + } + if(isPresent) break; // we've found a matching condition, no need to check rest of conditions + } + if(!isPresent) continue; // if we did not find a match, means this charge does not apply to this transaction + + boolean isExempt = false; + for (ChargeCondition chargeCondition: charge.getExcludes()){ + List conditionTypes = Arrays.asList(chargeCondition.getCodes().split(",")); + switch (chargeCondition.getType()){ + case GATEWAY: + isExempt = conditionTypes.contains(transaction.getPaymentProcessorLabel()); + break; + case PROVIDER: + isExempt = conditionTypes.contains(transaction.getIntegrationProcessorLabel()); + break; + } + if(isExempt) break; // we've found a matching condition, no need to check rest of conditions + } + if(isExempt) continue; // if we found a match, means this transaction is exempt from this charge + + appropriateCharges.add(charge); + } + + for (Charge charge : appropriateCharges) { + // if minimum amount chargeable is lower than the amount to charge, apply the min charge + if(charge.getMinimumAmount() != null) { + if(charge.getMinimumAmount().compareTo(amountToCharge) > 0) { + applyCharge(transaction, charge, charge.getMin()); + continue; + } + } + + // if maximum amount chargeable is higher than the amount to charge, apply the max charge + if(charge.getMaximumAmount() != null) { + if(charge.getMaximumAmount().compareTo(amountToCharge) < 0) { + applyCharge(transaction, charge, charge.getMax()); + continue; + } + } + + // calculate unrestricted charge amount + // if percentage rate is not present then it's a flat charge, else it's a percentage + if(charge.getPercentageRate() == null) { + if(charge.getFlat() == null) + continue; + + chargeAmount = charge.getFlat(); + }else { + BigDecimal fraction = charge.getPercentageRate().divide( + new BigDecimal(100), 4, RoundingMode.CEILING); + chargeAmount = amountToCharge.multiply(fraction) + .setScale(2, RoundingMode.CEILING); + + // if it's a composite charge then add the flat component as well + if(charge.isComposite()) { + chargeAmount = chargeAmount.add(charge.getFlat()) + .setScale(2, RoundingMode.CEILING); + } + } + + // if minimum charge is set, check if the charge amount is less than the minimum + if(charge.getMin() != null) { + if(charge.getMin().compareTo(chargeAmount) > 0){ + chargeAmount = charge.getMin(); + } + } + + // if maximum charge is set, check if the charge amount is more than the maximum + if(charge.getMax() != null) { + if(charge.getMax().compareTo(chargeAmount) < 0) { + chargeAmount = charge.getMax(); + } + } + + applyCharge(transaction, charge, chargeAmount); + } + return transaction; } + + void applyCharge(Transaction transaction, Charge charge, BigDecimal chargeAmount) { + switch (charge.getChargeLabel()) { + case FEE -> transaction.setCharge(chargeAmount); + case TAX -> transaction.setTax(chargeAmount); + case GATEWAY_FEE -> transaction.setGatewayCharge(chargeAmount); + } + } } diff --git a/src/main/java/zw/qantra/tm/exceptions/RestTemplateResponseErrorHandler.java b/src/main/java/zw/qantra/tm/exceptions/RestTemplateResponseErrorHandler.java index e34e715..e9a13ba 100755 --- a/src/main/java/zw/qantra/tm/exceptions/RestTemplateResponseErrorHandler.java +++ b/src/main/java/zw/qantra/tm/exceptions/RestTemplateResponseErrorHandler.java @@ -40,7 +40,7 @@ public class RestTemplateResponseErrorHandler BufferedReader reader = new BufferedReader(new InputStreamReader(httpResponse.getBody())); String httpBodyResponse = reader.lines().collect(Collectors.joining("")); - throw new RestException(httpBodyResponse, "Client Error"); + throw new RestException(httpBodyResponse, "Client Error: " + httpResponse.getStatusCode()); } } } \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/rest/RestService.java b/src/main/java/zw/qantra/tm/rest/RestService.java index c66a2b0..4c6802e 100755 --- a/src/main/java/zw/qantra/tm/rest/RestService.java +++ b/src/main/java/zw/qantra/tm/rest/RestService.java @@ -39,6 +39,13 @@ public class RestService { public final RestTemplate restTemplate; + @Value("${sbz.aggregator.client-id}") + private String clientId; + @Value("${sbz.aggregator.client-secret}") + private String clientSecret; + @Value("${sbz.aggregator.url}") + private String aggregatorUrl; + public RestService() { this.restTemplate = new RestTemplate( new BufferingClientHttpRequestFactory( @@ -54,7 +61,7 @@ public class RestService { if (CollectionUtils.isEmpty(interceptors)) { interceptors = new ArrayList<>(); } - interceptors.add(new RestTemplateInterceptor(gson)); + interceptors.add(new RestTemplateInterceptor()); this.restTemplate.setInterceptors(interceptors); } @@ -81,6 +88,24 @@ public class RestService { return clientToken; } + public String getMerchantToken() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + + Map payload = Map.of( + "clientId", clientId, + "clientSecret", clientSecret + ); + + HttpEntity> entity = new HttpEntity<>(payload, headers); + Map response = this.restTemplate.postForObject(aggregatorUrl + "/merchant/client/token",entity, Map.class); + + Map body = (Map) response.get("body"); + + return (String) body.get("access_token"); + } + public Map getToken(MultiValueMap payload){ HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); @@ -120,6 +145,12 @@ public class RestService { return this.restTemplate.postForObject(path, entity, clazz); } + public T getAs(String path, HttpHeaders headers, Class clazz){ + // build the request + HttpEntity entity = new HttpEntity<>(headers); + return this.restTemplate.exchange(path, HttpMethod.GET, entity, clazz).getBody(); + } + public T post(String path, Object payload, Class clazz){ HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); diff --git a/src/main/java/zw/qantra/tm/rest/RestTemplateInterceptor.java b/src/main/java/zw/qantra/tm/rest/RestTemplateInterceptor.java index ec01b34..731c0d5 100755 --- a/src/main/java/zw/qantra/tm/rest/RestTemplateInterceptor.java +++ b/src/main/java/zw/qantra/tm/rest/RestTemplateInterceptor.java @@ -1,6 +1,7 @@ package zw.qantra.tm.rest; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import lombok.RequiredArgsConstructor; import org.json.JSONArray; import org.json.JSONException; @@ -19,11 +20,10 @@ import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.stream.Collectors; -@RequiredArgsConstructor public class RestTemplateInterceptor implements ClientHttpRequestInterceptor { private final Logger logger = LoggerFactory.getLogger(getClass().getName()); - private final Gson gson; + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); @Override public ClientHttpResponse intercept( @@ -35,18 +35,20 @@ public class RestTemplateInterceptor // we don't log file data if(request.getHeaders().getContentLength() < 10000){ - logger.info("Request: " - + request.getMethod() + ": " - + request.getURI(), "http", gson.fromJson(requestBody, HashMap.class)); + logger.info("Request: {}: {}\n{}\n{}", + request.getMethod(), + request.getURI(), + request.getHeaders().toSingleValueMap(), + gson.toJson(gson.fromJson(requestBody, HashMap.class))); } ClientHttpResponse response = execution.execute(request, reqBody); InputStreamReader isr = new InputStreamReader(response.getBody()); String body = new BufferedReader(isr).lines() .collect(Collectors.joining("\n")); try{ - logger.info("Response body: {}", "http", gson.fromJson(body, HashMap.class)); + logger.info("Response body:\n{}", gson.toJson(gson.fromJson(body, HashMap.class))); }catch (Exception exception){ - logger.info("Problem serializing resp: {}", "http", exception.getMessage()); + logger.info("Problem serializing resp: {}", exception.getMessage()); } return response; } diff --git a/src/main/resources/application-lab.properties b/src/main/resources/application-lab.properties index ccb7dad..29aa4f1 100644 --- a/src/main/resources/application-lab.properties +++ b/src/main/resources/application-lab.properties @@ -6,11 +6,14 @@ logging.level.org.springframework.boot.autoconfigure=INFO server.port=6950 spring.jpa.hibernate.ddl-auto=update -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -spring.datasource.url=jdbc:mysql://localhost:3306/qpay?useLegacyDatetimeCode=false -spring.datasource.username=root +spring.datasource.url=jdbc:postgresql://localhost:5432/qpay?useLegacyDatetimeCode=false +spring.datasource.username=postgres spring.datasource.password=password sbz.aggregator.url=http://192.168.16.29:24050/v1 +sbz.aggregator.client-id=small_bakery +sbz.aggregator.client-secret=tmtCUeJA1dQezn405HSrYSdhV81XiBEi \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5939ee0..184b519 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,11 +9,13 @@ logging.level.org.springframework.boot.autoconfigure=INFO server.port=6950 spring.jpa.hibernate.ddl-auto=update -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -spring.datasource.url=jdbc:mysql://localhost:3306/qpay?useLegacyDatetimeCode=false -spring.datasource.username=root + +spring.datasource.url=jdbc:postgresql://localhost:5432/qpay?useLegacyDatetimeCode=false +spring.datasource.username=postgres spring.datasource.password=password sbz.aggregator.url=http://192.168.16.29:24050/v1 \ No newline at end of file diff --git a/src/main/resources/liquibase-lab.properties b/src/main/resources/liquibase-lab.properties index 98053c0..ebf00e1 100644 --- a/src/main/resources/liquibase-lab.properties +++ b/src/main/resources/liquibase-lab.properties @@ -1,9 +1,9 @@ changeLogFile=src/main/resources/liquibase-diff-changeLog.xml -url=jdbc:mysql://localhost:3306/qpay?serverTimezone=Africa/Harare&useLegacyDatetimeCode=false +url=jdbc:postgresql://localhost:5432/qpay?serverTimezone=Africa/Harare&useLegacyDatetimeCode=false username=root password=password -driver=com.mysql.cj.jdbc.Driver -referenceUrl=hibernate:spring:zw.qantra.tm.domain.models?dialect=org.hibernate.dialect.MySQLDialect&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy&hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy +driver=org.postgresql.Driver +referenceUrl=hibernate:spring:zw.qantra.tm.domain.models?dialect=org.hibernate.dialect.PostgreSQLDialect&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy&hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy diffChangeLogFile=src/main/resources/liquibase-diff-changeLog.xml outputChangeLogFile=src/main/resources/liquibase-changeLog.xml diff --git a/src/main/resources/liquibase.properties b/src/main/resources/liquibase.properties index 1b82a8e..ebf00e1 100644 --- a/src/main/resources/liquibase.properties +++ b/src/main/resources/liquibase.properties @@ -1,9 +1,9 @@ changeLogFile=src/main/resources/liquibase-diff-changeLog.xml -url=jdbc:mysql://localhost:3306/qpay?serverTimezone=Africa/Harare&useLegacyDatetimeCode=false +url=jdbc:postgresql://localhost:5432/qpay?serverTimezone=Africa/Harare&useLegacyDatetimeCode=false username=root password=password -driver=com.mysql.cj.jdbc.Driver -referenceUrl=hibernate:spring:zw.qantra.tm.domain.models?dialect=org.hibernate.dialect.MySQL5Dialect&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy&hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy +driver=org.postgresql.Driver +referenceUrl=hibernate:spring:zw.qantra.tm.domain.models?dialect=org.hibernate.dialect.PostgreSQLDialect&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy&hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy diffChangeLogFile=src/main/resources/liquibase-diff-changeLog.xml outputChangeLogFile=src/main/resources/liquibase-changeLog.xml