# Quick Reference: Pricing/Subscription/Payment System ## Files at a Glance ### 🎨 Frontend | File | Purpose | Status | |------|---------|--------| | `apps/web/app/[locale]/(public)/pricing/page.tsx` | Main pricing page | βœ… Complete | | `apps/web/lib/subscription-api.ts` | Subscription API client | βœ… Complete | | `apps/web/lib/payment-api.ts` | Payment API client | βœ… Complete | | `apps/web/lib/hooks/use-subscription.ts` | Subscription hooks | βœ… Complete | | `apps/web/lib/hooks/use-payments.ts` | Payment hooks | βœ… Complete | | `apps/web/app/.../dashboard/payments/page.tsx` | Payment history | βœ… Complete | ### πŸ”§ Backend | Directory | Purpose | Status | |-----------|---------|--------| | `apps/api/src/modules/subscriptions/` | Subscription CQRS module | βœ… Complete | | `apps/api/src/modules/payments/` | Payment CQRS module | βœ… Complete | | `apps/api/src/modules/payments/infrastructure/services/` | Payment gateways (VNPay, MoMo, ZaloPay) | βœ… Complete | ### πŸ“¦ Database | Model | Fields | Relationships | |-------|--------|---| | `Plan` | id, tier (unique), name, prices, features, isActive | 1β†’M Subscription | | `Subscription` | id, userId (unique), planId, status, periods, cancelledAt | M←1 Plan, 1←1 User | | `Payment` | id, userId, provider, type, amountVND, status, providerTxId, idempotencyKey | M←1 User | | `UsageRecord` | id, subscriptionId, metric, count, periods | M←1 Subscription | --- ## Key API Endpoints ### Plans (Public) ``` GET /subscriptions/plans GET /subscriptions/plans/:tier ``` ### Subscriptions (Auth Required) ``` POST /subscriptions # Create new PUT /subscriptions/upgrade # Upgrade DELETE /subscriptions # Cancel GET /subscriptions/quota/:metric # Check quota POST /subscriptions/usage # Record usage GET /subscriptions/billing # View history ``` ### Payments (Auth + Webhook) ``` POST /payments # Create payment β†’ returns paymentUrl POST /payments/callback/:provider # Webhook from gateway GET /payments/:id # Check status GET /payments # List transactions POST /payments/:id/refund # Refund (admin) ``` --- ## Type Definitions ### Frontend Types ```typescript // From subscription-api.ts interface PlanDto { id: string; tier: string; // FREE, AGENT_PRO, INVESTOR, ENTERPRISE name: string; priceMonthlyVND: string; // In VND priceYearlyVND: string; // In VND maxListings: number; maxSavedSearches: number; features: Record; isActive: boolean; } interface CreateSubscriptionResult { subscriptionId: string; planTier: string; status: string; // ACTIVE, PAST_DUE, CANCELLED, EXPIRED currentPeriodStart: string; // ISO datetime currentPeriodEnd: string; // ISO datetime } // From payment-api.ts interface CreatePaymentPayload { provider: 'VNPAY' | 'MOMO' | 'ZALOPAY' | 'BANK_TRANSFER'; type: 'SUBSCRIPTION' | 'LISTING_FEE' | 'DEPOSIT' | 'FEATURED_LISTING'; amountVND: number; // 1 to 100,000,000,000 description: string; returnUrl: string; // Redirect after payment idempotencyKey?: string; // Prevent duplicates transactionId?: string; // External transaction ID } interface CreatePaymentResult { paymentId: string; paymentUrl: string; // Redirect user here providerTxId: string; } interface PaymentStatusDto { id: string; provider: string; type: string; amountVND: string; status: string; // PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED providerTxId: string | null; createdAt: string; updatedAt: string; } ``` --- ## How to Use in Frontend ### Get Plans ```typescript import { usePlans } from '@/lib/hooks/use-subscription'; export function MyComponent() { const { data: plans, isLoading } = usePlans(); return (
{isLoading ? 'Loading...' : plans?.map(plan =>
{plan.name}
)}
); } ``` ### Create Payment ```typescript import { paymentApi } from '@/lib/payment-api'; import { useMutation } from '@tanstack/react-query'; const createPaymentMutation = useMutation({ mutationFn: (payload) => paymentApi.createPayment(payload), }); // When user clicks "Pay Now" const handlePayment = async (planTier: string, provider: 'VNPAY' | 'MOMO' | 'ZALOPAY') => { const result = await createPaymentMutation.mutateAsync({ provider, type: 'SUBSCRIPTION', amountVND: 499000, description: `Subscription to ${planTier}`, returnUrl: `${window.location.origin}/payment-return`, idempotencyKey: crypto.randomUUID(), }); // Redirect to payment gateway window.location = result.paymentUrl; }; ``` ### Check Payment Status (on return page) ```typescript import { paymentApi } from '@/lib/payment-api'; import { useEffect, useState } from 'react'; export function PaymentReturnPage() { const searchParams = new URLSearchParams(window.location.search); const paymentId = searchParams.get('paymentId'); const [status, setStatus] = useState('loading'); useEffect(() => { if (!paymentId) return; const poll = async () => { const payment = await paymentApi.getPaymentStatus(paymentId); if (payment.status === 'COMPLETED') { // Create subscription await subscriptionApi.createSubscription('AGENT_PRO', 'monthly'); setStatus('success'); // Redirect to dashboard window.location = '/dashboard'; } else if (payment.status === 'FAILED') { setStatus('failed'); } else { // Poll again in 2 seconds setTimeout(poll, 2000); } }; poll(); }, [paymentId]); return
{status === 'loading' ? 'Processing payment...' : status}
; } ``` ### Create Subscription ```typescript import { subscriptionApi } from '@/lib/subscription-api'; const result = await subscriptionApi.createSubscription('AGENT_PRO', 'monthly'); console.log(result); // { // subscriptionId: 'cuid...', // planTier: 'AGENT_PRO', // status: 'ACTIVE', // currentPeriodStart: '2024-04-12T...', // currentPeriodEnd: '2024-05-12T...' // } ``` --- ## How to Use in Backend ### Create Plan (Admin) ```sql INSERT INTO "Plan" (id, tier, name, "priceMonthlyVND", "priceYearlyVND", "maxListings", features, "isActive") VALUES ( 'cuid123', 'AGENT_PRO', 'Agent Pro', 499000, 4990000, 50, '{"analytics": true, "aiValuation": true}', true ); ``` ### Create Payment (via API) ``` POST /payments Authorization: Bearer Content-Type: application/json { "provider": "VNPAY", "type": "SUBSCRIPTION", "amountVND": 499000, "description": "Agent Pro - Monthly", "returnUrl": "https://goodgo.vn/payment-return", "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000" } Response: { "paymentId": "cuid456", "paymentUrl": "https://sandbox.vnpayment.vn/paymentv2/vpcpay.html?...", "providerTxId": "cuid456" } ``` ### Handle Payment Callback (Webhook) ``` POST /payments/callback/vnpay?vnp_TxnRef=cuid456&vnp_ResponseCode=00&vnp_SecureHash=... Response: { "orderId": "cuid456", "isSuccess": true, "status": "COMPLETED" } ``` ### Create Subscription (via API) ``` POST /subscriptions Authorization: Bearer Content-Type: application/json { "planTier": "AGENT_PRO", "billingCycle": "monthly" } Response: { "subscriptionId": "cuid789", "planTier": "AGENT_PRO", "status": "ACTIVE", "currentPeriodStart": "2024-04-12T...", "currentPeriodEnd": "2024-05-12T..." } ``` --- ## Pricing Structure ``` FREE (0 VND) β”œβ”€β”€ 3 listings β”œβ”€β”€ 5 saved searches └── Basic features AGENT_PRO (499,000 VND/month | 4,990,000/year = -17%) β”œβ”€β”€ 50 listings β”œβ”€β”€ 30 saved searches β”œβ”€β”€ Analytics β”œβ”€β”€ AI Valuation β”œβ”€β”€ Priority support └── Lead management INVESTOR (999,000 VND/month | 9,990,000/year = -17%) β”œβ”€β”€ 20 listings β”œβ”€β”€ 100 saved searches β”œβ”€β”€ Analytics β”œβ”€β”€ AI Valuation β”œβ”€β”€ Market reports β”œβ”€β”€ Price alerts └── Portfolio tracking ENTERPRISE (4,990,000 VND/month | 49,900,000/year = -17%) β”œβ”€β”€ Unlimited listings β”œβ”€β”€ Unlimited searches β”œβ”€β”€ All INVESTOR features β”œβ”€β”€ API access β”œβ”€β”€ White label └── Dedicated support ``` --- ## Environment Variables ```bash # Backend (.env) VNPAY_TMN_CODE=your_tmn_code VNPAY_HASH_SECRET=your_hash_secret VNPAY_BASE_URL=https://sandbox.vnpayment.vn/paymentv2/vpcpay.html VNPAY_API_URL=https://sandbox.vnpayment.vn/merchant_webapi/api/transaction MOMO_PARTNER_CODE=your_partner_code MOMO_ACCESS_KEY=your_access_key MOMO_SECRET_KEY=your_secret_key MOMO_ENDPOINT=https://test-payment.momo.vn/v2/gateway/api ZALOPAY_APP_ID=your_app_id ZALOPAY_KEY1=your_key1 ZALOPAY_KEY2=your_key2 ZALOPAY_ENDPOINT=https://sandbox.zalopay.com.vn # Frontend (.env.local) NEXT_PUBLIC_APP_URL=https://goodgo.vn ``` --- ## Testing Credentials ### VNPay Sandbox ``` Terminal: 0 Account: 0968323286 Password: 123456 Card: 9704198526191432198 OTP: 123456 ``` ### MoMo Sandbox ``` Phone: 0987654321 Password: 123456 OTP: 123456 ``` ### ZaloPay Sandbox ``` Phone: 0987654321 OTP: 123456 ``` --- ## Common Errors | Error | Cause | Solution | |-------|-------|----------| | `ConflictException: User already has active subscription` | User trying to create 2nd subscription | Check existing subscription first | | `ValidationException: Sα»‘ tiền phαΊ£i lα»›n hΖ‘n 0` | Amount is 0 or negative | Ensure amount > 0 | | `NotFoundException: Plan not found` | Plan tier doesn't exist in DB | Check plan is created and isActive=true | | `Payment gateway failed` | Payment gateway credentials wrong | Verify ENV vars | | `Cannot complete payment in status X` | Payment already completed/failed | Check idempotencyKey | | `Idempotency check failed` | Same idempotencyKey used twice | Generate unique UUID each time | --- ## Debugging Checklist - [ ] Check payment provider credentials in .env - [ ] Verify idempotencyKey is unique per request - [ ] Ensure amountVND matches plan price - [ ] Check returnUrl is publicly accessible - [ ] Verify JWT token is valid when calling protected endpoints - [ ] Check payment status with `GET /payments/:id` - [ ] Review payment provider logs/dashboard - [ ] Test with sandbox credentials first - [ ] Verify callback signature matches gateway requirements - [ ] Check subscription was created after successful payment --- ## Links - Detailed Audit: `PRICING_CHECKOUT_AUDIT.md` - Summary: `PRICING_AUDIT_SUMMARY.md` - Pricing Page: `apps/web/app/[locale]/(public)/pricing/page.tsx` - Subscriptions Module: `apps/api/src/modules/subscriptions/` - Payments Module: `apps/api/src/modules/payments/` - Schema: `prisma/schema.prisma` (lines 451-514)