17 KiB
Tóm Tắt Kiểm Tra Định Giá → Thanh Toán GoodGo
🎯 Tổng Quan Nhanh
| Khía cạnh | Trạng thái | Chi tiết chính |
|---|---|---|
| Trang Định Giá | ✅ Hoàn chỉnh | /pricing hiển thị 4 gói, chuyển đổi tháng/năm |
| API Gói dịch vụ | ✅ Hoàn chỉnh | GET /subscriptions/plans với dữ liệu dự phòng |
| Backend Đăng ký | ✅ Hoàn chỉnh | Mẫu CQRS, thực thể miền, kho lưu trữ |
| Tích hợp Cổng Thanh toán | ✅ Hoàn chỉnh | VNPay, MoMo, ZaloPay sẵn sàng sử dụng |
| API Thanh toán | ✅ Hoàn chỉnh | Tạo thanh toán, lấy trạng thái, xử lý callback |
| Mô hình Cơ sở dữ liệu | ✅ Hoàn chỉnh | Plan, Subscription, Payment, UsageRecord |
| Luồng Thanh toán Frontend | ❌ THIẾU | Không có modal/trang để khởi tạo thanh toán |
| Trình xử lý Trả về Thanh toán | ❌ THIẾU | Không có trang xử lý chuyển hướng từ cổng |
| Tự động Tạo Đăng ký | ❌ THIẾU | Quy trình thủ công sau khi thanh toán |
🏗️ Tổng Quan Kiến Trúc
Stack Frontend
Trang Định Giá (/pricing)
↓ hook usePlans()
↓ React Query
API Client: subscriptionApi.getPlans()
↓ GET /subscriptions/plans
Backend (endpoint /subscriptions/plans)
Luồng Thanh toán (Hiện bị Hỏng)
Trang Định Giá (Chọn Gói)
✅ Hiển thị gói, giá, tính năng
❌ Nút CTA trỏ đến /register thay vì thanh toán
[THIẾU] Modal/Trang Thanh toán
❌ Chưa triển khai
❌ Không có xác nhận gói
❌ Không có lựa chọn phương thức thanh toán
[THIẾU] Tạo Thanh toán
❌ Cần gọi POST /payments
❌ Cần chuyển hướng đến paymentUrl
Cổng Thanh toán (VNPay/MoMo/ZaloPay)
✅ Backend có triển khai createPaymentUrl
✅ Xác minh chữ ký đã sẵn sàng
❌ Chưa triển khai chuyển hướng frontend
[THIẾU] Trình xử lý Trả về
❌ Không có trang nhận callback từ cổng
❌ Không có cơ chế poll trạng thái thanh toán
❌ Không có tạo đăng ký
[THIẾU] Tạo Đăng ký
❌ Cần gọi POST /subscriptions
❌ Cần hiển thị thông báo thành công
Bảng điều khiển/Trang chủ
✅ Có trang thanh toán để xem lịch sử
❌ Không có giao diện quản lý đăng ký
📁 Cấu Trúc Tệp Frontend
apps/web/
├── app/[locale]/(public)/pricing/
│ └── page.tsx ✅ Trang định giá chính
│
├── lib/
│ ├── subscription-api.ts ✅ API client & types (PlanDto, CreateSubscriptionResult, v.v.)
│ ├── payment-api.ts ✅ API client & types (CreatePaymentResult, PaymentStatusDto, v.v.)
│ └── hooks/
│ ├── use-subscription.ts ✅ usePlans(), useBillingHistory(), useQuota()
│ └── use-payments.ts ✅ useTransactions(), usePaymentStatus()
│
├── app/[locale]/(dashboard)/dashboard/
│ └── payments/page.tsx ✅ Trình xem lịch sử giao dịch
│
└── components/
└── (cần thêm component mới cho thanh toán)
├── checkout-modal/ ❌ Thiếu
├── payment-provider-select/ ❌ Thiếu
└── subscription-status/ ❌ Thiếu
🔧 Cấu Trúc Tệp Backend
apps/api/src/modules/
│
├── subscriptions/
│ ├── presentation/
│ │ ├── controllers/subscriptions.controller.ts ✅ 8 endpoint
│ │ └── dto/
│ │ ├── create-subscription.dto.ts ✅ { planTier, billingCycle }
│ │ ├── upgrade-subscription.dto.ts ✅
│ │ ├── cancel-subscription.dto.ts ✅
│ │ └── meter-usage.dto.ts ✅
│ │
│ ├── application/
│ │ ├── commands/
│ │ │ ├── create-subscription/ ✅ Tạo đăng ký
│ │ │ ├── upgrade-subscription/ ✅
│ │ │ ├── cancel-subscription/ ✅
│ │ │ └── meter-usage/ ✅
│ │ └── queries/
│ │ ├── get-plan/ ✅ Trả về PlanDto[]
│ │ ├── check-quota/ ✅
│ │ └── get-billing-history/ ✅
│ │
│ ├── domain/
│ │ ├── entities/subscription.entity.ts ✅ Aggregate CQRS
│ │ ├── events/ ✅ 5 sự kiện miền
│ │ └── repositories/subscription.repository.ts ✅ Interface
│ │
│ └── infrastructure/
│ ├── repositories/prisma-subscription.repository.ts ✅
│ └── event-handlers/listing-created-usage.handler.ts ✅
│
└── payments/
├── presentation/
│ ├── controllers/payments.controller.ts ✅ 5 endpoint
│ └── dto/
│ ├── create-payment.dto.ts ✅ { provider, type, amountVND, description, returnUrl }
│ ├── refund-payment.dto.ts ✅
│ └── list-transactions.dto.ts ✅
│
├── application/
│ ├── commands/
│ │ ├── create-payment/ ✅ Logic tạo thanh toán chính
│ │ ├── handle-callback/ ✅ Trình xử lý webhook
│ │ └── refund-payment/ ✅
│ └── queries/
│ ├── get-payment-status/ ✅ Poll trạng thái
│ └── list-transactions/ ✅
│
├── domain/
│ ├── entities/payment.entity.ts ✅ Aggregate CQRS
│ ├── events/ ✅ 4 sự kiện miền
│ ├── value-objects/money.vo.ts ✅
│ └── repositories/payment.repository.ts ✅ Interface
│
└── infrastructure/
├── repositories/prisma-payment.repository.ts ✅
└── services/
├── payment-gateway.interface.ts ✅ IPaymentGateway
├── payment-gateway.factory.ts ✅ Lấy cổng thanh toán phù hợp
├── vnpay.service.ts ✅ createPaymentUrl() + verifyCallback()
├── momo.service.ts ✅ createPaymentUrl() + verifyCallback()
└── zalopay.service.ts ✅ createPaymentUrl() + verifyCallback()
🔌 Tóm Tắt Endpoint API
Endpoint Đăng ký
GET /subscriptions/plans → PlanDto[]
GET /subscriptions/plans/:tier → PlanDto
POST /subscriptions → CreateSubscriptionResult (yêu cầu xác thực)
PUT /subscriptions/upgrade → UpgradeSubscriptionResult (yêu cầu xác thực)
DELETE /subscriptions → CancelSubscriptionResult (yêu cầu xác thực)
POST /subscriptions/usage → MeterUsageResult (yêu cầu xác thực)
GET /subscriptions/quota/:metric → QuotaCheckResult (yêu cầu xác thực)
GET /subscriptions/billing → BillingHistoryDto (yêu cầu xác thực)
Endpoint Thanh toán
POST /payments → CreatePaymentResult (yêu cầu xác thực)
POST /payments/callback/:provider → HandleCallbackResult (webhook)
GET /payments/:id → PaymentStatusDto (yêu cầu xác thực)
GET /payments → TransactionListDto (yêu cầu xác thực)
POST /payments/:id/refund → RefundPaymentResult (chỉ quản trị viên)
💰 Các Gói Định Giá
const TIERS = [
{
tier: 'FREE',
monthlyVND: '0',
yearlyVND: '0',
maxListings: 3,
maxSearches: 5,
},
{
tier: 'AGENT_PRO',
monthlyVND: '499,000',
yearlyVND: '4,990,000',
maxListings: 50,
maxSearches: 30,
popular: true,
},
{
tier: 'INVESTOR',
monthlyVND: '999,000',
yearlyVND: '9,990,000',
maxListings: 20,
maxSearches: 100,
},
{
tier: 'ENTERPRISE',
monthlyVND: '4,990,000',
yearlyVND: '49,900,000',
maxListings: -1, // Không giới hạn
maxSearches: -1, // Không giới hạn
},
];
📊 Mô Hình Dữ Liệu (Prisma)
Plan
id: String @id
tier: PlanTier @unique (FREE, AGENT_PRO, INVESTOR, ENTERPRISE)
name: String
priceMonthlyVND: BigInt
priceYearlyVND: BigInt
maxListings: Int?
maxSavedSearches: Int?
maxAnalyticsQueries: Int?
maxMediaUploads: Int?
features: Json // { analytics: true, aiValuation: false, ... }
isActive: Boolean
Subscription
id: String @id
userId: String @unique
user: User
planId: String
plan: Plan
status: SubscriptionStatus (ACTIVE, PAST_DUE, CANCELLED, EXPIRED)
currentPeriodStart: DateTime
currentPeriodEnd: DateTime
cancelledAt: DateTime?
createdAt: DateTime
updatedAt: DateTime
Payment
id: String @id
userId: String
provider: PaymentProvider (VNPAY, MOMO, ZALOPAY, BANK_TRANSFER)
type: PaymentType (SUBSCRIPTION, LISTING_FEE, DEPOSIT, FEATURED_LISTING)
amountVND: BigInt
status: PaymentStatus (PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED)
providerTxId: String?
callbackData: Json?
idempotencyKey: String? ← Ngăn chặn thanh toán trùng lặp
createdAt: DateTime
updatedAt: DateTime
🔑 Chi Tiết Triển Khai Quan Trọng
Luồng Tạo Thanh toán (Backend)
Người dùng nhấn "Thanh toán ngay"
↓
Frontend: POST /payments {
provider: 'VNPAY',
type: 'SUBSCRIPTION',
amountVND: 499000,
description: 'Agent Pro - Monthly',
returnUrl: 'https://goodgo.vn/payment-return',
idempotencyKey: UUID ← Duy nhất cho mỗi lần thanh toán
}
↓
Backend CreatePaymentHandler:
1. Kiểm tra idempotencyKey (ngăn trùng lặp)
2. Xác thực số tiền (1 đến 100 tỷ VND)
3. Lấy cổng thanh toán (VNPay/MoMo/ZaloPay)
4. Gọi gateway.createPaymentUrl()
- Trả về paymentUrl: "https://gateway.com/pay?params..."
- Trả về providerTxId: "VNP-12345..."
5. Đánh dấu thanh toán là PROCESSING trong DB
6. Phát sự kiện PaymentCreatedEvent
7. Trả về cho client: { paymentId, paymentUrl, providerTxId }
↓
Frontend:
window.location = paymentUrl ← Chuyển hướng đến cổng
↓
Người dùng hoàn tất thanh toán tại cổng
↓
Cổng chuyển hướng đến returnUrl với tham số callback
↓
Backend webhook: POST /payments/callback/vnpay?params...
1. Xác minh chữ ký callback
2. Kiểm tra trạng thái thanh toán
3. Cập nhật trạng thái thanh toán trong DB
4. Phát sự kiện PaymentCompletedEvent
↓
PaymentCompletedEvent kích hoạt:
- Gửi thông báo qua email
- Cập nhật liên kết gói của người dùng (cuối cùng)
↓
Trình xử lý callback frontend (nếu được triển khai):
1. Lấy paymentId từ URL
2. Poll GET /payments/{paymentId}
3. Khi status = COMPLETED:
- POST /subscriptions { planTier, billingCycle }
- Hiển thị thông báo thành công
- Chuyển hướng đến bảng điều khiển
Triển Khai Cổng Thanh toán
VNPay
// Chữ ký: HMAC SHA-512
// Yêu cầu qua: Tham số URL
// Mã phản hồi: vnp_ResponseCode = '00' nghĩa là thành công
// Mã giao dịch: vnp_TransactionNo
MoMo
// Chữ ký: HMAC SHA-256
// Yêu cầu qua: Body JSON POST
// Mã phản hồi: resultCode = 0 nghĩa là thành công
// Mã giao dịch: transId
ZaloPay
// Chữ ký: HMAC SHA-256 (tương tự MoMo)
// Yêu cầu qua: Body JSON POST
// Mã phản hồi: return_code = 1 nghĩa là thành công
// Mã giao dịch: zp_trans_id
🚨 Những Thiếu Sót Nghiêm Trọng (Những Gì Còn Thiếu)
1. Modal/Trang Thanh toán ❌
Chức năng cần có:
- Hiển thị chi tiết gói đã chọn
- Hiện giá theo tháng và theo năm
- Cho phép chọn phương thức thanh toán (VNPay, MoMo, ZaloPay)
- Hiển thị điều khoản và điều kiện
- Xử lý tạo thanh toán và chuyển hướng
Hiện tại: Nút CTA trên trang định giá trỏ đến /register thay vì bắt đầu thanh toán
2. Trình xử lý Trả về Thanh toán ❌
Chức năng cần có:
- Nhận chuyển hướng từ cổng thanh toán
- Trích xuất trạng thái thanh toán từ URL/callback
- Poll trạng thái thanh toán qua GET /payments/:id
- Tạo đăng ký khi thanh toán thành công
- Hiển thị giao diện thành công/lỗi
Hiện tại: Không có trang nào cho luồng này
3. Tự động Tạo Đăng ký ❌
Chức năng cần có:
- Sau khi thanh toán thành công, gọi POST /subscriptions
- Truyền planTier và billingCycle
- Cập nhật trạng thái đăng ký của người dùng
- Chuyển hướng đến bảng điều khiển
Hiện tại: Quy trình thủ công, không có giao diện
4. Giao diện Quản lý Đăng ký ⚠️ Một phần
Những gì đã có:
- Trang thanh toán hiển thị lịch sử giao dịch
Những gì còn thiếu:
- Trang trạng thái/chi tiết đăng ký
- Giao diện nâng cấp/hạ cấp gói
- Giao diện hủy đăng ký
- Hiển thị mức sử dụng/hạn mức
📋 Lộ Trình Triển Khai
Giai đoạn 1: Thanh toán Cơ bản (1-2 ngày)
✅ Trang định giá đã tồn tại
❌ Thêm component CheckoutModal
❌ Thêm bộ chọn nhà cung cấp thanh toán
❌ Tạo trang /payment-return
❌ Triển khai poll thanh toán
❌ Kết nối tạo đăng ký
Giai đoạn 2: Tích hợp Đầy đủ (1-2 ngày)
✅ Tất cả endpoint backend đã sẵn sàng
❌ Xử lý các trường hợp ngoại lệ (hết thời gian, người dùng đóng cửa sổ, v.v.)
❌ Thêm luồng phục hồi lỗi
❌ Thêm giao diện đang tải/thành công
❌ Kiểm tra với cả 3 nhà cung cấp thanh toán
Giai đoạn 3: Quản lý Đăng ký (1-2 ngày)
✅ Endpoint API nâng cấp/hạ cấp đã tồn tại
✅ API hủy đăng ký đã tồn tại
❌ Xây dựng trang chi tiết đăng ký
❌ Thêm giao diện nâng cấp/hạ cấp
❌ Thêm giao diện hủy với xác nhận
❌ Thêm hiển thị hạn mức sử dụng
Giai đoạn 4: Kiểm tra & Hoàn thiện (1-2 ngày)
❌ Kiểm tra E2E cho tất cả nhà cung cấp thanh toán
❌ Xử lý lỗi & các trường hợp ngoại lệ
❌ Tối ưu hóa hiệu suất
❌ Tích hợp phân tích/theo dõi
🎯 Các Bước Tiếp Theo
-
Hiểu UX thanh toán mong muốn - Thanh toán nên bắt đầu ở đâu/như thế nào?
- Modal từ trang định giá?
- Trang thanh toán riêng?
- Trực tiếp trên trang định giá?
-
Tạo component CheckoutModal - Thiết kế phù hợp với trang định giá
- Tóm tắt gói
- Phân tích giá
- Bộ chọn nhà cung cấp thanh toán
- Nút "Tiến hành Thanh toán"
-
Triển khai mutation tạo thanh toán - Kết nối với React Query
- Hook
useCreatePayment() - Xử lý trạng thái đang tải/lỗi
- Chuyển hướng đến paymentUrl
- Hook
-
Xây dựng trang /payment-return - Xử lý chuyển hướng từ cổng
- Phân tích tham số URL
- Poll trạng thái thanh toán
- Tạo đăng ký khi thành công
-
Kiểm tra với cả 3 nhà cung cấp - Đảm bảo tất cả tích hợp hoạt động
- Sử dụng thông tin đăng nhập sandbox/kiểm tra
- Xác minh callback
-
Thêm giao diện quản lý đăng ký - Cho phép người dùng quản lý gói
- Xem đăng ký hiện tại
- Nâng cấp/hạ cấp
- Hủy với xác nhận
📚 Tài Liệu Tham Khảo
Tài liệu kiểm tra đầy đủ: PRICING_CHECKOUT_AUDIT.md
Các tệp cần xem xét:
- Frontend:
/apps/web/app/[locale]/(public)/pricing/page.tsx - Backend thanh toán:
/apps/api/src/modules/payments/ - Backend đăng ký:
/apps/api/src/modules/subscriptions/ - Prisma schema:
/prisma/schema.prisma(dòng 451-514)
Trạng thái: Sẵn sàng triển khai thanh toán
Ước tính công sức: 4-6 ngày
Độ phức tạp: Trung bình (toàn bộ hạ tầng backend đã sẵn sàng)