# GoodGo Platform AI - Pricing, Subscriptions & Payment Systems Audit

**Date:** April 12, 2026  
**Scope:** Comprehensive exploration of pricing pages, subscription plans, and payment checkout flows  
**Status:** Frontend pricing page ACTIVE, Backend subscription/payment modules ACTIVE, checkout flow NOT IMPLEMENTED

---

## Executive Summary

The GoodGo Platform has a **complete backend infrastructure** for subscriptions and payments with NestJS CQRS architecture, but the **frontend pricing-to-checkout flow is incomplete**. The pricing page exists and loads plans from the API, but there is **no checkout/payment flow component** linking the pricing page to the payment API.

### Key Findings:
- ✅ **Pricing Page:** Fully functional at `/pricing` with 4 tiers (FREE, AGENT_PRO, INVESTOR, ENTERPRISE)
- ✅ **Subscription Backend:** Complete with CQRS commands, entities, repositories
- ✅ **Payment Gateway Integration:** VNPay, MoMo, ZaloPay implementations ready
- ✅ **Payment API Endpoints:** Full CRUD operations available
- ❌ **Missing:** Checkout page/modal connecting pricing → payment creation → redirect to payment gateway
- ❌ **Missing:** Payment success callback handling on frontend
- ❌ **Missing:** Subscription setup after successful payment

---

## 1. Frontend Pricing Page

### Location
```
/Users/velikho/Desktop/WORKING/goodgo-platform-ai/apps/web/app/[locale]/(public)/pricing/page.tsx
```

### Current Implementation

**Route:** `/(public)/pricing`  
**Status:** ✅ Active and functional

#### Page Component Features:
```tsx
// Key Exports and Functionality
- PricingPage (default export)
- Uses useTranslations('pricing') for i18n
- Manages billingCycle state (monthly/yearly)
- Renders 4 plan cards
- Feature comparison table
- CTA sections
```

#### Plan Data Flow:
```
usePlans() hook (from use-subscription.ts)
  ↓
subscriptionApi.getPlans() 
  ↓
GET /subscriptions/plans (backend)
  ↓
Returns: PlanDto[] with pricing, features, limits
```

#### Fallback Plans (Hardcoded):
The page includes fallback plans when API unavailable:
- **FREE:** 0 VND, 3 listings, 5 saved searches
- **AGENT_PRO:** 499,000 VND/month, 50 listings, 30 searches (marked as "popular")
- **INVESTOR:** 999,000 VND/month, 20 listings, 100 searches
- **ENTERPRISE:** 4,990,000 VND/month, unlimited listings/searches

#### Key Components Used:
```tsx
- Badge, Button, Card
- Check, Crown, Rocket, Shield, X, Zap icons (lucide-react)
- useTranslations for 15+ translation keys
- formatVND for currency display
- cn() utility for conditional classes
```

#### Billing Cycle Toggle:
```tsx
// State management for monthly/yearly pricing
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');

// Displays appropriate price based on cycle
const price = billingCycle === 'monthly' 
  ? plan.priceMonthlyVND 
  : plan.priceYearlyVND;
```

#### Current CTAs:
All plans have a "Register" link:
```tsx
<Link href={'/register'}>
  <Button>
    {plan.tier === 'FREE' ? t('ctaFree') : t('ctaUpgrade')}
  </Button>
</Link>
```

**ISSUE:** CTAs link to `/register` instead of starting checkout flow

---

## 2. Frontend API Integration

### Files

#### `/apps/web/lib/subscription-api.ts`
**Type Definitions:**
```typescript
export interface PlanDto {
  id: string;
  tier: string;
  name: string;
  priceMonthlyVND: string;
  priceYearlyVND: string;
  maxListings: number;
  maxSavedSearches: number;
  features: Record<string, boolean | number | string>;
  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;
}
```

**API Methods:**
```typescript
subscriptionApi = {
  getPlans(): Promise<PlanDto[]>              // GET /subscriptions/plans
  getPlanByTier(tier): Promise<PlanDto>       // GET /subscriptions/plans/:tier
  getBillingHistory(limit, offset): Promise<BillingHistoryDto>
  checkQuota(metric): Promise<QuotaCheckResult>
  createSubscription(planTier, billingCycle): Promise<CreateSubscriptionResult>
  upgradeSubscription(newPlanTier): Promise<{message}>
  cancelSubscription(reason): Promise<{message}>
}
```

#### `/apps/web/lib/payment-api.ts`
**Type Definitions:**
```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;      // Direct payment gateway URL
  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;
}
```

**API Methods:**
```typescript
paymentApi = {
  createPayment(data): Promise<CreatePaymentResult>
  getPaymentStatus(id): Promise<PaymentStatusDto>
  getTransactions(params): Promise<TransactionListDto>
}
```

### 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,
};

// React Query hooks
export function usePlans()               // GET plans
export function useBillingHistory()      // GET billing history
export function useQuota(metric: string) // GET quota check
```

#### `/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,
};

// React Query hooks
export function useTransactions(params)  // GET user transactions
export function usePaymentStatus(id)     // GET single payment status
```

---

## 3. Subscription Backend (NestJS CQRS)

### Module Location
```
/apps/api/src/modules/subscriptions/
```

### Architecture

**Module:** `subscriptions.module.ts`  
**Pattern:** Hexagonal Architecture (DDD + CQRS)

#### Domain Layer
```
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)
```

**SubscriptionEntity Class:**
```typescript
export class SubscriptionEntity extends AggregateRoot<string> {
  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<void>
  cancel(): Result<void>
  markExpired(): Result<void>
  markPastDue(): Result<void>
  renewPeriod(newStart, newEnd): void
  isActive(): boolean
  isExpired(): boolean
}
```

#### Application Layer
**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              ← Returns PlanDto[]
├── check-quota/
│   ├── check-quota.query.ts
│   └── check-quota.handler.ts
└── get-billing-history/
    ├── get-billing-history.query.ts
    └── get-billing-history.handler.ts
```

#### Infrastructure Layer
```
infrastructure/
├── repositories/prisma-subscription.repository.ts
└── event-handlers/listing-created-usage.handler.ts
```

#### Presentation Layer
```
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
```

### Subscription API Endpoints

#### Public Endpoints
```
GET /subscriptions/plans
  → Returns: PlanDto[]
  
GET /subscriptions/plans/:tier
  → Returns: PlanDto
```

#### Authenticated Endpoints
```
POST /subscriptions
  Body: { planTier: PlanTier, billingCycle: 'monthly' | 'yearly' }
  Returns: CreateSubscriptionResult
  
PUT /subscriptions/upgrade
  Body: { newPlanTier: PlanTier }
  Returns: UpgradeSubscriptionResult
  
DELETE /subscriptions
  Body: { reason?: string }
  Returns: CancelSubscriptionResult
  
POST /subscriptions/usage
  Body: { metric: string, count: number }
  Returns: MeterUsageResult
  
GET /subscriptions/quota/:metric
  Returns: QuotaCheckResult
  
GET /subscriptions/billing?limit=20&offset=0
  Returns: BillingHistoryDto
```

### CreateSubscriptionHandler Logic

```typescript
async execute(command): Promise<CreateSubscriptionResult> {
  // 1. Check user doesn't already have active subscription
  if (existing && (status === 'ACTIVE' || 'PAST_DUE')) {
    throw ConflictException('User already has active subscription');
  }

  // 2. Fetch Plan from DB
  const plan = prisma.plan.findFirst({
    where: { tier: command.planTier, isActive: true }
  });

  // 3. Calculate period
  const now = new Date();
  const periodEnd = new Date(now);
  if (yearly) periodEnd.setFullYear(+1);
  else periodEnd.setMonth(+1);

  // 4. Create domain entity
  const subscription = SubscriptionEntity.createNew(
    id, userId, planId, planTier, now, periodEnd
  );

  // 5. Save to repo
  await subscriptionRepo.save(subscription);

  // 6. Publish domain events
  eventBus.publish(subscription.clearDomainEvents());

  return {
    subscriptionId,
    planTier,
    status: 'ACTIVE',
    currentPeriodStart: now,
    currentPeriodEnd: periodEnd,
  };
}
```

---

## 4. Payment Backend (NestJS CQRS)

### Module Location
```
/apps/api/src/modules/payments/
```

### Architecture

#### Domain Layer
```
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
```

**PaymentEntity Class:**
```typescript
export class PaymentEntity extends AggregateRoot<string> {
  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<void>
  markFailed(callbackData): Result<void>
  markRefunded(): Result<void>
  emitCompleted(): void
  emitFailed(): void
}
```

#### Application Layer
**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
│   └── (no DTO - webhook endpoint)
└── 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
```

#### Infrastructure Layer (Payment Gateways)
```
infrastructure/services/
├── payment-gateway.interface.ts           ← IPaymentGateway interface
├── payment-gateway.factory.ts
├── vnpay.service.ts                      ← ✅ Implemented
├── momo.service.ts                       ← ✅ Implemented
└── zalopay.service.ts                    ← ✅ Implemented
```

**IPaymentGateway Interface:**
```typescript
export interface IPaymentGateway {
  readonly provider: PaymentProvider;
  createPaymentUrl(params: CreatePaymentUrlParams): Promise<CreatePaymentUrlResult>;
  verifyCallback(data: Record<string, string>): CallbackVerifyResult;
  refund(params: RefundParams): Promise<RefundResult>;
}

export interface CreatePaymentUrlParams {
  orderId: string;
  amountVND: bigint;
  description: string;
  returnUrl: string;
  ipAddress: string;
}

export interface CreatePaymentUrlResult {
  paymentUrl: string;        // Direct to payment gateway
  providerTxId: string;      // Gateway transaction ID
}

export interface CallbackVerifyResult {
  isValid: boolean;
  orderId: string;           // Our payment ID
  providerTxId: string;      // Gateway transaction ID
  isSuccess: boolean;        // Payment successful?
  rawData: Record<string, unknown>;
}
```

### Payment Gateway Implementations

#### VNPay Service
```typescript
private readonly tmnCode: string;          // Terminal ID
private readonly hashSecret: string;       // HMAC key

async createPaymentUrl(params): Promise<{paymentUrl, providerTxId}> {
  // Build VNPay params (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 uses cents
    vnp_ReturnUrl: params.returnUrl,
    vnp_IpAddr: params.ipAddress,
    vnp_CreateDate: yyyyMMddHHmmss,
    vnp_ExpireDate: yyyyMMddHHmmss + 15min,
  };

  // Sign with HMAC SHA-512
  const signed = hmac('sha512', this.hashSecret).digest('hex');
  vnpParams['vnp_SecureHash'] = signed;

  // Return redirect URL
  return {
    paymentUrl: `https://sandbox.vnpayment.vn/paymentv2/vpcpay.html?${params}`,
    providerTxId: params.orderId,
  };
}

verifyCallback(data: Record<string, string>): CallbackVerifyResult {
  // VNPay returns: vnp_SecureHash, vnp_ResponseCode, vnp_TransactionNo, etc.
  // Verify hash signature
  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();
  
  // Build signature: 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 to 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
Similar structure with ZaloPay-specific endpoints and signature algorithm (HMAC SHA-256)

### Payment API Endpoints

#### Public Endpoints
```
POST /payments/callback/:provider
  Query/Body: Provider callback data (VNPay via query, MoMo/ZaloPay via body)
  Returns: HandleCallbackResult
  Webhook for payment provider callbacks
```

#### Authenticated Endpoints
```
POST /payments
  Body: CreatePaymentDto {
    provider: PaymentProvider,
    type: PaymentType,
    amountVND: number,
    description: string,
    returnUrl: string,
    transactionId?: string,
    idempotencyKey?: string
  }
  Returns: CreatePaymentResult {
    paymentId: string,
    paymentUrl: string,       ← Redirect user here
    providerTxId: string
  }

GET /payments
  Query: { status?, limit?, offset? }
  Returns: TransactionListDto

GET /payments/:id
  Returns: PaymentStatusDto

POST /payments/:id/refund (Admin only)
  Body: { reason: string }
  Returns: RefundPaymentResult
```

### CreatePaymentHandler Logic

```typescript
async execute(command): Promise<CreatePaymentResult> {
  // 1. Idempotency check (prevent duplicate payments)
  if (command.idempotencyKey) {
    const existing = await paymentRepo.findByIdempotencyKey(key);
    if (existing && (status === 'PENDING' || 'PROCESSING')) {
      throw ConflictException('Payment already exists');
    }
  }

  // 2. Validate amount (1 VND to 100 billion VND)
  const money = Money.create(command.amountVND);
  if (money.isErr) throw ValidationException();

  // 3. Create domain entity
  const payment = PaymentEntity.createNew(
    id, userId, provider, type, money, ...
  );

  // 4. Get payment gateway and create 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. Mark as PROCESSING and save
  payment.markProcessing(providerTxId);
  await paymentRepo.save(payment);

  // 6. Publish domain events
  eventBus.publish(payment.clearDomainEvents());

  return {
    paymentId,
    paymentUrl,               ← Client redirects to this
    providerTxId,
  };
}
```

---

## 5. Prisma Data Models

### Location
```
/prisma/schema.prisma (lines 451-514)
```

### Plan Model
```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      // JSON object with boolean/number features
  isActive            Boolean   @default(true)

  subscriptions Subscription[]
}

enum PlanTier {
  FREE
  AGENT_PRO
  INVESTOR
  ENTERPRISE
}
```

### Subscription Model
```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
}
```

### Payment Model
```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
}
```

### UsageRecord Model
```prisma
model UsageRecord {
  id             String       @id @default(cuid())
  subscriptionId String
  subscription   Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
  metric         String       // e.g., "listings_created", "searches_performed"
  count          Int
  periodStart    DateTime
  periodEnd      DateTime

  @@index([subscriptionId, metric])
}
```

---

## 6. Payments Dashboard

### Location
```
/apps/web/app/[locale]/(dashboard)/dashboard/payments/page.tsx
```

### Current State
✅ **Implemented:** View transaction history with filtering and pagination

#### Features:
- Transaction table with columns: Date, Type, Provider, Amount, Status, Transaction ID
- Status filter dropdown (ALL, PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED)
- Summary cards: Total transactions, Total completed amount, Pending count
- Pagination (20 items per page)
- Mobile-friendly card layout

#### Hooks Used:
```typescript
const { data: transactions, isLoading } = useTransactions({
  status: statusFilter,
  limit: 20,
  offset: page * 20,
});
```

#### No Payment Creation UI
❌ **Missing:** No button/flow to create new payment from this page

---

## 7. i18n Configuration

### Location
```
/apps/web/messages/
├── vi.json
└── en.json
```

### Pricing Translations (Vietnamese example)
```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. Missing Components & Flows

### ❌ Critical Missing Pieces

#### 1. Checkout Page/Modal Component
```
Status: NOT IMPLEMENTED
Needed: Component to initiate payment for selected plan

Flow Should Be:
  Pricing Page (Select plan + billing cycle)
    ↓
  [Missing] Checkout Modal/Page
    ↓ (Confirm plan details, display amount)
  User clicks "Pay Now"
    ↓
  Create Payment → POST /payments
    ↓
  Get paymentUrl + paymentId
    ↓
  Redirect to payment gateway (VNPay/MoMo/ZaloPay)
    ↓
  User pays
    ↓
  [Missing] Success callback handler
    ↓
  [Missing] Create subscription → POST /subscriptions
```

#### 2. Payment Success Callback Handler
```
Status: NOT IMPLEMENTED
Location: Backend has POST /payments/callback/:provider
Frontend Needs: Page to handle redirect from payment gateway after payment

Flow:
  Payment gateway redirects to returnUrl with callback params
    ↓
  Frontend callback handler page receives payment status
    ↓
  Query POST /payments/callback/:provider (verify)
    ↓
  If COMPLETED → Call POST /subscriptions (create subscription)
    ↓
  Redirect to success page or dashboard
```

#### 3. Subscription Setup After Payment
```
Status: NOT IMPLEMENTED
After successful payment:
  1. Check payment status
  2. If COMPLETED, create subscription automatically
  3. Sync subscription data to client
  4. Redirect to dashboard or subscription page
```

#### 4. Subscription Management UI
```
Status: Partially missing
Exists:
  - Billing history view (in payments page)
  - API endpoints for upgrade, cancel, meter usage
Missing:
  - UI to upgrade/downgrade plan
  - UI to cancel subscription
  - Quota/usage display component
```

---

## 9. Proposed Checkout Flow Architecture

### Frontend Checkout Journey
```
1. User on /pricing page
   ↓
2. Clicks plan "Upgrade" button
   ↓
3. Open Checkout Modal/Page
   - Show plan details
   - Show total amount (monthly or yearly)
   - Select payment provider (VNPay, MoMo, ZaloPay)
   - Show terms & conditions checkbox
   ↓
4. User clicks "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. Receive CreatePaymentResult:
   - paymentId
   - paymentUrl (from gateway)
   - providerTxId
   ↓
7. Redirect window.location = paymentUrl
   ↓
8. User completes payment at gateway
   ↓
9. Gateway redirects to returnUrl?paymentId=...&status=...
   ↓
10. Frontend callback handler page:
    - Extract paymentId from URL
    - Poll GET /payments/{paymentId} until status = COMPLETED
    - When completed, POST /subscriptions {
        planTier: selectedPlanTier,
        billingCycle: 'monthly' | 'yearly'
      }
    ↓
11. Show success message
    ↓
12. Redirect to /dashboard/subscription or home
```

### Key Components Needed
```typescript
// 1. Checkout Modal/Page
<CheckoutModal
  plan={selectedPlan}
  billingCycle="monthly"
  onClose={handleClose}
  onSuccess={handlePaymentSuccess}
/>

// 2. Payment Provider Selector
<PaymentProviderSelect
  providers={['VNPAY', 'MOMO', 'ZALOPAY']}
  selected={selectedProvider}
  onChange={setSelectedProvider}
/>

// 3. Payment Return Handler
// /payment-return page
// - Detects payment status from URL
// - Creates subscription
// - Redirects to dashboard

// 4. Subscription Status Component
<SubscriptionStatus
  subscription={currentSubscription}
  onUpgrade={handleUpgrade}
  onCancel={handleCancel}
/>
```

---

## 10. Implementation Checklist

### Phase 1: Checkout Modal (Priority HIGH)
- [ ] Create `<CheckoutModal />` component
- [ ] Add payment provider selector (VNPay, MoMo, ZaloPay)
- [ ] Show plan summary and total amount
- [ ] Add terms & conditions checkbox
- [ ] Replace pricing page CTA to open checkout instead of going to register

### Phase 2: Payment Gateway Redirect (Priority HIGH)
- [ ] Implement payment creation mutation (useMutation + paymentApi.createPayment)
- [ ] Handle paymentUrl redirect
- [ ] Pass idempotencyKey to prevent duplicate payments
- [ ] Handle loading/error states

### Phase 3: Payment Return Handler (Priority HIGH)
- [ ] Create `/payment-return` or `/checkout/return` page
- [ ] Extract paymentId from URL params
- [ ] Implement polling mechanism for payment status
- [ ] Create subscription after successful payment
- [ ] Show success/failure UI

### Phase 4: Subscription Management (Priority MEDIUM)
- [ ] Create subscription detail page
- [ ] Show current plan tier and renewal date
- [ ] Add upgrade plan button (opens checkout modal)
- [ ] Add cancel subscription button with confirmation
- [ ] Display usage/quota information

### Phase 5: Webhook Handling (Priority MEDIUM)
- [ ] Ensure backend webhook handlers working
- [ ] Test VNPay callback
- [ ] Test MoMo callback
- [ ] Test ZaloPay callback
- [ ] Verify payment status updates in real-time

### Phase 6: Testing & Optimization (Priority MEDIUM)
- [ ] E2E test complete checkout flow
- [ ] Test with all 3 payment providers
- [ ] Handle edge cases (payment timeout, user closes window, etc.)
- [ ] Add error recovery flows
- [ ] Performance optimization (lazy load modals, etc.)

---

## 11. Environment Configuration

### Required Env Vars (Backend)
```
# VNPay
VNPAY_TMN_CODE=xxxxx
VNPAY_HASH_SECRET=xxxxx
VNPAY_BASE_URL=https://sandbox.vnpayment.vn/paymentv2/vpcpay.html (or 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 (or production)

# ZaloPay
ZALOPAY_APP_ID=xxxxx
ZALOPAY_KEY1=xxxxx
ZALOPAY_KEY2=xxxxx
ZALOPAY_ENDPOINT=https://sandbox.zalopay.com.vn (or production)
```

### Frontend Config
```typescript
// Return URL for all payment providers
const PAYMENT_RETURN_URL = `${process.env.NEXT_PUBLIC_APP_URL}/payment-return`;

// Payment polling config
const PAYMENT_POLL_INTERVAL = 2000;   // 2 seconds
const PAYMENT_POLL_MAX_ATTEMPTS = 30; // 60 seconds total
```

---

## 12. Edge Cases & Error Handling

### Scenarios to Handle

1. **User closes payment gateway window**
   - Payment remains PENDING in DB
   - Show "Payment incomplete" message
   - Offer retry option

2. **Payment timeout**
   - Gateway doesn't complete within expected time
   - Show "Payment timed out" message
   - Allow retry

3. **Payment gateway error**
   - Gateway returns error during createPaymentUrl
   - Show provider-specific error message
   - Allow retry with different provider

4. **Duplicate payment attempts**
   - Use idempotencyKey to prevent duplicates
   - Backend returns existing payment if key matches

5. **Subscription creation fails after payment**
   - Payment COMPLETED but subscription creation fails
   - Manual intervention needed (admin dashboard)
   - Show message: "Payment successful but subscription not created. Please contact support."

6. **User navigates away from return page**
   - Payment was successful but redirect didn't complete
   - Subscription was created in background
   - User should see subscription in dashboard on next login

---

## 13. Testing Strategy

### Unit Tests
```typescript
// usePaymentCheckout hook
describe('usePaymentCheckout', () => {
  it('should create payment and return paymentUrl');
  it('should handle payment provider errors');
  it('should validate idempotency key');
});

// CheckoutModal component
describe('<CheckoutModal />', () => {
  it('should render plan details');
  it('should show payment providers');
  it('should create payment on submit');
  it('should disable submit while loading');
});
```

### Integration Tests
```typescript
// Full checkout flow
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 Tests (Playwright)
```typescript
// Full user journey with test payment providers
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. Current State Summary Table

| Component | Location | Status | Notes |
|-----------|----------|--------|-------|
| **Pricing Page** | `/pricing` | ✅ Active | Displays 4 tiers with correct prices |
| **Plan API** | `GET /subscriptions/plans` | ✅ Active | Returns PlanDto[] |
| **usePlans Hook** | `lib/hooks/use-subscription.ts` | ✅ Active | Uses React Query |
| **Payment API** | `POST /payments` | ✅ Active | Creates payment & returns paymentUrl |
| **Payment Gateways** | 3x services (VNPay, MoMo, ZaloPay) | ✅ Implemented | Ready to use |
| **Subscription Creation** | `POST /subscriptions` | ✅ Active | Creates subscription after payment |
| **Checkout Modal** | - | ❌ Missing | Needed to link pricing → payment |
| **Payment Return Handler** | - | ❌ Missing | Needed to handle gateway redirect |
| **Subscription Management UI** | - | ⚠️ Partial | Exists for viewing, missing for modify |
| **Idempotency Support** | Payment model | ✅ Active | Built into schema |
| **Webhook Handlers** | `POST /payments/callback/:provider` | ✅ Active | Receives & verifies callbacks |
| **Payment Status Polling** | `GET /payments/:id` | ✅ Active | Can poll status on return |
| **Event-Driven Updates** | Domain events | ✅ Active | PaymentCompleted events published |
| **Notification Listeners** | 3x listeners | ✅ Active | payment-completed, payment-failed, payment-refunded |

---

## 15. Key Files Reference

### Frontend
```
/apps/web/
├── app/[locale]/(public)/pricing/page.tsx          [Main pricing page]
├── lib/
│   ├── subscription-api.ts                         [Subscription API client]
│   ├── payment-api.ts                              [Payment API client]
│   └── hooks/
│       ├── use-subscription.ts                     [Subscription hooks]
│       └── use-payments.ts                         [Payment hooks]
└── app/[locale]/(dashboard)/dashboard/payments/page.tsx [Transaction history]
```

### 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 (lines 469-483)
├── model Subscription (lines 485-502)
├── model Payment (lines 424-449)
└── model UsageRecord (lines 504-514)
```

---

## Conclusion

The GoodGo Platform has **excellent backend infrastructure** for subscriptions and payments with a complete payment gateway integration (VNPay, MoMo, ZaloPay). The frontend pricing page is functional but **lacks the critical checkout flow** to convert viewers into paying customers.

### Immediate Next Steps:
1. **Create Checkout Modal** - Connect pricing page to payment creation
2. **Implement Payment Return Handler** - Handle payment gateway redirects
3. **Add Subscription Auto-Creation** - Automatically create subscription after successful payment
4. **Build Subscription Management UI** - Allow users to view and manage their subscriptions

The foundation is solid; only the user-facing checkout flow is missing.

