# 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.