Payment & Subscription
Relevant source files
The following files were used as context for generating this wiki page:- src/app/api/payment/polar/webhook/route.ts
- src/app/api/payment/sepay/webhook/route.ts
- src/app/api/subscription/current/route.ts
- src/server/services/subscription/index.ts
- src/server/services/billing/sepay.ts
- src/app/api/payment/sepay/create/route.ts
- src/libs/sepay/index.ts
- src/app/[variants]/(main)/subscription/checkout/Client.tsx/subscription/checkout/Client.tsx)
- src/config/customizations.ts
- WEBHOOK_FIX_DEPLOYED.md
- src/app/[variants]/(main)/settings/usage/features/BillingInfo.tsx/settings/usage/features/BillingInfo.tsx)
- SUBSCRIPTION_AUTH_FIX.md
- PAYMENT_ISSUE_ANALYSIS_2025-11-04.md
The Payment and Subscription system manages user access to premium features by handling payments through various gateways and maintaining subscription statuses. It is designed to support both local (Vietnamese) and international payment methods, providing a seamless checkout experience and automated subscription activation.
The system is built around two primary payment providers: Sepay for Vietnamese bank transfers (VietQR) and Polar.sh for international credit card payments. Upon successful payment, webhooks trigger the activation of user subscriptions, granting them access to paid features like advanced AI models.
Architecture Overview
The system follows a standard web payment architecture, involving the client-side UI, backend API endpoints, external payment gateways, and the application's database.
This diagram shows the high-level flow of interactions between the components.
Payment Gateways
The application integrates two distinct payment gateways to cater to different user segments. The choice of gateway is determined by the payment method selected by the user on the checkout page.
Sources: src/app/[variants]/(main)/subscription/checkout/Client.tsx:219-226, src/config/customizations.ts:100-131
Sepay (Bank Transfer)
Sepay is the primary gateway for Vietnamese users, facilitating payments via bank transfer using QR codes (VietQR).
Sepay Payment Flow
The following diagram illustrates the sequence of events for a Sepay bank transfer payment.
SepayPaymentGateway Class
This class encapsulates all interactions with the Sepay service. It is responsible for generating payment requests, creating QR codes, and querying transaction statuses. It includes a mock mode for development when API keys are not present.
createPayment(request): Generates a payment waiting URL that includes a QR code for the user to scan. It constructs the QR code URL using the configured bank account details.queryPaymentStatus(orderId): Fetches recent transactions from the Sepay API and searches for a transaction matching theorderIdand expected amount.generateOrderId(prefix): Creates a unique order ID using a prefix, timestamp, and a random string.verifyWebhookSignature(data): Validates incoming webhooks, though it is noted in tests that processing may continue even if verification fails for debugging purposes.
Sources: src/libs/sepay/index.ts:121-496, src/app/api/payment/sepay/webhook/route.test.ts:70-86
Polar.sh (Credit Card)
Polar.sh is used for handling international credit card payments. The integration redirects users to a hosted checkout page provided by Polar.
Polar.sh Payment Flow
API Endpoints
The system exposes several API endpoints to manage the payment and subscription lifecycle.
Payment Creation
| Endpoint | Method | Description |
|---|---|---|
/api/payment/sepay/create | POST | Initiates a bank transfer payment with Sepay. Creates a pending payment record and returns a paymentUrl for the user to complete the transaction. |
/api/payment/polar/create | POST | Creates a credit card checkout session with Polar.sh. Returns a checkoutUrl for redirection. |
Sources: src/app/api/payment/sepay/create/route.ts, src/app/[variants]/(main)/subscription/checkout/Client.tsx:255-293
POST /api/payment/sepay/create Request Body:
{
"planId": "premium",
"billingCycle": "monthly",
"amount": 129000,
"currency": "VND",
"customerInfo": {
"email": "user@email.com",
"name": "User Name"
}
}
Sources: src/app/api/payment/sepay/create/route.ts:11-25
Webhook Handlers
Webhook endpoints are crucial for automating subscription activation upon successful payment.
| Endpoint | Method | Provider | Description |
|---|---|---|---|
/api/payment/sepay/webhook | POST | Sepay | Receives notifications from Sepay. It handles different payload formats (VietQR vs. standard) and activates the user's subscription. |
/api/payment/polar/webhook | POST | Polar.sh | Receives checkout.completed events from Polar.sh, verifies the payload, and activates the subscription. |
Sepay Webhook Logic
The Sepay webhook handler is designed to parse two different payload formats: one for bank transfers (VietQR) and another for standard credit card payments. For bank transfers, the orderId is not a direct field but is embedded within the content string. A helper function is used to extract it.
// src/app/api/payment/sepay/webhook/route.ts
function extractOrderIdFromContent(content: string): string | null {
const pattern = /(\d{13})([A-Z0-9]{6})/;
const match = content.match(pattern);
if (match) {
const timestamp = match[1];
const randomCode = match[2];
return `PHO_SUB_${timestamp}_${randomCode}`;
}
return null;
}
Sources: WEBHOOK_FIX_DEPLOYED.md, src/app/api/payment/sepay/webhook/route.ts
Subscription Status
| Endpoint | Method | Description |
|---|---|---|
/api/subscription/current | GET | Fetches the currently active subscription for the authenticated user. Used by the BillingInfo component to display plan details. |
/api/payment/sepay/status | GET | Queries the status of a Sepay payment by orderId. Used by the payment success page to verify payment completion. |
Sources: src/app/api/subscription/current/route.ts, src/app/[variants]/(main)/payment/success/page.tsx:80-90
Subscription Management
Subscriptions are managed by the SubscriptionService, which handles creation, retrieval, and access control based on the user's plan.
SubscriptionService
This service provides core business logic for subscriptions.
createFreeSubscription(userId): Creates a default free plan for new users.getActiveSubscription(userId): Retrieves the user's active subscription from the database.canAccessAIModels(userId): A crucial method that checks if the user's current plan allows access to paid AI features. Returnsfalsefor the 'free' plan.hasPaidSubscription(userId): Checks if a user has any non-free, active subscription.
Sources: src/server/services/subscription/index.ts, SUBSCRIPTION_AUTH_FIX.md
Data Models
The system uses sepay_payments and subscriptions tables to persist state.
This ER diagram shows the relationship between users, subscriptions, and payments.
Subscription Activation
After a webhook confirms a successful payment, the activateUserSubscription function is called. This function creates a new record in the subscriptions table or updates an existing one, setting the status to active and defining the subscription period.
Sources: src/server/services/billing/sepay.ts:60-85, src/app/api/payment/sepay/webhook/route.ts, src/app/api/payment/polar/webhook/route.ts
Configuration
All payment-related configurations are centralized in src/config/customizations.ts and managed via environment variables.
| Variable | Description |
|---|---|
NEXT_PUBLIC_PAYMENT_PROVIDER | Defines the default payment provider (e.g., 'sepay'). |
SEPAY_API_URL | The base URL for the Sepay API. |
SEPAY_MERCHANT_ID | The merchant ID for Sepay integration. |
SEPAY_SECRET_KEY | The secret key for signing Sepay API requests. |
SEPAY_NOTIFY_URL | The URL for the Sepay webhook handler. |
SEPAY_RETURN_URL | The URL to redirect users to after a successful payment. |
SEPAY_CANCEL_URL | The URL to redirect users to after a cancelled payment. |
POLAR_ACCESS_TOKEN | The access token for the Polar.sh API. |
POLAR_WEBHOOK_SECRET | The secret for verifying Polar.sh webhooks. |
Sources: src/config/customizations.ts:93-131
Conclusion
The Payment & Subscription system is a well-structured module that effectively integrates multiple payment gateways to manage user subscriptions. It uses a combination of API endpoints for payment initiation, webhooks for asynchronous confirmation, and a dedicated service layer for managing subscription business logic. The centralization of configuration and clear separation of concerns between UI, API, and service layers make the system maintainable and extensible.