Skip to content

Commit

Permalink
#1268 Step3 Customer Execute Payment
Browse files Browse the repository at this point in the history
  • Loading branch information
tuannguyenh1 committed Nov 18, 2024
1 parent 169b288 commit 3a8e162
Show file tree
Hide file tree
Showing 54 changed files with 1,057 additions and 304 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ public ResponseEntity<CheckoutVm> createCheckout(@Valid @RequestBody CheckoutPos
return ResponseEntity.ok(checkoutService.createCheckout(checkoutPostVm));
}

@PostMapping("/storefront/checkouts/{id}/process-payment")
public ResponseEntity<Void> processPayment(@PathVariable String id) {
checkoutService.processPayment(id);
return ResponseEntity.ok().build();
}

@PutMapping("/storefront/checkouts/status")
public ResponseEntity<Long> updateCheckoutStatus(@Valid @RequestBody CheckoutStatusPutVm checkoutStatusPutVm) {
return ResponseEntity.ok(checkoutService.updateCheckoutStatus(checkoutStatusPutVm));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CheckoutItem> checkoutItemList = checkoutItemService.getAllByCheckoutId(checkout.getId());

Order order = createOrder(checkout, checkoutItemList);
createOrderItems(order, checkoutItemList);
updateCheckoutStatus(checkout);
}

private Order createOrder(Checkout checkout, List<CheckoutItem> 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<CheckoutItem> checkoutItemList) {

List<OrderItem> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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"
Expand All @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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)) {

Expand All @@ -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));

Expand Down
Loading

0 comments on commit 3a8e162

Please sign in to comment.