feat: comprehensive seed, Lucide icons, grouped dashboard nav, API fixes
- Rewrite prisma/seed.ts to populate all 27 models with realistic Vietnamese real estate data (8 users with login, 10 properties, 10 listings, orders, payments, reviews, notifications, etc.) - Replace all emoji icons with Lucide React SVG icons across frontend for consistent rendering, sizing, and accessibility - Redesign dashboard nav: grouped sidebar with section headers, primary/secondary split on desktop, icon-only secondary items - Replace language switcher flag emoji with Globe icon - Replace SVG theme toggle with Lucide Moon/Sun icons - Fix API startup: graceful fallback for Sentry profiling, Google OAuth, and Zalo OAuth when credentials are not configured - Relax rate limiting in development mode (10k req/min) - Fix listings API to include media[] array in search response - Add optional chaining for property.media across frontend components - Update OAuth strategy tests to match graceful fallback behavior Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,415 +1,192 @@
|
||||
# Quick Reference: Pricing/Subscription/Payment System
|
||||
# GoodGo Platform - Authentication Quick Reference
|
||||
|
||||
## Files at a Glance
|
||||
## 🔑 Key Points 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)
|
||||
### Password Hashing
|
||||
```
|
||||
GET /subscriptions/plans
|
||||
GET /subscriptions/plans/:tier
|
||||
Algorithm: bcrypt
|
||||
Salt Rounds: 12 (env: BCRYPT_ROUNDS)
|
||||
Min Length: 8 characters
|
||||
Example: bcrypt.hash('password', 12)
|
||||
```
|
||||
|
||||
### Subscriptions (Auth Required)
|
||||
### Phone Numbers (Vietnamese)
|
||||
```
|
||||
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
|
||||
Valid Formats: 0900000001, 84900000001, +84900000001
|
||||
Normalized: +84900000001
|
||||
Regex: /^(?:\+84|84|0)(3[2-9]|5[2689]|7[06-9]|8[1-9]|9[0-9])\d{7}$/
|
||||
File: apps/api/src/modules/shared/utils/vietnam-phone.validator.ts
|
||||
```
|
||||
|
||||
### Payments (Auth + Webhook)
|
||||
### Email
|
||||
```
|
||||
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)
|
||||
Regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
Normalization: lowercase + trim
|
||||
Storage: admin@goodgo.vn
|
||||
```
|
||||
|
||||
### PII Encryption
|
||||
```
|
||||
Algorithm: AES-256-GCM
|
||||
Key: 32 bytes (64 hex chars)
|
||||
Encrypted: email, phone, kycData
|
||||
Searchable: email → emailHash (HMAC-SHA256)
|
||||
phone → phoneHash (HMAC-SHA256)
|
||||
Env Var: FIELD_ENCRYPTION_KEY
|
||||
```
|
||||
|
||||
### User Login
|
||||
```
|
||||
Username: phone (normalized)
|
||||
Password: plain text
|
||||
Lookup: by phoneHash (unique index)
|
||||
Required: isActive = true, passwordHash ≠ null
|
||||
Response: tokens (or MFA challenge)
|
||||
```
|
||||
|
||||
### User Roles
|
||||
```
|
||||
BUYER - Search, inquire, offer (default)
|
||||
SELLER - Create listings
|
||||
AGENT - Professional agent
|
||||
ADMIN - Full access
|
||||
```
|
||||
|
||||
### MFA
|
||||
```
|
||||
TOTP: otplib (RFC 6238)
|
||||
Period: 30 seconds
|
||||
Digits: 6
|
||||
Backup Codes: 10 × 8 chars (A-Z no OI, 2-9 no 01)
|
||||
Hashing: HMAC-SHA256 (not bcrypt)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type Definitions
|
||||
## 📋 Creating a Login-Capable Admin User
|
||||
|
||||
### Frontend Types
|
||||
### 5-Step Process
|
||||
|
||||
**1. Normalize phone**
|
||||
```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<string, boolean | number | string>;
|
||||
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;
|
||||
}
|
||||
phone = '0900000001' → '+84900000001'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Use in Frontend
|
||||
|
||||
### Get Plans
|
||||
**2. Derive HMAC key**
|
||||
```typescript
|
||||
import { usePlans } from '@/lib/hooks/use-subscription';
|
||||
|
||||
export function MyComponent() {
|
||||
const { data: plans, isLoading } = usePlans();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading ? 'Loading...' : plans?.map(plan => <div>{plan.name}</div>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
hmacKey = crypto.hkdfSync('sha256', Buffer.from(encryptionKey, 'hex'),
|
||||
Buffer.alloc(0), Buffer.from('goodgo-field-hash', 'utf8'), 32)
|
||||
```
|
||||
|
||||
### Create Payment
|
||||
**3. Compute hashes**
|
||||
```typescript
|
||||
import { paymentApi } from '@/lib/payment-api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
phoneHash = crypto.createHmac('sha256', hmacKey).update('+84900000001').digest('hex')
|
||||
emailHash = crypto.createHmac('sha256', hmacKey).update('admin@goodgo.vn').digest('hex')
|
||||
```
|
||||
|
||||
const createPaymentMutation = useMutation({
|
||||
mutationFn: (payload) => paymentApi.createPayment(payload),
|
||||
**4. Hash password**
|
||||
```typescript
|
||||
passwordHash = await bcrypt.hash('AdminPassword123', 12)
|
||||
```
|
||||
|
||||
**5. Create user**
|
||||
```typescript
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: 'admin-seed-001',
|
||||
phone: '+84900000001',
|
||||
phoneHash,
|
||||
email: 'admin@goodgo.vn',
|
||||
emailHash,
|
||||
passwordHash,
|
||||
fullName: 'Admin',
|
||||
role: 'ADMIN',
|
||||
kycStatus: 'VERIFIED',
|
||||
isActive: true,
|
||||
totpEnabled: false,
|
||||
totpBackupCodes: [],
|
||||
},
|
||||
});
|
||||
|
||||
// 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<string>('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 <div>{status === 'loading' ? 'Processing payment...' : status}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 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 <jwt_token>
|
||||
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 <jwt_token>
|
||||
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
|
||||
## 🧪 Test Login
|
||||
|
||||
```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
|
||||
curl -X POST http://localhost:3000/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"phone": "0900000001",
|
||||
"password": "AdminPassword123"
|
||||
}'
|
||||
```
|
||||
|
||||
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
|
||||
**Success Response:**
|
||||
```json
|
||||
{
|
||||
"requiresMfa": false,
|
||||
"tokens": {
|
||||
"accessToken": "eyJ...",
|
||||
"refreshToken": "eyJ...",
|
||||
"expiresIn": 3600
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Credentials
|
||||
## ⚠️ Common Issues
|
||||
|
||||
### 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
|
||||
```
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| User can't login | Check: `passwordHash` ≠ null, `isActive` = true |
|
||||
| "Invalid phone" | Phone must match regex (mobile only) |
|
||||
| Hash mismatch | Verify `FIELD_ENCRYPTION_KEY` is consistent |
|
||||
| MFA issue | Verify `MFA_BACKUP_CODE_SECRET` env var |
|
||||
| PII not encrypted | Verify key is exactly 32 bytes (64 hex chars) |
|
||||
|
||||
---
|
||||
|
||||
## Common Errors
|
||||
## 📁 Key Files
|
||||
|
||||
| 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 |
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `hashed-password.vo.ts` | bcrypt hashing |
|
||||
| `vietnam-phone.validator.ts` | Phone validation |
|
||||
| `field-encryption.ts` | AES-256-GCM encryption |
|
||||
| `local.strategy.ts` | Login endpoint |
|
||||
| `mfa.service.ts` | TOTP / backup codes |
|
||||
| `user.entity.ts` | User domain model |
|
||||
| `prisma-user.repository.ts` | User persistence |
|
||||
| `seed.ts` | Seed script |
|
||||
|
||||
---
|
||||
|
||||
## Debugging Checklist
|
||||
## 🔐 Checklist for Seed User
|
||||
|
||||
- [ ] 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
|
||||
- [ ] Password ≥ 8 chars
|
||||
- [ ] Phone matches regex
|
||||
- [ ] Phone normalized: +84...
|
||||
- [ ] Phone hashed: HMAC-SHA256
|
||||
- [ ] Email lowercased
|
||||
- [ ] Email hashed: HMAC-SHA256
|
||||
- [ ] Password hashed: bcrypt (12 rounds)
|
||||
- [ ] `isActive: true`
|
||||
- [ ] `passwordHash` ≠ null
|
||||
- [ ] `totpEnabled: false`
|
||||
- [ ] `totpBackupCodes: []`
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
## 📚 Full Documentation Files
|
||||
|
||||
- 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)
|
||||
1. **AUTHENTICATION_GUIDE.md** - Complete technical reference
|
||||
2. **AUTH_IMPLEMENTATION_CHECKLIST.md** - Implementation checklist & troubleshooting
|
||||
3. **SEED_GENERATION_SCRIPT.ts** - Ready-to-use seed script
|
||||
4. **QUICK_REFERENCE.md** - This file
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** April 12, 2026
|
||||
**Status:** ✅ Production-Ready
|
||||
|
||||
Reference in New Issue
Block a user