From cc8e1024ed0e4cdb78f6262efab8b14050126ed9 Mon Sep 17 00:00:00 2001
From: tuannguyenh1
Date: Fri, 8 Nov 2024 13:49:51 +0700
Subject: [PATCH] #1180 & #1237 intergration with front end - create payment
and order in paypal
---
.../kafka/consumer/OrderStatusConsumer.java | 4 +-
.../kafka/consumer/PaymentUpdateConsumer.java | 14 +-
.../order/viewmodel/checkout/CheckoutVm.java | 13 ++
.../controller/CheckoutControllerTest.java | 8 +
.../consumer/OrderStatusConsumerTest.java | 2 +-
.../com/yas/payment/config/JsonConfig.java | 22 +++
.../payment/controller/PaymentController.java | 6 +-
.../kafka/consumer/PaymentCreateConsumer.java | 10 ++
.../java/com/yas/payment/model/Payment.java | 7 +
.../java/com/yas/payment/utils/Constants.java | 2 +-
.../java/com/yas/payment/utils/JsonUtils.java | 33 ++++
.../com/yas/payment/viewmodel/PaymentVm.java | 8 +-
.../db/changelog/ddl/changelog-0005.sql | 2 +
.../consumer/PaymentCreateConsumerTest.java | 4 +-
.../modules/common/models/EPaymentMethod.ts | 5 +
.../order/components/CheckOutDetail.tsx | 14 ++
storefront/modules/order/models/Checkout.ts | 10 +-
.../modules/order/models/ECheckoutProgress.ts | 9 ++
.../modules/order/models/ECheckoutState.ts | 10 ++
.../modules/order/services/OrderService.ts | 8 +-
.../modules/payment/models/EPaymentStatus.ts | 7 +
storefront/modules/payment/models/Payment.ts | 16 ++
.../payment/services/PaymentService.ts | 9 ++
storefront/pages/cart/index.tsx | 2 +-
storefront/pages/checkout/[id].tsx | 148 ++++++++++++++++--
25 files changed, 337 insertions(+), 36 deletions(-)
create mode 100644 payment/src/main/java/com/yas/payment/config/JsonConfig.java
create mode 100644 payment/src/main/java/com/yas/payment/utils/JsonUtils.java
create mode 100644 payment/src/main/resources/db/changelog/ddl/changelog-0005.sql
create mode 100644 storefront/modules/common/models/EPaymentMethod.ts
create mode 100644 storefront/modules/order/models/ECheckoutProgress.ts
create mode 100644 storefront/modules/order/models/ECheckoutState.ts
create mode 100644 storefront/modules/payment/models/EPaymentStatus.ts
create mode 100644 storefront/modules/payment/models/Payment.ts
create mode 100644 storefront/modules/payment/services/PaymentService.ts
diff --git a/order/src/main/java/com/yas/order/kafka/consumer/OrderStatusConsumer.java b/order/src/main/java/com/yas/order/kafka/consumer/OrderStatusConsumer.java
index e575a1867c..0c78375085 100644
--- a/order/src/main/java/com/yas/order/kafka/consumer/OrderStatusConsumer.java
+++ b/order/src/main/java/com/yas/order/kafka/consumer/OrderStatusConsumer.java
@@ -58,7 +58,9 @@ public void listen(ConsumerRecord, ?> consumerRecord) {
private void processCheckoutEvent(JsonObject valueObject) {
Optional.ofNullable(valueObject)
- .filter(value -> value.has("after"))
+ .filter(
+ value -> value.has("op") && "u".equals(value.get("op").getAsString())
+ )
.map(value -> value.getAsJsonObject("after"))
.ifPresent(this::handleAfterJson);
}
diff --git a/order/src/main/java/com/yas/order/kafka/consumer/PaymentUpdateConsumer.java b/order/src/main/java/com/yas/order/kafka/consumer/PaymentUpdateConsumer.java
index e3be435c6a..0f78037556 100644
--- a/order/src/main/java/com/yas/order/kafka/consumer/PaymentUpdateConsumer.java
+++ b/order/src/main/java/com/yas/order/kafka/consumer/PaymentUpdateConsumer.java
@@ -89,20 +89,12 @@ private void updateCheckOut(String checkoutId, String paymentProviderCheckoutId)
checkout.setCheckoutState(CheckoutState.PAYMENT_PROCESSING);
checkout.setProgress(CheckoutProgress.PAYMENT_CREATED);
- ObjectNode updatedAttributes = updateAttributesWithCheckout(checkout.getAttributes(),
- paymentProviderCheckoutId);
- checkout.setAttributes(convertObjectToString(objectMapper, updatedAttributes));
-
- checkoutService.updateCheckout(checkout);
- }
-
- private ObjectNode updateAttributesWithCheckout(String attributes, String paymentProviderCheckoutId) {
-
- ObjectNode attributesNode = getAttributesNode(objectMapper, attributes);
+ ObjectNode attributesNode = getAttributesNode(objectMapper, checkout.getAttributes());
attributesNode.put(Constants.Column.CHECKOUT_ATTRIBUTES_PAYMENT_PROVIDER_CHECKOUT_ID_FIELD,
paymentProviderCheckoutId);
+ checkout.setAttributes(convertObjectToString(objectMapper, attributesNode));
- return attributesNode;
+ checkoutService.updateCheckout(checkout);
}
}
\ No newline at end of file
diff --git a/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java b/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java
index 8a99b697d2..33035a421f 100644
--- a/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java
+++ b/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java
@@ -1,6 +1,9 @@
package com.yas.order.viewmodel.checkout;
import com.yas.order.model.Checkout;
+import com.yas.order.model.enumeration.CheckoutProgress;
+import com.yas.order.model.enumeration.CheckoutState;
+import com.yas.order.model.enumeration.PaymentMethod;
import java.math.BigDecimal;
import java.util.Set;
import lombok.Builder;
@@ -11,6 +14,11 @@ public record CheckoutVm(
String email,
String note,
String couponCode,
+ CheckoutState checkoutState,
+ CheckoutProgress progress,
+ PaymentMethod paymentMethodId,
+ String attributes,
+ String lastError,
BigDecimal totalAmount,
BigDecimal totalDiscountAmount,
Set checkoutItemVms
@@ -22,6 +30,11 @@ public static CheckoutVm fromModel(Checkout checkout, Set checko
.email(checkout.getEmail())
.note(checkout.getNote())
.couponCode(checkout.getCouponCode())
+ .checkoutState(checkout.getCheckoutState())
+ .progress(checkout.getProgress())
+ .paymentMethodId(checkout.getPaymentMethodId())
+ .attributes(checkout.getAttributes())
+ .lastError(checkout.getLastError())
.checkoutItemVms(checkoutItemVms)
.build();
}
diff --git a/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java b/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java
index 19fc7ce794..a95dd214d9 100644
--- a/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java
+++ b/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java
@@ -12,6 +12,9 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.yas.order.OrderApplication;
+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.service.CheckoutService;
import com.yas.order.viewmodel.checkout.CheckoutItemPostVm;
import com.yas.order.viewmodel.checkout.CheckoutItemVm;
@@ -201,6 +204,11 @@ private CheckoutVm getCheckoutVm() {
"user@example.com",
"Please deliver after 5 PM",
"DISCOUNT20",
+ CheckoutState.PAYMENT_PROCESSING,
+ CheckoutProgress.PROMOTION_CODE_APPLIED,
+ PaymentMethod.PAYPAL,
+ null,
+ null,
BigDecimal.valueOf(900),
BigDecimal.valueOf(9),
checkoutItemVms
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 d6bc855f4e..1e95152491 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
@@ -34,7 +34,7 @@ class OrderStatusConsumerTest {
private OrderStatusConsumer orderStatusConsumer;
- private final String jsonRecord = "{\"after\": {"
+ private final String jsonRecord = "{\"op\": \"u\", \"after\": {"
+ " \"status\": \"PAYMENT_PROCESSING\","
+ " \"progress\": \"STOCK_LOCKED\","
+ " \"id\": 12345,"
diff --git a/payment/src/main/java/com/yas/payment/config/JsonConfig.java b/payment/src/main/java/com/yas/payment/config/JsonConfig.java
new file mode 100644
index 0000000000..fa2c9f7997
--- /dev/null
+++ b/payment/src/main/java/com/yas/payment/config/JsonConfig.java
@@ -0,0 +1,22 @@
+package com.yas.payment.config;
+
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.Gson;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+
+@Configuration
+public class JsonConfig {
+
+ @Bean
+ public ObjectMapper objectMapper() {
+ return new ObjectMapper();
+ }
+
+ @Bean
+ public Gson gson() {
+ return new Gson();
+ }
+}
\ No newline at end of file
diff --git a/payment/src/main/java/com/yas/payment/controller/PaymentController.java b/payment/src/main/java/com/yas/payment/controller/PaymentController.java
index b4a7ca4743..e289e4d3de 100644
--- a/payment/src/main/java/com/yas/payment/controller/PaymentController.java
+++ b/payment/src/main/java/com/yas/payment/controller/PaymentController.java
@@ -30,8 +30,10 @@ public InitPaymentResponseVm initPayment(@Valid @RequestBody InitPaymentRequestV
}
@PostMapping(value = "/capture")
- public CapturePaymentResponseVm capturePayment(@Valid @RequestBody CapturePaymentRequestVm capturePaymentRequestVM) {
- return paymentService.capturePayment(capturePaymentRequestVM);
+ public CapturePaymentResponseVm capturePayment(
+ @Valid @RequestBody CapturePaymentRequestVm capturePaymentRequestVm
+ ) {
+ return paymentService.capturePayment(capturePaymentRequestVm);
}
@GetMapping(value = "/cancel")
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 03ca84bd10..81931a607f 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
@@ -1,5 +1,10 @@
package com.yas.payment.kafka.consumer;
+import static com.yas.payment.utils.JsonUtils.convertObjectToString;
+import static com.yas.payment.utils.JsonUtils.getAttributesNode;
+
+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;
@@ -27,6 +32,7 @@ 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(
@@ -92,6 +98,10 @@ private void createOrderOnPaypal(Payment payment) {
payment.setPaymentStatus(PaymentStatus.PROCESSING);
payment.setPaymentProviderCheckoutId(initPaymentResponseVm.paymentId());
+ ObjectNode attributesNode = getAttributesNode(objectMapper, payment.getAttributes());
+ attributesNode.put(Constants.Column.REDIRECT_URL_ID_COLUMN, initPaymentResponseVm.redirectUrl());
+ payment.setAttributes(convertObjectToString(objectMapper, attributesNode));
+
paymentService.updatePayment(payment);
} else {
LOGGER.warn(Constants.ErrorCode.ORDER_CREATION_FAILED, payment.getId());
diff --git a/payment/src/main/java/com/yas/payment/model/Payment.java b/payment/src/main/java/com/yas/payment/model/Payment.java
index 0a9ca2d80e..7a2a455644 100644
--- a/payment/src/main/java/com/yas/payment/model/Payment.java
+++ b/payment/src/main/java/com/yas/payment/model/Payment.java
@@ -2,6 +2,7 @@
import com.yas.payment.model.enumeration.PaymentMethod;
import com.yas.payment.model.enumeration.PaymentStatus;
+import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
@@ -15,6 +16,8 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
+import org.hibernate.annotations.JdbcTypeCode;
+import org.hibernate.type.SqlTypes;
@Entity
@Table(name = "payment")
@@ -49,4 +52,8 @@ public class Payment extends AbstractAuditEntity {
private String paymentProviderCheckoutId;
+ @JdbcTypeCode(SqlTypes.JSON)
+ @Column(name = "attributes", columnDefinition = "jsonb")
+ private String attributes;
+
}
diff --git a/payment/src/main/java/com/yas/payment/utils/Constants.java b/payment/src/main/java/com/yas/payment/utils/Constants.java
index 1b3be05f90..d98f69308c 100644
--- a/payment/src/main/java/com/yas/payment/utils/Constants.java
+++ b/payment/src/main/java/com/yas/payment/utils/Constants.java
@@ -22,7 +22,7 @@ private Column() {
public static final String ID_COLUMN = "id";
// Column name of Checkout table
- public static final String REDIRECT_URL_ID_COLUMN = "redirect-url";
+ public static final String REDIRECT_URL_ID_COLUMN = "redirect_url";
}
public final class Message {
diff --git a/payment/src/main/java/com/yas/payment/utils/JsonUtils.java b/payment/src/main/java/com/yas/payment/utils/JsonUtils.java
new file mode 100644
index 0000000000..4e99823cbd
--- /dev/null
+++ b/payment/src/main/java/com/yas/payment/utils/JsonUtils.java
@@ -0,0 +1,33 @@
+package com.yas.payment.utils;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.yas.commonlibrary.exception.BadRequestException;
+
+public class JsonUtils {
+
+ private JsonUtils() {
+ throw new UnsupportedOperationException();
+ }
+
+ public static String convertObjectToString(ObjectMapper objectMapper, Object value) {
+ try {
+ return objectMapper.writeValueAsString(value);
+ } catch (JsonProcessingException e) {
+ throw new BadRequestException(Constants.ErrorCode.CANNOT_CONVERT_TO_STRING, value);
+ }
+ }
+
+ public static ObjectNode getAttributesNode(ObjectMapper objectMapper, String attributes) {
+ try {
+ if (attributes == null || attributes.isBlank()) {
+ return objectMapper.createObjectNode();
+ } else {
+ return (ObjectNode) objectMapper.readTree(attributes);
+ }
+ } catch (JsonProcessingException e) {
+ throw new BadRequestException("Invalid Json: {}", attributes);
+ }
+ }
+}
\ No newline at end of file
diff --git a/payment/src/main/java/com/yas/payment/viewmodel/PaymentVm.java b/payment/src/main/java/com/yas/payment/viewmodel/PaymentVm.java
index 5f3c3eb9a0..b655c36247 100644
--- a/payment/src/main/java/com/yas/payment/viewmodel/PaymentVm.java
+++ b/payment/src/main/java/com/yas/payment/viewmodel/PaymentVm.java
@@ -15,7 +15,9 @@ public record PaymentVm(
PaymentMethod paymentMethod,
PaymentStatus paymentStatus,
String gatewayTransactionId,
- String failureMessage
+ String failureMessage,
+ String paymentProviderCheckoutId,
+ String attributes
) {
public static PaymentVm fromModel(Payment payment) {
@@ -28,7 +30,9 @@ public static PaymentVm fromModel(Payment payment) {
payment.getPaymentMethod(),
payment.getPaymentStatus(),
payment.getGatewayTransactionId(),
- payment.getFailureMessage()
+ payment.getFailureMessage(),
+ payment.getPaymentProviderCheckoutId(),
+ payment.getAttributes()
);
}
}
\ No newline at end of file
diff --git a/payment/src/main/resources/db/changelog/ddl/changelog-0005.sql b/payment/src/main/resources/db/changelog/ddl/changelog-0005.sql
new file mode 100644
index 0000000000..a824753ab3
--- /dev/null
+++ b/payment/src/main/resources/db/changelog/ddl/changelog-0005.sql
@@ -0,0 +1,2 @@
+ALTER TABLE IF EXISTS "payment"
+ADD COLUMN attributes JSONB;
\ 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 911880ac8e..7bf3c6a9d2 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
@@ -8,6 +8,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.yas.commonlibrary.exception.BadRequestException;
import com.yas.payment.model.Payment;
@@ -31,8 +32,9 @@ class PaymentCreateConsumerTest {
@BeforeEach
void setUp() {
paymentService = mock(PaymentService.class);
+ ObjectMapper objectMapper = new ObjectMapper();
Gson gson = new Gson();
- paymentCreateConsumer = new PaymentCreateConsumer(paymentService, gson);
+ paymentCreateConsumer = new PaymentCreateConsumer(paymentService, objectMapper, gson);
}
@Test
diff --git a/storefront/modules/common/models/EPaymentMethod.ts b/storefront/modules/common/models/EPaymentMethod.ts
new file mode 100644
index 0000000000..e399862508
--- /dev/null
+++ b/storefront/modules/common/models/EPaymentMethod.ts
@@ -0,0 +1,5 @@
+export enum EPaymentMethod {
+ COD = 'COD',
+ BANKING = 'BANKING',
+ PAYPAL = 'PAYPAL',
+}
diff --git a/storefront/modules/order/components/CheckOutDetail.tsx b/storefront/modules/order/components/CheckOutDetail.tsx
index e1eae4066e..74e29628b4 100644
--- a/storefront/modules/order/components/CheckOutDetail.tsx
+++ b/storefront/modules/order/components/CheckOutDetail.tsx
@@ -137,6 +137,7 @@ const CheckOutDetail = ({ orderItems, disablePaymentProcess, setPaymentMethod }:
+
>
);
diff --git a/storefront/modules/order/models/Checkout.ts b/storefront/modules/order/models/Checkout.ts
index 12f1e1497d..de5f2f6825 100644
--- a/storefront/modules/order/models/Checkout.ts
+++ b/storefront/modules/order/models/Checkout.ts
@@ -1,11 +1,19 @@
+import { EPaymentMethod } from '@/modules/common/models/EPaymentMethod';
import { CheckoutItem } from './CheckoutItem';
+import { ECheckoutState } from './ECheckoutState';
+import { ECheckoutProgress } from './ECheckoutProgress';
export type Checkout = {
id?: string;
email: string;
note?: string;
couponCode?: string;
+ checkoutState?: ECheckoutState;
+ progress?: ECheckoutProgress;
+ paymentMethodId?: EPaymentMethod;
+ attributes?: string;
+ lastError?: string;
totalAmount: number;
totalDiscountAmount: number;
- checkoutItemPostVms: CheckoutItem[];
+ checkoutItemVms: CheckoutItem[];
};
diff --git a/storefront/modules/order/models/ECheckoutProgress.ts b/storefront/modules/order/models/ECheckoutProgress.ts
new file mode 100644
index 0000000000..9a42e573d4
--- /dev/null
+++ b/storefront/modules/order/models/ECheckoutProgress.ts
@@ -0,0 +1,9 @@
+export enum ECheckoutProgress {
+ INIT = 'INIT',
+ PROMOTION_CODE_APPLIED = 'PROMOTION_CODE_APPLIED',
+ PROMOTION_CODE_APPLIED_FAILED = 'PROMOTION_CODE_APPLIED_FAILED',
+ STOCK_LOCKED = 'STOCK_LOCKED',
+ STOCK_LOCKED_FAILED = 'STOCK_LOCKED_FAILED',
+ PAYMENT_CREATED = 'PAYMENT_CREATED',
+ PAYMENT_CREATED_FAILED = 'PAYMENT_CREATED_FAILED',
+}
diff --git a/storefront/modules/order/models/ECheckoutState.ts b/storefront/modules/order/models/ECheckoutState.ts
new file mode 100644
index 0000000000..afc72210e8
--- /dev/null
+++ b/storefront/modules/order/models/ECheckoutState.ts
@@ -0,0 +1,10 @@
+export enum ECheckoutState {
+ COMPLETED = 'COMPLETED',
+ PENDING = 'PENDING',
+ LOCK = 'LOCK',
+ CHECKED_OUT = 'CHECKED_OUT',
+ PAYMENT_PROCESSING = 'PAYMENT_PROCESSING',
+ PAYMENT_FAILED = 'PAYMENT_FAILED',
+ PAYMENT_CONFIRMED = 'PAYMENT_CONFIRMED',
+ FULFILLED = 'FULFILLED',
+}
diff --git a/storefront/modules/order/services/OrderService.ts b/storefront/modules/order/services/OrderService.ts
index be46b296b7..dd2a11e651 100644
--- a/storefront/modules/order/services/OrderService.ts
+++ b/storefront/modules/order/services/OrderService.ts
@@ -33,8 +33,14 @@ export async function createCheckout(checkout: Checkout): Promise {
const response = await apiClientService.get(`${baseUrl}/checkouts/${id}`);
if (response.status >= 200 && response.status < 300) return response.json();
throw new Error(response.statusText);
}
+
+export async function processPayment(id: string): Promise {
+ const response = await apiClientService.post(`${baseUrl}/checkouts/${id}/process-payment`, {});
+ if (response.status >= 200 && response.status < 300) return;
+ throw new Error(response.statusText);
+}
diff --git a/storefront/modules/payment/models/EPaymentStatus.ts b/storefront/modules/payment/models/EPaymentStatus.ts
new file mode 100644
index 0000000000..dd6ac2602e
--- /dev/null
+++ b/storefront/modules/payment/models/EPaymentStatus.ts
@@ -0,0 +1,7 @@
+export enum EPaymentStatus {
+ NEW = 'NEW',
+ PROCESSING = 'PROCESSING',
+ PENDING = 'PENDING',
+ COMPLETED = 'COMPLETED',
+ CANCELLED = 'CANCELLED',
+}
diff --git a/storefront/modules/payment/models/Payment.ts b/storefront/modules/payment/models/Payment.ts
new file mode 100644
index 0000000000..41570e93ce
--- /dev/null
+++ b/storefront/modules/payment/models/Payment.ts
@@ -0,0 +1,16 @@
+import { EPaymentMethod } from '../../common/models/EPaymentMethod';
+import { EPaymentStatus } from './EPaymentStatus';
+
+export type Payment = {
+ id?: number;
+ orderId?: number;
+ checkoutId: string;
+ amount: number;
+ paymentFee?: number;
+ paymentMethod?: EPaymentMethod;
+ paymentStatus?: EPaymentStatus;
+ gatewayTransactionId?: string;
+ failureMessage?: string;
+ paymentProviderCheckoutId?: string;
+ attributes?: string;
+};
diff --git a/storefront/modules/payment/services/PaymentService.ts b/storefront/modules/payment/services/PaymentService.ts
new file mode 100644
index 0000000000..ed25f1c362
--- /dev/null
+++ b/storefront/modules/payment/services/PaymentService.ts
@@ -0,0 +1,9 @@
+import apiClientService from '@/common/services/ApiClientService';
+
+const baseUrl = '/api/payment';
+
+export async function getPaymentById(id: string) {
+ const response = await apiClientService.get(`${baseUrl}/storefront/payments/${id}`);
+ if (response.status >= 200 && response.status < 300) return response.json();
+ throw new Error(response.statusText);
+}
diff --git a/storefront/pages/cart/index.tsx b/storefront/pages/cart/index.tsx
index 53479d2612..f8fdbd61c3 100644
--- a/storefront/pages/cart/index.tsx
+++ b/storefront/pages/cart/index.tsx
@@ -205,7 +205,7 @@ const Cart = () => {
couponCode: couponCode,
totalAmount: totalPrice,
totalDiscountAmount: discountMoney,
- checkoutItemPostVms: checkoutItems,
+ checkoutItemVms: checkoutItems,
};
createCheckout(checkout)
.then((res) => {
diff --git a/storefront/pages/checkout/[id].tsx b/storefront/pages/checkout/[id].tsx
index 27980ec441..710e88a297 100644
--- a/storefront/pages/checkout/[id].tsx
+++ b/storefront/pages/checkout/[id].tsx
@@ -4,12 +4,16 @@ import { SubmitHandler, useForm } from 'react-hook-form';
import { Order } from '@/modules/order/models/Order';
import CheckOutDetail from 'modules/order/components/CheckOutDetail';
import { OrderItem } from '@/modules/order/models/OrderItem';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/router';
import { Input } from 'common/items/Input';
import { Address } from '@/modules/address/models/AddressModel';
import AddressForm from '@/modules/address/components/AddressForm';
-import { createOrder, getCheckoutById } from '@/modules/order/services/OrderService';
+import {
+ createOrder,
+ getCheckoutById,
+ processPayment,
+} from '@/modules/order/services/OrderService';
import * as yup from 'yup';
import {
createUserAddress,
@@ -17,12 +21,17 @@ import {
} from '@/modules/customer/services/CustomerService';
import ModalAddressList from '@/modules/order/components/ModalAddressList';
import CheckOutAddress from '@/modules/order/components/CheckOutAddress';
-import { Checkout } from '@/modules/order/models/Checkout';
+import type { Checkout } from '@/modules/order/models/Checkout';
import { toastError } from '@/modules/catalog/services/ToastService';
import { CheckoutItem } from '@/modules/order/models/CheckoutItem';
import { initPaymentPaypal } from '@/modules/paymentPaypal/services/PaymentPaypalService';
import { InitPaymentPaypalRequest } from '@/modules/paymentPaypal/models/InitPaymentPaypalRequest';
import SpinnerComponent from '@/common/components/SpinnerComponent';
+import { getPaymentById } from '@/modules/payment/services/PaymentService';
+import { Payment } from '@/modules/payment/models/Payment';
+import { ECheckoutState } from '@/modules/order/models/ECheckoutState';
+import { ECheckoutProgress } from '@/modules/order/models/ECheckoutProgress';
+import { EPaymentStatus } from '@/modules/payment/models/EPaymentStatus';
const phoneRegExp =
/^((\+[1-9]{1,4}[ -]*)|(\([0-9]{2,3}\)[ -]*)|[0-9]{2,4}[ -]*)?[0-9]{3,4}?[ -]*[0-9]{3,4}?$/;
@@ -40,6 +49,11 @@ const Checkout = () => {
const router = useRouter();
const { id } = router.query;
const [checkout, setCheckout] = useState();
+
+ const CREATE_PAYMENT_TIMEOUT = 10;
+ const elapsedTimeRef = useRef(0);
+ let intervalId: NodeJS.Timeout | undefined;
+
const {
handleSubmit,
register,
@@ -142,7 +156,12 @@ const Checkout = () => {
});
};
- const onSubmitForm: SubmitHandler = async (data) => {
+ const onSubmitForm: SubmitHandler = async (data, event) => {
+ const nativeEvent = event?.nativeEvent as SubmitEvent;
+
+ const clickedButton = nativeEvent?.submitter as HTMLButtonElement | undefined;
+ const clickedButtonId = clickedButton?.id;
+
let isValidate = true;
if (addShippingAddress) {
@@ -202,18 +221,114 @@ const Checkout = () => {
order.orderItemPostVms = orderItems;
setIsShowSpinner(true);
setDisableProcessPayment(true);
- createOrder(order)
- .then(() => {
- handleCheckOutProcess(order);
- })
- .catch(() => {
- setIsShowSpinner(false);
- setDisableProcessPayment(false);
- toast.error('Place order failed');
- });
+
+ 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');
+ });
+ }
+ }
+ };
+
+ const fetchOrderCheckout = async (id: string): Promise => {
+ try {
+ const res = await getCheckoutById(id);
+ return res;
+ } catch (err) {
+ console.log('Fetch Order Checkout Failed: ', err);
+ return null;
+ }
+ };
+
+ const fetchOrderPayment = async (id: string): Promise => {
+ try {
+ const res = await getPaymentById(id);
+ return res;
+ } catch (err) {
+ console.log('Fetch Order Payment Failed: ', err);
+ return null;
+ }
+ };
+
+ const handlePaymentProcessForPaypal = () => {
+ intervalId = setInterval(async () => {
+ try {
+ elapsedTimeRef.current += 2;
+
+ const orderCheckout = await fetchOrderCheckout(id as string);
+
+ if (isCreatedPayment(orderCheckout)) {
+ const attributes = orderCheckout?.attributes
+ ? JSON.parse(orderCheckout.attributes)
+ : null;
+ const orderPayment = await fetchOrderPayment(attributes?.payment_id);
+
+ if (isPaymentProcessingAndExistedOrderId(orderPayment)) {
+ redirectToPayment(orderPayment);
+ clearInterval(intervalId);
+ }
+ } else if (elapsedTimeRef.current >= CREATE_PAYMENT_TIMEOUT) {
+ handleErrorCreatingOrder();
+ }
+ } catch (err) {
+ console.error('Handler check status Checkout and Payment Failed: ', err);
+ handleErrorCreatingOrder();
+ }
+ }, 2000);
+ };
+
+ const isCreatedPayment = (orderCheckout: Checkout | null): boolean => {
+ return (
+ orderCheckout?.checkoutState === ECheckoutState.PAYMENT_PROCESSING &&
+ orderCheckout?.progress === ECheckoutProgress.PAYMENT_CREATED
+ );
+ };
+
+ const isPaymentProcessingAndExistedOrderId = (orderPayment: Payment | null): boolean => {
+ return (
+ orderPayment?.paymentStatus === EPaymentStatus.PROCESSING &&
+ orderPayment.paymentProviderCheckoutId !== null
+ );
+ };
+
+ const redirectToPayment = (orderPayment: Payment | null) => {
+ const attributes = orderPayment?.attributes ? JSON.parse(orderPayment.attributes) : null;
+ window.location.replace(attributes.redirect_url);
+ };
+
+ const handlePaymentProcess = (order: Order) => {
+ const paymentMethod = order?.paymentMethod ?? '';
+ switch (paymentMethod.toUpperCase()) {
+ case 'COD':
+ processCodPayment(order);
+ break;
+ case 'PAYPAL':
+ handlePaymentProcessForPaypal();
+ break;
+ default:
+ setIsShowSpinner(false);
+ setDisableProcessPayment(false);
+ toast.error('Place order failed');
}
};
+ const handleErrorCreatingOrder = () => {
+ setIsShowSpinner(false);
+ clearInterval(intervalId);
+ elapsedTimeRef.current = 0;
+ setDisableProcessPayment(false);
+ toast.error('Payment processing failed, please try again later.');
+ };
+
const handleCheckOutProcess = async (order: Order) => {
const paymentMethod = order.paymentMethod ?? '';
switch (paymentMethod.toUpperCase()) {
@@ -254,7 +369,12 @@ const Checkout = () => {