diff --git a/kafka/connects/debezium-checkout-status.json b/kafka/connects/debezium-checkout-status.json
new file mode 100644
index 0000000000..96bdf06300
--- /dev/null
+++ b/kafka/connects/debezium-checkout-status.json
@@ -0,0 +1,16 @@
+{
+ "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
+ "topic.prefix": "dbcheckout-status",
+ "database.user": "admin",
+ "database.dbname": "order",
+ "database.hostname": "postgres",
+ "database.password": "admin",
+ "database.port": "5432",
+ "key.converter.schemas.enable": "false",
+ "value.converter.schemas.enable": "false",
+ "value.converter": "org.apache.kafka.connect.json.JsonConverter",
+ "key.converter": "org.apache.kafka.connect.json.JsonConverter",
+ "schema.include.list": "public",
+ "table.include.list": "public.checkout",
+ "slot.name": "checkout_status_slot"
+}
diff --git a/kafka/connects/debezium-payment.json b/kafka/connects/debezium-payment.json
new file mode 100644
index 0000000000..3ccd1e9465
--- /dev/null
+++ b/kafka/connects/debezium-payment.json
@@ -0,0 +1,16 @@
+{
+ "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
+ "topic.prefix": "dbpayment",
+ "database.user": "admin",
+ "database.dbname": "payment",
+ "database.hostname": "postgres",
+ "database.password": "admin",
+ "database.port": "5432",
+ "key.converter.schemas.enable": "false",
+ "value.converter.schemas.enable": "false",
+ "value.converter": "org.apache.kafka.connect.json.JsonConverter",
+ "key.converter": "org.apache.kafka.connect.json.JsonConverter",
+ "schema.include.list": "public",
+ "table.include.list": "public.payment",
+ "slot.name": "payment_slot"
+}
diff --git a/order/pom.xml b/order/pom.xml
index aa3f863f96..83bb68d8fe 100644
--- a/order/pom.xml
+++ b/order/pom.xml
@@ -29,6 +29,14 @@
org.springframework.boot
spring-boot-starter-validation
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ com.google.code.gson
+ gson
+
org.springframework.boot
spring-boot-starter-data-jpa
@@ -51,6 +59,11 @@
org.liquibase
liquibase-core
+
+ org.testcontainers
+ kafka
+ test
+
com.yas
common-library
diff --git a/order/src/it/java/com/yas/order/service/CustomerServiceIT.java b/order/src/it/java/com/yas/order/service/CustomerServiceIT.java
index f759051d11..4b08f63a73 100644
--- a/order/src/it/java/com/yas/order/service/CustomerServiceIT.java
+++ b/order/src/it/java/com/yas/order/service/CustomerServiceIT.java
@@ -6,6 +6,7 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
+import com.yas.order.config.KafkaIntegrationTestConfiguration;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import org.junit.jupiter.api.Test;
@@ -13,12 +14,14 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Import;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Testcontainers
+@Import({KafkaIntegrationTestConfiguration.class})
class CustomerServiceIT {
@Container
@ServiceConnection
diff --git a/order/src/it/java/com/yas/order/service/OrderServiceIT.java b/order/src/it/java/com/yas/order/service/OrderServiceIT.java
index d64c4ba3b5..14e1e84687 100644
--- a/order/src/it/java/com/yas/order/service/OrderServiceIT.java
+++ b/order/src/it/java/com/yas/order/service/OrderServiceIT.java
@@ -9,6 +9,7 @@
import com.yas.commonlibrary.IntegrationTestConfiguration;
import com.yas.commonlibrary.exception.NotFoundException;
import com.yas.order.OrderApplication;
+import com.yas.order.config.KafkaIntegrationTestConfiguration;
import com.yas.order.model.Order;
import com.yas.order.model.enumeration.OrderStatus;
import com.yas.order.model.enumeration.PaymentStatus;
@@ -37,7 +38,7 @@
import org.springframework.data.util.Pair;
@SpringBootTest(classes = OrderApplication.class)
-@Import(IntegrationTestConfiguration.class)
+@Import({IntegrationTestConfiguration.class, KafkaIntegrationTestConfiguration.class})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderServiceIT {
diff --git a/order/src/it/java/com/yas/order/service/ProductServiceIT.java b/order/src/it/java/com/yas/order/service/ProductServiceIT.java
index 9255bb95e8..dabd957ba2 100644
--- a/order/src/it/java/com/yas/order/service/ProductServiceIT.java
+++ b/order/src/it/java/com/yas/order/service/ProductServiceIT.java
@@ -6,9 +6,9 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
+import com.yas.order.config.KafkaIntegrationTestConfiguration;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
-import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -16,12 +16,14 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Import;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Testcontainers
+@Import({KafkaIntegrationTestConfiguration.class})
class ProductServiceIT {
@Container
@ServiceConnection
diff --git a/order/src/it/java/com/yas/order/service/TaxServiceIT.java b/order/src/it/java/com/yas/order/service/TaxServiceIT.java
index 2f0edd6cdb..67cff8dffd 100644
--- a/order/src/it/java/com/yas/order/service/TaxServiceIT.java
+++ b/order/src/it/java/com/yas/order/service/TaxServiceIT.java
@@ -6,6 +6,7 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
+import com.yas.order.config.KafkaIntegrationTestConfiguration;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import org.junit.jupiter.api.Test;
@@ -13,12 +14,14 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Import;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Testcontainers
+@Import({KafkaIntegrationTestConfiguration.class})
class TaxServiceIT {
@Container
@ServiceConnection
diff --git a/order/src/it/resources/application.properties b/order/src/it/resources/application.properties
index 1d72b33cea..7c6e1c0ba3 100644
--- a/order/src/it/resources/application.properties
+++ b/order/src/it/resources/application.properties
@@ -12,4 +12,21 @@ spring.security.oauth2.resourceserver.jwt.issuer-uri=test
springdoc.oauthflow.authorization-url=test
springdoc.oauthflow.token-url=test
spring.jpa.open-in-view=true
-cors.allowed-origins=*
\ No newline at end of file
+cors.allowed-origins=*
+
+cdc.event.checkout.status.topic-name=dbcheckout-status.public.checkout
+cdc.event.checkout.status.group-id=checkout-status
+
+cdc.event.payment.topic-name=dbpayment.public.payment
+cdc.event.payment.update.group-id=payment-update
+
+kafka.version=7.0.9
+
+spring.kafka.consumer.bootstrap-servers=kafka:9092
+spring.aop.proxy-target-class=true
+
+spring.kafka.producer.bootstrap-servers=kafka:9092
+spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
+spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
+
+spring.kafka.bootstrap-servers=localhost:9092
\ No newline at end of file
diff --git a/order/src/main/java/com/yas/order/config/JsonConfig.java b/order/src/main/java/com/yas/order/config/JsonConfig.java
new file mode 100644
index 0000000000..d6478dc5cc
--- /dev/null
+++ b/order/src/main/java/com/yas/order/config/JsonConfig.java
@@ -0,0 +1,22 @@
+package com.yas.order.config;
+
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.Gson;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+
+@Configuration
+public class JsonConfig {
+
+ @Bean
+ public ObjectMapper objectMapper() {
+ return new ObjectMapper();
+ }
+
+ @Bean
+ public Gson gson() {
+ return new Gson();
+ }
+}
\ No newline at end of file
diff --git a/order/src/main/java/com/yas/order/config/ServiceUrlConfig.java b/order/src/main/java/com/yas/order/config/ServiceUrlConfig.java
index 9c5c794291..8319dbf6ca 100644
--- a/order/src/main/java/com/yas/order/config/ServiceUrlConfig.java
+++ b/order/src/main/java/com/yas/order/config/ServiceUrlConfig.java
@@ -4,5 +4,5 @@
@ConfigurationProperties(prefix = "yas.services")
public record ServiceUrlConfig(
- String cart, String customer, String product, String tax, String promotion) {
+ String cart, String customer, String product, String tax, String promotion, String payment) {
}
diff --git a/order/src/main/java/com/yas/order/controller/CheckoutController.java b/order/src/main/java/com/yas/order/controller/CheckoutController.java
index 655638e5a1..96a6bf0993 100644
--- a/order/src/main/java/com/yas/order/controller/CheckoutController.java
+++ b/order/src/main/java/com/yas/order/controller/CheckoutController.java
@@ -37,11 +37,16 @@ public ResponseEntity updateCheckoutStatus(@Valid @RequestBody CheckoutSta
return ResponseEntity.ok(checkoutService.updateCheckoutStatus(checkoutStatusPutVm));
}
- @GetMapping("/storefront/checkouts/{id}")
- public ResponseEntity getOrderWithItemsById(@PathVariable String id) {
+ @GetMapping("/storefront/checkouts/pending/{id}")
+ public ResponseEntity getPendingCheckoutDetailsById(@PathVariable String id) {
return ResponseEntity.ok(checkoutService.getCheckoutPendingStateWithItemsById(id));
}
+ @GetMapping("/storefront/checkouts/{id}")
+ public ResponseEntity getCheckoutById(@PathVariable String id) {
+ return ResponseEntity.ok(checkoutService.findCheckoutWithItemsById(id));
+ }
+
@PutMapping("/storefront/checkouts/{id}/payment-method")
@ApiResponses(value = {
@ApiResponse(responseCode = ApiConstant.CODE_200, description = ApiConstant.OK,
diff --git a/order/src/main/java/com/yas/order/kafka/AppKafkaListenerConfigurer.java b/order/src/main/java/com/yas/order/kafka/AppKafkaListenerConfigurer.java
new file mode 100644
index 0000000000..f7053b5231
--- /dev/null
+++ b/order/src/main/java/com/yas/order/kafka/AppKafkaListenerConfigurer.java
@@ -0,0 +1,24 @@
+package com.yas.order.kafka;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.annotation.EnableKafka;
+import org.springframework.kafka.annotation.KafkaListenerConfigurer;
+import org.springframework.kafka.config.KafkaListenerEndpointRegistrar;
+import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
+
+@EnableKafka
+@Configuration
+public class AppKafkaListenerConfigurer implements KafkaListenerConfigurer {
+
+ private LocalValidatorFactoryBean validator;
+
+ public AppKafkaListenerConfigurer(LocalValidatorFactoryBean validator) {
+ this.validator = validator;
+ }
+
+ @Override
+ public void configureKafkaListeners(KafkaListenerEndpointRegistrar registrar) {
+ // Enable message validation
+ registrar.setValidator(this.validator);
+ }
+}
diff --git a/order/src/main/java/com/yas/order/kafka/consumer/OrderStatusConsumer.java b/order/src/main/java/com/yas/order/kafka/consumer/OrderStatusConsumer.java
new file mode 100644
index 0000000000..e575a1867c
--- /dev/null
+++ b/order/src/main/java/com/yas/order/kafka/consumer/OrderStatusConsumer.java
@@ -0,0 +1,144 @@
+package com.yas.order.kafka.consumer;
+
+import static com.yas.order.utils.JsonUtils.convertObjectToString;
+import static com.yas.order.utils.JsonUtils.createJsonErrorObject;
+import static com.yas.order.utils.JsonUtils.getAttributesNode;
+import static com.yas.order.utils.JsonUtils.getJsonValueOrThrow;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.yas.commonlibrary.exception.BadRequestException;
+import com.yas.commonlibrary.exception.NotFoundException;
+import com.yas.order.model.Checkout;
+import com.yas.order.model.enumeration.CheckoutProgress;
+import com.yas.order.model.enumeration.CheckoutState;
+import com.yas.order.repository.CheckoutRepository;
+import com.yas.order.service.PaymentService;
+import com.yas.order.utils.Constants;
+import com.yas.order.viewmodel.payment.CheckoutPaymentVm;
+import java.util.Objects;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.annotation.RetryableTopic;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class OrderStatusConsumer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(OrderStatusConsumer.class);
+ private final PaymentService paymentService;
+ private final CheckoutRepository checkoutRepository;
+ private final ObjectMapper objectMapper;
+ private final Gson gson;
+
+ @KafkaListener(
+ topics = "${cdc.event.checkout.status.topic-name}",
+ groupId = "${cdc.event.checkout.status.group-id}"
+ )
+ @RetryableTopic(
+ attempts = "1"
+ )
+ public void listen(ConsumerRecord, ?> consumerRecord) {
+
+ if (Objects.isNull(consumerRecord)) {
+ LOGGER.info("ConsumerRecord is null");
+ return;
+ }
+ JsonObject valueObject = gson.fromJson((String) consumerRecord.value(), JsonObject.class);
+ processCheckoutEvent(valueObject);
+
+ }
+
+ private void processCheckoutEvent(JsonObject valueObject) {
+ Optional.ofNullable(valueObject)
+ .filter(value -> value.has("after"))
+ .map(value -> value.getAsJsonObject("after"))
+ .ifPresent(this::handleAfterJson);
+ }
+
+ private void handleAfterJson(JsonObject after) {
+
+ String id = getJsonValueOrThrow(after, Constants.Column.ID_COLUMN,
+ Constants.ErrorCode.ID_NOT_EXISTED);
+ String status = getJsonValueOrThrow(after, Constants.Column.STATUS_COLUMN,
+ Constants.ErrorCode.STATUS_NOT_EXISTED, id);
+ String progress = getJsonValueOrThrow(after, Constants.Column.CHECKOUT_PROGRESS_COLUMN,
+ Constants.ErrorCode.PROGRESS_NOT_EXISTED, id);
+
+ if (!isPaymentProcessing(status, progress)) {
+ LOGGER.info("Checkout record with ID {} lacks the status 'PAYMENT_PROCESSING' and progress 'STOCK_LOCKED'",
+ id);
+ return;
+ }
+
+ LOGGER.info("Checkout record with ID {} has the status 'PAYMENT_PROCESSING' and the process 'STOCK_LOCKED'",
+ id);
+
+ Checkout checkout = checkoutRepository
+ .findById(id)
+ .orElseThrow(() -> new NotFoundException(Constants.ErrorCode.CHECKOUT_NOT_FOUND, id));
+
+ processPaymentAndUpdateCheckout(checkout);
+ }
+
+ private boolean isPaymentProcessing(String status, String process) {
+ return CheckoutState.PAYMENT_PROCESSING.name().equalsIgnoreCase(status)
+ && CheckoutProgress.STOCK_LOCKED.name().equalsIgnoreCase(process);
+ }
+
+ private void processPaymentAndUpdateCheckout(Checkout checkout) {
+
+ try {
+ Long paymentId = processPayment(checkout);
+ checkout.setProgress(CheckoutProgress.PAYMENT_CREATED);
+ checkout.setLastError(null);
+
+ ObjectNode updatedAttributes = updateAttributesWithPayment(checkout.getAttributes(), paymentId);
+ checkout.setAttributes(convertObjectToString(objectMapper, updatedAttributes));
+
+ } catch (Exception e) {
+
+ checkout.setProgress(CheckoutProgress.PAYMENT_CREATED_FAILED);
+
+ ObjectNode error = createJsonErrorObject(objectMapper, CheckoutProgress.PAYMENT_CREATED_FAILED.name(),
+ e.getMessage());
+ checkout.setLastError(convertObjectToString(objectMapper, error));
+
+ LOGGER.error(e.getMessage());
+ throw new BadRequestException(Constants.ErrorCode.PROCESS_CHECKOUT_FAILED, checkout.getId());
+
+ } finally {
+ checkoutRepository.save(checkout);
+ }
+ }
+
+ private Long processPayment(Checkout checkout) {
+
+ CheckoutPaymentVm requestDto = new CheckoutPaymentVm(
+ checkout.getId(),
+ checkout.getPaymentMethodId(),
+ checkout.getTotalAmount()
+ );
+
+ Long paymentId = paymentService.createPaymentFromEvent(requestDto);
+ LOGGER.info("Payment created successfully with ID: {}", paymentId);
+
+ return paymentId;
+ }
+
+ private ObjectNode updateAttributesWithPayment(String attributes, Long paymentId) {
+
+ ObjectNode attributesNode = getAttributesNode(objectMapper, attributes);
+ attributesNode.put(Constants.Column.CHECKOUT_ATTRIBUTES_PAYMENT_ID_FIELD, paymentId);
+
+ return attributesNode;
+ }
+
+}
diff --git a/order/src/main/java/com/yas/order/kafka/consumer/PaymentUpdateConsumer.java b/order/src/main/java/com/yas/order/kafka/consumer/PaymentUpdateConsumer.java
new file mode 100644
index 0000000000..e3be435c6a
--- /dev/null
+++ b/order/src/main/java/com/yas/order/kafka/consumer/PaymentUpdateConsumer.java
@@ -0,0 +1,108 @@
+package com.yas.order.kafka.consumer;
+
+import static com.yas.order.utils.JsonUtils.convertObjectToString;
+import static com.yas.order.utils.JsonUtils.getAttributesNode;
+import static com.yas.order.utils.JsonUtils.getJsonValueOrNull;
+import static com.yas.order.utils.JsonUtils.getJsonValueOrThrow;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.yas.order.model.Checkout;
+import com.yas.order.model.enumeration.CheckoutProgress;
+import com.yas.order.model.enumeration.CheckoutState;
+import com.yas.order.service.CheckoutService;
+import com.yas.order.utils.Constants;
+import java.util.Objects;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.annotation.RetryableTopic;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class PaymentUpdateConsumer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PaymentUpdateConsumer.class);
+ private final CheckoutService checkoutService;
+ private final ObjectMapper objectMapper;
+ private final Gson gson;
+
+ @KafkaListener(
+ topics = "${cdc.event.payment.topic-name}",
+ groupId = "${cdc.event.payment.update.group-id}"
+ )
+ @RetryableTopic
+ public void listen(ConsumerRecord, ?> consumerRecord) {
+
+ if (Objects.isNull(consumerRecord)) {
+ LOGGER.info("Consumer Record is null");
+ return;
+ }
+ JsonObject valueObject = gson.fromJson((String) consumerRecord.value(), JsonObject.class);
+ processPaymentEvent(valueObject);
+
+ }
+
+ private void processPaymentEvent(JsonObject valueObject) {
+ Optional.ofNullable(valueObject)
+ .filter(
+ value -> value.has("op") && "u".equals(value.get("op").getAsString())
+ )
+ .filter(value -> value.has("before") && value.has("after"))
+ .ifPresent(this::handleJsonForUpdateCheckout);
+ }
+
+ private void handleJsonForUpdateCheckout(JsonObject valueObject) {
+
+ JsonObject before = valueObject.getAsJsonObject("before");
+ JsonObject after = valueObject.getAsJsonObject("after");
+
+ String id = getJsonValueOrThrow(after, Constants.Column.ID_COLUMN,
+ Constants.ErrorCode.ID_NOT_EXISTED);
+
+ String beforePaypalOrderId = getJsonValueOrNull(before,
+ Constants.Column.CHECKOUT_ATTRIBUTES_PAYMENT_PROVIDER_CHECKOUT_ID_FIELD);
+ String afterPaypalOrderId = getJsonValueOrNull(after,
+ Constants.Column.CHECKOUT_ATTRIBUTES_PAYMENT_PROVIDER_CHECKOUT_ID_FIELD);
+
+ if (!Objects.isNull(afterPaypalOrderId) && !afterPaypalOrderId.equals(beforePaypalOrderId)) {
+
+ LOGGER.info("Handle json for update Checkout with Payment {}", id);
+
+ String checkoutId = getJsonValueOrThrow(after, Constants.Column.CHECKOUT_ID_COLUMN,
+ Constants.ErrorCode.CHECKOUT_ID_NOT_EXISTED);
+ updateCheckOut(checkoutId, afterPaypalOrderId);
+ } else {
+ LOGGER.warn("It is not an event to update an Order on PayPal with Payment ID {}", id);
+ }
+ }
+
+ private void updateCheckOut(String checkoutId, String paymentProviderCheckoutId) {
+
+ Checkout checkout = checkoutService.findCheckoutById(checkoutId);
+ checkout.setCheckoutState(CheckoutState.PAYMENT_PROCESSING);
+ checkout.setProgress(CheckoutProgress.PAYMENT_CREATED);
+
+ ObjectNode updatedAttributes = updateAttributesWithCheckout(checkout.getAttributes(),
+ paymentProviderCheckoutId);
+ checkout.setAttributes(convertObjectToString(objectMapper, updatedAttributes));
+
+ checkoutService.updateCheckout(checkout);
+ }
+
+ private ObjectNode updateAttributesWithCheckout(String attributes, String paymentProviderCheckoutId) {
+
+ ObjectNode attributesNode = getAttributesNode(objectMapper, attributes);
+ attributesNode.put(Constants.Column.CHECKOUT_ATTRIBUTES_PAYMENT_PROVIDER_CHECKOUT_ID_FIELD,
+ paymentProviderCheckoutId);
+
+ return attributesNode;
+ }
+
+}
\ No newline at end of file
diff --git a/order/src/main/java/com/yas/order/mapper/CheckoutMapper.java b/order/src/main/java/com/yas/order/mapper/CheckoutMapper.java
index 54829ae941..e53f14c1c0 100644
--- a/order/src/main/java/com/yas/order/mapper/CheckoutMapper.java
+++ b/order/src/main/java/com/yas/order/mapper/CheckoutMapper.java
@@ -16,11 +16,22 @@
public interface CheckoutMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "checkout", ignore = true)
+ @Mapping(target = "checkoutId", ignore = true)
CheckoutItem toModel(CheckoutItemPostVm checkoutItemPostVm);
@Mapping(target = "id", ignore = true)
@Mapping(target = "checkoutState", ignore = true)
- @Mapping(target = "totalAmount", source = "totalAmount") // Ánh xạ tường minh cho totalAmount
+ @Mapping(target = "progress", ignore = true)
+ @Mapping(target = "customerId", ignore = true)
+ @Mapping(target = "shipmentMethodId", ignore = true)
+ @Mapping(target = "paymentMethodId", ignore = true)
+ @Mapping(target = "shippingAddressId", ignore = true)
+ @Mapping(target = "lastError", ignore = true)
+ @Mapping(target = "attributes", ignore = true)
+ @Mapping(target = "totalShipmentFee", ignore = true)
+ @Mapping(target = "totalShipmentTax", ignore = true)
+ @Mapping(target = "totalTax", ignore = true)
+ @Mapping(target = "totalAmount", source = "totalAmount")
@Mapping(target = "totalDiscountAmount", source = "totalDiscountAmount")
Checkout toModel(CheckoutPostVm checkoutPostVm);
diff --git a/order/src/main/java/com/yas/order/model/Checkout.java b/order/src/main/java/com/yas/order/model/Checkout.java
index c2c555628f..4478783238 100644
--- a/order/src/main/java/com/yas/order/model/Checkout.java
+++ b/order/src/main/java/com/yas/order/model/Checkout.java
@@ -1,6 +1,8 @@
package com.yas.order.model;
+import com.yas.order.model.enumeration.CheckoutProgress;
import com.yas.order.model.enumeration.CheckoutState;
+import com.yas.order.model.enumeration.PaymentMethod;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
@@ -39,8 +41,8 @@ public class Checkout extends AbstractAuditEntity {
@Enumerated(EnumType.STRING)
private CheckoutState checkoutState;
- @SuppressWarnings("unused")
- private String progress;
+ @Enumerated(EnumType.STRING)
+ private CheckoutProgress progress;
@SuppressWarnings("unused")
private Long customerId;
@@ -48,13 +50,12 @@ public class Checkout extends AbstractAuditEntity {
@SuppressWarnings("unused")
private String shipmentMethodId;
- @Column(name = "payment_method_id")
- private String paymentMethodId;
+ @Enumerated(EnumType.STRING)
+ private PaymentMethod paymentMethodId;
@SuppressWarnings("unused")
private Long shippingAddressId;
- @SuppressWarnings("unused")
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "last_error", columnDefinition = "jsonb")
private String lastError;
diff --git a/order/src/main/java/com/yas/order/model/enumeration/CheckoutProgress.java b/order/src/main/java/com/yas/order/model/enumeration/CheckoutProgress.java
new file mode 100644
index 0000000000..5f21ca4535
--- /dev/null
+++ b/order/src/main/java/com/yas/order/model/enumeration/CheckoutProgress.java
@@ -0,0 +1,20 @@
+package com.yas.order.model.enumeration;
+
+public enum CheckoutProgress {
+ INIT("Init"),
+ PROMOTION_CODE_APPLIED("Promotion code applied"),
+ PROMOTION_CODE_APPLIED_FAILED("Promotion Code applied failed"),
+ STOCK_LOCKED("Stock locked"),
+ STOCK_LOCKED_FAILED("Stock locked failed"),
+ PAYMENT_CREATED("Payment created"),
+ PAYMENT_CREATED_FAILED("Payment created failed");
+ private final String name;
+
+ CheckoutProgress(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/order/src/main/java/com/yas/order/model/enumeration/CheckoutState.java b/order/src/main/java/com/yas/order/model/enumeration/CheckoutState.java
index 22c2cec975..3bf0046bee 100644
--- a/order/src/main/java/com/yas/order/model/enumeration/CheckoutState.java
+++ b/order/src/main/java/com/yas/order/model/enumeration/CheckoutState.java
@@ -1,7 +1,15 @@
package com.yas.order.model.enumeration;
public enum CheckoutState {
- COMPLETED("Completed"), PENDING("Pending"), LOCK("LOCK");
+ COMPLETED("Completed"),
+ PENDING("Pending"),
+ LOCK("LOCK"),
+ CHECKED_OUT("Checked Out"),
+ PAYMENT_PROCESSING("Payment Processing"),
+ PAYMENT_FAILED("Payment Failed"),
+ PAYMENT_CONFIRMED("Payment Confirmed"),
+ FULFILLED("Fulfilled")
+ ;
private final String name;
CheckoutState(String name) {
diff --git a/order/src/main/java/com/yas/order/model/enumeration/PaymentMethod.java b/order/src/main/java/com/yas/order/model/enumeration/PaymentMethod.java
index eb189d80b2..42ea52f8b9 100644
--- a/order/src/main/java/com/yas/order/model/enumeration/PaymentMethod.java
+++ b/order/src/main/java/com/yas/order/model/enumeration/PaymentMethod.java
@@ -1,5 +1,14 @@
package com.yas.order.model.enumeration;
public enum PaymentMethod {
- COD, BANKING, PAYPAL
+ COD, BANKING, PAYPAL;
+
+ public static PaymentMethod fromValue(String value) {
+ try {
+ return PaymentMethod.valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid payment method: " + value);
+ }
+ }
}
+
diff --git a/order/src/main/java/com/yas/order/service/CheckoutService.java b/order/src/main/java/com/yas/order/service/CheckoutService.java
index f8ab6b5169..d499a54dee 100644
--- a/order/src/main/java/com/yas/order/service/CheckoutService.java
+++ b/order/src/main/java/com/yas/order/service/CheckoutService.java
@@ -2,6 +2,7 @@
import static com.yas.order.utils.Constants.ErrorCode.CHECKOUT_NOT_FOUND;
+import com.yas.commonlibrary.exception.BadRequestException;
import com.yas.commonlibrary.exception.Forbidden;
import com.yas.commonlibrary.exception.NotFoundException;
import com.yas.order.mapper.CheckoutMapper;
@@ -9,6 +10,7 @@
import com.yas.order.model.CheckoutItem;
import com.yas.order.model.Order;
import com.yas.order.model.enumeration.CheckoutState;
+import com.yas.order.model.enumeration.PaymentMethod;
import com.yas.order.repository.CheckoutItemRepository;
import com.yas.order.repository.CheckoutRepository;
import com.yas.order.utils.AuthenticationUtils;
@@ -18,7 +20,10 @@
import com.yas.order.viewmodel.checkout.CheckoutPostVm;
import com.yas.order.viewmodel.checkout.CheckoutStatusPutVm;
import com.yas.order.viewmodel.checkout.CheckoutVm;
+import java.util.Collections;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -104,9 +109,43 @@ public Long updateCheckoutStatus(CheckoutStatusPutVm checkoutStatusPutVm) {
}
public void updateCheckoutPaymentMethod(String id, CheckoutPaymentMethodPutVm checkoutPaymentMethodPutVm) {
+
+ if (Objects.isNull(checkoutPaymentMethodPutVm.paymentMethodId())) {
+ return;
+ }
+
Checkout checkout = checkoutRepository.findById(id)
.orElseThrow(() -> new NotFoundException(CHECKOUT_NOT_FOUND, id));
- checkout.setPaymentMethodId(checkoutPaymentMethodPutVm.paymentMethodId());
+ checkout.setPaymentMethodId(PaymentMethod.fromValue(checkoutPaymentMethodPutVm.paymentMethodId()));
+ checkoutRepository.save(checkout);
+ }
+
+ public Checkout findCheckoutById(String id) {
+
+ return this.checkoutRepository.findById(id)
+ .orElseThrow(() -> new NotFoundException(CHECKOUT_NOT_FOUND, id));
+ }
+
+ public CheckoutVm findCheckoutWithItemsById(String id) {
+
+ Checkout checkout = findCheckoutById(id);
+
+ List checkoutItems = checkoutItemRepository.findAllByCheckoutId(checkout.getId());
+
+ Set checkoutItemVms = Optional.ofNullable(checkoutItems)
+ .orElse(Collections.emptyList())
+ .stream()
+ .map(checkoutMapper::toVm)
+ .collect(Collectors.toSet());
+
+ return CheckoutVm.fromModel(checkout, checkoutItemVms);
+ }
+
+ public void updateCheckout(Checkout checkout) {
+
+ if (Objects.isNull(checkout.getId())) {
+ throw new BadRequestException(Constants.ErrorCode.ID_NOT_EXISTED);
+ }
checkoutRepository.save(checkout);
}
}
diff --git a/order/src/main/java/com/yas/order/service/PaymentService.java b/order/src/main/java/com/yas/order/service/PaymentService.java
new file mode 100644
index 0000000000..0ee8334cf0
--- /dev/null
+++ b/order/src/main/java/com/yas/order/service/PaymentService.java
@@ -0,0 +1,42 @@
+package com.yas.order.service;
+
+import com.yas.order.config.ServiceUrlConfig;
+import com.yas.order.viewmodel.payment.CheckoutPaymentVm;
+import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
+import io.github.resilience4j.retry.annotation.Retry;
+import java.net.URI;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.util.UriComponentsBuilder;
+
+@Service
+@RequiredArgsConstructor
+public class PaymentService extends AbstractCircuitBreakFallbackHandler {
+
+ private final RestClient restClient;
+
+ private final ServiceUrlConfig serviceUrlConfig;
+
+ @Retry(name = "restApi")
+ @CircuitBreaker(name = "restCircuitBreaker", fallbackMethod = "handleLongFallback")
+ public Long createPaymentFromEvent(CheckoutPaymentVm checkoutPaymentRequestDto) {
+
+ final URI url = UriComponentsBuilder
+ .fromHttpUrl(serviceUrlConfig.payment())
+ .path("/events/payments")
+ .buildAndExpand()
+ .toUri();
+
+ return restClient.post()
+ .uri(url)
+ .body(checkoutPaymentRequestDto)
+ .retrieve()
+ .body(Long.class);
+ }
+
+ private Long handleLongFallback(Throwable throwable)
+ throws Throwable {
+ return handleTypedFallback(throwable);
+ }
+}
diff --git a/order/src/main/java/com/yas/order/utils/Constants.java b/order/src/main/java/com/yas/order/utils/Constants.java
index 7a85f82528..3e2f18f55c 100644
--- a/order/src/main/java/com/yas/order/utils/Constants.java
+++ b/order/src/main/java/com/yas/order/utils/Constants.java
@@ -9,6 +9,12 @@ private ErrorCode() {}
public static final String CHECKOUT_NOT_FOUND = "CHECKOUT_NOT_FOUND";
public static final String SIGN_IN_REQUIRED = "SIGN_IN_REQUIRED";
public static final String FORBIDDEN = "FORBIDDEN";
+ public static final String CANNOT_CONVERT_TO_STRING = "CANNOT_CONVERT_TO_STRING";
+ public static final String PROCESS_CHECKOUT_FAILED = "PROCESS_CHECKOUT_FAILED";
+ public static final String ID_NOT_EXISTED = "ID_NOT_EXISTED";
+ public static final String STATUS_NOT_EXISTED = "STATUS_NOT_EXISTED";
+ public static final String PROGRESS_NOT_EXISTED = "PROGRESS_NOT_EXISTED";
+ public static final String CHECKOUT_ID_NOT_EXISTED = "CHECKOUT_ID_NOT_EXISTED";
}
public final class Column {
@@ -17,6 +23,7 @@ private Column() {}
// common column
public static final String ID_COLUMN = "id";
+ public static final String STATUS_COLUMN = "status";
public static final String CREATE_ON_COLUMN = "createdOn";
public static final String CREATE_BY_COLUMN = "createdBy";
@@ -33,5 +40,12 @@ private Column() {}
public static final String ORDER_ITEM_PRODUCT_ID_COLUMN = "productId";
public static final String ORDER_ITEM_PRODUCT_NAME_COLUMN = "productName";
+ // Column name of Checkout table
+ public static final String CHECKOUT_PROGRESS_COLUMN = "progress";
+ public static final String CHECKOUT_ATTRIBUTES_PAYMENT_ID_FIELD = "payment_id";
+ public static final String CHECKOUT_ID_COLUMN = "checkout_id";
+ public static final String CHECKOUT_STATUS_COLUMN = "status";
+ public static final String CHECKOUT_ATTRIBUTES_PAYMENT_PROVIDER_CHECKOUT_ID_FIELD
+ = "payment_provider_checkout_id";
}
}
diff --git a/order/src/main/java/com/yas/order/utils/JsonUtils.java b/order/src/main/java/com/yas/order/utils/JsonUtils.java
new file mode 100644
index 0000000000..1143c70339
--- /dev/null
+++ b/order/src/main/java/com/yas/order/utils/JsonUtils.java
@@ -0,0 +1,66 @@
+package com.yas.order.utils;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.yas.commonlibrary.exception.BadRequestException;
+import java.util.Optional;
+
+public class JsonUtils {
+
+ private JsonUtils() {
+ throw new UnsupportedOperationException();
+ }
+
+ public static String convertObjectToString(ObjectMapper objectMapper, Object value) {
+ try {
+ return objectMapper.writeValueAsString(value);
+ } catch (JsonProcessingException e) {
+ throw new BadRequestException(Constants.ErrorCode.CANNOT_CONVERT_TO_STRING, value);
+ }
+ }
+
+ public static ObjectNode getAttributesNode(ObjectMapper objectMapper, String attributes) {
+ try {
+ if (attributes == null || attributes.isBlank()) {
+ return objectMapper.createObjectNode();
+ } else {
+ return (ObjectNode) objectMapper.readTree(attributes);
+ }
+ } catch (JsonProcessingException e) {
+ throw new BadRequestException("Invalid Json: {}", attributes);
+ }
+ }
+
+ public static ObjectNode createJsonErrorObject(ObjectMapper objectMapper, String errorCode, String message) {
+ ObjectNode objectNode = objectMapper.createObjectNode();
+ objectNode.put("errorCode", errorCode);
+ objectNode.put("message", message);
+ return objectNode;
+ }
+
+ public static String getJsonValueOrThrow(
+ JsonObject jsonObject,
+ String columnName,
+ String errorCode,
+ Object... errorParams
+ ) {
+ return Optional.ofNullable(jsonObject.get(columnName))
+ .filter(jsonElement -> !jsonElement.isJsonNull())
+ .map(JsonElement::getAsString)
+ .orElseThrow(() -> new BadRequestException(errorCode, errorParams));
+ }
+
+ public static String getJsonValueOrNull(
+ JsonObject jsonObject,
+ String columnName
+ ) {
+ JsonElement jsonElement = jsonObject.get(columnName);
+ if (jsonElement != null && !jsonElement.isJsonNull()) {
+ return jsonElement.getAsString();
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java b/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java
index 815200c8b6..8a99b697d2 100644
--- a/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java
+++ b/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java
@@ -1,5 +1,6 @@
package com.yas.order.viewmodel.checkout;
+import com.yas.order.model.Checkout;
import java.math.BigDecimal;
import java.util.Set;
import lombok.Builder;
@@ -14,4 +15,14 @@ public record CheckoutVm(
BigDecimal totalDiscountAmount,
Set checkoutItemVms
) {
+
+ public static CheckoutVm fromModel(Checkout checkout, Set checkoutItemVms) {
+ return CheckoutVm.builder()
+ .id(checkout.getId())
+ .email(checkout.getEmail())
+ .note(checkout.getNote())
+ .couponCode(checkout.getCouponCode())
+ .checkoutItemVms(checkoutItemVms)
+ .build();
+ }
}
\ No newline at end of file
diff --git a/order/src/main/java/com/yas/order/viewmodel/orderaddress/OrderAddressPostVm.java b/order/src/main/java/com/yas/order/viewmodel/orderaddress/OrderAddressPostVm.java
index 5007f714f4..6510dc451d 100644
--- a/order/src/main/java/com/yas/order/viewmodel/orderaddress/OrderAddressPostVm.java
+++ b/order/src/main/java/com/yas/order/viewmodel/orderaddress/OrderAddressPostVm.java
@@ -6,6 +6,7 @@
@Builder
public record OrderAddressPostVm(
+ Long id,
@NotBlank String contactName,
@NotBlank String phone,
@NotBlank String addressLine1,
diff --git a/order/src/main/java/com/yas/order/viewmodel/payment/CheckoutPaymentVm.java b/order/src/main/java/com/yas/order/viewmodel/payment/CheckoutPaymentVm.java
new file mode 100644
index 0000000000..019f7b5770
--- /dev/null
+++ b/order/src/main/java/com/yas/order/viewmodel/payment/CheckoutPaymentVm.java
@@ -0,0 +1,8 @@
+package com.yas.order.viewmodel.payment;
+
+import com.yas.order.model.enumeration.PaymentMethod;
+import java.math.BigDecimal;
+
+public record CheckoutPaymentVm(String checkoutId, PaymentMethod paymentMethod, BigDecimal totalAmount) {
+
+}
\ No newline at end of file
diff --git a/order/src/main/resources/application.properties b/order/src/main/resources/application.properties
index 31e9b86291..ce6651f706 100644
--- a/order/src/main/resources/application.properties
+++ b/order/src/main/resources/application.properties
@@ -16,6 +16,7 @@ yas.services.customer=http://api.yas.local/customer
yas.services.product=http://api.yas.local/product
yas.services.tax=http://api.yas.local/tax
yas.services.promotion=http://api.yas.local/promotion
+yas.services.payment=http://api.yas.local/payment
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/order
@@ -49,4 +50,19 @@ resilience4j.circuitbreaker.instances.rest-circuit-breaker.failure-rate-threshol
resilience4j.circuitbreaker.instances.rest-circuit-breaker.minimum-number-of-calls=5
resilience4j.circuitbreaker.instances.rest-circuit-breaker.automatic-transition-from-open-to-half-open-enabled=true
resilience4j.circuitbreaker.instances.rest-circuit-breaker.permitted-number-of-calls-in-half-open-state=3
-cors.allowed-origins=*
\ No newline at end of file
+cors.allowed-origins=*
+
+cdc.event.checkout.status.topic-name=dbcheckout-status.public.checkout
+cdc.event.checkout.status.group-id=checkout-status
+
+cdc.event.payment.topic-name=dbpayment.public.payment
+cdc.event.payment.update.group-id=payment-update
+
+# Kafka Consumer
+spring.kafka.consumer.bootstrap-servers=kafka:9092
+spring.aop.proxy-target-class=true
+
+# Kafka Producer
+spring.kafka.producer.bootstrap-servers=kafka:9092
+spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
+spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
\ No newline at end of file
diff --git a/order/src/main/resources/messages/messages.properties b/order/src/main/resources/messages/messages.properties
index f00590fe86..414c0feeca 100644
--- a/order/src/main/resources/messages/messages.properties
+++ b/order/src/main/resources/messages/messages.properties
@@ -2,4 +2,11 @@ ORDER_NOT_FOUND=Order {} is not found
CHECKOUT_NOT_FOUND=Checkout {} is not found
SUCCESS_MESSAGE=Success
SIGN_IN_REQUIRED=Authentication required
-FORBIDDEN=You don't have permission to access this page
\ No newline at end of file
+FORBIDDEN=You don't have permission to access this page
+PAYMENT_METHOD_NOT_EXISTED=Payment method id is not existed in Checkout {}
+CANNOT_CONVERT_TO_STRING=Can not convert object to String : {}
+PROCESS_CHECKOUT_FAILED=Failed to process checkout event for ID {}
+ID_NOT_EXISTED=ID is not existed
+CHECKOUT_ID_NOT_EXISTED=Checkout ID is not existed
+STATUS_NOT_EXISTED=Status is missing for Checkout ID {}
+PROGRESS_NOT_EXISTED=Progress is missing for Checkout ID {}
\ No newline at end of file
diff --git a/order/src/test/java/com/yas/order/config/IntegrationTestConfiguration.java b/order/src/test/java/com/yas/order/config/IntegrationTestConfiguration.java
new file mode 100644
index 0000000000..622eaca43b
--- /dev/null
+++ b/order/src/test/java/com/yas/order/config/IntegrationTestConfiguration.java
@@ -0,0 +1,17 @@
+package com.yas.order.config;
+
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Bean;
+import org.testcontainers.containers.PostgreSQLContainer;
+
+@TestConfiguration
+public class IntegrationTestConfiguration {
+
+ @Bean(destroyMethod = "stop")
+ @ServiceConnection
+ public PostgreSQLContainer> postgresContainer() {
+ return new PostgreSQLContainer<>("postgres:16")
+ .withReuse(true);
+ }
+}
diff --git a/order/src/test/java/com/yas/order/config/KafkaIntegrationTestConfiguration.java b/order/src/test/java/com/yas/order/config/KafkaIntegrationTestConfiguration.java
new file mode 100644
index 0000000000..7cf7e5e2a1
--- /dev/null
+++ b/order/src/test/java/com/yas/order/config/KafkaIntegrationTestConfiguration.java
@@ -0,0 +1,23 @@
+package com.yas.order.config;
+
+import common.container.ContainerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Bean;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.testcontainers.containers.KafkaContainer;
+
+@TestConfiguration
+public class KafkaIntegrationTestConfiguration {
+
+ @Value("${kafka.version}")
+ private String kafkaVersion;
+
+ @Bean
+ @ServiceConnection
+ public KafkaContainer kafkaContainer(DynamicPropertyRegistry registry) {
+ return ContainerFactory.kafkaContainer(registry, kafkaVersion);
+ }
+
+}
diff --git a/order/src/test/java/com/yas/order/consumer/OrderStatusConsumerTest.java b/order/src/test/java/com/yas/order/consumer/OrderStatusConsumerTest.java
new file mode 100644
index 0000000000..9606677370
--- /dev/null
+++ b/order/src/test/java/com/yas/order/consumer/OrderStatusConsumerTest.java
@@ -0,0 +1,206 @@
+package com.yas.order.consumer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.yas.commonlibrary.exception.BadRequestException;
+import com.yas.commonlibrary.exception.NotFoundException;
+import com.yas.order.kafka.consumer.OrderStatusConsumer;
+import com.yas.order.model.Checkout;
+import com.yas.order.model.enumeration.CheckoutProgress;
+import com.yas.order.repository.CheckoutRepository;
+import com.yas.order.service.PaymentService;
+import java.util.Optional;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+class OrderStatusConsumerTest {
+
+ private PaymentService paymentService;
+
+ private CheckoutRepository checkoutRepository;
+
+ private OrderStatusConsumer orderStatusConsumer;
+
+ private final String jsonRecord = "{\"after\": {"
+ + " \"status\": \"PAYMENT_PROCESSING\","
+ + " \"progress\": \"STOCK_LOCKED\","
+ + " \"id\": 12345,"
+ + " \"payment_method_id\": \"PAYPAL\","
+ + " \"total_amount\": 123}"
+ + "}";
+
+ @BeforeEach
+ void setUp() {
+ paymentService = Mockito.mock(PaymentService.class);
+ checkoutRepository = Mockito.mock(CheckoutRepository.class);
+ ObjectMapper objectMapper = new ObjectMapper();
+ Gson gson = new Gson();
+ orderStatusConsumer = new OrderStatusConsumer(paymentService, checkoutRepository, objectMapper, gson);
+
+ }
+
+ @Test
+ void testListen_whenConsumerRecordIsNull_shouldNotThing() {
+
+ orderStatusConsumer.listen(null);
+
+ verify(checkoutRepository, never()).findById(any());
+ verify(paymentService, never()).createPaymentFromEvent(any());
+ verify(checkoutRepository, never()).save(any());
+ }
+
+ @Test
+ void testListen_whenHaveNoAfter_shouldNotThing() {
+
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+
+ when(consumerRecord.value()).thenReturn("{}");
+
+ orderStatusConsumer.listen(consumerRecord);
+
+ verify(checkoutRepository, never()).save(any());
+ verify(checkoutRepository, never()).findById(any());
+ verify(paymentService, never()).createPaymentFromEvent(any());
+ }
+
+ @Test
+ void testListen_whenIsNotPaymentProcess_shouldNotThing() {
+
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+
+ String invalidDataJson = "{\"after\": {"
+ + " \"status\": \"CHECKED_OUT\","
+ + " \"progress\": \"STOCK_LOCKED\","
+ + " \"id\": 12345,"
+ + " \"payment_method_id\": \"PAYPAL\","
+ + " \"total_amount\": 123}"
+ + "}";
+
+ when(consumerRecord.value()).thenReturn(invalidDataJson);
+
+ orderStatusConsumer.listen(consumerRecord);
+
+ verify(checkoutRepository, never()).findById(any());
+ verify(checkoutRepository, never()).save(any());
+ verify(paymentService, never()).createPaymentFromEvent(any());
+ }
+
+ @Test
+ void testListen_whenProgressIsNotStockLocked_shouldNotThing() {
+
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+
+ String invalidDataJson = "{\"after\": {"
+ + " \"status\": \"PAYMENT_PROCESSING\","
+ + " \"progress\": \"INIT\","
+ + " \"id\": 12345,"
+ + " \"payment_method_id\": \"PAYPAL\","
+ + " \"total_amount\": 123}"
+ + "}";
+
+ when(consumerRecord.value()).thenReturn(invalidDataJson);
+
+ orderStatusConsumer.listen(consumerRecord);
+
+ verify(checkoutRepository, never()).findById(any());
+ verify(checkoutRepository, never()).save(any());
+ verify(paymentService, never()).createPaymentFromEvent(any());
+ }
+
+ @Test
+ void testListen_whenCheckoutIsNotFound_shouldNotSave() {
+
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+
+ when(consumerRecord.value()).thenReturn(jsonRecord);
+ JsonObject jsonObject = mock(JsonObject.class);
+ when(jsonObject.has("after")).thenReturn(true);
+ when(jsonObject.getAsJsonObject("after")).thenReturn(mock(JsonObject.class));
+
+ when(checkoutRepository.findById(anyString())).thenReturn(Optional.empty());
+
+ when(paymentService.createPaymentFromEvent(any())).thenReturn(1L);
+
+ NotFoundException notFoundException
+ = Assertions.assertThrows(NotFoundException.class, () -> orderStatusConsumer.listen(consumerRecord));
+
+ assertThat(notFoundException.getMessage()).isEqualTo("Checkout 12345 is not found");
+ verify(checkoutRepository, atLeastOnce()).findById(any());
+ verify(checkoutRepository, never()).save(any());
+ verify(paymentService, never()).createPaymentFromEvent(any());
+ }
+
+ @Test
+ void testListen_whenCreatePaymentSuccess_shouldProcessCheckoutEvent() {
+
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+
+ when(consumerRecord.value()).thenReturn(jsonRecord);
+ JsonObject jsonObject = mock(JsonObject.class);
+ when(jsonObject.has("after")).thenReturn(true);
+ when(jsonObject.getAsJsonObject("after")).thenReturn(mock(JsonObject.class));
+
+ ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Checkout.class);
+
+ when(checkoutRepository.findById(anyString())).thenReturn(Optional.of(new Checkout()));
+
+ when(paymentService.createPaymentFromEvent(any())).thenReturn(2L);
+
+ orderStatusConsumer.listen(consumerRecord);
+
+ verify(checkoutRepository, atLeastOnce()).findById(any());
+ verify(paymentService, atLeastOnce()).createPaymentFromEvent(any());
+ verify(checkoutRepository, atLeastOnce()).save(argumentCaptor.capture());
+
+ Checkout actual = argumentCaptor.getValue();
+ assertThat(actual.getProgress()).isEqualTo(CheckoutProgress.PAYMENT_CREATED);
+ assertThat(actual.getAttributes()).isEqualTo("{\"payment_id\":2}");
+ }
+
+ @Test
+ void testListen_whenCreatePaymentFailure_shouldThrowException() {
+
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+
+ when(consumerRecord.value()).thenReturn(jsonRecord);
+ JsonObject jsonObject = mock(JsonObject.class);
+ when(jsonObject.has("after")).thenReturn(true);
+ when(jsonObject.getAsJsonObject("after")).thenReturn(mock(JsonObject.class));
+
+ Checkout checkout = new Checkout();
+ checkout.setId("12345");
+ when(checkoutRepository.findById(anyString())).thenReturn(Optional.of(checkout));
+
+ BadRequestException badRequestException = new BadRequestException("test exception");
+ when(paymentService.createPaymentFromEvent(any())).thenThrow(badRequestException);
+
+ BadRequestException badRequest
+ = Assertions.assertThrows(BadRequestException.class, () -> orderStatusConsumer.listen(consumerRecord));
+ assertThat(badRequest.getMessage()).isEqualTo("Failed to process checkout event for ID 12345");
+
+ ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Checkout.class);
+ verify(checkoutRepository, atLeastOnce()).findById(any());
+ verify(checkoutRepository, atLeastOnce()).save(argumentCaptor.capture());
+ verify(paymentService, atLeastOnce()).createPaymentFromEvent(any());
+
+ Checkout actual = argumentCaptor.getValue();
+ assertThat(actual.getProgress()).isEqualTo(CheckoutProgress.PAYMENT_CREATED_FAILED);
+ assertThat(actual.getLastError())
+ .isEqualTo("{\"errorCode\":\"PAYMENT_CREATED_FAILED\",\"message\":\"test exception\"}");
+
+ }
+}
diff --git a/order/src/test/java/com/yas/order/consumer/PaymentUpdateConsumerTest.java b/order/src/test/java/com/yas/order/consumer/PaymentUpdateConsumerTest.java
new file mode 100644
index 0000000000..c4783b0ab9
--- /dev/null
+++ b/order/src/test/java/com/yas/order/consumer/PaymentUpdateConsumerTest.java
@@ -0,0 +1,136 @@
+package com.yas.order.consumer;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.Gson;
+import com.yas.order.kafka.consumer.PaymentUpdateConsumer;
+import com.yas.order.model.Checkout;
+import com.yas.order.model.enumeration.CheckoutProgress;
+import com.yas.order.model.enumeration.CheckoutState;
+import com.yas.order.service.CheckoutService;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+class PaymentUpdateConsumerTest {
+
+ private CheckoutService checkoutService;
+
+ private PaymentUpdateConsumer paymentUpdateConsumer;
+
+ private final String jsonRecord = "{\"op\":\"u\"," +
+ " \"before\":{\"payment_provider_checkout_id\":\"OLD\"}," +
+ " \"after\":{\"payment_provider_checkout_id\":\"NEW\"," +
+ " \"id\":\"1\"," +
+ " \"checkout_id\":\"12345\"}}";
+
+ @BeforeEach
+ void setUp() {
+ checkoutService = mock(CheckoutService.class);
+ ObjectMapper objectMapper = new ObjectMapper();
+ Gson gson = new Gson();
+ paymentUpdateConsumer = new PaymentUpdateConsumer(checkoutService, objectMapper, gson);
+ }
+
+ @Test
+ void testListen_whenConsumerRecordIsNull_shouldLogInfoAndDoNothing() {
+
+ paymentUpdateConsumer.listen(null);
+
+ verify(checkoutService, never()).findCheckoutById(anyString());
+ verify(checkoutService, never()).updateCheckout(any());
+ }
+
+ @Test
+ void testListen_whenValueDoesNotContainOpField_shouldNotUpdateCheckout() {
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+
+ when(consumerRecord.value()).thenReturn("{\"invalidKey\":\"value\"}");
+
+ paymentUpdateConsumer.listen(consumerRecord);
+
+ verify(checkoutService, never()).findCheckoutById(anyString());
+ verify(checkoutService, never()).updateCheckout(any());
+ }
+
+ @Test
+ void testListen_whenEventDoesNotContainBeforeField_shouldNotUpdateCheckout() {
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+ when(consumerRecord.value()).thenReturn("{\"op\":\"u\"," +
+ " \"after\":{\"payment_provider_checkout_id\":\"OLD\"}}");
+
+ paymentUpdateConsumer.listen(consumerRecord);
+
+ verify(checkoutService, never()).findCheckoutById(anyString());
+ verify(checkoutService, never()).updateCheckout(any());
+ }
+
+ @Test
+ void testListen_whenEventDoesNotContainAfterField_shouldNotUpdateCheckout() {
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+ when(consumerRecord.value()).thenReturn("{\"op\":\"u\"," +
+ " \"before\":{\"payment_provider_checkout_id\":\"OLD\"}}");
+
+ paymentUpdateConsumer.listen(consumerRecord);
+
+ verify(checkoutService, never()).updateCheckout(any());
+ verify(checkoutService, never()).findCheckoutById(anyString());
+ }
+
+ @Test
+ void testListen_whenEventAfterPaypalOrderIdIsNull_shouldNotUpdateCheckout() {
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+ when(consumerRecord.value()).thenReturn("{\"op\":\"u\"," +
+ " \"before\":{\"payment_provider_checkout_id\":\"OLD\"}," +
+ " \"after\":{" +
+ " \"id\":\"1\"," +
+ " \"checkout_id\":\"12345\"}}");
+
+ paymentUpdateConsumer.listen(consumerRecord);
+
+ verify(checkoutService, never()).updateCheckout(any());
+ }
+
+ @Test
+ void testListen_whenEventAfterPaypalOrderIdEqualsBefore_shouldNotUpdateCheckout() {
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+ when(consumerRecord.value()).thenReturn("{\"op\":\"u\"," +
+ " \"before\":{\"payment_provider_checkout_id\":\"OLD\"}," +
+ " \"after\":{\"payment_provider_checkout_id\":\"OLD\"," +
+ " \"id\":\"1\"," +
+ " \"checkout_id\":\"12345\"}}");
+
+ paymentUpdateConsumer.listen(consumerRecord);
+
+ verify(checkoutService, never()).updateCheckout(any());
+ verify(checkoutService, never()).findCheckoutById(anyString());
+ }
+
+ @Test
+ void testListen_whenEventIsUpdateAndAttributesChanged_shouldUpdateCheckout() {
+
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+ when(consumerRecord.value()).thenReturn(jsonRecord);
+
+ Checkout mockCheckout = new Checkout();
+ mockCheckout.setId("12345");
+ when(checkoutService.findCheckoutById("12345")).thenReturn(mockCheckout);
+
+ paymentUpdateConsumer.listen(consumerRecord);
+
+ ArgumentCaptor checkoutCaptor = ArgumentCaptor.forClass(Checkout.class);
+ verify(checkoutService).updateCheckout(checkoutCaptor.capture());
+
+ Checkout updatedCheckout = checkoutCaptor.getValue();
+ assertEquals(CheckoutState.PAYMENT_PROCESSING, updatedCheckout.getCheckoutState());
+ assertEquals(CheckoutProgress.PAYMENT_CREATED, updatedCheckout.getProgress());
+ }
+}
diff --git a/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java b/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java
index 7e6e38a702..19fc7ce794 100644
--- a/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java
+++ b/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java
@@ -1,7 +1,9 @@
package com.yas.order.controller;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
@@ -13,17 +15,14 @@
import com.yas.order.service.CheckoutService;
import com.yas.order.viewmodel.checkout.CheckoutItemPostVm;
import com.yas.order.viewmodel.checkout.CheckoutItemVm;
+import com.yas.order.viewmodel.checkout.CheckoutPaymentMethodPutVm;
import com.yas.order.viewmodel.checkout.CheckoutPostVm;
import com.yas.order.viewmodel.checkout.CheckoutStatusPutVm;
import com.yas.order.viewmodel.checkout.CheckoutVm;
-
-import com.yas.order.viewmodel.checkout.*;
-
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -96,18 +95,31 @@ void testUpdateCheckoutStatus_whenRequestIsValid_thenReturnLong() throws Excepti
}
@Test
- void testGetOrderWithItemsById_whenRequestIsValid_thenReturnCheckoutVm() throws Exception {
+ void testGetPendingCheckoutDetailsById_whenRequestIsValid_thenReturnCheckoutVm() throws Exception {
String id = "123";
CheckoutVm response = getCheckoutVm();
when(checkoutService.getCheckoutPendingStateWithItemsById(id)).thenReturn(response);
- mockMvc.perform(get("/storefront/checkouts/{id}", id)
+ mockMvc.perform(get("/storefront/checkouts/pending/{id}", id)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().json(objectWriter.writeValueAsString(response)));
}
+ @Test
+ void testGetCheckoutById_whenRequestIsValid_thenReturnCheckoutVm() throws Exception {
+
+ String id = "123";
+ CheckoutVm response = getCheckoutVm();
+ when(checkoutService.findCheckoutWithItemsById(id)).thenReturn(response);
+
+ mockMvc.perform(get("/storefront/checkouts/{id}", id)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(MockMvcResultMatchers.content().json(objectWriter.writeValueAsString(response)));
+ }
+
@Test
void testUpdatePaymentMethod_whenRequestIsValid_thenReturnNoContent() throws Exception {
String id = "123";
diff --git a/order/src/test/java/com/yas/order/controller/OrderControllerTest.java b/order/src/test/java/com/yas/order/controller/OrderControllerTest.java
index 1939c4b0a1..ad92143799 100644
--- a/order/src/test/java/com/yas/order/controller/OrderControllerTest.java
+++ b/order/src/test/java/com/yas/order/controller/OrderControllerTest.java
@@ -332,6 +332,7 @@ private Set getOrderItemVms() {
private OrderPostVm getOrderPostVm() {
OrderAddressPostVm shippingAddress = new OrderAddressPostVm(
+ 1L,
"John Doe",
"+123456789",
"123 Main St",
@@ -347,6 +348,7 @@ private OrderPostVm getOrderPostVm() {
);
OrderAddressPostVm billingAddress = new OrderAddressPostVm(
+ 1L,
"Jane Smith",
"+1987654321",
"789 Elm Street",
diff --git a/order/src/test/java/com/yas/order/service/CheckoutServiceTest.java b/order/src/test/java/com/yas/order/service/CheckoutServiceTest.java
index cfa16d5f78..ac2b4e8610 100644
--- a/order/src/test/java/com/yas/order/service/CheckoutServiceTest.java
+++ b/order/src/test/java/com/yas/order/service/CheckoutServiceTest.java
@@ -7,9 +7,12 @@
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import com.yas.commonlibrary.exception.BadRequestException;
import com.yas.commonlibrary.exception.Forbidden;
import com.yas.commonlibrary.exception.NotFoundException;
import com.yas.order.mapper.CheckoutMapperImpl;
@@ -20,11 +23,12 @@
import com.yas.order.repository.CheckoutRepository;
import com.yas.order.viewmodel.checkout.CheckoutPaymentMethodPutVm;
import com.yas.order.viewmodel.checkout.CheckoutPostVm;
+import com.yas.order.viewmodel.checkout.CheckoutVm;
+import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.instancio.Instancio;
-import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -181,7 +185,7 @@ void testUpdateCheckoutPaymentMethod_whenCheckoutExists_thenUpdatePaymentMethod(
Checkout checkout = new Checkout();
checkout.setId(id);
- CheckoutPaymentMethodPutVm request = new CheckoutPaymentMethodPutVm("new-payment-method-id");
+ CheckoutPaymentMethodPutVm request = new CheckoutPaymentMethodPutVm("BANKING");
when(checkoutRepository.findById(id)).thenReturn(Optional.of(checkout));
@@ -190,7 +194,7 @@ void testUpdateCheckoutPaymentMethod_whenCheckoutExists_thenUpdatePaymentMethod(
// Assert
verify(checkoutRepository).save(checkout);
- assertThat(checkout.getPaymentMethodId()).isEqualTo(request.paymentMethodId());
+ assertThat(checkout.getPaymentMethodId().name()).isEqualTo(request.paymentMethodId());
}
@Test
@@ -220,7 +224,83 @@ void testUpdateCheckoutPaymentMethod_whenPaymentMethodIdIsNull_thenDoNotUpdate()
checkoutService.updateCheckoutPaymentMethod(id, request);
// Assert
- verify(checkoutRepository).save(checkout);
+ verify(checkoutRepository, never()).save(checkout);
assertThat(checkout.getPaymentMethodId()).isNull();
}
+
+ @Test
+ void testFindCheckoutById_whenNotFound_throwNotFoundException() {
+ String id = "123";
+ when(checkoutRepository.findById(id))
+ .thenReturn(Optional.empty());
+ NotFoundException notFound = assertThrows(NotFoundException.class,
+ () -> checkoutService.findCheckoutById(id));
+ assertThat(notFound.getMessage()).isEqualTo("Checkout 123 is not found");
+ }
+
+ @Test
+ void testFindCheckoutWithItemsById_whenNormalCase_returnCheckoutVm() {
+ String id = "123";
+ Checkout checkout = new Checkout();
+ checkout.setId(id);
+ when(checkoutRepository.findById(id))
+ .thenReturn(Optional.of(checkout));
+
+ List checkoutItemVms = getCheckoutItems();
+ when(checkoutItemRepository.findAllByCheckoutId(id))
+ .thenReturn(checkoutItemVms);
+ CheckoutVm checkoutVm = checkoutService.findCheckoutWithItemsById(id);
+ assertThat(checkoutVm.id()).isEqualTo(id);
+ assertThat(checkoutVm.checkoutItemVms()).hasSize(2);
+ }
+
+ @Test
+ void testUpdateCheckout_whenNormalCase_thenUpdateCheckout() {
+ String id = "123";
+ Checkout checkout = new Checkout();
+ checkout.setId(id);
+ checkoutService.updateCheckout(checkout);
+ verify(checkoutRepository, times(1)).save(checkout);
+ }
+
+ @Test
+ void testUpdateCheckout_whenIdIsNull_throwBadRequestException() {
+ Checkout checkout = new Checkout();
+ BadRequestException badRequestException = assertThrows(BadRequestException.class,
+ () -> checkoutService.updateCheckout(checkout));
+ assertThat(badRequestException.getMessage()).isEqualTo("ID is not existed");
+ }
+
+ private List getCheckoutItems() {
+ return List.of(
+ CheckoutItem.builder()
+ .id(1L)
+ .productId(101L)
+ .checkoutId("checkout123")
+ .productName("Product A")
+ .quantity(2)
+ .productPrice(BigDecimal.valueOf(50.0))
+ .note("Note A")
+ .discountAmount(BigDecimal.valueOf(5.0))
+ .taxAmount(BigDecimal.valueOf(1.0))
+ .taxPercent(BigDecimal.valueOf(0.02))
+ .shipmentTax(BigDecimal.valueOf(0.5))
+ .shipmentFee(BigDecimal.valueOf(3.0))
+ .build(),
+ CheckoutItem.builder()
+ .id(2L)
+ .productId(102L)
+ .checkoutId("checkout124")
+ .productName("Product B")
+ .quantity(1)
+ .productPrice(BigDecimal.valueOf(30.0))
+ .note("Note B")
+ .discountAmount(BigDecimal.valueOf(3.0))
+ .taxAmount(BigDecimal.valueOf(0.6))
+ .taxPercent(BigDecimal.valueOf(0.02))
+ .shipmentTax(BigDecimal.valueOf(0.3))
+ .shipmentFee(BigDecimal.valueOf(2.0))
+ .build()
+ );
+ }
}
\ No newline at end of file
diff --git a/order/src/test/java/com/yas/order/service/PaymentServiceTest.java b/order/src/test/java/com/yas/order/service/PaymentServiceTest.java
new file mode 100644
index 0000000000..a04566d581
--- /dev/null
+++ b/order/src/test/java/com/yas/order/service/PaymentServiceTest.java
@@ -0,0 +1,74 @@
+package com.yas.order.service;
+
+import static com.yas.order.utils.SecurityContextUtils.setUpSecurityContext;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.yas.order.config.ServiceUrlConfig;
+import com.yas.order.model.enumeration.PaymentMethod;
+import com.yas.order.viewmodel.payment.CheckoutPaymentVm;
+import java.math.BigDecimal;
+import java.net.URI;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.util.UriComponentsBuilder;
+
+class PaymentServiceTest {
+
+ private RestClient restClient;
+
+ private ServiceUrlConfig serviceUrlConfig;
+
+ private PaymentService paymentService;
+
+ private RestClient.ResponseSpec responseSpec;
+
+ private static final String PAYMENT_URL = "http://api.yas.local/payment";
+
+ @BeforeEach
+ void setUp() {
+ restClient = mock(RestClient.class);
+ serviceUrlConfig = mock(ServiceUrlConfig.class);
+ paymentService = new PaymentService(restClient, serviceUrlConfig);
+ responseSpec = Mockito.mock(RestClient.ResponseSpec.class);
+ setUpSecurityContext("test");
+ when(serviceUrlConfig.payment()).thenReturn(PAYMENT_URL);
+ }
+
+ @Test
+ void testCreatePaymentFromEvent_ifNormalCase_returnLong() {
+
+ final URI url = UriComponentsBuilder
+ .fromHttpUrl(serviceUrlConfig.payment())
+ .path("/events/payments")
+ .buildAndExpand()
+ .toUri();
+
+ RestClient.RequestBodyUriSpec requestBodyUriSpec = mock(RestClient.RequestBodyUriSpec.class);
+ when(restClient.post()).thenReturn(requestBodyUriSpec);
+ when(requestBodyUriSpec.uri(url)).thenReturn(requestBodyUriSpec);
+
+ CheckoutPaymentVm checkoutPaymentRequestDto = new CheckoutPaymentVm(
+ "123",
+ PaymentMethod.PAYPAL,
+ new BigDecimal(12)
+ );
+
+ when(requestBodyUriSpec.headers(any())).thenReturn(requestBodyUriSpec);
+ when(requestBodyUriSpec.body(checkoutPaymentRequestDto)).thenReturn(requestBodyUriSpec);
+ when(requestBodyUriSpec.retrieve()).thenReturn(responseSpec);
+ when(responseSpec.body(Long.class)).thenReturn(1L);
+
+ Long result = paymentService.createPaymentFromEvent(checkoutPaymentRequestDto);
+
+ assertNotNull(result);
+ assertThat(result).isEqualTo(1L);
+
+ }
+
+}
\ No newline at end of file
diff --git a/order/src/test/resources/application.properties b/order/src/test/resources/application.properties
index 8587b5409a..5605601269 100644
--- a/order/src/test/resources/application.properties
+++ b/order/src/test/resources/application.properties
@@ -2,19 +2,27 @@
server.servlet.context-path=/v1
server.port=8085
-# Setting Spring profile
-spring.profiles.active=test
-
-spring.datasource.url=jdbc:h2:mem:testdb;NON_KEYWORDS=VALUE
-spring.datasource.driverClassName=org.h2.Driver
-spring.datasource.username=sa
-spring.datasource.password=
-spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
-
spring.jpa.hibernate.ddl-auto=update
spring.liquibase.enabled=false
spring.security.oauth2.resourceserver.jwt.issuer-uri=test
springdoc.oauthflow.authorization-url=test
springdoc.oauthflow.token-url=test
-cors.allowed-origins=*
\ No newline at end of file
+cors.allowed-origins=*
+
+cdc.event.checkout.status.topic-name=dbcheckout-status.public.checkout
+cdc.event.checkout.status.group-id=checkout-status
+
+cdc.event.payment.topic-name=dbpayment.public.payment
+cdc.event.payment.update.group-id=payment-update
+
+kafka.version=7.0.9
+
+spring.kafka.consumer.bootstrap-servers=kafka:9092
+spring.aop.proxy-target-class=true
+
+spring.kafka.producer.bootstrap-servers=kafka:9092
+spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
+spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
+
+spring.kafka.bootstrap-servers=localhost:9092
\ No newline at end of file
diff --git a/payment-paypal/src/main/java/com/yas/payment/paypal/service/PaypalService.java b/payment-paypal/src/main/java/com/yas/payment/paypal/service/PaypalService.java
index 6684077b74..5116c37453 100644
--- a/payment-paypal/src/main/java/com/yas/payment/paypal/service/PaypalService.java
+++ b/payment-paypal/src/main/java/com/yas/payment/paypal/service/PaypalService.java
@@ -2,22 +2,28 @@
import com.paypal.core.PayPalHttpClient;
import com.paypal.http.HttpResponse;
-import com.paypal.orders.*;
+import com.paypal.orders.AmountWithBreakdown;
+import com.paypal.orders.ApplicationContext;
+import com.paypal.orders.Capture;
+import com.paypal.orders.Order;
+import com.paypal.orders.OrderRequest;
+import com.paypal.orders.OrdersCaptureRequest;
+import com.paypal.orders.OrdersCreateRequest;
+import com.paypal.orders.PurchaseUnitRequest;
import com.yas.payment.paypal.model.CheckoutIdHelper;
import com.yas.payment.paypal.utils.Constants;
import com.yas.payment.paypal.viewmodel.PaypalCapturePaymentRequest;
import com.yas.payment.paypal.viewmodel.PaypalCapturePaymentResponse;
import com.yas.payment.paypal.viewmodel.PaypalCreatePaymentRequest;
import com.yas.payment.paypal.viewmodel.PaypalCreatePaymentResponse;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Service;
-
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.NoSuchElementException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
@Service
@Slf4j
@@ -31,7 +37,8 @@ public class PaypalService {
private String cancelUrl;
public PaypalCreatePaymentResponse createPayment(PaypalCreatePaymentRequest createPaymentRequest) {
- PayPalHttpClient payPalHttpClient = payPalHttpClientInitializer.createPaypalClient(createPaymentRequest.paymentSettings());
+ PayPalHttpClient payPalHttpClient
+ = payPalHttpClientInitializer.createPaypalClient(createPaymentRequest.paymentSettings());
OrderRequest orderRequest = new OrderRequest();
orderRequest.checkoutPaymentIntent("CAPTURE");
@@ -45,7 +52,8 @@ public PaypalCreatePaymentResponse createPayment(PaypalCreatePaymentRequest crea
.value(totalPrice.toString());
PurchaseUnitRequest purchaseUnitRequest = new PurchaseUnitRequest().amountWithBreakdown(amountWithBreakdown);
orderRequest.purchaseUnits(List.of(purchaseUnitRequest));
- String paymentMethodReturnUrl = String.format("%s?paymentMethod=%s", returnUrl, createPaymentRequest.paymentMethod());
+ String paymentMethodReturnUrl
+ = String.format("%s?paymentMethod=%s", returnUrl, createPaymentRequest.paymentMethod());
ApplicationContext applicationContext = new ApplicationContext()
.returnUrl(paymentMethodReturnUrl)
.cancelUrl(cancelUrl)
@@ -75,7 +83,8 @@ public PaypalCreatePaymentResponse createPayment(PaypalCreatePaymentRequest crea
}
public PaypalCapturePaymentResponse capturePayment(PaypalCapturePaymentRequest capturePaymentRequest) {
- PayPalHttpClient payPalHttpClient = payPalHttpClientInitializer.createPaypalClient(capturePaymentRequest.paymentSettings());
+ PayPalHttpClient payPalHttpClient
+ = payPalHttpClientInitializer.createPaypalClient(capturePaymentRequest.paymentSettings());
OrdersCaptureRequest ordersCaptureRequest = new OrdersCaptureRequest(capturePaymentRequest.token());
try {
HttpResponse httpResponse = payPalHttpClient.execute(ordersCaptureRequest);
@@ -87,7 +96,7 @@ public PaypalCapturePaymentResponse capturePayment(PaypalCapturePaymentRequest c
BigDecimal paymentFee = new BigDecimal(paypalFee);
BigDecimal amount = new BigDecimal(capture.amount().value());
- PaypalCapturePaymentResponse capturedPayment = PaypalCapturePaymentResponse.builder()
+ return PaypalCapturePaymentResponse.builder()
.paymentFee(paymentFee)
.gatewayTransactionId(order.id())
.amount(amount)
@@ -95,7 +104,6 @@ public PaypalCapturePaymentResponse capturePayment(PaypalCapturePaymentRequest c
.paymentMethod("PAYPAL")
.checkoutId(CheckoutIdHelper.getCheckoutId())
.build();
- return capturedPayment;
}
} catch (IOException e) {
log.error(e.getMessage());
diff --git a/payment-paypal/src/main/java/com/yas/payment/paypal/viewmodel/PaypalCreatePaymentRequest.java b/payment-paypal/src/main/java/com/yas/payment/paypal/viewmodel/PaypalCreatePaymentRequest.java
index 326ce70253..5421ea3558 100644
--- a/payment-paypal/src/main/java/com/yas/payment/paypal/viewmodel/PaypalCreatePaymentRequest.java
+++ b/payment-paypal/src/main/java/com/yas/payment/paypal/viewmodel/PaypalCreatePaymentRequest.java
@@ -1,9 +1,10 @@
package com.yas.payment.paypal.viewmodel;
-import lombok.Builder;
-
import java.math.BigDecimal;
+import lombok.Builder;
@Builder
-public record PaypalCreatePaymentRequest(BigDecimal totalPrice, String checkoutId, String paymentMethod, String paymentSettings) {
+public record PaypalCreatePaymentRequest(
+ BigDecimal totalPrice, String checkoutId, String paymentMethod, String paymentSettings
+) {
}
\ No newline at end of file
diff --git a/payment/pom.xml b/payment/pom.xml
index b0b81a3714..03f8167280 100644
--- a/payment/pom.xml
+++ b/payment/pom.xml
@@ -25,6 +25,14 @@
org.springframework.boot
spring-boot-starter-oauth2-resource-server
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ com.google.code.gson
+ gson
+
org.springframework.boot
spring-boot-starter-validation
diff --git a/payment/src/main/java/com/yas/payment/config/SecurityConfig.java b/payment/src/main/java/com/yas/payment/config/SecurityConfig.java
index e0ee4bb6f8..cd140341d9 100644
--- a/payment/src/main/java/com/yas/payment/config/SecurityConfig.java
+++ b/payment/src/main/java/com/yas/payment/config/SecurityConfig.java
@@ -28,6 +28,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/storefront/**").permitAll()
.requestMatchers("/backoffice/**").hasRole("ADMIN")
.requestMatchers("/payment-providers/**").permitAll()
+ .requestMatchers("/capture-payment").permitAll()
+ .requestMatchers("/events/payments").permitAll()
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
diff --git a/payment/src/main/java/com/yas/payment/consumer/PaymentCreateConsumer.java b/payment/src/main/java/com/yas/payment/consumer/PaymentCreateConsumer.java
new file mode 100644
index 0000000000..0045a95ae8
--- /dev/null
+++ b/payment/src/main/java/com/yas/payment/consumer/PaymentCreateConsumer.java
@@ -0,0 +1,101 @@
+package com.yas.payment.consumer;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.yas.commonlibrary.exception.BadRequestException;
+import com.yas.payment.model.Payment;
+import com.yas.payment.model.enumeration.PaymentMethod;
+import com.yas.payment.model.enumeration.PaymentStatus;
+import com.yas.payment.service.PaymentService;
+import com.yas.payment.utils.Constants;
+import com.yas.payment.viewmodel.InitPaymentRequestVm;
+import com.yas.payment.viewmodel.InitPaymentResponseVm;
+import java.util.Objects;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.annotation.RetryableTopic;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class PaymentCreateConsumer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PaymentCreateConsumer.class);
+ private final PaymentService paymentService;
+ private final Gson gson;
+
+ @KafkaListener(
+ topics = "${cdc.event.payment.topic-name}",
+ groupId = "${cdc.event.payment.create.group-id}"
+ )
+ @RetryableTopic
+ public void listen(ConsumerRecord, ?> consumerRecord) {
+
+ if (Objects.isNull(consumerRecord)) {
+ LOGGER.info("Consumer Record is null");
+ return;
+ }
+ JsonObject valueObject = gson.fromJson((String) consumerRecord.value(), JsonObject.class);
+ processCheckoutEvent(valueObject);
+
+ }
+
+ private void processCheckoutEvent(JsonObject valueObject) {
+ Optional.ofNullable(valueObject)
+ .filter(
+ value -> value.has("op") && "c".equals(value.get("op").getAsString())
+ )
+ .filter(value -> value.has("after"))
+ .map(value -> value.getAsJsonObject("after"))
+ .ifPresent(this::handleAfterJsonForCreatingOrder);
+ }
+
+ private void handleAfterJsonForCreatingOrder(JsonObject after) {
+
+ Long id = Optional.ofNullable(after.get(Constants.Column.ID_COLUMN))
+ .filter(jsonElement -> !jsonElement.isJsonNull())
+ .map(JsonElement::getAsLong)
+ .orElseThrow(() -> new BadRequestException(Constants.ErrorCode.ID_NOT_EXISTED));
+
+ LOGGER.info("Handle after json for creating order Payment ID {}", id);
+
+ Payment payment = paymentService.findPaymentById(id);
+
+ if (PaymentMethod.COD.equals(payment.getPaymentMethod())) {
+ LOGGER.warn(Constants.Message.PAYMENT_METHOD_COD, payment.getId());
+ return;
+ }
+
+ if (PaymentMethod.PAYPAL.equals(payment.getPaymentMethod())) {
+ LOGGER.info(Constants.Message.PAYMENT_METHOD_PAYPAL, payment.getId());
+ createOrderOnPaypal(payment);
+ } else {
+ LOGGER.warn("Currently only support payment method is Paypal");
+ }
+ }
+
+ private void createOrderOnPaypal(Payment payment) {
+
+ InitPaymentRequestVm initPaymentRequestVm = InitPaymentRequestVm.builder()
+ .paymentMethod(payment.getPaymentMethod().name())
+ .totalPrice(payment.getAmount())
+ .checkoutId(payment.getCheckoutId()).build();
+ InitPaymentResponseVm initPaymentResponseVm = paymentService.initPayment(initPaymentRequestVm);
+
+ if ("success".equals(initPaymentResponseVm.status())) {
+
+ payment.setPaymentStatus(PaymentStatus.PROCESSING);
+ payment.setPaymentProviderCheckoutId(initPaymentResponseVm.paymentId());
+
+ paymentService.updatePayment(payment);
+ } else {
+ LOGGER.warn(Constants.ErrorCode.ORDER_CREATION_FAILED, payment.getId());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/payment/src/main/java/com/yas/payment/controller/PaymentController.java b/payment/src/main/java/com/yas/payment/controller/PaymentController.java
index 02a845e858..b4a7ca4743 100644
--- a/payment/src/main/java/com/yas/payment/controller/PaymentController.java
+++ b/payment/src/main/java/com/yas/payment/controller/PaymentController.java
@@ -1,14 +1,19 @@
package com.yas.payment.controller;
+import com.yas.payment.model.Payment;
import com.yas.payment.service.PaymentService;
import com.yas.payment.viewmodel.CapturePaymentRequestVm;
import com.yas.payment.viewmodel.CapturePaymentResponseVm;
+import com.yas.payment.viewmodel.CheckoutPaymentVm;
import com.yas.payment.viewmodel.InitPaymentRequestVm;
import com.yas.payment.viewmodel.InitPaymentResponseVm;
+import com.yas.payment.viewmodel.PaymentVm;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@@ -33,4 +38,19 @@ public CapturePaymentResponseVm capturePayment(@Valid @RequestBody CapturePaymen
public ResponseEntity cancelPayment() {
return ResponseEntity.ok("Payment cancelled");
}
+
+ @PostMapping("/events/payments")
+ public ResponseEntity createPaymentFromEvent(
+ @Valid @RequestBody CheckoutPaymentVm checkoutPaymentRequestDto
+ ) {
+
+ Long paymentId = paymentService.createPaymentFromEvent(checkoutPaymentRequestDto);
+ return new ResponseEntity<>(paymentId, HttpStatus.CREATED);
+ }
+
+ @GetMapping("/storefront/payments/{id}")
+ public ResponseEntity getPaymentById(@PathVariable Long id) {
+ Payment payment = paymentService.findPaymentById(id);
+ return ResponseEntity.ok(PaymentVm.fromModel(payment));
+ }
}
diff --git a/payment/src/main/java/com/yas/payment/model/CapturedPayment.java b/payment/src/main/java/com/yas/payment/model/CapturedPayment.java
index ce67b48099..f3c69e97ac 100644
--- a/payment/src/main/java/com/yas/payment/model/CapturedPayment.java
+++ b/payment/src/main/java/com/yas/payment/model/CapturedPayment.java
@@ -2,13 +2,12 @@
import com.yas.payment.model.enumeration.PaymentMethod;
import com.yas.payment.model.enumeration.PaymentStatus;
+import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
-import java.math.BigDecimal;
-
@AllArgsConstructor
@Builder
@Setter
diff --git a/payment/src/main/java/com/yas/payment/model/Payment.java b/payment/src/main/java/com/yas/payment/model/Payment.java
index 5c2b93d583..0a9ca2d80e 100644
--- a/payment/src/main/java/com/yas/payment/model/Payment.java
+++ b/payment/src/main/java/com/yas/payment/model/Payment.java
@@ -47,7 +47,6 @@ public class Payment extends AbstractAuditEntity {
private String failureMessage;
- @SuppressWarnings("unused")
private String paymentProviderCheckoutId;
}
diff --git a/payment/src/main/java/com/yas/payment/model/enumeration/PaymentStatus.java b/payment/src/main/java/com/yas/payment/model/enumeration/PaymentStatus.java
index ac8c623274..d3b3280ef0 100644
--- a/payment/src/main/java/com/yas/payment/model/enumeration/PaymentStatus.java
+++ b/payment/src/main/java/com/yas/payment/model/enumeration/PaymentStatus.java
@@ -1,6 +1,8 @@
package com.yas.payment.model.enumeration;
public enum PaymentStatus {
+ NEW,
+ PROCESSING,
PENDING,
COMPLETED,
CANCELLED
diff --git a/payment/src/main/java/com/yas/payment/service/OrderService.java b/payment/src/main/java/com/yas/payment/service/OrderService.java
index b852e8433f..283cb9010b 100644
--- a/payment/src/main/java/com/yas/payment/service/OrderService.java
+++ b/payment/src/main/java/com/yas/payment/service/OrderService.java
@@ -6,6 +6,7 @@
import com.yas.payment.viewmodel.PaymentOrderStatusVm;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
+import java.net.URI;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -14,8 +15,6 @@
import org.springframework.web.client.RestClient;
import org.springframework.web.util.UriComponentsBuilder;
-import java.net.URI;
-
@Service
@Slf4j
@RequiredArgsConstructor
diff --git a/payment/src/main/java/com/yas/payment/service/PaymentService.java b/payment/src/main/java/com/yas/payment/service/PaymentService.java
index 407bbfbee4..e7f519aa2a 100644
--- a/payment/src/main/java/com/yas/payment/service/PaymentService.java
+++ b/payment/src/main/java/com/yas/payment/service/PaymentService.java
@@ -1,30 +1,45 @@
package com.yas.payment.service;
+import com.yas.commonlibrary.exception.BadRequestException;
+import com.yas.commonlibrary.exception.NotFoundException;
import com.yas.payment.model.CapturedPayment;
import com.yas.payment.model.InitiatedPayment;
import com.yas.payment.model.Payment;
+import com.yas.payment.model.enumeration.PaymentMethod;
+import com.yas.payment.model.enumeration.PaymentStatus;
import com.yas.payment.repository.PaymentRepository;
import com.yas.payment.service.provider.handler.PaymentHandler;
-import com.yas.payment.viewmodel.*;
+import com.yas.payment.utils.Constants;
+import com.yas.payment.viewmodel.CapturePaymentRequestVm;
+import com.yas.payment.viewmodel.CapturePaymentResponseVm;
+import com.yas.payment.viewmodel.CheckoutPaymentVm;
+import com.yas.payment.viewmodel.InitPaymentRequestVm;
+import com.yas.payment.viewmodel.InitPaymentResponseVm;
+import com.yas.payment.viewmodel.PaymentOrderStatusVm;
import jakarta.annotation.PostConstruct;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentService {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PaymentService.class);
+
private final PaymentRepository paymentRepository;
+
private final OrderService orderService;
+
private final Map providers = new HashMap<>();
- @Autowired
private final List paymentHandlers;
@PostConstruct
@@ -52,9 +67,9 @@ public InitPaymentResponseVm initPayment(InitPaymentRequestVm initPaymentRequest
.build();
}
- public CapturePaymentResponseVm capturePayment(CapturePaymentRequestVm capturePaymentRequestVM) {
- PaymentHandler paymentHandler = getPaymentHandler(capturePaymentRequestVM.paymentMethod());
- CapturedPayment capturedPayment = paymentHandler.capturePayment(capturePaymentRequestVM);
+ public CapturePaymentResponseVm capturePayment(CapturePaymentRequestVm capturePaymentRequestVm) {
+ PaymentHandler paymentHandler = getPaymentHandler(capturePaymentRequestVm.paymentMethod());
+ CapturedPayment capturedPayment = paymentHandler.capturePayment(capturePaymentRequestVm);
Long orderId = orderService.updateCheckoutStatus(capturedPayment);
capturedPayment.setOrderId(orderId);
Payment payment = createPayment(capturedPayment);
@@ -90,4 +105,40 @@ private Payment createPayment(CapturedPayment capturedPayment) {
.build();
return paymentRepository.save(payment);
}
+
+ public Long createPaymentFromEvent(CheckoutPaymentVm checkoutPaymentVm) {
+
+ Payment payment = Payment.builder()
+ .checkoutId(checkoutPaymentVm.checkoutId())
+ .paymentStatus(
+ PaymentMethod.COD.equals(checkoutPaymentVm.paymentMethod())
+ ? PaymentStatus.NEW : PaymentStatus.PROCESSING
+ )
+ .paymentMethod(checkoutPaymentVm.paymentMethod())
+ .amount(checkoutPaymentVm.totalAmount())
+ .build();
+
+ Payment createdPayment = paymentRepository.save(payment);
+
+ LOGGER.info("Payment is created successfully with ID: {}", createdPayment.getId());
+
+ return createdPayment.getId();
+ }
+
+ public Payment findPaymentById(Long id) {
+
+ return paymentRepository
+ .findById(id)
+ .orElseThrow(
+ () -> new NotFoundException(Constants.ErrorCode.PAYMENT_NOT_FOUND, id));
+ }
+
+ public void updatePayment(Payment payment) {
+
+ if (Objects.isNull(payment.getId())) {
+ throw new BadRequestException(Constants.Message.PAYMENT_ID_REQUIRED);
+ }
+ paymentRepository.save(payment);
+ }
+
}
diff --git a/payment/src/main/java/com/yas/payment/service/provider/handler/PaymentHandler.java b/payment/src/main/java/com/yas/payment/service/provider/handler/PaymentHandler.java
index 8dd8312a8a..180a238ed9 100644
--- a/payment/src/main/java/com/yas/payment/service/provider/handler/PaymentHandler.java
+++ b/payment/src/main/java/com/yas/payment/service/provider/handler/PaymentHandler.java
@@ -6,7 +6,10 @@
import com.yas.payment.viewmodel.InitPaymentRequestVm;
public interface PaymentHandler {
+
String getProviderId();
+
InitiatedPayment initPayment(InitPaymentRequestVm initPaymentRequestVm);
- CapturedPayment capturePayment(CapturePaymentRequestVm capturePaymentRequestVM);
+
+ CapturedPayment capturePayment(CapturePaymentRequestVm capturePaymentRequestVm);
}
diff --git a/payment/src/main/java/com/yas/payment/service/provider/handler/PaypalHandler.java b/payment/src/main/java/com/yas/payment/service/provider/handler/PaypalHandler.java
index c7cd278769..732cf7e98a 100644
--- a/payment/src/main/java/com/yas/payment/service/provider/handler/PaypalHandler.java
+++ b/payment/src/main/java/com/yas/payment/service/provider/handler/PaypalHandler.java
@@ -46,12 +46,13 @@ public InitiatedPayment initPayment(InitPaymentRequestVm initPaymentRequestVm) {
}
@Override
- public CapturedPayment capturePayment(CapturePaymentRequestVm capturePaymentRequestVM) {
+ public CapturedPayment capturePayment(CapturePaymentRequestVm capturePaymentRequestVm) {
PaypalCapturePaymentRequest paypalCapturePaymentRequest = PaypalCapturePaymentRequest.builder()
- .token(capturePaymentRequestVM.token())
+ .token(capturePaymentRequestVm.token())
.paymentSettings(getPaymentSettings(getProviderId()))
.build();
- PaypalCapturePaymentResponse paypalCapturePaymentResponse = paypalService.capturePayment(paypalCapturePaymentRequest);
+ PaypalCapturePaymentResponse paypalCapturePaymentResponse
+ = paypalService.capturePayment(paypalCapturePaymentRequest);
return CapturedPayment.builder()
.checkoutId(paypalCapturePaymentResponse.checkoutId())
.amount(paypalCapturePaymentResponse.amount())
diff --git a/payment/src/main/java/com/yas/payment/utils/Constants.java b/payment/src/main/java/com/yas/payment/utils/Constants.java
index de7796d689..1b3be05f90 100644
--- a/payment/src/main/java/com/yas/payment/utils/Constants.java
+++ b/payment/src/main/java/com/yas/payment/utils/Constants.java
@@ -1,14 +1,39 @@
package com.yas.payment.utils;
public final class Constants {
+
public final class ErrorCode {
+
public static final String PAYMENT_PROVIDER_NOT_FOUND = "PAYMENT_PROVIDER_NOT_FOUND";
+ public static final String CANNOT_CONVERT_TO_STRING = "CANNOT_CONVERT_TO_STRING";
+ public static final String ID_NOT_EXISTED = "ID_NOT_EXISTED";
+ public static final String PAYMENT_NOT_FOUND = "PAYMENT_NOT_FOUND";
+ public static final String ORDER_CREATION_FAILED = "ORDER_CREATION_FAILED";
private ErrorCode() {
}
}
+ public final class Column {
+
+ private Column() {
+ }
+
+ public static final String ID_COLUMN = "id";
+
+ // Column name of Checkout table
+ public static final String REDIRECT_URL_ID_COLUMN = "redirect-url";
+ }
+
public final class Message {
+
+ private Message() {
+ }
+
public static final String SUCCESS_MESSAGE = "SUCCESS";
+ public static final String PAYMENT_METHOD_COD = "PAYMENT_METHOD_COD";
+ public static final String PAYMENT_METHOD_PAYPAL = "PAYMENT_METHOD_PAYPAL";
+ public static final String PAYMENT_ID_REQUIRED = "PAYMENT_ID_REQUIRED";
}
+
}
diff --git a/payment/src/main/java/com/yas/payment/viewmodel/CheckoutPaymentVm.java b/payment/src/main/java/com/yas/payment/viewmodel/CheckoutPaymentVm.java
new file mode 100644
index 0000000000..6a63624fee
--- /dev/null
+++ b/payment/src/main/java/com/yas/payment/viewmodel/CheckoutPaymentVm.java
@@ -0,0 +1,8 @@
+package com.yas.payment.viewmodel;
+
+import com.yas.payment.model.enumeration.PaymentMethod;
+import java.math.BigDecimal;
+
+public record CheckoutPaymentVm(String checkoutId, PaymentMethod paymentMethod, BigDecimal totalAmount) {
+
+}
\ No newline at end of file
diff --git a/payment/src/main/java/com/yas/payment/viewmodel/InitPaymentRequestVm.java b/payment/src/main/java/com/yas/payment/viewmodel/InitPaymentRequestVm.java
index 6ac0038ef7..5173c1bdaf 100644
--- a/payment/src/main/java/com/yas/payment/viewmodel/InitPaymentRequestVm.java
+++ b/payment/src/main/java/com/yas/payment/viewmodel/InitPaymentRequestVm.java
@@ -1,8 +1,7 @@
package com.yas.payment.viewmodel;
-import lombok.Builder;
-
import java.math.BigDecimal;
+import lombok.Builder;
@Builder
public record InitPaymentRequestVm(String paymentMethod, BigDecimal totalPrice, String checkoutId) {
diff --git a/payment/src/main/java/com/yas/payment/viewmodel/PaymentVm.java b/payment/src/main/java/com/yas/payment/viewmodel/PaymentVm.java
new file mode 100644
index 0000000000..5f3c3eb9a0
--- /dev/null
+++ b/payment/src/main/java/com/yas/payment/viewmodel/PaymentVm.java
@@ -0,0 +1,34 @@
+package com.yas.payment.viewmodel;
+
+import com.yas.payment.model.Payment;
+import com.yas.payment.model.enumeration.PaymentMethod;
+import com.yas.payment.model.enumeration.PaymentStatus;
+import jakarta.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+public record PaymentVm(
+ @NotNull Long id,
+ @NotNull Long orderId,
+ @NotNull String checkoutId,
+ @NotNull BigDecimal amount,
+ BigDecimal paymentFee,
+ PaymentMethod paymentMethod,
+ PaymentStatus paymentStatus,
+ String gatewayTransactionId,
+ String failureMessage
+) {
+
+ public static PaymentVm fromModel(Payment payment) {
+ return new PaymentVm(
+ payment.getId(),
+ payment.getOrderId(),
+ payment.getCheckoutId(),
+ payment.getAmount(),
+ payment.getPaymentFee(),
+ payment.getPaymentMethod(),
+ payment.getPaymentStatus(),
+ payment.getGatewayTransactionId(),
+ payment.getFailureMessage()
+ );
+ }
+}
\ No newline at end of file
diff --git a/payment/src/main/resources/application.properties b/payment/src/main/resources/application.properties
index 69efcb2188..529986d12b 100644
--- a/payment/src/main/resources/application.properties
+++ b/payment/src/main/resources/application.properties
@@ -44,4 +44,7 @@ resilience4j.circuitbreaker.instances.rest-circuit-breaker.failure-rate-threshol
resilience4j.circuitbreaker.instances.rest-circuit-breaker.minimum-number-of-calls=5
resilience4j.circuitbreaker.instances.rest-circuit-breaker.automatic-transition-from-open-to-half-open-enabled=true
resilience4j.circuitbreaker.instances.rest-circuit-breaker.permitted-number-of-calls-in-half-open-state=3
-cors.allowed-origins=*
\ No newline at end of file
+cors.allowed-origins=*
+
+cdc.event.payment.topic-name=dbpayment.public.payment
+cdc.event.payment.create.group-id=payment-create
\ No newline at end of file
diff --git a/payment/src/main/resources/db/changelog/ddl/changelog-0004.sql b/payment/src/main/resources/db/changelog/ddl/changelog-0004.sql
new file mode 100644
index 0000000000..e22215f5c3
--- /dev/null
+++ b/payment/src/main/resources/db/changelog/ddl/changelog-0004.sql
@@ -0,0 +1 @@
+ALTER TABLE public.payment REPLICA IDENTITY FULL;
\ No newline at end of file
diff --git a/payment/src/main/resources/messages/messages.properties b/payment/src/main/resources/messages/messages.properties
index 7d38ef6993..dd215b1af3 100644
--- a/payment/src/main/resources/messages/messages.properties
+++ b/payment/src/main/resources/messages/messages.properties
@@ -1,2 +1,8 @@
SUCCESS_MESSAGE=SUCCESS
PAYMENT_PROVIDER_NOT_FOUND=Payment provider {} is not found
+PAYMENT_NOT_FOUND=Payment {} is not found
+ORDER_CREATION_FAILED=Order creation failed with payment {}
+ID_NOT_EXISTED=Payment ID is not existed
+PAYMENT_METHOD_COD=Payment {} has Payment Method is COD. Will not be creating Order on external system.
+PAYMENT_METHOD_PAYPAL=Payment {} has Payment Method is PayPal. Continue to create order on PayPal
+PAYMENT_ID_REQUIRED=Payment ID is required.
\ No newline at end of file
diff --git a/payment/src/test/java/com/yas/payment/consumer/PaymentCreateConsumerTest.java b/payment/src/test/java/com/yas/payment/consumer/PaymentCreateConsumerTest.java
new file mode 100644
index 0000000000..406a2f9a6e
--- /dev/null
+++ b/payment/src/test/java/com/yas/payment/consumer/PaymentCreateConsumerTest.java
@@ -0,0 +1,162 @@
+package com.yas.payment.consumer;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.gson.Gson;
+import com.yas.commonlibrary.exception.BadRequestException;
+import com.yas.payment.model.Payment;
+import com.yas.payment.model.enumeration.PaymentMethod;
+import com.yas.payment.model.enumeration.PaymentStatus;
+import com.yas.payment.service.PaymentService;
+import com.yas.payment.viewmodel.InitPaymentResponseVm;
+import java.math.BigDecimal;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+class PaymentCreateConsumerTest {
+
+ private PaymentService paymentService;
+
+ private PaymentCreateConsumer paymentCreateConsumer;
+
+ @BeforeEach
+ void setUp() {
+ paymentService = mock(PaymentService.class);
+ Gson gson = new Gson();
+ paymentCreateConsumer = new PaymentCreateConsumer(paymentService, gson);
+ }
+
+ @Test
+ void testListen_whenConsumerRecordIsNull_shouldLogInfoAndDoNothing() {
+ ConsumerRecord, ?> consumerRecord = null;
+
+ paymentCreateConsumer.listen(consumerRecord);
+
+ verify(paymentService, never()).findPaymentById(anyLong());
+ verify(paymentService, never()).updatePayment(any());
+ }
+
+ @Test
+ void testListen_whenValueDoesNotContainOpField_shouldNotProcessEvent() {
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+ when(consumerRecord.value()).thenReturn("{\"invalidKey\":\"value\"}");
+
+ paymentCreateConsumer.listen(consumerRecord);
+
+ verify(paymentService, never()).findPaymentById(anyLong());
+ verify(paymentService, never()).updatePayment(any());
+ }
+
+ @Test
+ void testListen_whenEventIsCreateAndPaymentMethodIsNotPaypal_shouldNotCreateOrderOnPaypal() {
+
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+ when(consumerRecord.value()).thenReturn("{\"op\":\"c\", \"after\":{\"id\":1234}}");
+
+ Payment payment = new Payment();
+ payment.setId(1234L);
+ payment.setPaymentMethod(PaymentMethod.BANKING);
+ payment.setAmount(new BigDecimal("100"));
+ payment.setCheckoutId("CHECKOUT123");
+
+ when(paymentService.findPaymentById(1234L)).thenReturn(payment);
+
+ InitPaymentResponseVm responseVm = new InitPaymentResponseVm(
+ "success",
+ "PAYMENT123",
+ "http://localhost:8080/test"
+ );
+ when(paymentService.initPayment(any())).thenReturn(responseVm);
+ paymentCreateConsumer.listen(consumerRecord);
+ verify(paymentService, never()).updatePayment(any());
+ }
+
+ @Test
+ void testListen_whenEventIsCreateAndPaymentMethodIsPaypal_shouldCreateOrderOnPaypal() {
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+ when(consumerRecord.value()).thenReturn("{\"op\":\"c\", \"after\":{\"id\":123}}");
+
+ Payment payment = new Payment();
+ payment.setId(123L);
+ payment.setPaymentMethod(PaymentMethod.PAYPAL);
+ payment.setAmount(new BigDecimal("100"));
+ payment.setCheckoutId("CHECKOUT123");
+
+ when(paymentService.findPaymentById(123L)).thenReturn(payment);
+
+ InitPaymentResponseVm responseVm = new InitPaymentResponseVm(
+ "success",
+ "PAYMENT123",
+ "http://localhost:8080/test"
+ );
+ when(paymentService.initPayment(any())).thenReturn(responseVm);
+
+ paymentCreateConsumer.listen(consumerRecord);
+
+ ArgumentCaptor paymentCaptor = ArgumentCaptor.forClass(Payment.class);
+ verify(paymentService).updatePayment(paymentCaptor.capture());
+
+ Payment updatedPayment = paymentCaptor.getValue();
+ Assertions.assertEquals(PaymentStatus.PROCESSING, updatedPayment.getPaymentStatus());
+ Assertions.assertEquals("PAYMENT123", updatedPayment.getPaymentProviderCheckoutId());
+ }
+
+ @Test
+ void testListen_whenEventResponseIsFailed_shouldNotUpdatePayment() {
+
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+ when(consumerRecord.value()).thenReturn("{\"op\":\"c\", \"after\":{\"id\":123}}");
+
+ Payment payment = new Payment();
+ payment.setId(123L);
+ payment.setPaymentMethod(PaymentMethod.PAYPAL);
+ payment.setAmount(new BigDecimal("100"));
+ payment.setCheckoutId("CHECKOUT123");
+
+ when(paymentService.findPaymentById(123L)).thenReturn(payment);
+
+ InitPaymentResponseVm responseVm = new InitPaymentResponseVm(
+ "failed",
+ "PAYMENT123",
+ "http://localhost:8080/test"
+ );
+ when(paymentService.initPayment(any())).thenReturn(responseVm);
+
+ paymentCreateConsumer.listen(consumerRecord);
+
+ verify(paymentService, never()).updatePayment(any());
+ }
+
+ @Test
+ void testListen_whenEventIsCreateAndPaymentMethodIsCOD_shouldNotCreateOrderOnPaypal() {
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+ when(consumerRecord.value()).thenReturn("{\"op\":\"c\", \"after\":{\"id\":123}}");
+
+ Payment payment = new Payment();
+ payment.setId(123L);
+ payment.setPaymentMethod(PaymentMethod.COD);
+
+ when(paymentService.findPaymentById(123L)).thenReturn(payment);
+
+ paymentCreateConsumer.listen(consumerRecord);
+
+ verify(paymentService, never()).initPayment(any());
+ }
+
+ @Test
+ void testListen_whenPaymentIdDoesNotExist_shouldThrowBadRequestException() {
+ ConsumerRecord, String> consumerRecord = mock(ConsumerRecord.class);
+ when(consumerRecord.value()).thenReturn("{\"op\":\"c\", \"after\":{}}");
+
+ assertThrows(BadRequestException.class, () -> paymentCreateConsumer.listen(consumerRecord));
+ }
+}
diff --git a/payment/src/test/java/com/yas/payment/service/PaymentServiceTest.java b/payment/src/test/java/com/yas/payment/service/PaymentServiceTest.java
index 26053a631f..82aad08056 100644
--- a/payment/src/test/java/com/yas/payment/service/PaymentServiceTest.java
+++ b/payment/src/test/java/com/yas/payment/service/PaymentServiceTest.java
@@ -1,5 +1,15 @@
package com.yas.payment.service;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.yas.commonlibrary.exception.NotFoundException;
import com.yas.payment.model.CapturedPayment;
import com.yas.payment.model.InitiatedPayment;
import com.yas.payment.model.Payment;
@@ -7,22 +17,19 @@
import com.yas.payment.model.enumeration.PaymentStatus;
import com.yas.payment.repository.PaymentRepository;
import com.yas.payment.service.provider.handler.PaymentHandler;
-import com.yas.payment.service.provider.handler.PaypalHandler;
-import com.yas.payment.viewmodel.*;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
-
+import com.yas.payment.viewmodel.CapturePaymentRequestVm;
+import com.yas.payment.viewmodel.CapturePaymentResponseVm;
+import com.yas.payment.viewmodel.CheckoutPaymentVm;
+import com.yas.payment.viewmodel.InitPaymentRequestVm;
+import com.yas.payment.viewmodel.InitPaymentResponseVm;
+import com.yas.payment.viewmodel.PaymentOrderStatusVm;
import java.math.BigDecimal;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
class PaymentServiceTest {
private PaymentRepository paymentRepository;
@@ -70,19 +77,63 @@ void initPayment_Success() {
@Test
void capturePayment_Success() {
- CapturePaymentRequestVm capturePaymentRequestVM = CapturePaymentRequestVm.builder()
+ CapturePaymentRequestVm capturePaymentRequestVm = CapturePaymentRequestVm.builder()
.paymentMethod(PaymentMethod.PAYPAL.name()).token("123").build();
CapturedPayment capturedPayment = prepareCapturedPayment();
Long orderId = 999L;
- when(paymentHandler.capturePayment(capturePaymentRequestVM)).thenReturn(capturedPayment);
+ when(paymentHandler.capturePayment(capturePaymentRequestVm)).thenReturn(capturedPayment);
when(orderService.updateCheckoutStatus(capturedPayment)).thenReturn(orderId);
when(paymentRepository.save(any())).thenReturn(payment);
- CapturePaymentResponseVm capturePaymentResponseVm = paymentService.capturePayment(capturePaymentRequestVM);
+ CapturePaymentResponseVm capturePaymentResponseVm = paymentService.capturePayment(capturePaymentRequestVm);
verifyPaymentCreation(capturePaymentResponseVm);
verifyOrderServiceInteractions(capturedPayment);
verifyResult(capturedPayment, capturePaymentResponseVm);
}
+ @Test
+ void testCreatePaymentFromEvent() {
+
+ CheckoutPaymentVm checkoutPaymentRequestDto = new CheckoutPaymentVm(
+ "123",
+ PaymentMethod.PAYPAL,
+ new BigDecimal(12)
+ );
+
+ Payment actualPayment = Payment.builder().id(1L).paymentMethod(PaymentMethod.PAYPAL).build();
+ ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Payment.class);
+ when(paymentRepository.save(argumentCaptor.capture())).thenReturn(actualPayment);
+
+ Long result = paymentService.createPaymentFromEvent(checkoutPaymentRequestDto);
+
+ assertThat(result).isEqualTo(1);
+ Payment captorValue = argumentCaptor.getValue();
+ assertThat(captorValue.getPaymentMethod()).isEqualTo(PaymentMethod.PAYPAL);
+ assertThat(captorValue.getPaymentStatus()).isEqualTo(PaymentStatus.PROCESSING);
+ assertThat(captorValue.getAmount()).isEqualTo(new BigDecimal(12));
+
+ }
+
+ @Test
+ void testFindPaymentById_whenNormalCase_methodSuccess() {
+ when(paymentRepository.findById(1L)).thenReturn(Optional.of(payment));
+ Payment actual = paymentService.findPaymentById(1L);
+ assertThat(actual).isNotNull();
+ }
+
+ @Test
+ void testFindPaymentById_whenNotFound_throwNotFoundException() {
+ when(paymentRepository.findById(1L)).thenReturn(Optional.empty());
+ NotFoundException foundException
+ = assertThrows(NotFoundException.class, () -> paymentService.findPaymentById(1L));
+ assertThat(foundException.getMessage()).isEqualTo("Payment 1 is not found");
+ }
+
+ @Test
+ void testUpdatePayment_whenNormalCase_methodSuccess() {
+ paymentService.updatePayment(payment);
+ verify(paymentRepository, times(1)).save(any());
+ }
+
private CapturedPayment prepareCapturedPayment() {
return CapturedPayment.builder()
.orderId(2L)
diff --git a/start-source-connectors.sh b/start-source-connectors.sh
index 8cc7fcafde..84e00f1a06 100644
--- a/start-source-connectors.sh
+++ b/start-source-connectors.sh
@@ -5,4 +5,12 @@ curl -i -X PUT -H "Content-Type:application/json" \
curl -i -X PUT -H "Content-Type:application/json" \
http://localhost:8083/connectors/order-connector/config \
- -d @kafka/connects/debezium-order.json
\ No newline at end of file
+ -d @kafka/connects/debezium-order.json
+
+curl -i -X PUT -H "Content-Type:application/json" \
+ http://localhost:8083/connectors/checkout-connector/config \
+ -d @kafka/connects/debezium-checkout-status.json
+
+curl -i -X PUT -H "Content-Type:application/json" \
+ http://localhost:8083/connectors/payment-connector/config \
+ -d @kafka/connects/debezium-payment.json
\ No newline at end of file