# GoodGo Platform AI - Audit Hệ thống Giá, Subscription & Thanh toán
**Ngày:** 12 tháng 4, 2026
**Phạm vi:** Khảo sát toàn diện các trang pricing, plan subscription và luồng checkout thanh toán
**Trạng thái:** Trang pricing frontend ACTIVE, các module subscription/payment backend ACTIVE, luồng checkout CHƯA ĐƯỢC TRIỂN KHAI
---
## Tóm tắt Điều hành
GoodGo Platform có **hạ tầng backend hoàn chỉnh** cho subscription và thanh toán với kiến trúc NestJS CQRS, nhưng **luồng pricing-đến-checkout ở frontend vẫn chưa hoàn thiện**. Trang pricing đã tồn tại và load plan từ API, nhưng **không có component checkout/payment flow** kết nối trang pricing với API thanh toán.
### Các phát hiện chính:
- ✅ **Trang Pricing:** Hoạt động đầy đủ tại `/pricing` với 4 tier (FREE, AGENT_PRO, INVESTOR, ENTERPRISE)
- ✅ **Backend Subscription:** Hoàn chỉnh với command CQRS, entity, repository
- ✅ **Tích hợp Payment Gateway:** Triển khai VNPay, MoMo, ZaloPay đã sẵn sàng
- ✅ **API Endpoint Thanh toán:** Các thao tác CRUD đầy đủ đã có
- ❌ **Thiếu:** Trang/modal checkout kết nối pricing → tạo payment → redirect tới payment gateway
- ❌ **Thiếu:** Xử lý callback thành công của thanh toán ở frontend
- ❌ **Thiếu:** Thiết lập subscription sau khi thanh toán thành công
---
## 1. Trang Pricing Frontend
### Vị trí
```
/Users/velikho/Desktop/WORKING/goodgo-platform-ai/apps/web/app/[locale]/(public)/pricing/page.tsx
```
### Triển khai hiện tại
**Route:** `/(public)/pricing`
**Trạng thái:** ✅ Active và hoạt động
#### Tính năng Page Component:
```tsx
// Các Export và Chức năng chính
- PricingPage (default export)
- Dùng useTranslations('pricing') cho i18n
- Quản lý state billingCycle (monthly/yearly)
- Render 4 thẻ plan
- Bảng so sánh tính năng
- Các section CTA
```
#### Luồng dữ liệu Plan:
```
usePlans() hook (từ use-subscription.ts)
↓
subscriptionApi.getPlans()
↓
GET /subscriptions/plans (backend)
↓
Trả về: PlanDto[] với thông tin giá, tính năng, giới hạn
```
#### Plan Fallback (Hardcoded):
Trang có sẵn các plan fallback khi API không khả dụng:
- **FREE:** 0 VND, 3 tin đăng, 5 tìm kiếm đã lưu
- **AGENT_PRO:** 499,000 VND/tháng, 50 tin đăng, 30 tìm kiếm (được đánh dấu "popular")
- **INVESTOR:** 999,000 VND/tháng, 20 tin đăng, 100 tìm kiếm
- **ENTERPRISE:** 4,990,000 VND/tháng, tin đăng/tìm kiếm không giới hạn
#### Các Component chính được sử dụng:
```tsx
- Badge, Button, Card
- Icon Check, Crown, Rocket, Shield, X, Zap (lucide-react)
- useTranslations cho 15+ translation key
- formatVND để hiển thị tiền tệ
- Tiện ích cn() cho class có điều kiện
```
#### Toggle Chu kỳ Thanh toán:
```tsx
// Quản lý state cho giá monthly/yearly
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
// Hiển thị giá phù hợp dựa trên chu kỳ
const price = billingCycle === 'monthly'
? plan.priceMonthlyVND
: plan.priceYearlyVND;
```
#### CTA hiện tại:
Tất cả các plan đều có link "Register":
```tsx
```
**VẤN ĐỀ:** Các CTA liên kết đến `/register` thay vì bắt đầu luồng checkout
---
## 2. Tích hợp API Frontend
### Files
#### `/apps/web/lib/subscription-api.ts`
**Định nghĩa Type:**
```typescript
export interface PlanDto {
id: string;
tier: string;
name: string;
priceMonthlyVND: string;
priceYearlyVND: string;
maxListings: number;
maxSavedSearches: number;
features: Record;
isActive: boolean;
}
export interface CreateSubscriptionResult {
subscriptionId: string;
planTier: string;
status: string;
currentPeriodStart: string;
currentPeriodEnd: string;
}
export interface BillingHistoryDto {
subscription: SubscriptionInfo | null;
payments: Array<{ id, provider, type, amountVND, status, createdAt }>;
total: number;
}
export interface QuotaCheckResult {
metric: string;
used: number;
limit: number;
remaining: number;
}
```
**Phương thức API:**
```typescript
subscriptionApi = {
getPlans(): Promise // GET /subscriptions/plans
getPlanByTier(tier): Promise // GET /subscriptions/plans/:tier
getBillingHistory(limit, offset): Promise
checkQuota(metric): Promise
createSubscription(planTier, billingCycle): Promise
upgradeSubscription(newPlanTier): Promise<{message}>
cancelSubscription(reason): Promise<{message}>
}
```
#### `/apps/web/lib/payment-api.ts`
**Định nghĩa Type:**
```typescript
export interface CreatePaymentPayload {
provider: 'VNPAY' | 'MOMO' | 'ZALOPAY' | 'BANK_TRANSFER';
type: 'SUBSCRIPTION' | 'LISTING_FEE' | 'DEPOSIT' | 'FEATURED_LISTING';
amountVND: number;
description: string;
returnUrl: string;
idempotencyKey?: string;
}
export interface CreatePaymentResult {
paymentId: string;
paymentUrl: string; // URL payment gateway trực tiếp
providerTxId: string;
}
export interface PaymentStatusDto {
id: string;
provider: string;
type: string;
amountVND: string;
status: string; // PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED
providerTxId: string | null;
createdAt: string;
updatedAt: string;
}
```
**Phương thức API:**
```typescript
paymentApi = {
createPayment(data): Promise
getPaymentStatus(id): Promise
getTransactions(params): Promise
}
```
### Frontend Hooks
#### `/apps/web/lib/hooks/use-subscription.ts`
```typescript
export const subscriptionKeys = {
all: ['subscription'] as const,
plans: () => ['subscription', 'plans'] as const,
billing: () => ['subscription', 'billing'] as const,
quota: (metric: string) => ['subscription', 'quota', metric] as const,
};
// Hook React Query
export function usePlans() // GET plans
export function useBillingHistory() // GET lịch sử billing
export function useQuota(metric: string) // GET kiểm tra quota
```
#### `/apps/web/lib/hooks/use-payments.ts`
```typescript
export const paymentKeys = {
all: ['payments'] as const,
transactions: (params) => ['payments', 'transactions', params] as const,
status: (id: string) => ['payments', 'status', id] as const,
};
// Hook React Query
export function useTransactions(params) // GET các giao dịch của user
export function usePaymentStatus(id) // GET trạng thái của một payment
```
---
## 3. Subscription Backend (NestJS CQRS)
### Vị trí Module
```
/apps/api/src/modules/subscriptions/
```
### Kiến trúc
**Module:** `subscriptions.module.ts`
**Pattern:** Hexagonal Architecture (DDD + CQRS)
#### Tầng Domain
```
domain/
├── entities/subscription.entity.ts
├── events/
│ ├── subscription-created.event.ts
│ ├── subscription-upgraded.event.ts
│ ├── subscription-cancelled.event.ts
│ ├── subscription-renewed.event.ts
│ └── subscription-expired.event.ts
└── repositories/subscription.repository.ts (interface)
```
**Class SubscriptionEntity:**
```typescript
export class SubscriptionEntity extends AggregateRoot {
private _userId: string;
private _planId: string;
private _planTier: PlanTier;
private _status: SubscriptionStatus; // ACTIVE, PAST_DUE, CANCELLED, EXPIRED
private _currentPeriodStart: Date;
private _currentPeriodEnd: Date;
private _cancelledAt: Date | null;
// Methods
static createNew(userId, planId, planTier, periodStart, periodEnd)
upgrade(newPlanId, newPlanTier): Result
cancel(): Result
markExpired(): Result
markPastDue(): Result
renewPeriod(newStart, newEnd): void
isActive(): boolean
isExpired(): boolean
}
```
#### Tầng Application
**Commands:**
```
application/commands/
├── create-subscription/
│ ├── create-subscription.command.ts
│ ├── create-subscription.handler.ts ← CreateSubscriptionResult
│ └── create-subscription.dto.ts
├── upgrade-subscription/
│ ├── upgrade-subscription.command.ts
│ ├── upgrade-subscription.handler.ts
│ └── upgrade-subscription.dto.ts
├── cancel-subscription/
│ ├── cancel-subscription.command.ts
│ ├── cancel-subscription.handler.ts
│ └── cancel-subscription.dto.ts
└── meter-usage/
├── meter-usage.command.ts
├── meter-usage.handler.ts
└── meter-usage.dto.ts
```
**Queries:**
```
application/queries/
├── get-plan/
│ ├── get-plan.query.ts
│ └── get-plan.handler.ts ← Trả về PlanDto[]
├── check-quota/
│ ├── check-quota.query.ts
│ └── check-quota.handler.ts
└── get-billing-history/
├── get-billing-history.query.ts
└── get-billing-history.handler.ts
```
#### Tầng Infrastructure
```
infrastructure/
├── repositories/prisma-subscription.repository.ts
└── event-handlers/listing-created-usage.handler.ts
```
#### Tầng Presentation
```
presentation/
├── controllers/subscriptions.controller.ts
├── dto/
│ ├── create-subscription.dto.ts
│ ├── upgrade-subscription.dto.ts
│ ├── cancel-subscription.dto.ts
│ ├── meter-usage.dto.ts
│ └── billing-history.dto.ts
└── guards/quota.guard.ts
```
### Endpoint API Subscription
#### Endpoint Công khai
```
GET /subscriptions/plans
→ Trả về: PlanDto[]
GET /subscriptions/plans/:tier
→ Trả về: PlanDto
```
#### Endpoint Cần Authenticate
```
POST /subscriptions
Body: { planTier: PlanTier, billingCycle: 'monthly' | 'yearly' }
Trả về: CreateSubscriptionResult
PUT /subscriptions/upgrade
Body: { newPlanTier: PlanTier }
Trả về: UpgradeSubscriptionResult
DELETE /subscriptions
Body: { reason?: string }
Trả về: CancelSubscriptionResult
POST /subscriptions/usage
Body: { metric: string, count: number }
Trả về: MeterUsageResult
GET /subscriptions/quota/:metric
Trả về: QuotaCheckResult
GET /subscriptions/billing?limit=20&offset=0
Trả về: BillingHistoryDto
```
### Logic của CreateSubscriptionHandler
```typescript
async execute(command): Promise {
// 1. Kiểm tra user chưa có subscription đang hoạt động
if (existing && (status === 'ACTIVE' || 'PAST_DUE')) {
throw ConflictException('User already has active subscription');
}
// 2. Lấy Plan từ DB
const plan = prisma.plan.findFirst({
where: { tier: command.planTier, isActive: true }
});
// 3. Tính toán period
const now = new Date();
const periodEnd = new Date(now);
if (yearly) periodEnd.setFullYear(+1);
else periodEnd.setMonth(+1);
// 4. Tạo domain entity
const subscription = SubscriptionEntity.createNew(
id, userId, planId, planTier, now, periodEnd
);
// 5. Lưu vào repo
await subscriptionRepo.save(subscription);
// 6. Publish domain event
eventBus.publish(subscription.clearDomainEvents());
return {
subscriptionId,
planTier,
status: 'ACTIVE',
currentPeriodStart: now,
currentPeriodEnd: periodEnd,
};
}
```
---
## 4. Backend Thanh toán (NestJS CQRS)
### Vị trí Module
```
/apps/api/src/modules/payments/
```
### Kiến trúc
#### Tầng Domain
```
domain/
├── entities/payment.entity.ts
├── events/
│ ├── payment-created.event.ts
│ ├── payment-completed.event.ts
│ ├── payment-failed.event.ts
│ └── payment-refunded.event.ts
├── repositories/payment.repository.ts
└── value-objects/money.vo.ts
```
**Class PaymentEntity:**
```typescript
export class PaymentEntity extends AggregateRoot {
private _userId: string;
private _transactionId: string | null;
private _provider: PaymentProvider; // VNPAY, MOMO, ZALOPAY, BANK_TRANSFER
private _type: PaymentType; // SUBSCRIPTION, LISTING_FEE, DEPOSIT, FEATURED_LISTING
private _amount: Money;
private _status: PaymentStatus; // PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED
private _providerTxId: string | null;
private _callbackData: unknown;
private _idempotencyKey: string | null;
// Methods
static createNew(id, userId, provider, type, amount, ...)
markProcessing(providerTxId): void
markCompleted(callbackData): Result
markFailed(callbackData): Result
markRefunded(): Result
emitCompleted(): void
emitFailed(): void
}
```
#### Tầng Application
**Commands:**
```
application/commands/
├── create-payment/
│ ├── create-payment.command.ts
│ ├── create-payment.handler.ts ← CreatePaymentResult
│ └── create-payment.dto.ts
├── handle-callback/
│ ├── handle-callback.command.ts
│ ├── handle-callback.handler.ts
│ └── (không có DTO - endpoint webhook)
└── refund-payment/
├── refund-payment.command.ts
├── refund-payment.handler.ts
└── refund-payment.dto.ts
```
**Queries:**
```
application/queries/
├── get-payment-status/
│ ├── get-payment-status.query.ts
│ └── get-payment-status.handler.ts
└── list-transactions/
├── list-transactions.query.ts
└── list-transactions.handler.ts
```
#### Tầng Infrastructure (Payment Gateway)
```
infrastructure/services/
├── payment-gateway.interface.ts ← Interface IPaymentGateway
├── payment-gateway.factory.ts
├── vnpay.service.ts ← ✅ Đã triển khai
├── momo.service.ts ← ✅ Đã triển khai
└── zalopay.service.ts ← ✅ Đã triển khai
```
**Interface IPaymentGateway:**
```typescript
export interface IPaymentGateway {
readonly provider: PaymentProvider;
createPaymentUrl(params: CreatePaymentUrlParams): Promise;
verifyCallback(data: Record): CallbackVerifyResult;
refund(params: RefundParams): Promise;
}
export interface CreatePaymentUrlParams {
orderId: string;
amountVND: bigint;
description: string;
returnUrl: string;
ipAddress: string;
}
export interface CreatePaymentUrlResult {
paymentUrl: string; // Trực tiếp đến payment gateway
providerTxId: string; // Gateway transaction ID
}
export interface CallbackVerifyResult {
isValid: boolean;
orderId: string; // Payment ID của chúng ta
providerTxId: string; // Gateway transaction ID
isSuccess: boolean; // Thanh toán có thành công?
rawData: Record;
}
```
### Triển khai Payment Gateway
#### VNPay Service
```typescript
private readonly tmnCode: string; // Terminal ID
private readonly hashSecret: string; // HMAC key
async createPaymentUrl(params): Promise<{paymentUrl, providerTxId}> {
// Xây dựng tham số VNPay (v2.1.0)
const vnpParams = {
vnp_Version: '2.1.0',
vnp_Command: 'pay',
vnp_TmnCode: this.tmnCode,
vnp_Locale: 'vn',
vnp_CurrCode: 'VND',
vnp_TxnRef: params.orderId,
vnp_OrderInfo: params.description,
vnp_Amount: (params.amountVND * 100n).toString(), // VNPay dùng cent
vnp_ReturnUrl: params.returnUrl,
vnp_IpAddr: params.ipAddress,
vnp_CreateDate: yyyyMMddHHmmss,
vnp_ExpireDate: yyyyMMddHHmmss + 15min,
};
// Ký bằng HMAC SHA-512
const signed = hmac('sha512', this.hashSecret).digest('hex');
vnpParams['vnp_SecureHash'] = signed;
// Trả về URL redirect
return {
paymentUrl: `https://sandbox.vnpayment.vn/paymentv2/vpcpay.html?${params}`,
providerTxId: params.orderId,
};
}
verifyCallback(data: Record): CallbackVerifyResult {
// VNPay trả về: vnp_SecureHash, vnp_ResponseCode, vnp_TransactionNo, v.v.
// Xác minh chữ ký hash
const secureHash = data['vnp_SecureHash'];
const isValid = crypto.timingSafeEqual(hash1, hash2);
const isSuccess = isValid && data['vnp_ResponseCode'] === '00';
return {
isValid,
orderId: data['vnp_TxnRef'],
providerTxId: data['vnp_TransactionNo'],
isSuccess,
rawData: data,
};
}
```
#### MoMo Service
```typescript
private readonly partnerCode: string;
private readonly accessKey: string;
private readonly secretKey: string;
private readonly endpoint: string; // https://test-payment.momo.vn/v2/gateway/api
async createPaymentUrl(params): Promise<{paymentUrl, providerTxId}> {
const requestId = crypto.randomUUID();
// Xây dựng chữ ký: accessKey=...&amount=...&...&signature=
const rawSignature = [
`accessKey=${this.accessKey}`,
`amount=${params.amountVND}`,
`extraData=`,
`ipnUrl=${params.returnUrl}`,
`orderId=${params.orderId}`,
`orderInfo=${params.description}`,
`partnerCode=${this.partnerCode}`,
`redirectUrl=${params.returnUrl}`,
`requestId=${requestId}`,
`requestType=payWithMethod`,
].join('&');
const signature = hmac('sha256', this.secretKey).update(rawSignature).digest('hex');
const body = {
partnerCode: this.partnerCode,
requestId,
amount: params.amountVND,
orderId: params.orderId,
orderInfo: params.description,
redirectUrl: params.returnUrl,
ipnUrl: params.returnUrl,
requestType: 'payWithMethod',
signature,
};
// POST tới MoMo API
const response = await fetch(`${this.endpoint}/create`, {
method: 'POST',
body: JSON.stringify(body),
});
const result = await response.json(); // { resultCode: 0, payUrl: "..." }
return {
paymentUrl: result.payUrl,
providerTxId: params.orderId,
};
}
```
#### ZaloPay Service
Cấu trúc tương tự với endpoint và thuật toán chữ ký (HMAC SHA-256) đặc thù của ZaloPay
### Endpoint API Thanh toán
#### Endpoint Công khai
```
POST /payments/callback/:provider
Query/Body: Dữ liệu callback của provider (VNPay qua query, MoMo/ZaloPay qua body)
Trả về: HandleCallbackResult
Webhook cho callback của payment provider
```
#### Endpoint Cần Authenticate
```
POST /payments
Body: CreatePaymentDto {
provider: PaymentProvider,
type: PaymentType,
amountVND: number,
description: string,
returnUrl: string,
transactionId?: string,
idempotencyKey?: string
}
Trả về: CreatePaymentResult {
paymentId: string,
paymentUrl: string, ← Redirect user đến đây
providerTxId: string
}
GET /payments
Query: { status?, limit?, offset? }
Trả về: TransactionListDto
GET /payments/:id
Trả về: PaymentStatusDto
POST /payments/:id/refund (Chỉ Admin)
Body: { reason: string }
Trả về: RefundPaymentResult
```
### Logic của CreatePaymentHandler
```typescript
async execute(command): Promise {
// 1. Kiểm tra idempotency (tránh thanh toán trùng)
if (command.idempotencyKey) {
const existing = await paymentRepo.findByIdempotencyKey(key);
if (existing && (status === 'PENDING' || 'PROCESSING')) {
throw ConflictException('Payment already exists');
}
}
// 2. Xác thực số tiền (từ 1 VND đến 100 tỷ VND)
const money = Money.create(command.amountVND);
if (money.isErr) throw ValidationException();
// 3. Tạo domain entity
const payment = PaymentEntity.createNew(
id, userId, provider, type, money, ...
);
// 4. Lấy payment gateway và tạo URL
const gateway = gatewayFactory.getGateway(command.provider);
const { paymentUrl, providerTxId } = await gateway.createPaymentUrl({
orderId: paymentId,
amountVND: command.amountVND,
description: command.description,
returnUrl: command.returnUrl,
ipAddress: command.ipAddress,
});
// 5. Đánh dấu là PROCESSING và lưu
payment.markProcessing(providerTxId);
await paymentRepo.save(payment);
// 6. Publish domain event
eventBus.publish(payment.clearDomainEvents());
return {
paymentId,
paymentUrl, ← Client redirect tới URL này
providerTxId,
};
}
```
---
## 5. Các Model Dữ liệu Prisma
### Vị trí
```
/prisma/schema.prisma (dòng 451-514)
```
### Model Plan
```prisma
model Plan {
id String @id @default(cuid())
tier PlanTier @unique
name String
priceMonthlyVND BigInt
priceYearlyVND BigInt
maxListings Int?
maxSavedSearches Int?
maxAnalyticsQueries Int?
maxMediaUploads Int?
features Json // Object JSON chứa các feature boolean/number
isActive Boolean @default(true)
subscriptions Subscription[]
}
enum PlanTier {
FREE
AGENT_PRO
INVESTOR
ENTERPRISE
}
```
### Model Subscription
```prisma
model Subscription {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
planId String
plan Plan @relation(fields: [planId], references: [id], onDelete: Restrict)
status SubscriptionStatus @default(ACTIVE)
currentPeriodStart DateTime
currentPeriodEnd DateTime
cancelledAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
usageRecords UsageRecord[]
@@index([planId])
@@index([status])
}
enum SubscriptionStatus {
ACTIVE
PAST_DUE
CANCELLED
EXPIRED
}
```
### Model Payment
```prisma
model Payment {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
transactionId String?
transaction Transaction? @relation(fields: [transactionId], references: [id], onDelete: SetNull)
provider PaymentProvider
type PaymentType
amountVND BigInt
status PaymentStatus @default(PENDING)
providerTxId String?
callbackData Json?
idempotencyKey String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, provider, idempotencyKey], name: "Payment_idempotency_unique")
@@index([userId])
@@index([transactionId])
@@index([status])
@@index([providerTxId])
@@index([createdAt])
@@index([userId, status, createdAt(sort: Desc)])
@@index([userId, type, createdAt(sort: Desc)])
}
enum PaymentProvider {
VNPAY
MOMO
ZALOPAY
BANK_TRANSFER
}
enum PaymentStatus {
PENDING
PROCESSING
COMPLETED
FAILED
REFUNDED
}
enum PaymentType {
SUBSCRIPTION
LISTING_FEE
DEPOSIT
FEATURED_LISTING
}
```
### Model UsageRecord
```prisma
model UsageRecord {
id String @id @default(cuid())
subscriptionId String
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
metric String // ví dụ: "listings_created", "searches_performed"
count Int
periodStart DateTime
periodEnd DateTime
@@index([subscriptionId, metric])
}
```
---
## 6. Dashboard Thanh toán
### Vị trí
```
/apps/web/app/[locale]/(dashboard)/dashboard/payments/page.tsx
```
### Trạng thái hiện tại
✅ **Đã triển khai:** Xem lịch sử giao dịch với filter và phân trang
#### Tính năng:
- Bảng giao dịch với các cột: Ngày, Loại, Provider, Số tiền, Trạng thái, Transaction ID
- Dropdown filter trạng thái (ALL, PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED)
- Thẻ tóm tắt: Tổng số giao dịch, Tổng số tiền đã hoàn tất, Số lượng đang chờ
- Phân trang (20 mục/trang)
- Layout thẻ thân thiện với mobile
#### Hook được sử dụng:
```typescript
const { data: transactions, isLoading } = useTransactions({
status: statusFilter,
limit: 20,
offset: page * 20,
});
```
#### Không có UI Tạo Thanh toán
❌ **Thiếu:** Không có nút/luồng để tạo thanh toán mới từ trang này
---
## 7. Cấu hình i18n
### Vị trí
```
/apps/web/messages/
├── vi.json
└── en.json
```
### Translation cho Pricing (ví dụ tiếng Việt)
```json
"pricing": {
"badge": "Bảng giá dịch vụ",
"title": "Chọn gói dịch vụ phù hợp",
"subtitle": "Từ cá nhân đến doanh nghiệp...",
"monthly": "Theo tháng",
"yearly": "Theo năm",
"yearlyDiscount": "-17%",
"perMonth": "tháng",
"perYear": "năm",
"loading": "Đang tải gói dịch vụ...",
"popular": "Phổ biến nhất",
"unlimited": "Không giới hạn",
"listingsCount": "tin đăng",
"savedSearchesCount": "tìm kiếm đã lưu",
"photosPerListing": "ảnh/tin đăng",
"tiers": {
"FREE": "Miễn phí",
"AGENT_PRO": "Môi giới Pro",
"INVESTOR": "Nhà đầu tư",
"ENTERPRISE": "Doanh nghiệp"
},
"tierDescriptions": {...},
"features": {...},
"ctaFree": "Bắt đầu miễn phí",
"ctaUpgrade": "Nâng cấp ngay",
"ctaEnterprise": "Liên hệ Sales",
"ctaTitle": "Bạn có bất động sản muốn đăng?",
"ctaDescription": "...",
"ctaRegister": "Đăng ký miễn phí",
"ctaLearnMore": "Tìm hiểu thêm",
"comparisonTitle": "So sánh các gói dịch vụ",
"comparisonSubtitle": "...",
"feature": "Tính năng"
}
```
---
## 8. Các Component & Luồng còn thiếu
### ❌ Các mảnh quan trọng còn thiếu
#### 1. Component Trang/Modal Checkout
```
Trạng thái: CHƯA TRIỂN KHAI
Cần: Component để khởi tạo thanh toán cho plan đã chọn
Luồng nên là:
Trang Pricing (chọn plan + chu kỳ billing)
↓
[Thiếu] Modal/Page Checkout
↓ (Xác nhận chi tiết plan, hiển thị số tiền)
User nhấp "Pay Now"
↓
Tạo Payment → POST /payments
↓
Nhận paymentUrl + paymentId
↓
Redirect tới payment gateway (VNPay/MoMo/ZaloPay)
↓
User thanh toán
↓
[Thiếu] Xử lý callback thành công
↓
[Thiếu] Tạo subscription → POST /subscriptions
```
#### 2. Xử lý Callback Thành công của Thanh toán
```
Trạng thái: CHƯA TRIỂN KHAI
Vị trí: Backend có POST /payments/callback/:provider
Frontend cần: Trang để xử lý redirect từ payment gateway sau khi thanh toán
Luồng:
Payment gateway redirect tới returnUrl với tham số callback
↓
Trang xử lý callback ở frontend nhận trạng thái thanh toán
↓
Gọi POST /payments/callback/:provider (xác minh)
↓
Nếu COMPLETED → Gọi POST /subscriptions (tạo subscription)
↓
Redirect tới trang success hoặc dashboard
```
#### 3. Thiết lập Subscription sau khi Thanh toán
```
Trạng thái: CHƯA TRIỂN KHAI
Sau khi thanh toán thành công:
1. Kiểm tra trạng thái thanh toán
2. Nếu COMPLETED, tự động tạo subscription
3. Đồng bộ dữ liệu subscription về client
4. Redirect tới dashboard hoặc trang subscription
```
#### 4. UI Quản lý Subscription
```
Trạng thái: Thiếu một phần
Đã có:
- View lịch sử billing (trong trang payments)
- API endpoint cho upgrade, cancel, meter usage
Thiếu:
- UI để upgrade/downgrade plan
- UI để hủy subscription
- Component hiển thị quota/usage
```
---
## 9. Kiến trúc Luồng Checkout Đề xuất
### Hành trình Checkout ở Frontend
```
1. Người dùng ở trang /pricing
↓
2. Nhấp nút "Upgrade" của plan
↓
3. Mở Modal/Page Checkout
- Hiển thị chi tiết plan
- Hiển thị tổng số tiền (theo tháng hoặc theo năm)
- Chọn payment provider (VNPay, MoMo, ZaloPay)
- Hiển thị checkbox điều khoản & điều kiện
↓
4. Người dùng nhấp "Pay Now"
↓
5. POST /payments {
provider: 'VNPAY',
type: 'SUBSCRIPTION',
amountVND: plan.priceMonthlyVND,
description: `Subscription to ${plan.name}`,
returnUrl: 'https://goodgo.vn/payment-return',
idempotencyKey: UUID
}
↓
6. Nhận CreatePaymentResult:
- paymentId
- paymentUrl (từ gateway)
- providerTxId
↓
7. Redirect window.location = paymentUrl
↓
8. Người dùng hoàn tất thanh toán tại gateway
↓
9. Gateway redirect tới returnUrl?paymentId=...&status=...
↓
10. Trang xử lý callback ở frontend:
- Trích xuất paymentId từ URL
- Poll GET /payments/{paymentId} cho đến khi status = COMPLETED
- Khi hoàn tất, POST /subscriptions {
planTier: selectedPlanTier,
billingCycle: 'monthly' | 'yearly'
}
↓
11. Hiển thị thông báo thành công
↓
12. Redirect tới /dashboard/subscription hoặc trang chủ
```
### Các Component cần thiết
```typescript
// 1. Modal/Page Checkout
// 2. Selector Payment Provider
// 3. Trình xử lý Payment Return
// Trang /payment-return
// - Phát hiện trạng thái thanh toán từ URL
// - Tạo subscription
// - Redirect tới dashboard
// 4. Component Trạng thái Subscription
```
---
## 10. Checklist Triển khai
### Phase 1: Modal Checkout (Ưu tiên CAO)
- [ ] Tạo component ``
- [ ] Thêm selector payment provider (VNPay, MoMo, ZaloPay)
- [ ] Hiển thị tóm tắt plan và tổng số tiền
- [ ] Thêm checkbox điều khoản & điều kiện
- [ ] Thay thế CTA trang pricing để mở checkout thay vì chuyển tới register
### Phase 2: Redirect Payment Gateway (Ưu tiên CAO)
- [ ] Triển khai mutation tạo thanh toán (useMutation + paymentApi.createPayment)
- [ ] Xử lý redirect paymentUrl
- [ ] Truyền idempotencyKey để tránh thanh toán trùng
- [ ] Xử lý trạng thái loading/error
### Phase 3: Trình xử lý Payment Return (Ưu tiên CAO)
- [ ] Tạo trang `/payment-return` hoặc `/checkout/return`
- [ ] Trích xuất paymentId từ tham số URL
- [ ] Triển khai cơ chế polling cho trạng thái thanh toán
- [ ] Tạo subscription sau khi thanh toán thành công
- [ ] Hiển thị UI thành công/thất bại
### Phase 4: Quản lý Subscription (Ưu tiên TRUNG BÌNH)
- [ ] Tạo trang chi tiết subscription
- [ ] Hiển thị tier plan hiện tại và ngày renewal
- [ ] Thêm nút upgrade plan (mở modal checkout)
- [ ] Thêm nút cancel subscription với xác nhận
- [ ] Hiển thị thông tin usage/quota
### Phase 5: Xử lý Webhook (Ưu tiên TRUNG BÌNH)
- [ ] Đảm bảo webhook handler backend đang hoạt động
- [ ] Test callback VNPay
- [ ] Test callback MoMo
- [ ] Test callback ZaloPay
- [ ] Xác minh trạng thái thanh toán cập nhật theo thời gian thực
### Phase 6: Testing & Tối ưu (Ưu tiên TRUNG BÌNH)
- [ ] E2E test toàn bộ luồng checkout
- [ ] Test với cả 3 payment provider
- [ ] Xử lý edge case (timeout thanh toán, user đóng cửa sổ, v.v.)
- [ ] Thêm luồng khôi phục lỗi
- [ ] Tối ưu hiệu năng (lazy load modal, v.v.)
---
## 11. Cấu hình Môi trường
### Biến Env Cần thiết (Backend)
```
# VNPay
VNPAY_TMN_CODE=xxxxx
VNPAY_HASH_SECRET=xxxxx
VNPAY_BASE_URL=https://sandbox.vnpayment.vn/paymentv2/vpcpay.html (hoặc production)
VNPAY_API_URL=https://sandbox.vnpayment.vn/merchant_webapi/api/transaction
# MoMo
MOMO_PARTNER_CODE=xxxxx
MOMO_ACCESS_KEY=xxxxx
MOMO_SECRET_KEY=xxxxx
MOMO_ENDPOINT=https://test-payment.momo.vn/v2/gateway/api (hoặc production)
# ZaloPay
ZALOPAY_APP_ID=xxxxx
ZALOPAY_KEY1=xxxxx
ZALOPAY_KEY2=xxxxx
ZALOPAY_ENDPOINT=https://sandbox.zalopay.com.vn (hoặc production)
```
### Cấu hình Frontend
```typescript
// Return URL cho tất cả các payment provider
const PAYMENT_RETURN_URL = `${process.env.NEXT_PUBLIC_APP_URL}/payment-return`;
// Cấu hình polling thanh toán
const PAYMENT_POLL_INTERVAL = 2000; // 2 giây
const PAYMENT_POLL_MAX_ATTEMPTS = 30; // tổng cộng 60 giây
```
---
## 12. Edge Case & Xử lý Lỗi
### Các kịch bản cần xử lý
1. **Người dùng đóng cửa sổ payment gateway**
- Thanh toán vẫn ở trạng thái PENDING trong DB
- Hiển thị thông báo "Thanh toán chưa hoàn tất"
- Cho phép thử lại
2. **Timeout thanh toán**
- Gateway không hoàn tất trong thời gian dự kiến
- Hiển thị thông báo "Thanh toán đã hết thời gian"
- Cho phép thử lại
3. **Lỗi payment gateway**
- Gateway trả về lỗi trong quá trình createPaymentUrl
- Hiển thị thông báo lỗi đặc thù của provider
- Cho phép thử lại với provider khác
4. **Lần thử thanh toán trùng lặp**
- Sử dụng idempotencyKey để tránh trùng lặp
- Backend trả về thanh toán hiện có nếu key khớp
5. **Tạo subscription thất bại sau khi thanh toán**
- Thanh toán COMPLETED nhưng tạo subscription thất bại
- Cần can thiệp thủ công (dashboard admin)
- Hiển thị thông báo: "Thanh toán thành công nhưng subscription chưa được tạo. Vui lòng liên hệ hỗ trợ."
6. **Người dùng rời khỏi trang return**
- Thanh toán thành công nhưng redirect không hoàn tất
- Subscription đã được tạo ở background
- Người dùng sẽ thấy subscription trong dashboard vào lần đăng nhập tiếp theo
---
## 13. Chiến lược Testing
### Unit Test
```typescript
// hook usePaymentCheckout
describe('usePaymentCheckout', () => {
it('should create payment and return paymentUrl');
it('should handle payment provider errors');
it('should validate idempotency key');
});
// Component CheckoutModal
describe('', () => {
it('should render plan details');
it('should show payment providers');
it('should create payment on submit');
it('should disable submit while loading');
});
```
### Integration Test
```typescript
// Luồng checkout đầy đủ
describe('Checkout Flow', () => {
it('should go from pricing page to successful subscription');
it('should handle payment provider callbacks correctly');
it('should create subscription after successful payment');
});
```
### E2E Test (Playwright)
```typescript
// Hành trình người dùng đầy đủ với payment provider test
describe('E2E: Subscription Checkout', () => {
it('should complete checkout with VNPay');
it('should complete checkout with MoMo');
it('should complete checkout with ZaloPay');
it('should handle payment failures gracefully');
});
```
---
## 14. Bảng Tóm tắt Trạng thái Hiện tại
| Component | Vị trí | Trạng thái | Ghi chú |
|-----------|----------|--------|-------|
| **Trang Pricing** | `/pricing` | ✅ Active | Hiển thị 4 tier với giá chính xác |
| **Plan API** | `GET /subscriptions/plans` | ✅ Active | Trả về PlanDto[] |
| **Hook usePlans** | `lib/hooks/use-subscription.ts` | ✅ Active | Dùng React Query |
| **Payment API** | `POST /payments` | ✅ Active | Tạo payment & trả về paymentUrl |
| **Payment Gateway** | 3 service (VNPay, MoMo, ZaloPay) | ✅ Đã triển khai | Sẵn sàng sử dụng |
| **Tạo Subscription** | `POST /subscriptions` | ✅ Active | Tạo subscription sau khi thanh toán |
| **Modal Checkout** | - | ❌ Thiếu | Cần để kết nối pricing → payment |
| **Trình xử lý Payment Return** | - | ❌ Thiếu | Cần để xử lý redirect từ gateway |
| **UI Quản lý Subscription** | - | ⚠️ Một phần | Đã có để xem, thiếu để chỉnh sửa |
| **Hỗ trợ Idempotency** | Model Payment | ✅ Active | Đã tích hợp vào schema |
| **Webhook Handler** | `POST /payments/callback/:provider` | ✅ Active | Nhận & xác minh callback |
| **Polling Trạng thái Thanh toán** | `GET /payments/:id` | ✅ Active | Có thể poll trạng thái khi return |
| **Cập nhật Hướng Sự kiện** | Domain event | ✅ Active | Sự kiện PaymentCompleted được publish |
| **Listener Thông báo** | 3 listener | ✅ Active | payment-completed, payment-failed, payment-refunded |
---
## 15. Tham chiếu File chính
### Frontend
```
/apps/web/
├── app/[locale]/(public)/pricing/page.tsx [Trang pricing chính]
├── lib/
│ ├── subscription-api.ts [Client API Subscription]
│ ├── payment-api.ts [Client API Payment]
│ └── hooks/
│ ├── use-subscription.ts [Hook Subscription]
│ └── use-payments.ts [Hook Payment]
└── app/[locale]/(dashboard)/dashboard/payments/page.tsx [Lịch sử giao dịch]
```
### Backend
```
/apps/api/src/modules/
├── subscriptions/
│ ├── presentation/controllers/subscriptions.controller.ts
│ ├── application/commands/create-subscription/
│ ├── domain/entities/subscription.entity.ts
│ └── infrastructure/repositories/prisma-subscription.repository.ts
└── payments/
├── presentation/controllers/payments.controller.ts
├── application/commands/create-payment/
├── domain/entities/payment.entity.ts
├── infrastructure/services/
│ ├── vnpay.service.ts
│ ├── momo.service.ts
│ ├── zalopay.service.ts
│ └── payment-gateway.factory.ts
└── infrastructure/repositories/prisma-payment.repository.ts
```
### Database
```
/prisma/schema.prisma
├── model Plan (dòng 469-483)
├── model Subscription (dòng 485-502)
├── model Payment (dòng 424-449)
└── model UsageRecord (dòng 504-514)
```
---
## Kết luận
GoodGo Platform có **hạ tầng backend xuất sắc** cho subscription và thanh toán với tích hợp payment gateway hoàn chỉnh (VNPay, MoMo, ZaloPay). Trang pricing frontend hoạt động nhưng **thiếu luồng checkout quan trọng** để chuyển đổi người xem thành khách hàng trả phí.
### Các bước tiếp theo cần làm ngay:
1. **Tạo Modal Checkout** - Kết nối trang pricing với việc tạo thanh toán
2. **Triển khai Trình xử lý Payment Return** - Xử lý redirect từ payment gateway
3. **Thêm Tự động Tạo Subscription** - Tự động tạo subscription sau khi thanh toán thành công
4. **Xây dựng UI Quản lý Subscription** - Cho phép người dùng xem và quản lý subscription của họ
Nền tảng đã vững chắc; chỉ còn thiếu luồng checkout hướng tới người dùng.