From 3a8e162508d3679b54b8bc780a2e31cd7e3e4d98 Mon Sep 17 00:00:00 2001 From: tuannguyenh1 Date: Mon, 18 Nov 2024 22:00:45 +0700 Subject: [PATCH] #1268 Step3 Customer Execute Payment --- .../order/controller/CheckoutController.java | 6 + .../CheckoutConfirmedStatusConsumer.java | 160 ++++++++++++++++++ ....java => CheckoutFulfillmentConsumer.java} | 37 ++-- ...> PaymentPaypalOrderIdUpdateConsumer.java} | 35 ++-- .../consumer/PaymentStatusUpdateConsumer.java | 95 +++++++++++ .../java/com/yas/order/model/Checkout.java | 14 +- .../main/java/com/yas/order/model/Order.java | 4 +- .../order/model/enumeration/OrderStatus.java | 12 +- .../model/enumeration/PaymentStatus.java | 3 + .../order/service/CheckoutItemService.java | 32 ++++ .../yas/order/service/CheckoutService.java | 12 ++ .../order/service/OrderAddressService.java | 24 +++ .../yas/order/service/OrderItemService.java | 29 ++++ .../com/yas/order/service/OrderService.java | 8 + .../java/com/yas/order/utils/Constants.java | 8 +- .../java/com/yas/order/utils/JsonUtils.java | 31 +++- .../order/viewmodel/order/OrderPostVm.java | 4 +- .../yas/order/viewmodel/order/OrderVm.java | 4 +- .../db/changelog/ddl/changelog-0016.sql | 2 + .../db/changelog/ddl/changelog-0017.sql | 1 + .../resources/messages/messages.properties | 1 + .../order/controller/OrderControllerTest.java | 8 +- .../consumer/OrderStatusConsumerTest.java | 75 +++----- ... => PaymentOrderIdUpdateConsumerTest.java} | 8 +- .../yas/order/service/CartServiceTest.java | 8 +- .../src/test/resources/application.properties | 6 +- .../paypal/model/PaypalPaymentAmount.java | 19 +++ .../model/PaypalPaymentPurchaseUnit.java | 22 +++ .../paypal/model/PaypalPaymentResource.java | 29 ++++ .../paypal/model/PaypalWebhookEvent.java | 32 ++++ .../enumeration/PaypalPaymentStatus.java | 39 +++++ .../payment/paypal/service/PaypalService.java | 6 +- .../paypal/service/PaypalServiceTest.java | 32 ++-- .../yas/payment/config/SecurityConfig.java | 1 + .../controller/event/EventController.java | 31 ++++ .../kafka/consumer/PaymentCreateConsumer.java | 22 ++- .../model/enumeration/PaymentStatus.java | 1 + .../payment/repository/PaymentRepository.java | 3 + .../yas/payment/service/PaymentService.java | 8 + .../order/handler/CompleteCaptureHandler.java | 36 ++++ .../order/handler/DeclinedCaptureHandler.java | 37 ++++ .../order/handler/RefundedCaptureHandler.java | 37 ++++ .../order/handler/ReversedCaptureHandler.java | 37 ++++ .../handler/VoidedAuthorizationHandler.java | 37 ++++ .../handler/base/IPaypalWebhookHandler.java | 14 ++ .../base/IPaypalWebhookHandlerRegistry.java | 10 ++ .../handler/base/PaypalWebhookHandler.java | 41 +++++ .../base/PaypalWebhookHandlerRegistry.java | 27 +++ .../java/com/yas/payment/utils/JsonUtils.java | 15 ++ .../consumer/PaymentCreateConsumerTest.java | 4 +- .../order/components/CheckOutDetail.tsx | 15 +- storefront/pages/checkout/[id].tsx | 17 +- .../pages/complete-payment/[capture].tsx | 114 ------------- .../pages/complete-payment/[success].tsx | 48 ++++++ 54 files changed, 1057 insertions(+), 304 deletions(-) create mode 100644 order/src/main/java/com/yas/order/kafka/consumer/CheckoutConfirmedStatusConsumer.java rename order/src/main/java/com/yas/order/kafka/consumer/{OrderStatusConsumer.java => CheckoutFulfillmentConsumer.java} (83%) rename order/src/main/java/com/yas/order/kafka/consumer/{PaymentUpdateConsumer.java => PaymentPaypalOrderIdUpdateConsumer.java} (74%) create mode 100644 order/src/main/java/com/yas/order/kafka/consumer/PaymentStatusUpdateConsumer.java create mode 100644 order/src/main/java/com/yas/order/service/CheckoutItemService.java create mode 100644 order/src/main/java/com/yas/order/service/OrderAddressService.java create mode 100644 order/src/main/java/com/yas/order/service/OrderItemService.java create mode 100644 order/src/main/resources/db/changelog/ddl/changelog-0016.sql create mode 100644 order/src/main/resources/db/changelog/ddl/changelog-0017.sql rename order/src/test/java/com/yas/order/kafka/consumer/{PaymentUpdateConsumerTest.java => PaymentOrderIdUpdateConsumerTest.java} (95%) create mode 100644 payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentAmount.java create mode 100644 payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentPurchaseUnit.java create mode 100644 payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentResource.java create mode 100644 payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalWebhookEvent.java create mode 100644 payment-paypal/src/main/java/com/yas/payment/paypal/model/enumeration/PaypalPaymentStatus.java create mode 100644 payment/src/main/java/com/yas/payment/controller/event/EventController.java create mode 100644 payment/src/main/java/com/yas/payment/service/order/handler/CompleteCaptureHandler.java create mode 100644 payment/src/main/java/com/yas/payment/service/order/handler/DeclinedCaptureHandler.java create mode 100644 payment/src/main/java/com/yas/payment/service/order/handler/RefundedCaptureHandler.java create mode 100644 payment/src/main/java/com/yas/payment/service/order/handler/ReversedCaptureHandler.java create mode 100644 payment/src/main/java/com/yas/payment/service/order/handler/VoidedAuthorizationHandler.java create mode 100644 payment/src/main/java/com/yas/payment/service/order/handler/base/IPaypalWebhookHandler.java create mode 100644 payment/src/main/java/com/yas/payment/service/order/handler/base/IPaypalWebhookHandlerRegistry.java create mode 100644 payment/src/main/java/com/yas/payment/service/order/handler/base/PaypalWebhookHandler.java create mode 100644 payment/src/main/java/com/yas/payment/service/order/handler/base/PaypalWebhookHandlerRegistry.java delete mode 100644 storefront/pages/complete-payment/[capture].tsx create mode 100644 storefront/pages/complete-payment/[success].tsx 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 96a6bf0993..5f364e22ac 100644 --- a/order/src/main/java/com/yas/order/controller/CheckoutController.java +++ b/order/src/main/java/com/yas/order/controller/CheckoutController.java @@ -32,6 +32,12 @@ public ResponseEntity createCheckout(@Valid @RequestBody CheckoutPos return ResponseEntity.ok(checkoutService.createCheckout(checkoutPostVm)); } + @PostMapping("/storefront/checkouts/{id}/process-payment") + public ResponseEntity processPayment(@PathVariable String id) { + checkoutService.processPayment(id); + return ResponseEntity.ok().build(); + } + @PutMapping("/storefront/checkouts/status") public ResponseEntity updateCheckoutStatus(@Valid @RequestBody CheckoutStatusPutVm checkoutStatusPutVm) { return ResponseEntity.ok(checkoutService.updateCheckoutStatus(checkoutStatusPutVm)); diff --git a/order/src/main/java/com/yas/order/kafka/consumer/CheckoutConfirmedStatusConsumer.java b/order/src/main/java/com/yas/order/kafka/consumer/CheckoutConfirmedStatusConsumer.java new file mode 100644 index 0000000000..ef1e879705 --- /dev/null +++ b/order/src/main/java/com/yas/order/kafka/consumer/CheckoutConfirmedStatusConsumer.java @@ -0,0 +1,160 @@ +package com.yas.order.kafka.consumer; + +import static com.yas.order.utils.JsonUtils.getJsonNodeByValue; +import static com.yas.order.utils.JsonUtils.getJsonValueOrNull; +import static com.yas.order.utils.JsonUtils.getJsonValueOrThrow; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yas.commonlibrary.exception.BadRequestException; +import com.yas.order.model.Checkout; +import com.yas.order.model.CheckoutItem; +import com.yas.order.model.Order; +import com.yas.order.model.OrderItem; +import com.yas.order.model.enumeration.CheckoutState; +import com.yas.order.model.enumeration.DeliveryStatus; +import com.yas.order.model.enumeration.OrderStatus; +import com.yas.order.service.CheckoutItemService; +import com.yas.order.service.CheckoutService; +import com.yas.order.service.OrderAddressService; +import com.yas.order.service.OrderItemService; +import com.yas.order.service.OrderService; +import com.yas.order.utils.Constants; +import java.util.List; +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; +import org.springframework.transaction.annotation.Transactional; + +/** + * After the Checkout Status is set to PAYMENT_CONFIRMED, an order will be created. + */ +@Service +@Transactional +@RequiredArgsConstructor +public class CheckoutConfirmedStatusConsumer { + + private static final Logger LOGGER = LoggerFactory.getLogger(CheckoutConfirmedStatusConsumer.class); + private final OrderService orderService; + private final OrderItemService orderItemService; + private final CheckoutService checkoutService; + private final CheckoutItemService checkoutItemService; + private final OrderAddressService orderAddressService; + private final ObjectMapper objectMapper; + + @KafkaListener( + topics = "${cdc.event.checkout.status.topic-name}", + groupId = "${cdc.event.checkout.confirmed.status.group-id}" + ) + @RetryableTopic + public void listen(ConsumerRecord consumerRecord) { + + if (Objects.isNull(consumerRecord)) { + LOGGER.info("ConsumerRecord is null"); + return; + } + String jsonValue = (String) consumerRecord.value(); + JsonNode valueObject = getJsonNodeByValue(objectMapper, jsonValue, LOGGER); + processCheckoutEvent(valueObject); + } + + private void processCheckoutEvent(JsonNode valueObject) { + Optional.ofNullable(valueObject) + .filter(value -> value.has("op") && "u".equals(value.get("op").asText())) + .filter(value -> value.has("before") && value.has("after")) + .ifPresentOrElse( + this::handleJsonForUpdateCheckout, + () -> LOGGER.warn("Message does not match expected update structure: {}", valueObject) + ); + } + + private void handleJsonForUpdateCheckout(JsonNode valueObject) { + + JsonNode before = valueObject.get("before"); + JsonNode after = valueObject.get("after"); + + String id = getJsonValueOrThrow(after, Constants.Column.ID_COLUMN, Constants.ErrorCode.ID_NOT_EXISTED); + String beforeStatus = getJsonValueOrNull(before, Constants.Column.STATUS_COLUMN); + String afterStatus = getJsonValueOrNull(after, Constants.Column.STATUS_COLUMN); + + if (Objects.isNull(afterStatus) + || afterStatus.equals(beforeStatus) + || !CheckoutState.PAYMENT_CONFIRMED.name().equals(afterStatus) + ) { + LOGGER.info("It's not an event to create Order with Checkout Id {}", id); + return; + } + + LOGGER.info("Checkout record with ID {} has the status 'PAYMENT_CONFIRMED'", id); + + Checkout checkout = checkoutService.findCheckoutById(id); + List checkoutItemList = checkoutItemService.getAllByCheckoutId(checkout.getId()); + + Order order = createOrder(checkout, checkoutItemList); + createOrderItems(order, checkoutItemList); + updateCheckoutStatus(checkout); + } + + private Order createOrder(Checkout checkout, List checkoutItemList) { + + Order order = Order.builder() + .email(checkout.getEmail()) + .numberItem(checkoutItemList.size()) + .note(checkout.getNote()) + .tax(checkout.getTotalTax()) + .discount(checkout.getTotalDiscountAmount()) + .totalPrice(checkout.getTotalAmount()) + .couponCode(checkout.getCouponCode()) + .orderStatus(OrderStatus.PAYMENT_CONFIRMED) + .deliveryFee(checkout.getTotalShipmentFee()) + .deliveryMethod(checkout.getShipmentMethodId()) + .deliveryStatus(DeliveryStatus.PREPARING) + .totalShipmentTax(checkout.getTotalShipmentTax()) + .customerId(checkout.getCustomerId()) + .shippingAddressId( + Optional.ofNullable(checkout.getShippingAddressId()) + .map(orderAddressService::findOrderAddressById) + .orElseThrow(() -> new BadRequestException("Shipping Address Id is not existed: {}", + checkout.getShippingAddressId())) + ) + .billingAddressId( + Optional.ofNullable(checkout.getBillingAddressId()) + .map(orderAddressService::findOrderAddressById) + .orElseThrow(() -> new BadRequestException("Billing Address Id is not existed: {}", + checkout.getBillingAddressId())) + ) + .checkoutId(checkout.getId()) + .build(); + + return orderService.updateOrder(order); + + } + + private void createOrderItems(Order order, List checkoutItemList) { + + List orderItems = checkoutItemList.stream() + .map(item -> OrderItem.builder() + .productId(item.getProductId()) + .productName(item.getProductName()) + .quantity(item.getQuantity()) + .productPrice(item.getProductPrice()) + .note(item.getNote()) + .orderId(order.getId()) + .build()) + .toList(); + + orderItemService.saveAll(orderItems); + } + + private void updateCheckoutStatus(Checkout checkout) { + checkout.setCheckoutState(CheckoutState.FULFILLED); + checkoutService.updateCheckout(checkout); + } + +} diff --git a/order/src/main/java/com/yas/order/kafka/consumer/OrderStatusConsumer.java b/order/src/main/java/com/yas/order/kafka/consumer/CheckoutFulfillmentConsumer.java similarity index 83% rename from order/src/main/java/com/yas/order/kafka/consumer/OrderStatusConsumer.java rename to order/src/main/java/com/yas/order/kafka/consumer/CheckoutFulfillmentConsumer.java index 0c78375085..cca23bc4a9 100644 --- a/order/src/main/java/com/yas/order/kafka/consumer/OrderStatusConsumer.java +++ b/order/src/main/java/com/yas/order/kafka/consumer/CheckoutFulfillmentConsumer.java @@ -3,18 +3,17 @@ 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.getJsonNodeByValue; import static com.yas.order.utils.JsonUtils.getJsonValueOrThrow; +import com.fasterxml.jackson.databind.JsonNode; 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.CheckoutService; import com.yas.order.service.PaymentService; import com.yas.order.utils.Constants; import com.yas.order.viewmodel.payment.CheckoutPaymentVm; @@ -28,19 +27,21 @@ import org.springframework.kafka.annotation.RetryableTopic; import org.springframework.stereotype.Service; +/** + * After fulfillment, process payment and create order in PayPal. + */ @Service @RequiredArgsConstructor -public class OrderStatusConsumer { +public class CheckoutFulfillmentConsumer { - private static final Logger LOGGER = LoggerFactory.getLogger(OrderStatusConsumer.class); + private static final Logger LOGGER = LoggerFactory.getLogger(CheckoutFulfillmentConsumer.class); private final PaymentService paymentService; - private final CheckoutRepository checkoutRepository; + private final CheckoutService checkoutService; private final ObjectMapper objectMapper; - private final Gson gson; @KafkaListener( topics = "${cdc.event.checkout.status.topic-name}", - groupId = "${cdc.event.checkout.status.group-id}" + groupId = "${cdc.event.checkout.fulfillment.group-id}" ) @RetryableTopic( attempts = "1" @@ -51,21 +52,21 @@ public void listen(ConsumerRecord consumerRecord) { LOGGER.info("ConsumerRecord is null"); return; } - JsonObject valueObject = gson.fromJson((String) consumerRecord.value(), JsonObject.class); + String jsonValue = (String) consumerRecord.value(); + JsonNode valueObject = getJsonNodeByValue(objectMapper, jsonValue, LOGGER); processCheckoutEvent(valueObject); - } - private void processCheckoutEvent(JsonObject valueObject) { + private void processCheckoutEvent(JsonNode valueObject) { Optional.ofNullable(valueObject) .filter( - value -> value.has("op") && "u".equals(value.get("op").getAsString()) + value -> value.has("op") && "u".equals(value.get("op").asText()) ) - .map(value -> value.getAsJsonObject("after")) + .map(value -> value.get("after")) .ifPresent(this::handleAfterJson); } - private void handleAfterJson(JsonObject after) { + private void handleAfterJson(JsonNode after) { String id = getJsonValueOrThrow(after, Constants.Column.ID_COLUMN, Constants.ErrorCode.ID_NOT_EXISTED); @@ -83,9 +84,7 @@ private void handleAfterJson(JsonObject after) { 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)); + Checkout checkout = checkoutService.findCheckoutById(id); processPaymentAndUpdateCheckout(checkout); } @@ -117,7 +116,7 @@ private void processPaymentAndUpdateCheckout(Checkout checkout) { throw new BadRequestException(Constants.ErrorCode.PROCESS_CHECKOUT_FAILED, checkout.getId()); } finally { - checkoutRepository.save(checkout); + checkoutService.updateCheckout(checkout); } } diff --git a/order/src/main/java/com/yas/order/kafka/consumer/PaymentUpdateConsumer.java b/order/src/main/java/com/yas/order/kafka/consumer/PaymentPaypalOrderIdUpdateConsumer.java similarity index 74% rename from order/src/main/java/com/yas/order/kafka/consumer/PaymentUpdateConsumer.java rename to order/src/main/java/com/yas/order/kafka/consumer/PaymentPaypalOrderIdUpdateConsumer.java index 0f78037556..5527d634dd 100644 --- a/order/src/main/java/com/yas/order/kafka/consumer/PaymentUpdateConsumer.java +++ b/order/src/main/java/com/yas/order/kafka/consumer/PaymentPaypalOrderIdUpdateConsumer.java @@ -2,13 +2,13 @@ import static com.yas.order.utils.JsonUtils.convertObjectToString; import static com.yas.order.utils.JsonUtils.getAttributesNode; +import static com.yas.order.utils.JsonUtils.getJsonNodeByValue; import static com.yas.order.utils.JsonUtils.getJsonValueOrNull; import static com.yas.order.utils.JsonUtils.getJsonValueOrThrow; +import com.fasterxml.jackson.databind.JsonNode; 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; @@ -24,18 +24,21 @@ import org.springframework.kafka.annotation.RetryableTopic; import org.springframework.stereotype.Service; +/** + * After the PayPal Order id is updated in payment_provider_checkout_id column. + * Update Checkout state is PAYMENT_PROCESSING, progress is PAYMENT_CREATED + */ @Service @RequiredArgsConstructor -public class PaymentUpdateConsumer { +public class PaymentPaypalOrderIdUpdateConsumer { - private static final Logger LOGGER = LoggerFactory.getLogger(PaymentUpdateConsumer.class); + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentPaypalOrderIdUpdateConsumer.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}" + groupId = "${cdc.event.payment.order.id.update.group-id}" ) @RetryableTopic public void listen(ConsumerRecord consumerRecord) { @@ -44,32 +47,32 @@ public void listen(ConsumerRecord consumerRecord) { LOGGER.info("Consumer Record is null"); return; } - JsonObject valueObject = gson.fromJson((String) consumerRecord.value(), JsonObject.class); + String jsonValue = (String) consumerRecord.value(); + JsonNode valueObject = getJsonNodeByValue(objectMapper, jsonValue, LOGGER); processPaymentEvent(valueObject); - } - private void processPaymentEvent(JsonObject valueObject) { + private void processPaymentEvent(JsonNode valueObject) { Optional.ofNullable(valueObject) .filter( - value -> value.has("op") && "u".equals(value.get("op").getAsString()) + value -> value.has("op") && "u".equals(value.get("op").asText()) ) .filter(value -> value.has("before") && value.has("after")) .ifPresent(this::handleJsonForUpdateCheckout); } - private void handleJsonForUpdateCheckout(JsonObject valueObject) { + private void handleJsonForUpdateCheckout(JsonNode valueObject) { - JsonObject before = valueObject.getAsJsonObject("before"); - JsonObject after = valueObject.getAsJsonObject("after"); + JsonNode before = valueObject.get("before"); + JsonNode after = valueObject.get("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); + Constants.Column.PAYMENT_ATTRIBUTES_PAYMENT_PROVIDER_CHECKOUT_ID_FIELD); String afterPaypalOrderId = getJsonValueOrNull(after, - Constants.Column.CHECKOUT_ATTRIBUTES_PAYMENT_PROVIDER_CHECKOUT_ID_FIELD); + Constants.Column.PAYMENT_ATTRIBUTES_PAYMENT_PROVIDER_CHECKOUT_ID_FIELD); if (!Objects.isNull(afterPaypalOrderId) && !afterPaypalOrderId.equals(beforePaypalOrderId)) { @@ -90,7 +93,7 @@ private void updateCheckOut(String checkoutId, String paymentProviderCheckoutId) checkout.setProgress(CheckoutProgress.PAYMENT_CREATED); ObjectNode attributesNode = getAttributesNode(objectMapper, checkout.getAttributes()); - attributesNode.put(Constants.Column.CHECKOUT_ATTRIBUTES_PAYMENT_PROVIDER_CHECKOUT_ID_FIELD, + attributesNode.put(Constants.Column.PAYMENT_ATTRIBUTES_PAYMENT_PROVIDER_CHECKOUT_ID_FIELD, paymentProviderCheckoutId); checkout.setAttributes(convertObjectToString(objectMapper, attributesNode)); diff --git a/order/src/main/java/com/yas/order/kafka/consumer/PaymentStatusUpdateConsumer.java b/order/src/main/java/com/yas/order/kafka/consumer/PaymentStatusUpdateConsumer.java new file mode 100644 index 0000000000..6c52f0fbc8 --- /dev/null +++ b/order/src/main/java/com/yas/order/kafka/consumer/PaymentStatusUpdateConsumer.java @@ -0,0 +1,95 @@ +package com.yas.order.kafka.consumer; + +import static com.yas.order.utils.JsonUtils.getJsonNodeByValue; +import static com.yas.order.utils.JsonUtils.getJsonValueOrNull; +import static com.yas.order.utils.JsonUtils.getJsonValueOrThrow; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yas.order.model.Checkout; +import com.yas.order.model.enumeration.CheckoutState; +import com.yas.order.model.enumeration.PaymentStatus; +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; + +/** + * After Status Payment is changed to COMPLETED/FAILURE. + * Update Status Checkout to PAYMENT_CONFIRMED + */ +@Service +@RequiredArgsConstructor +public class PaymentStatusUpdateConsumer { + + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentStatusUpdateConsumer.class); + private final CheckoutService checkoutService; + private final ObjectMapper objectMapper; + + @KafkaListener( + topics = "${cdc.event.payment.topic-name}", + groupId = "${cdc.event.payment.status.update.group-id}" + ) + @RetryableTopic + public void listen(ConsumerRecord consumerRecord) { + + if (Objects.isNull(consumerRecord)) { + LOGGER.info("Consumer Record is null"); + return; + } + String jsonValue = (String) consumerRecord.value(); + JsonNode valueObject = getJsonNodeByValue(objectMapper, jsonValue, LOGGER); + processPaymentEvent(valueObject); + } + + private void processPaymentEvent(JsonNode valueObject) { + Optional.ofNullable(valueObject) + .filter( + value -> value.has("op") && "u".equals(value.get("op").asText()) + ) + .filter(value -> value.has("before") && value.has("after")) + .ifPresent(this::handleJsonForUpdateCheckout); + } + + private void handleJsonForUpdateCheckout(JsonNode valueObject) { + + JsonNode before = valueObject.get("before"); + JsonNode after = valueObject.get("after"); + + String id = getJsonValueOrThrow(after, Constants.Column.ID_COLUMN, + Constants.ErrorCode.ID_NOT_EXISTED); + + String beforePaymentStatus = getJsonValueOrNull(before, + Constants.Column.PAYMENT_STATUS_FIELD); + String afterPaymentStatus = getJsonValueOrNull(after, + Constants.Column.PAYMENT_STATUS_FIELD); + + if ((PaymentStatus.COMPLETED.name().equals(afterPaymentStatus) + || PaymentStatus.FAILURE.name().equals(afterPaymentStatus)) + && !afterPaymentStatus.equals(beforePaymentStatus)) { + + LOGGER.info("Update Checkout Payment Status to PAYMENT_CONFIRMED with Payment {}", id); + + String checkoutId = getJsonValueOrThrow(after, Constants.Column.CHECKOUT_ID_COLUMN, + Constants.ErrorCode.CHECKOUT_ID_NOT_EXISTED); + updateCheckoutPaymentStatus(checkoutId); + } else { + LOGGER.warn("It is not an event to confirm payment with Payment ID {}", id); + } + } + + private void updateCheckoutPaymentStatus(String checkoutId) { + + Checkout checkout = checkoutService.findCheckoutById(checkoutId); + checkout.setCheckoutState(CheckoutState.PAYMENT_CONFIRMED); + checkoutService.updateCheckout(checkout); + } + +} \ No newline at end of file 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 4478783238..3023c46082 100644 --- a/order/src/main/java/com/yas/order/model/Checkout.java +++ b/order/src/main/java/com/yas/order/model/Checkout.java @@ -2,6 +2,7 @@ import com.yas.order.model.enumeration.CheckoutProgress; import com.yas.order.model.enumeration.CheckoutState; +import com.yas.order.model.enumeration.DeliveryMethod; import com.yas.order.model.enumeration.PaymentMethod; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -44,40 +45,33 @@ public class Checkout extends AbstractAuditEntity { @Enumerated(EnumType.STRING) private CheckoutProgress progress; - @SuppressWarnings("unused") private Long customerId; - @SuppressWarnings("unused") - private String shipmentMethodId; + private DeliveryMethod shipmentMethodId; @Enumerated(EnumType.STRING) private PaymentMethod paymentMethodId; - @SuppressWarnings("unused") private Long shippingAddressId; + private Long billingAddressId; + @JdbcTypeCode(SqlTypes.JSON) @Column(name = "last_error", columnDefinition = "jsonb") private String lastError; - @SuppressWarnings("unused") @JdbcTypeCode(SqlTypes.JSON) @Column(name = "attributes", columnDefinition = "jsonb") private String attributes; - @SuppressWarnings("unused") private BigDecimal totalAmount; - @SuppressWarnings("unused") private BigDecimal totalShipmentFee; - @SuppressWarnings("unused") private BigDecimal totalShipmentTax; - @SuppressWarnings("unused") private BigDecimal totalTax; - @SuppressWarnings("unused") private BigDecimal totalDiscountAmount; } \ No newline at end of file diff --git a/order/src/main/java/com/yas/order/model/Order.java b/order/src/main/java/com/yas/order/model/Order.java index 834d684f49..009e4d41fd 100644 --- a/order/src/main/java/com/yas/order/model/Order.java +++ b/order/src/main/java/com/yas/order/model/Order.java @@ -52,10 +52,10 @@ public class Order extends AbstractAuditEntity { private String note; @Column(name = "total_tax") - private float tax; + private BigDecimal tax; @Column(name = "total_discount_amount") - private float discount; + private BigDecimal discount; private int numberItem; diff --git a/order/src/main/java/com/yas/order/model/enumeration/OrderStatus.java b/order/src/main/java/com/yas/order/model/enumeration/OrderStatus.java index 045d4bdd69..652d03b301 100644 --- a/order/src/main/java/com/yas/order/model/enumeration/OrderStatus.java +++ b/order/src/main/java/com/yas/order/model/enumeration/OrderStatus.java @@ -8,9 +8,17 @@ public enum OrderStatus { SHIPPING("SHIPPING"), COMPLETED("COMPLETED"), REFUND("REFUND"), - CANCELLED("CANCELLED"), + REJECT("REJECT"), - REJECT("REJECT"); + PAYMENT_CONFIRMED("PAYMENT_CONFIRMED"), + READY_TO_SHIP("READY_TO_SHIP"), + SHIPPED("SHIPPED"), + CANCEL_REQUESTED("CANCEL_REQUESTED"), + RECEIVED("RECEIVED"), + SHIPMENT_FAILED("SHIPMENT_FAILED"), + REFUND_PROCESSING("REFUND_PROCESSING"), + CONFIRMED("CONFIRMED"), + CANCELLED("CANCELLED"); private final String name; diff --git a/order/src/main/java/com/yas/order/model/enumeration/PaymentStatus.java b/order/src/main/java/com/yas/order/model/enumeration/PaymentStatus.java index 422d3ad6a6..480f2dc029 100644 --- a/order/src/main/java/com/yas/order/model/enumeration/PaymentStatus.java +++ b/order/src/main/java/com/yas/order/model/enumeration/PaymentStatus.java @@ -1,7 +1,10 @@ package com.yas.order.model.enumeration; public enum PaymentStatus { + NEW, + PROCESSING, PENDING, + FAILURE, COMPLETED, CANCELLED } diff --git a/order/src/main/java/com/yas/order/service/CheckoutItemService.java b/order/src/main/java/com/yas/order/service/CheckoutItemService.java new file mode 100644 index 0000000000..923c51a719 --- /dev/null +++ b/order/src/main/java/com/yas/order/service/CheckoutItemService.java @@ -0,0 +1,32 @@ +package com.yas.order.service; + +import com.yas.order.model.CheckoutItem; +import com.yas.order.repository.CheckoutItemRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CheckoutItemService { + private final CheckoutItemRepository checkoutItemRepository; + + public List getAllByCheckoutId(String checkoutId) { + List checkoutItemList = checkoutItemRepository.findAllByCheckoutId(checkoutId); + if (CollectionUtils.isEmpty(checkoutItemList)) { + return List.of(); + } + return checkoutItemList; + } + + public void saveAll(List items) { + if (CollectionUtils.isEmpty(items)) { + return; + } + checkoutItemRepository.saveAll(items); + } + +} 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 a66897c26f..b37ee96cf9 100644 --- a/order/src/main/java/com/yas/order/service/CheckoutService.java +++ b/order/src/main/java/com/yas/order/service/CheckoutService.java @@ -9,6 +9,7 @@ import com.yas.order.model.Checkout; import com.yas.order.model.CheckoutItem; import com.yas.order.model.Order; +import com.yas.order.model.enumeration.CheckoutProgress; import com.yas.order.model.enumeration.CheckoutState; import com.yas.order.model.enumeration.PaymentMethod; import com.yas.order.repository.CheckoutItemRepository; @@ -43,6 +44,17 @@ public class CheckoutService { private final OrderService orderService; private final CheckoutMapper checkoutMapper; + public void processPayment(String checkoutId) { + Checkout checkout = checkoutRepository.findById(checkoutId) + .orElseThrow(() -> new NotFoundException(CHECKOUT_NOT_FOUND, checkoutId)); + checkout.setCheckoutState(CheckoutState.PAYMENT_PROCESSING); + checkout.setProgress(CheckoutProgress.STOCK_LOCKED); + checkout.setPaymentMethodId(PaymentMethod.PAYPAL); + checkout.setShippingAddressId(1L); + checkout.setBillingAddressId(1L); + checkoutRepository.save(checkout); + } + public CheckoutVm createCheckout(CheckoutPostVm checkoutPostVm) { Checkout checkout = checkoutMapper.toModel(checkoutPostVm); diff --git a/order/src/main/java/com/yas/order/service/OrderAddressService.java b/order/src/main/java/com/yas/order/service/OrderAddressService.java new file mode 100644 index 0000000000..e07f18a8b7 --- /dev/null +++ b/order/src/main/java/com/yas/order/service/OrderAddressService.java @@ -0,0 +1,24 @@ +package com.yas.order.service; + +import static com.yas.order.utils.Constants.ErrorCode.ORDER_ADDRESS_NOT_FOUND; + +import com.yas.commonlibrary.exception.NotFoundException; +import com.yas.order.model.OrderAddress; +import com.yas.order.repository.OrderAddressRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OrderAddressService { + + private final OrderAddressRepository orderAddressRepository; + + public OrderAddress findOrderAddressById(Long id) { + return this.orderAddressRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ORDER_ADDRESS_NOT_FOUND, id)); + } +} + diff --git a/order/src/main/java/com/yas/order/service/OrderItemService.java b/order/src/main/java/com/yas/order/service/OrderItemService.java new file mode 100644 index 0000000000..38c6e7b37f --- /dev/null +++ b/order/src/main/java/com/yas/order/service/OrderItemService.java @@ -0,0 +1,29 @@ +package com.yas.order.service; + +import com.yas.order.model.OrderItem; +import com.yas.order.repository.OrderItemRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OrderItemService { + + private final OrderItemRepository orderItemRepository; + + public List saveAll(List items) { + if (CollectionUtils.isEmpty(items)) { + return List.of(); + } + return orderItemRepository.saveAll(items); + } + + public void findAllByOrderId(Long orderId) { + orderItemRepository.findAllByOrderId(orderId); + } + +} diff --git a/order/src/main/java/com/yas/order/service/OrderService.java b/order/src/main/java/com/yas/order/service/OrderService.java index 47cab4d04b..61a929edd4 100644 --- a/order/src/main/java/com/yas/order/service/OrderService.java +++ b/order/src/main/java/com/yas/order/service/OrderService.java @@ -4,6 +4,7 @@ import com.yas.commonlibrary.csv.BaseCsv; import com.yas.commonlibrary.csv.CsvExporter; +import com.yas.commonlibrary.exception.BadRequestException; import com.yas.commonlibrary.exception.NotFoundException; import com.yas.order.mapper.OrderMapper; import com.yas.order.model.Order; @@ -288,6 +289,13 @@ public void acceptOrder(Long orderId) { this.orderRepository.save(order); } + public Order updateOrder(Order order) { + if (Objects.isNull(order)) { + throw new BadRequestException("Order is not existed."); + } + return orderRepository.save(order); + } + public byte[] exportCsv(OrderRequest orderRequest) throws IOException { ZonedDateTime createdFrom = orderRequest.getCreatedFrom(); ZonedDateTime createdTo = orderRequest.getCreatedTo(); 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 3e2f18f55c..f8b5b7538e 100644 --- a/order/src/main/java/com/yas/order/utils/Constants.java +++ b/order/src/main/java/com/yas/order/utils/Constants.java @@ -7,6 +7,7 @@ private ErrorCode() {} public static final String ORDER_NOT_FOUND = "ORDER_NOT_FOUND"; public static final String CHECKOUT_NOT_FOUND = "CHECKOUT_NOT_FOUND"; + public static final String ORDER_ADDRESS_NOT_FOUND = "ORDER_ADDRESS_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"; @@ -44,8 +45,11 @@ private Column() {} 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 + + // Column name of Payment table + public static final String PAYMENT_ATTRIBUTES_PAYMENT_PROVIDER_CHECKOUT_ID_FIELD = "payment_provider_checkout_id"; + public static final String PAYMENT_STATUS_FIELD + = "payment_status"; } } diff --git a/order/src/main/java/com/yas/order/utils/JsonUtils.java b/order/src/main/java/com/yas/order/utils/JsonUtils.java index 1143c70339..271a8e2f71 100644 --- a/order/src/main/java/com/yas/order/utils/JsonUtils.java +++ b/order/src/main/java/com/yas/order/utils/JsonUtils.java @@ -1,12 +1,12 @@ package com.yas.order.utils; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; 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; +import org.slf4j.Logger; public class JsonUtils { @@ -42,25 +42,38 @@ public static ObjectNode createJsonErrorObject(ObjectMapper objectMapper, String } public static String getJsonValueOrThrow( - JsonObject jsonObject, + JsonNode jsonObject, String columnName, String errorCode, Object... errorParams ) { return Optional.ofNullable(jsonObject.get(columnName)) - .filter(jsonElement -> !jsonElement.isJsonNull()) - .map(JsonElement::getAsString) + .map(JsonNode::asText) .orElseThrow(() -> new BadRequestException(errorCode, errorParams)); } public static String getJsonValueOrNull( - JsonObject jsonObject, + JsonNode jsonObject, String columnName ) { - JsonElement jsonElement = jsonObject.get(columnName); - if (jsonElement != null && !jsonElement.isJsonNull()) { - return jsonElement.getAsString(); + JsonNode jsonElement = jsonObject.get(columnName); + if (jsonElement != null && !jsonElement.isNull()) { + return jsonElement.asText(); } return null; } + + + public static JsonNode getJsonNodeByValue( + ObjectMapper objectMapper, + String jsonValue, + Logger logger + ) { + try { + return objectMapper.readTree(jsonValue); + } catch (JsonProcessingException e) { + logger.error("Failed to parse message as JSON. Message: {}", jsonValue, e); + throw new BadRequestException("Failed to parse message as JSON. Message: {}", jsonValue); + } + } } \ No newline at end of file diff --git a/order/src/main/java/com/yas/order/viewmodel/order/OrderPostVm.java b/order/src/main/java/com/yas/order/viewmodel/order/OrderPostVm.java index 8d2e74a3ce..942fbcdb65 100644 --- a/order/src/main/java/com/yas/order/viewmodel/order/OrderPostVm.java +++ b/order/src/main/java/com/yas/order/viewmodel/order/OrderPostVm.java @@ -17,8 +17,8 @@ public record OrderPostVm( @NotNull OrderAddressPostVm shippingAddressPostVm, @NotNull OrderAddressPostVm billingAddressPostVm, String note, - float tax, - float discount, + BigDecimal tax, + BigDecimal discount, int numberItem, @NotNull BigDecimal totalPrice, BigDecimal deliveryFee, diff --git a/order/src/main/java/com/yas/order/viewmodel/order/OrderVm.java b/order/src/main/java/com/yas/order/viewmodel/order/OrderVm.java index ab54204e0f..7ce16fea03 100644 --- a/order/src/main/java/com/yas/order/viewmodel/order/OrderVm.java +++ b/order/src/main/java/com/yas/order/viewmodel/order/OrderVm.java @@ -20,8 +20,8 @@ public record OrderVm( OrderAddressVm shippingAddressVm, OrderAddressVm billingAddressVm, String note, - float tax, - float discount, + BigDecimal tax, + BigDecimal discount, int numberItem, BigDecimal totalPrice, BigDecimal deliveryFee, diff --git a/order/src/main/resources/db/changelog/ddl/changelog-0016.sql b/order/src/main/resources/db/changelog/ddl/changelog-0016.sql new file mode 100644 index 0000000000..52563b9e22 --- /dev/null +++ b/order/src/main/resources/db/changelog/ddl/changelog-0016.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS "checkout" +ADD COLUMN billing_address_id bigint; \ No newline at end of file diff --git a/order/src/main/resources/db/changelog/ddl/changelog-0017.sql b/order/src/main/resources/db/changelog/ddl/changelog-0017.sql new file mode 100644 index 0000000000..c9e2188575 --- /dev/null +++ b/order/src/main/resources/db/changelog/ddl/changelog-0017.sql @@ -0,0 +1 @@ +ALTER TABLE public.checkout REPLICA IDENTITY FULL; \ 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 414c0feeca..19ecd57b80 100644 --- a/order/src/main/resources/messages/messages.properties +++ b/order/src/main/resources/messages/messages.properties @@ -1,5 +1,6 @@ ORDER_NOT_FOUND=Order {} is not found CHECKOUT_NOT_FOUND=Checkout {} is not found +ORDER_ADDRESS_NOT_FOUND=Order Address {} is not found SUCCESS_MESSAGE=Success SIGN_IN_REQUIRED=Authentication required FORBIDDEN=You don't have permission to access this page 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 ad92143799..fc8a69d1da 100644 --- a/order/src/test/java/com/yas/order/controller/OrderControllerTest.java +++ b/order/src/test/java/com/yas/order/controller/OrderControllerTest.java @@ -281,8 +281,8 @@ private OrderVm getOrderVm() { shippingAddress, billingAddress, "Please deliver by next week.", - 7.50f, - 15.00f, + new BigDecimal("7.50"), + new BigDecimal("15.00"), 3, new BigDecimal("159.97"), new BigDecimal("7.99"), @@ -371,8 +371,8 @@ private OrderPostVm getOrderPostVm() { shippingAddress, billingAddress, "Please handle with care.", - 5.00f, - 10.00f, + new BigDecimal("5.00"), + new BigDecimal("10.00"), 2, new BigDecimal("89.97"), new BigDecimal("5.00"), diff --git a/order/src/test/java/com/yas/order/kafka/consumer/OrderStatusConsumerTest.java b/order/src/test/java/com/yas/order/kafka/consumer/OrderStatusConsumerTest.java index 1e95152491..dab9354331 100644 --- a/order/src/test/java/com/yas/order/kafka/consumer/OrderStatusConsumerTest.java +++ b/order/src/test/java/com/yas/order/kafka/consumer/OrderStatusConsumerTest.java @@ -9,16 +9,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.fasterxml.jackson.databind.JsonNode; 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.model.Checkout; import com.yas.order.model.enumeration.CheckoutProgress; -import com.yas.order.repository.CheckoutRepository; +import com.yas.order.service.CheckoutService; 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; @@ -30,9 +27,9 @@ class OrderStatusConsumerTest { private PaymentService paymentService; - private CheckoutRepository checkoutRepository; + private CheckoutService checkoutService; - private OrderStatusConsumer orderStatusConsumer; + private CheckoutFulfillmentConsumer orderStatusConsumer; private final String jsonRecord = "{\"op\": \"u\", \"after\": {" + " \"status\": \"PAYMENT_PROCESSING\"," @@ -45,10 +42,9 @@ class OrderStatusConsumerTest { @BeforeEach void setUp() { paymentService = Mockito.mock(PaymentService.class); - checkoutRepository = Mockito.mock(CheckoutRepository.class); + checkoutService = Mockito.mock(CheckoutService.class); ObjectMapper objectMapper = new ObjectMapper(); - Gson gson = new Gson(); - orderStatusConsumer = new OrderStatusConsumer(paymentService, checkoutRepository, objectMapper, gson); + orderStatusConsumer = new CheckoutFulfillmentConsumer(paymentService, checkoutService, objectMapper); } @@ -57,9 +53,9 @@ void testListen_whenConsumerRecordIsNull_shouldNotThing() { orderStatusConsumer.listen(null); - verify(checkoutRepository, never()).findById(any()); + verify(checkoutService, never()).findCheckoutById(any()); verify(paymentService, never()).createPaymentFromEvent(any()); - verify(checkoutRepository, never()).save(any()); + verify(checkoutService, never()).updateCheckout(any()); } @Test @@ -71,8 +67,8 @@ void testListen_whenHaveNoAfter_shouldNotThing() { orderStatusConsumer.listen(consumerRecord); - verify(checkoutRepository, never()).save(any()); - verify(checkoutRepository, never()).findById(any()); + verify(checkoutService, never()).updateCheckout(any()); + verify(checkoutService, never()).findCheckoutById(any()); verify(paymentService, never()).createPaymentFromEvent(any()); } @@ -93,8 +89,8 @@ void testListen_whenIsNotPaymentProcess_shouldNotThing() { orderStatusConsumer.listen(consumerRecord); - verify(checkoutRepository, never()).findById(any()); - verify(checkoutRepository, never()).save(any()); + verify(checkoutService, never()).findCheckoutById(any()); + verify(checkoutService, never()).updateCheckout(any()); verify(paymentService, never()).createPaymentFromEvent(any()); } @@ -115,31 +111,8 @@ void testListen_whenProgressIsNotStockLocked_shouldNotThing() { orderStatusConsumer.listen(consumerRecord); - verify(checkoutRepository, never()).findById(any()); - verify(checkoutRepository, never()).save(any()); - verify(paymentService, never()).createPaymentFromEvent(any()); - } - - @Test - void testListen_whenCheckoutIsNotFound_shouldNotSave() { - - ConsumerRecord 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(checkoutService, never()).findCheckoutById(any()); + verify(checkoutService, never()).updateCheckout(any()); verify(paymentService, never()).createPaymentFromEvent(any()); } @@ -149,21 +122,21 @@ void testListen_whenCreatePaymentSuccess_shouldProcessCheckoutEvent() { ConsumerRecord consumerRecord = mock(ConsumerRecord.class); when(consumerRecord.value()).thenReturn(jsonRecord); - JsonObject jsonObject = mock(JsonObject.class); + JsonNode jsonObject = mock(JsonNode.class); when(jsonObject.has("after")).thenReturn(true); - when(jsonObject.getAsJsonObject("after")).thenReturn(mock(JsonObject.class)); + when(jsonObject.get("after")).thenReturn(mock(JsonNode.class)); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Checkout.class); - when(checkoutRepository.findById(anyString())).thenReturn(Optional.of(new Checkout())); + when(checkoutService.findCheckoutById(anyString())).thenReturn(new Checkout()); when(paymentService.createPaymentFromEvent(any())).thenReturn(2L); orderStatusConsumer.listen(consumerRecord); - verify(checkoutRepository, atLeastOnce()).findById(any()); + verify(checkoutService, atLeastOnce()).findCheckoutById(any()); verify(paymentService, atLeastOnce()).createPaymentFromEvent(any()); - verify(checkoutRepository, atLeastOnce()).save(argumentCaptor.capture()); + verify(checkoutService, atLeastOnce()).updateCheckout(argumentCaptor.capture()); Checkout actual = argumentCaptor.getValue(); assertThat(actual.getProgress()).isEqualTo(CheckoutProgress.PAYMENT_CREATED); @@ -176,13 +149,13 @@ void testListen_whenCreatePaymentFailure_shouldThrowException() { ConsumerRecord consumerRecord = mock(ConsumerRecord.class); when(consumerRecord.value()).thenReturn(jsonRecord); - JsonObject jsonObject = mock(JsonObject.class); + JsonNode jsonObject = mock(JsonNode.class); when(jsonObject.has("after")).thenReturn(true); - when(jsonObject.getAsJsonObject("after")).thenReturn(mock(JsonObject.class)); + when(jsonObject.get("after")).thenReturn(mock(JsonNode.class)); Checkout checkout = new Checkout(); checkout.setId("12345"); - when(checkoutRepository.findById(anyString())).thenReturn(Optional.of(checkout)); + when(checkoutService.findCheckoutById(anyString())).thenReturn(checkout); BadRequestException badRequestException = new BadRequestException("test exception"); when(paymentService.createPaymentFromEvent(any())).thenThrow(badRequestException); @@ -192,8 +165,8 @@ void testListen_whenCreatePaymentFailure_shouldThrowException() { 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(checkoutService, atLeastOnce()).findCheckoutById(any()); + verify(checkoutService, atLeastOnce()).updateCheckout(argumentCaptor.capture()); verify(paymentService, atLeastOnce()).createPaymentFromEvent(any()); Checkout actual = argumentCaptor.getValue(); diff --git a/order/src/test/java/com/yas/order/kafka/consumer/PaymentUpdateConsumerTest.java b/order/src/test/java/com/yas/order/kafka/consumer/PaymentOrderIdUpdateConsumerTest.java similarity index 95% rename from order/src/test/java/com/yas/order/kafka/consumer/PaymentUpdateConsumerTest.java rename to order/src/test/java/com/yas/order/kafka/consumer/PaymentOrderIdUpdateConsumerTest.java index 9a858e0ad0..b9dc185fdf 100644 --- a/order/src/test/java/com/yas/order/kafka/consumer/PaymentUpdateConsumerTest.java +++ b/order/src/test/java/com/yas/order/kafka/consumer/PaymentOrderIdUpdateConsumerTest.java @@ -9,7 +9,6 @@ import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.gson.Gson; import com.yas.order.model.Checkout; import com.yas.order.model.enumeration.CheckoutProgress; import com.yas.order.model.enumeration.CheckoutState; @@ -19,11 +18,11 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -class PaymentUpdateConsumerTest { +class PaymentOrderIdUpdateConsumerTest { private CheckoutService checkoutService; - private PaymentUpdateConsumer paymentUpdateConsumer; + private PaymentPaypalOrderIdUpdateConsumer paymentUpdateConsumer; private final String jsonRecord = "{\"op\":\"u\"," + " \"before\":{\"payment_provider_checkout_id\":\"OLD\"}," + @@ -35,8 +34,7 @@ class PaymentUpdateConsumerTest { void setUp() { checkoutService = mock(CheckoutService.class); ObjectMapper objectMapper = new ObjectMapper(); - Gson gson = new Gson(); - paymentUpdateConsumer = new PaymentUpdateConsumer(checkoutService, objectMapper, gson); + paymentUpdateConsumer = new PaymentPaypalOrderIdUpdateConsumer(checkoutService, objectMapper); } @Test diff --git a/order/src/test/java/com/yas/order/service/CartServiceTest.java b/order/src/test/java/com/yas/order/service/CartServiceTest.java index 265d08147e..1696ce775b 100644 --- a/order/src/test/java/com/yas/order/service/CartServiceTest.java +++ b/order/src/test/java/com/yas/order/service/CartServiceTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.yas.order.config.ServiceUrlConfig; @@ -17,16 +16,13 @@ import java.math.BigDecimal; import java.net.URI; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.UUID; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.web.client.RestClient; -import org.springframework.web.util.UriComponentsBuilder; class CartServiceTest { @@ -74,8 +70,8 @@ void testDeleteCartItems_ifNormalCase_shouldNoException() { null, null, "Please deliver between 9 AM and 5 PM", - 5.0f, - 10.0f, + new BigDecimal("5.0"), + new BigDecimal("10.0"), 3, new BigDecimal("89.97"), new BigDecimal("5.00"), diff --git a/order/src/test/resources/application.properties b/order/src/test/resources/application.properties index 5605601269..57af33b025 100644 --- a/order/src/test/resources/application.properties +++ b/order/src/test/resources/application.properties @@ -11,10 +11,12 @@ springdoc.oauthflow.token-url=test cors.allowed-origins=* cdc.event.checkout.status.topic-name=dbcheckout-status.public.checkout -cdc.event.checkout.status.group-id=checkout-status +cdc.event.checkout.fulfillment.group-id=checkout-fulfillment +cdc.event.checkout.confirmed.status.group-id=checkout-confirmed-status cdc.event.payment.topic-name=dbpayment.public.payment -cdc.event.payment.update.group-id=payment-update +cdc.event.payment.order.id.update.group-id=payment-order-id-update +cdc.event.payment.status.update.group-id=payment-status-update kafka.version=7.0.9 diff --git a/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentAmount.java b/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentAmount.java new file mode 100644 index 0000000000..0a95de96bf --- /dev/null +++ b/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentAmount.java @@ -0,0 +1,19 @@ +package com.yas.payment.paypal.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaypalPaymentAmount { + private String currencyCode; + private String value; +} diff --git a/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentPurchaseUnit.java b/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentPurchaseUnit.java new file mode 100644 index 0000000000..e81efcaed7 --- /dev/null +++ b/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentPurchaseUnit.java @@ -0,0 +1,22 @@ +package com.yas.payment.paypal.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaypalPaymentPurchaseUnit { + + @JsonProperty("reference_id") + private String referenceId; + private PaypalPaymentAmount amount; +} diff --git a/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentResource.java b/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentResource.java new file mode 100644 index 0000000000..7ad006de93 --- /dev/null +++ b/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalPaymentResource.java @@ -0,0 +1,29 @@ +package com.yas.payment.paypal.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaypalPaymentResource { + + @NotNull + private String id; + + @NotNull + private String status; + + @JsonProperty("purchase_units") + private List purchaseUnits; +} diff --git a/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalWebhookEvent.java b/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalWebhookEvent.java new file mode 100644 index 0000000000..2e7a1819d2 --- /dev/null +++ b/payment-paypal/src/main/java/com/yas/payment/paypal/model/PaypalWebhookEvent.java @@ -0,0 +1,32 @@ +package com.yas.payment.paypal.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaypalWebhookEvent { + + @NotNull + private String id; + + @NotNull + @JsonProperty("event_type") + private PaypalPaymentStatus eventType; + + @NotNull + @Valid + private PaypalPaymentResource resource; +} diff --git a/payment-paypal/src/main/java/com/yas/payment/paypal/model/enumeration/PaypalPaymentStatus.java b/payment-paypal/src/main/java/com/yas/payment/paypal/model/enumeration/PaypalPaymentStatus.java new file mode 100644 index 0000000000..839b58882a --- /dev/null +++ b/payment-paypal/src/main/java/com/yas/payment/paypal/model/enumeration/PaypalPaymentStatus.java @@ -0,0 +1,39 @@ +package com.yas.payment.paypal.model.enumeration; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.yas.commonlibrary.exception.BadRequestException; +import lombok.Getter; + +@Getter +public enum PaypalPaymentStatus { + + PAYMENT_AUTHORIZATION_CREATED("PAYMENT.AUTHORIZATION.CREATED"), + PAYMENT_AUTHORIZATION_VOIDED("PAYMENT.AUTHORIZATION.VOIDED"), + PAYMENT_CAPTURE_DECLINED("PAYMENT.CAPTURE.DECLINED"), + PAYMENT_CAPTURE_COMPLETED("PAYMENT.CAPTURE.COMPLETED"), + PAYMENT_CAPTURE_PENDING("PAYMENT.CAPTURE.PENDING"), + PAYMENT_CAPTURE_REFUNDED("PAYMENT.CAPTURE.REFUNDED"), + PAYMENT_CAPTURE_REVERSED("PAYMENT.CAPTURE.REVERSED"); + + private final String status; + + PaypalPaymentStatus(String status) { + this.status = status; + } + + @JsonCreator + public static PaypalPaymentStatus fromString(String status) { + for (PaypalPaymentStatus paymentStatus : PaypalPaymentStatus.values()) { + if (paymentStatus.getStatus().equals(status)) { + return paymentStatus; + } + } + throw new BadRequestException("Unknown enum value: {}", status); + } + + @JsonValue + public String getStatus() { + return status; + } +} 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 5116c37453..d0f7f14e31 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 @@ -31,7 +31,7 @@ public class PaypalService { private final PayPalHttpClientInitializer payPalHttpClientInitializer; private final BigDecimal maxPay = BigDecimal.valueOf(1000); - @Value("${yas.public.url}/capture") + @Value("${yas.public.url}/success") private String returnUrl; @Value("${yas.public.url}/cancel") private String cancelUrl; @@ -52,10 +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()); ApplicationContext applicationContext = new ApplicationContext() - .returnUrl(paymentMethodReturnUrl) + .returnUrl(returnUrl) .cancelUrl(cancelUrl) .brandName(Constants.Yas.BRAND_NAME) .landingPage("BILLING") diff --git a/payment-paypal/src/test/java/com/yas/payment/paypal/service/PaypalServiceTest.java b/payment-paypal/src/test/java/com/yas/payment/paypal/service/PaypalServiceTest.java index fb1718003b..7a2fc88ef6 100644 --- a/payment-paypal/src/test/java/com/yas/payment/paypal/service/PaypalServiceTest.java +++ b/payment-paypal/src/test/java/com/yas/payment/paypal/service/PaypalServiceTest.java @@ -1,26 +1,38 @@ package com.yas.payment.paypal.service; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.paypal.core.PayPalHttpClient; import com.paypal.http.HttpResponse; -import com.paypal.orders.*; -import com.yas.payment.paypal.model.*; +import com.paypal.orders.Capture; +import com.paypal.orders.LinkDescription; +import com.paypal.orders.MerchantReceivableBreakdown; +import com.paypal.orders.Money; +import com.paypal.orders.Order; +import com.paypal.orders.OrdersCaptureRequest; +import com.paypal.orders.OrdersCreateRequest; +import com.paypal.orders.PaymentCollection; +import com.paypal.orders.PurchaseUnit; +import com.yas.payment.paypal.model.CheckoutIdHelper; 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 org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; class PaypalServiceTest { 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 cd140341d9..21ef1730b3 100644 --- a/payment/src/main/java/com/yas/payment/config/SecurityConfig.java +++ b/payment/src/main/java/com/yas/payment/config/SecurityConfig.java @@ -25,6 +25,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/prometheus", "/actuator/health/**", "/swagger-ui", "/swagger-ui/**", "/error", "/v3/api-docs/**").permitAll() + .requestMatchers("/webhook/paypal/payment-events").permitAll() .requestMatchers("/storefront/**").permitAll() .requestMatchers("/backoffice/**").hasRole("ADMIN") .requestMatchers("/payment-providers/**").permitAll() diff --git a/payment/src/main/java/com/yas/payment/controller/event/EventController.java b/payment/src/main/java/com/yas/payment/controller/event/EventController.java new file mode 100644 index 0000000000..5e1ed96c08 --- /dev/null +++ b/payment/src/main/java/com/yas/payment/controller/event/EventController.java @@ -0,0 +1,31 @@ +package com.yas.payment.controller.event; + +import com.yas.payment.paypal.model.PaypalWebhookEvent; +import com.yas.payment.service.order.handler.base.IPaypalWebhookHandlerRegistry; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class EventController { + + private static final Logger LOGGER = LoggerFactory.getLogger(EventController.class); + + private final IPaypalWebhookHandlerRegistry registry; + + @PostMapping("/webhook/paypal/payment-events") + public ResponseEntity handlePaypalPaymentEvent(@RequestBody @Valid PaypalWebhookEvent payload) { + + LOGGER.info("Received webhook from PayPal: {}", payload); + + registry.get(payload.getEventType().getStatus()).handle(payload); + + return ResponseEntity.ok("Paypal webhook handled successfully"); + } +} diff --git a/payment/src/main/java/com/yas/payment/kafka/consumer/PaymentCreateConsumer.java b/payment/src/main/java/com/yas/payment/kafka/consumer/PaymentCreateConsumer.java index 81931a607f..bbdc923921 100644 --- a/payment/src/main/java/com/yas/payment/kafka/consumer/PaymentCreateConsumer.java +++ b/payment/src/main/java/com/yas/payment/kafka/consumer/PaymentCreateConsumer.java @@ -2,12 +2,11 @@ import static com.yas.payment.utils.JsonUtils.convertObjectToString; import static com.yas.payment.utils.JsonUtils.getAttributesNode; +import static com.yas.payment.utils.JsonUtils.getJsonNodeByValue; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -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; @@ -33,7 +32,6 @@ public class PaymentCreateConsumer { private static final Logger LOGGER = LoggerFactory.getLogger(PaymentCreateConsumer.class); private final PaymentService paymentService; private final ObjectMapper objectMapper; - private final Gson gson; @KafkaListener( topics = "${cdc.event.payment.topic-name}", @@ -46,26 +44,26 @@ public void listen(ConsumerRecord consumerRecord) { LOGGER.info("Consumer Record is null"); return; } - JsonObject valueObject = gson.fromJson((String) consumerRecord.value(), JsonObject.class); + String jsonValue = (String) consumerRecord.value(); + JsonNode valueObject = getJsonNodeByValue(objectMapper, jsonValue, LOGGER); processCheckoutEvent(valueObject); - } - private void processCheckoutEvent(JsonObject valueObject) { + private void processCheckoutEvent(JsonNode valueObject) { Optional.ofNullable(valueObject) .filter( - value -> value.has("op") && "c".equals(value.get("op").getAsString()) + value -> value.has("op") && "c".equals(value.get("op").asText()) ) .filter(value -> value.has("after")) - .map(value -> value.getAsJsonObject("after")) + .map(value -> value.get("after")) .ifPresent(this::handleAfterJsonForCreatingOrder); } - private void handleAfterJsonForCreatingOrder(JsonObject after) { + private void handleAfterJsonForCreatingOrder(JsonNode after) { Long id = Optional.ofNullable(after.get(Constants.Column.ID_COLUMN)) - .filter(jsonElement -> !jsonElement.isJsonNull()) - .map(JsonElement::getAsLong) + .filter(jsonElement -> !jsonElement.isNull()) + .map(JsonNode::asLong) .orElseThrow(() -> new BadRequestException(Constants.ErrorCode.ID_NOT_EXISTED)); LOGGER.info("Handle after json for creating order Payment ID {}", id); 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 d3b3280ef0..6858f48a96 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 @@ -4,6 +4,7 @@ public enum PaymentStatus { NEW, PROCESSING, PENDING, + FAILURE, COMPLETED, CANCELLED } diff --git a/payment/src/main/java/com/yas/payment/repository/PaymentRepository.java b/payment/src/main/java/com/yas/payment/repository/PaymentRepository.java index f802eb9334..f52939891d 100644 --- a/payment/src/main/java/com/yas/payment/repository/PaymentRepository.java +++ b/payment/src/main/java/com/yas/payment/repository/PaymentRepository.java @@ -1,9 +1,12 @@ package com.yas.payment.repository; import com.yas.payment.model.Payment; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface PaymentRepository extends JpaRepository { + + Optional findByPaymentProviderCheckoutId(String paymentProviderCheckoutId); } \ No newline at end of file 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 e7f519aa2a..cfb56576c5 100644 --- a/payment/src/main/java/com/yas/payment/service/PaymentService.java +++ b/payment/src/main/java/com/yas/payment/service/PaymentService.java @@ -133,6 +133,14 @@ public Payment findPaymentById(Long id) { () -> new NotFoundException(Constants.ErrorCode.PAYMENT_NOT_FOUND, id)); } + public Payment findPaymentByPaypalOrderId(String id) { + + return paymentRepository + .findByPaymentProviderCheckoutId(id) + .orElseThrow( + () -> new NotFoundException(Constants.ErrorCode.PAYMENT_NOT_FOUND, id)); + } + public void updatePayment(Payment payment) { if (Objects.isNull(payment.getId())) { diff --git a/payment/src/main/java/com/yas/payment/service/order/handler/CompleteCaptureHandler.java b/payment/src/main/java/com/yas/payment/service/order/handler/CompleteCaptureHandler.java new file mode 100644 index 0000000000..909a221297 --- /dev/null +++ b/payment/src/main/java/com/yas/payment/service/order/handler/CompleteCaptureHandler.java @@ -0,0 +1,36 @@ +package com.yas.payment.service.order.handler; + +import static com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus.PAYMENT_CAPTURE_COMPLETED; + +import com.yas.payment.model.enumeration.PaymentStatus; +import com.yas.payment.paypal.model.PaypalWebhookEvent; +import com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus; +import com.yas.payment.service.PaymentService; +import com.yas.payment.service.order.handler.base.IPaypalWebhookHandlerRegistry; +import com.yas.payment.service.order.handler.base.PaypalWebhookHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class CompleteCaptureHandler extends PaypalWebhookHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(CompleteCaptureHandler.class); + + public CompleteCaptureHandler( + IPaypalWebhookHandlerRegistry registry, + PaymentService paymentService + ) { + super(registry, paymentService, LOGGER); + } + + @Override + protected PaypalPaymentStatus providePaypalPaymentStatus() { + return PAYMENT_CAPTURE_COMPLETED; + } + + @Override + protected PaymentStatus getPaymentStatus() { + return PaymentStatus.COMPLETED; + } +} diff --git a/payment/src/main/java/com/yas/payment/service/order/handler/DeclinedCaptureHandler.java b/payment/src/main/java/com/yas/payment/service/order/handler/DeclinedCaptureHandler.java new file mode 100644 index 0000000000..99362f1475 --- /dev/null +++ b/payment/src/main/java/com/yas/payment/service/order/handler/DeclinedCaptureHandler.java @@ -0,0 +1,37 @@ +package com.yas.payment.service.order.handler; + +import static com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus.PAYMENT_CAPTURE_DECLINED; + +import com.yas.payment.model.enumeration.PaymentStatus; +import com.yas.payment.paypal.model.PaypalWebhookEvent; +import com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus; +import com.yas.payment.service.PaymentService; +import com.yas.payment.service.order.handler.base.IPaypalWebhookHandlerRegistry; +import com.yas.payment.service.order.handler.base.PaypalWebhookHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class DeclinedCaptureHandler extends PaypalWebhookHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(DeclinedCaptureHandler.class); + + public DeclinedCaptureHandler( + IPaypalWebhookHandlerRegistry registry, + PaymentService paymentService + ) { + super(registry, paymentService, LOGGER); + } + + @Override + protected PaypalPaymentStatus providePaypalPaymentStatus() { + return PAYMENT_CAPTURE_DECLINED; + } + + @Override + protected PaymentStatus getPaymentStatus() { + return PaymentStatus.FAILURE; + } + +} diff --git a/payment/src/main/java/com/yas/payment/service/order/handler/RefundedCaptureHandler.java b/payment/src/main/java/com/yas/payment/service/order/handler/RefundedCaptureHandler.java new file mode 100644 index 0000000000..fa59ce0e95 --- /dev/null +++ b/payment/src/main/java/com/yas/payment/service/order/handler/RefundedCaptureHandler.java @@ -0,0 +1,37 @@ +package com.yas.payment.service.order.handler; + +import static com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus.PAYMENT_CAPTURE_REFUNDED; + +import com.yas.payment.model.enumeration.PaymentStatus; +import com.yas.payment.paypal.model.PaypalWebhookEvent; +import com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus; +import com.yas.payment.service.PaymentService; +import com.yas.payment.service.order.handler.base.IPaypalWebhookHandlerRegistry; +import com.yas.payment.service.order.handler.base.PaypalWebhookHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class RefundedCaptureHandler extends PaypalWebhookHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(RefundedCaptureHandler.class); + + public RefundedCaptureHandler( + IPaypalWebhookHandlerRegistry registry, + PaymentService paymentService + ) { + super(registry, paymentService, LOGGER); + } + + @Override + protected PaypalPaymentStatus providePaypalPaymentStatus() { + return PAYMENT_CAPTURE_REFUNDED; + } + + @Override + protected PaymentStatus getPaymentStatus() { + return PaymentStatus.FAILURE; + } + +} diff --git a/payment/src/main/java/com/yas/payment/service/order/handler/ReversedCaptureHandler.java b/payment/src/main/java/com/yas/payment/service/order/handler/ReversedCaptureHandler.java new file mode 100644 index 0000000000..931ea189d5 --- /dev/null +++ b/payment/src/main/java/com/yas/payment/service/order/handler/ReversedCaptureHandler.java @@ -0,0 +1,37 @@ +package com.yas.payment.service.order.handler; + +import static com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus.PAYMENT_CAPTURE_REVERSED; + +import com.yas.payment.model.enumeration.PaymentStatus; +import com.yas.payment.paypal.model.PaypalWebhookEvent; +import com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus; +import com.yas.payment.service.PaymentService; +import com.yas.payment.service.order.handler.base.IPaypalWebhookHandlerRegistry; +import com.yas.payment.service.order.handler.base.PaypalWebhookHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class ReversedCaptureHandler extends PaypalWebhookHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(ReversedCaptureHandler.class); + + public ReversedCaptureHandler( + IPaypalWebhookHandlerRegistry registry, + PaymentService paymentService + ) { + super(registry, paymentService, LOGGER); + } + + @Override + protected PaypalPaymentStatus providePaypalPaymentStatus() { + return PAYMENT_CAPTURE_REVERSED; + } + + @Override + protected PaymentStatus getPaymentStatus() { + return PaymentStatus.FAILURE; + } + +} diff --git a/payment/src/main/java/com/yas/payment/service/order/handler/VoidedAuthorizationHandler.java b/payment/src/main/java/com/yas/payment/service/order/handler/VoidedAuthorizationHandler.java new file mode 100644 index 0000000000..16fb67e7c0 --- /dev/null +++ b/payment/src/main/java/com/yas/payment/service/order/handler/VoidedAuthorizationHandler.java @@ -0,0 +1,37 @@ +package com.yas.payment.service.order.handler; + +import static com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus.PAYMENT_AUTHORIZATION_VOIDED; + +import com.yas.payment.model.enumeration.PaymentStatus; +import com.yas.payment.paypal.model.PaypalWebhookEvent; +import com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus; +import com.yas.payment.service.PaymentService; +import com.yas.payment.service.order.handler.base.IPaypalWebhookHandlerRegistry; +import com.yas.payment.service.order.handler.base.PaypalWebhookHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class VoidedAuthorizationHandler extends PaypalWebhookHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(VoidedAuthorizationHandler.class); + + public VoidedAuthorizationHandler( + IPaypalWebhookHandlerRegistry registry, + PaymentService paymentService + ) { + super(registry, paymentService, LOGGER); + } + + @Override + protected PaypalPaymentStatus providePaypalPaymentStatus() { + return PAYMENT_AUTHORIZATION_VOIDED; + } + + @Override + protected PaymentStatus getPaymentStatus() { + return PaymentStatus.FAILURE; + } + +} diff --git a/payment/src/main/java/com/yas/payment/service/order/handler/base/IPaypalWebhookHandler.java b/payment/src/main/java/com/yas/payment/service/order/handler/base/IPaypalWebhookHandler.java new file mode 100644 index 0000000000..06713ba81b --- /dev/null +++ b/payment/src/main/java/com/yas/payment/service/order/handler/base/IPaypalWebhookHandler.java @@ -0,0 +1,14 @@ +package com.yas.payment.service.order.handler.base; + +import com.yas.payment.paypal.model.PaypalWebhookEvent; + +public interface IPaypalWebhookHandler { + + /** + * Processes the webhook payload. + * + * @param payload the webhook payload of type PaypalWebhookEvent + */ + void handle(T payload); + +} diff --git a/payment/src/main/java/com/yas/payment/service/order/handler/base/IPaypalWebhookHandlerRegistry.java b/payment/src/main/java/com/yas/payment/service/order/handler/base/IPaypalWebhookHandlerRegistry.java new file mode 100644 index 0000000000..711d61bbda --- /dev/null +++ b/payment/src/main/java/com/yas/payment/service/order/handler/base/IPaypalWebhookHandlerRegistry.java @@ -0,0 +1,10 @@ +package com.yas.payment.service.order.handler.base; + +import com.yas.payment.paypal.model.PaypalWebhookEvent; + +public interface IPaypalWebhookHandlerRegistry { + + IPaypalWebhookHandler get(String type); + + void register(String type, IPaypalWebhookHandler handler); +} diff --git a/payment/src/main/java/com/yas/payment/service/order/handler/base/PaypalWebhookHandler.java b/payment/src/main/java/com/yas/payment/service/order/handler/base/PaypalWebhookHandler.java new file mode 100644 index 0000000000..e42d675b59 --- /dev/null +++ b/payment/src/main/java/com/yas/payment/service/order/handler/base/PaypalWebhookHandler.java @@ -0,0 +1,41 @@ +package com.yas.payment.service.order.handler.base; + +import com.yas.payment.model.Payment; +import com.yas.payment.model.enumeration.PaymentStatus; +import com.yas.payment.paypal.model.PaypalWebhookEvent; +import com.yas.payment.paypal.model.enumeration.PaypalPaymentStatus; +import com.yas.payment.service.PaymentService; +import org.slf4j.Logger; + +public abstract class PaypalWebhookHandler implements IPaypalWebhookHandler { + + private final Logger logger; + protected final PaymentService paymentService; + + protected PaypalWebhookHandler( + IPaypalWebhookHandlerRegistry registry, + PaymentService paymentService, + Logger logger + ) { + registry.register(this.providePaypalPaymentStatus().getStatus(), this); + this.paymentService = paymentService; + this.logger = logger; + } + + protected abstract PaypalPaymentStatus providePaypalPaymentStatus(); + + protected abstract PaymentStatus getPaymentStatus(); + + @Override + public void handle(PaypalWebhookEvent payload) { + + logger.info("Start handling for status {}, Update Payment Status to {}", + providePaypalPaymentStatus(), getPaymentStatus()); + Payment payment = paymentService.findPaymentByPaypalOrderId(payload.getResource().getId()); + payment.setPaymentStatus(getPaymentStatus()); + paymentService.updatePayment(payment); + logger.info("End webhook from PayPal: {}", providePaypalPaymentStatus()); + + } + +} diff --git a/payment/src/main/java/com/yas/payment/service/order/handler/base/PaypalWebhookHandlerRegistry.java b/payment/src/main/java/com/yas/payment/service/order/handler/base/PaypalWebhookHandlerRegistry.java new file mode 100644 index 0000000000..7d4a502b29 --- /dev/null +++ b/payment/src/main/java/com/yas/payment/service/order/handler/base/PaypalWebhookHandlerRegistry.java @@ -0,0 +1,27 @@ +package com.yas.payment.service.order.handler.base; + +import com.yas.commonlibrary.exception.NotFoundException; +import com.yas.payment.paypal.model.PaypalWebhookEvent; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.springframework.stereotype.Component; + +@Component +public class PaypalWebhookHandlerRegistry implements IPaypalWebhookHandlerRegistry { + + private final Map> registry = new HashMap<>(); + + public IPaypalWebhookHandler get(String type) { + IPaypalWebhookHandler handler = this.registry.get(type); + if (Objects.isNull(handler)) { + throw new NotFoundException("MessageType {} is not supported", type); + } + return handler; + } + + @Override + public void register(String type, IPaypalWebhookHandler handler) { + this.registry.put(type, handler); + } +} diff --git a/payment/src/main/java/com/yas/payment/utils/JsonUtils.java b/payment/src/main/java/com/yas/payment/utils/JsonUtils.java index 4e99823cbd..76061a8a5e 100644 --- a/payment/src/main/java/com/yas/payment/utils/JsonUtils.java +++ b/payment/src/main/java/com/yas/payment/utils/JsonUtils.java @@ -1,9 +1,11 @@ package com.yas.payment.utils; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.yas.commonlibrary.exception.BadRequestException; +import org.slf4j.Logger; public class JsonUtils { @@ -30,4 +32,17 @@ public static ObjectNode getAttributesNode(ObjectMapper objectMapper, String att throw new BadRequestException("Invalid Json: {}", attributes); } } + + public static JsonNode getJsonNodeByValue( + ObjectMapper objectMapper, + String jsonValue, + Logger logger + ) { + try { + return objectMapper.readTree(jsonValue); + } catch (JsonProcessingException e) { + logger.error("Invalid Json. Message: {}", jsonValue, e); + throw new BadRequestException("Failed to parse message as JSON. Message: {}", jsonValue); + } + } } \ No newline at end of file diff --git a/payment/src/test/java/com/yas/payment/kafka/consumer/PaymentCreateConsumerTest.java b/payment/src/test/java/com/yas/payment/kafka/consumer/PaymentCreateConsumerTest.java index 7bf3c6a9d2..b0c9b3b396 100644 --- a/payment/src/test/java/com/yas/payment/kafka/consumer/PaymentCreateConsumerTest.java +++ b/payment/src/test/java/com/yas/payment/kafka/consumer/PaymentCreateConsumerTest.java @@ -9,7 +9,6 @@ import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.gson.Gson; import com.yas.commonlibrary.exception.BadRequestException; import com.yas.payment.model.Payment; import com.yas.payment.model.enumeration.PaymentMethod; @@ -33,8 +32,7 @@ class PaymentCreateConsumerTest { void setUp() { paymentService = mock(PaymentService.class); ObjectMapper objectMapper = new ObjectMapper(); - Gson gson = new Gson(); - paymentCreateConsumer = new PaymentCreateConsumer(paymentService, objectMapper, gson); + paymentCreateConsumer = new PaymentCreateConsumer(paymentService, objectMapper); } @Test diff --git a/storefront/modules/order/components/CheckOutDetail.tsx b/storefront/modules/order/components/CheckOutDetail.tsx index 74e29628b4..a3a9957556 100644 --- a/storefront/modules/order/components/CheckOutDetail.tsx +++ b/storefront/modules/order/components/CheckOutDetail.tsx @@ -137,7 +137,7 @@ const CheckOutDetail = ({ orderItems, disablePaymentProcess, setPaymentMethod }:

- ); diff --git a/storefront/pages/checkout/[id].tsx b/storefront/pages/checkout/[id].tsx index 710e88a297..179c66043b 100644 --- a/storefront/pages/checkout/[id].tsx +++ b/storefront/pages/checkout/[id].tsx @@ -10,7 +10,6 @@ import { Input } from 'common/items/Input'; import { Address } from '@/modules/address/models/AddressModel'; import AddressForm from '@/modules/address/components/AddressForm'; import { - createOrder, getCheckoutById, processPayment, } from '@/modules/order/services/OrderService'; @@ -222,20 +221,8 @@ const Checkout = () => { setIsShowSpinner(true); setDisableProcessPayment(true); - if (clickedButtonId === 'newProcessToPaymentButton') { - processPayment(id as string); - handlePaymentProcess(order); - } else { - createOrder(order) - .then(() => { - handleCheckOutProcess(order); - }) - .catch(() => { - setIsShowSpinner(false); - setDisableProcessPayment(false); - toast.error('Place order failed'); - }); - } + processPayment(id as string); + handlePaymentProcess(order); } }; diff --git a/storefront/pages/complete-payment/[capture].tsx b/storefront/pages/complete-payment/[capture].tsx deleted file mode 100644 index 0c9443c55e..0000000000 --- a/storefront/pages/complete-payment/[capture].tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { BreadcrumbModel } from '@/modules/breadcrumb/model/BreadcrumbModel'; -import { Button, Container } from 'react-bootstrap'; -import BreadcrumbComponent from '../../common/components/BreadcrumbComponent'; -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { capturePaymentPaypal } from '@/modules/paymentPaypal/services/PaymentPaypalService'; -import SpinnerComponent from '@/common/components/SpinnerComponent'; -import Link from 'next/link'; -import { CapturePaymentPaypalResponse } from '@/modules/paymentPaypal/models/CapturePaymentPaypalResponse'; -import { PaymentPaypalFailureMessage } from '@/modules/paymentPaypal/models/PaymentPaypalFailureMesasge'; -import { CapturePaymentRequest } from '@/modules/paymentPaypal/models/CapturePaymentRequest'; - -const crumb: BreadcrumbModel[] = [ - { - pageName: 'Home', - url: '/', - }, - { - pageName: 'Complete Payment', - url: '/complete-payment', - }, -]; - -const CompletePayment = () => { - const router = useRouter(); - const { token, paymentMethod } = router.query; - const [isPaymentSuccess, setIsPaymentSuccess] = useState(false); - const [isAlreadyPaid, setIsAlreadyPaid] = useState(false); - const [isCancelPayment, setIsCancelPayment] = useState(false); - const [isPaymentUnsuccessful, setIsPaymentUnsuccessful] = useState(false); - const [isShowSpinner, setIsShowSpinner] = useState(false); - useEffect(() => { - if (token) { - const fetchCapturePaymentPaypal = async (capturePaymentRequestVM: CapturePaymentRequest) => { - setIsShowSpinner(true); - const res = await capturePaymentPaypal(capturePaymentRequestVM); - if (res.paymentStatus == 'COMPLETED') { - setIsPaymentSuccess(true); - } else { - extractPaymentPaypalFailure(res); - } - setIsShowSpinner(false); - }; - - const capturePaymentRequestVM: CapturePaymentRequest = { - token: token as string, - paymentMethod: paymentMethod as string, - }; - fetchCapturePaymentPaypal(capturePaymentRequestVM).then(); - } - }, [router.query, token, paymentMethod]); - - const extractPaymentPaypalFailure = (res: CapturePaymentPaypalResponse) => { - const failureMessage: PaymentPaypalFailureMessage = JSON.parse(res.failureMessage!!); - const details = failureMessage.details; - const issue = details[0].issue; - switch (issue) { - case 'ORDER_NOT_APPROVED': - setIsCancelPayment(true); - break; - case 'ORDER_ALREADY_CAPTURED': - setIsAlreadyPaid(true); - break; - default: - setIsPaymentUnsuccessful(true); - } - }; - - return ( - <> - -
-
- - -
-
- - - - -
-
- - - -
-
-
-
-
- - ); -}; - -export default CompletePayment; diff --git a/storefront/pages/complete-payment/[success].tsx b/storefront/pages/complete-payment/[success].tsx new file mode 100644 index 0000000000..8c05b69026 --- /dev/null +++ b/storefront/pages/complete-payment/[success].tsx @@ -0,0 +1,48 @@ +import { BreadcrumbModel } from '@/modules/breadcrumb/model/BreadcrumbModel'; +import { Button, Container } from 'react-bootstrap'; +import BreadcrumbComponent from '../../common/components/BreadcrumbComponent'; +import Link from 'next/link'; + +const crumb: BreadcrumbModel[] = [ + { + pageName: 'Home', + url: '/', + }, + { + pageName: 'Complete Payment', + url: '/complete-payment', + }, +]; + +const CompletePayment = () => { + + return ( + <> + +
+
+ +
+
+
+

+ YOUR ORDER PAID SUCCESSFUL +

+
+
+
+ + + +
+
+
+
+
+ + ); +}; + +export default CompletePayment;