Payment Integration
Relevant source files
The following files were used as context for generating this wiki page:- features/payments/hooks/use-payment-polling.ts
- features/payments/services/payments.service.ts
- features/payments/components/payment-waiting-dialog.tsx
- e2e/checkout-sepay.spec.ts
- types/payment.types.ts
- features/checkout/services/checkout.service.ts
- features/checkout/hooks/use-checkout.ts
- app/(shop)/cart/success/page.tsx/cart/success/page.tsx)
- features/payments/services/payments.service.spec.ts
The Payment Integration module is a core part of the e-commerce checkout flow, responsible for handling all payment-related operations. It is designed with a feature-based architecture, encapsulating all payment logic within the features/payments directory. This module supports two primary payment methods: Cash on Delivery (COD) and SePay, an asynchronous QR code-based payment system.
The system is built to handle both the immediate order creation for COD and the more complex, asynchronous flow of SePay, which involves generating a QR code, polling for payment confirmation, and managing user-facing dialogs for waiting and success states. The integration is managed through a combination of React components, custom hooks for state management, and a dedicated service layer that communicates with the backend API.
Architecture Overview
The payment feature follows the project's standard architecture, separating concerns into components, hooks, and services. The UI components are orchestrated by the useCheckout hook, which in turn leverages the CheckoutService and PaymentsService to interact with the backend.
The following diagram illustrates the high-level architecture of the payment integration during the checkout process.
Sources: features/checkout/components/checkout-content.tsx, features/checkout/hooks/use-checkout.ts, features/payments/components/payment-waiting-dialog.tsx, features/payments/hooks/use-payment-polling.ts, features/payments/services/payments.service.ts, features/checkout/services/checkout.service.ts
Data Models and Types
The payment system relies on several key data structures defined in types/payment.types.ts.
Enums
PaymentMethod: Defines the supported payment methods.COD: Cash on DeliverySEPAY: SePay QR payment
PaymentStatus: Represents the state of a payment.UNPAIDPAID
Sources: types/payment.types.ts:9-18
Core Interfaces
The main data structures for handling payments are Payment, PaymentProcessRequest, and PaymentProcessResponse.
The diagram below shows the structure of the Payment entity.
Sources: types/payment.types.ts:25-34
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the payment (CUID). |
orderId | string | The ID of the associated order. |
method | PaymentMethod | The payment method used (COD or SEPAY). |
amountInt | number | The payment amount in the smallest currency unit (e.g., VND). |
status | PaymentStatus | The current status of the payment (UNPAID or PAID). |
payload | Record<string, unknown> | null | Optional field for additional data from the payment provider. |
createdAt | string | ISO date string of when the payment was created. |
updatedAt | string | ISO date string of the last update. |
Sources: types/payment.types.ts:25-50
API Data Transfer Objects
PaymentProcessRequest: The payload sent to the backend to initiate a payment.orderId: stringmethod: PaymentMethodamountInt: number
PaymentProcessResponse: The response from the backend after a payment is initiated.paymentId: stringstatus: PaymentStatuspaymentUrl?: string(For SePay)qrCode?: string(For SePay bank transfer)message?: string
Sources: types/payment.types.ts:52-69
Payment Service
The PaymentsService class encapsulates all communication with the payment-related backend endpoints. It uses the api-client to make validated HTTP requests.
Sources: features/payments/services/payments.service.ts
Methods and API Endpoints
| Method | Endpoint | HTTP Verb | Description |
|---|---|---|---|
process | /payments/process | POST | Initiates a payment for a given order, method, and amount. Returns a PaymentProcessResponse which may include a QR code for SePay. |
getByOrder | /payments/order/:orderId | GET | Retrieves the payment details for a specific order ID. Used for polling payment status. |
getById | /payments/:paymentId | GET | Retrieves payment details by its unique ID. |
confirmCod | /payments/confirm-cod/:orderId | POST | Confirms a COD payment. This is likely an admin/shipper-only action. |
Sources: features/payments/services/payments.service.ts:46-103, features/payments/services/payments.service.spec.ts
Checkout Flow Integration
The payment logic is triggered within the useCheckout hook, which centralizes the entire checkout process.
Sources: features/checkout/hooks/use-checkout.ts
handleCheckout Logic
The handleCheckout function in useCheckout orchestrates the payment flow based on the user's selected payment method.
- Validation: It first calls
checkoutService.validateCheckoutRequestto ensure a shipping address is selected and the cart is valid. - Branching: It then proceeds based on
state.selectedPayment.
The flowchart below visualizes this logic.
- COD Flow: The
processCodCheckoutmethod is called, which only creates an order viaordersApi.create. The user is then immediately redirected to the success page. - SePay Flow: The
processSepayCheckoutmethod is called. This first creates an order and then callspaymentsApi.processto get apaymentIdandqrCode. This data is then used to open thePaymentWaitingDialog.
Sources: features/checkout/services/checkout.service.ts:79-122
SePay QR Payment Flow
The SePay flow is asynchronous and requires the frontend to wait for payment confirmation after displaying a QR code to the user.
UI: PaymentWaitingDialog
When a SePay checkout is initiated, the PaymentWaitingDialog is displayed. This component is responsible for:
- Displaying the SePay QR code image (
qrCode). - Showing the total amount (
amountInt) and order ID (orderId). - Providing manual bank transfer details (Bank, Account Number, etc.).
- Allowing the user to copy the order reference and account information.
- Preventing the dialog from being closed while payment polling is active.
Sources: features/payments/components/payment-waiting-dialog.tsx, features/checkout/components/checkout-content.tsx:120-136
Payment Status Polling (usePaymentPolling)
The usePaymentPolling hook is the engine behind the asynchronous payment confirmation. It's activated by the PaymentWaitingDialog and periodically checks the payment status until it's marked as PAID, times out, or encounters an error.
Configuration
| Option | Type | Default Value | Description |
|---|---|---|---|
orderId | string | - | The ID of the order to poll for. |
onSuccess | (payment: Payment) => void | undefined | Callback fired when payment status is PAID. |
onTimeout | () => void | undefined | Callback fired when polling exceeds MAX_ATTEMPTS. |
onError | (error: string) => void | undefined | Callback fired if the API call fails after all retries. |
enabled | boolean | true | Toggles the polling mechanism. |
POLLING_INTERVAL | number | 5000 (5s) | The interval between each poll. |
MAX_ATTEMPTS | number | 180 (15 mins) | The maximum number of polling attempts before a timeout. |
MAX_RETRIES | number | 3 | The number of retries for a single failed API call within a polling cycle. |
Sources: features/payments/hooks/use-payment-polling.ts:5-24
Polling Sequence
The following diagram shows the sequence of events during payment polling.
Success and Failure Handling
The useCheckout hook defines handlers for the outcomes of the polling process:
handlePaymentSuccess: Called when theonSuccesscallback fromusePaymentPollingis fired. It closes the waiting dialog, shows a success toast, clears the cart, and redirects the user to the success page (/cart/success).handlePaymentTimeout: Closes the dialog and shows a "payment timed out" error toast.handlePaymentError: Closes the dialog and shows a generic error toast with the message from the polling hook.
Sources: features/checkout/hooks/use-checkout.ts:106-130
End-to-End Testing
The SePay payment flow is covered by end-to-end tests using Playwright, which validate the entire user journey.
Key test cases from e2e/checkout-sepay.spec.ts:
- Dialog Visibility: Verifies that the
payment-waiting-dialogappears after submitting a SePay order. - Success Dialog: Mocks the payment status API to return
PAIDand asserts that thepayment-success-dialogis shown. - Auto-Redirect: Confirms that the user is automatically redirected to the order details page after the success countdown.
- Manual Redirect: Ensures the "View Order" button on the success dialog works correctly.
- Cart Clearance: Verifies that the cart is empty after a successful payment.
Sources: e2e/checkout-sepay.spec.ts
Summary
The Payment Integration module provides a robust and well-structured system for handling payments. It effectively supports both a simple, synchronous COD flow and a complex, asynchronous SePay QR flow. The use of dedicated services, stateful hooks, and clear component responsibilities makes the system maintainable and testable. The usePaymentPolling hook is a critical piece of the architecture, providing a reliable mechanism for confirming asynchronous payments without relying on WebSockets, making the user experience seamless.