- Pricing page: enhanced with checkout modal integration, plan comparison table, and subscription funnel - Payment return page: new VNPay/MoMo callback handler - Subscription components: new checkout-modal with payment method selection (VNPay, MoMo, ZaloPay) - API modules: type-safe PII encryption, improved error handling in MFA/auth/payments/analytics/search/notifications modules - Audit docs: comprehensive Wave 13 platform assessment, pricing audit, production readiness checklist - Updated PROJECT_TRACKER with Wave 13 status Co-Authored-By: Paperclip <noreply@paperclip.ing>
1319 lines
36 KiB
Markdown
1319 lines
36 KiB
Markdown
# 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.
|
|
|