docs: rewrite story 3.5 and epic 5 stories to match actual codebase

Keeps subscription SaaS vision from PRD while adding accurate as-is
analysis of existing code. Each story now has an "As-Is" table showing
what exists and where the gaps are.

Key points:
- Story 3.5: Transition from BYOK to system-managed models with token
  billing. BYOK stays for self-hosted mode (deployment_mode=self-hosted),
  system models + subscription quota for hosted mode.
- Story 5.1: Pricing UI exists (Free/PAYG/Enterprise) but needs redesign
  to subscription tiers (Free/Pro) with monthly/yearly toggle.
- Story 5.2: PAYG checkout exists (mode=payment), need NEW subscription
  endpoint (mode=subscription) with stripe_customer_id binding.
- Story 5.3: Webhook infrastructure exists (signature verify, PAYG handlers).
  Need subscription event handlers (customer.subscription.*) alongside.
- Story 5.4: PageLimitService fully implemented. Gap is HTTP-layer pre-check,
  plan-based limits, frontend quota indicator, and 402 error handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vonic 2026-04-14 14:19:57 +07:00
parent a368dbcb08
commit e7382b26de
5 changed files with 268 additions and 149 deletions

View file

@ -1,9 +1,7 @@
# Story 5.2: Tích hợp Stripe Checkout (Stripe Payment Integration)
# Story 5.2: Tích hợp Stripe Subscription Checkout (Stripe Payment Integration)
Status: ready-for-dev
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## Story
As a Người dùng,
@ -12,35 +10,56 @@ so that tôi có thể điền thông tin thẻ tín dụng mà không sợ bị
## Acceptance Criteria
1. Khi User bấm thanh toán, BE gọi API Stripe lấy `sessionId` của Stripe Checkout (Chế độ Subscription / Recurring Mode, không phải Chế độ Mua đứt OTP).
2. Hệ thống redirect User an toàn qua cổng Stripe được Hosted trực tiếp bởi Stripe Server.
1. Khi User bấm thanh toán, BE gọi API Stripe lấy `sessionId` với **`mode='subscription'`** (recurring billing, không phải one-time payment).
2. Hệ thống redirect User an toàn qua cổng Stripe Hosted Checkout.
3. Sau thanh toán thành công, user được redirect về app với subscription activated.
## As-Is (Code hiện tại)
| Component | Hiện trạng | File |
|-----------|-----------|------|
| Checkout Endpoint | **Đã tồn tại** nhưng chỉ hỗ trợ `mode='payment'` (one-time page packs) | `surfsense_backend/app/routes/stripe_routes.py` line ~205 |
| Checkout Request Schema | `CreateCheckoutSessionRequest(search_space_id, quantity)` — cho page packs | `stripe_routes.py` |
| Stripe Client | **Đã tồn tại**`get_stripe_client()`, config từ env vars | `stripe_routes.py` |
| Success/Cancel URLs | **Đã tồn tại**`_get_checkout_urls()` | `stripe_routes.py` |
| Stripe Config | `STRIPE_SECRET_KEY`, `STRIPE_PRICE_ID`, `STRIPE_WEBHOOK_SECRET` đã có | `config.py` |
| User ↔ Stripe mapping | **Không có** `stripe_customer_id` trên User model | `db.py` |
**Gap:** Cần thêm endpoint subscription checkout **mới** (giữ endpoint page purchase cũ nếu muốn). Cần `stripe_customer_id` để Stripe quản lý recurring billing.
## Tasks / Subtasks
- [ ] Task 1: Nâng cấp `stripe_routes.py`
- [ ] Subtask 1.1: Bổ sung Endpoint POST `/api/v1/stripe/create-subscription-checkout`.
- [ ] Subtask 1.2: Phân tích `plan_id` từ Request body hoặc User, cấu hình `mode='subscription'` trong dict truyền cho thư viện Stripe (thay vì chế độ `payment` cũ).
- [ ] Task 2: Liên kết Action Nút ở UI
- [ ] Subtask 2.1: Ở `Pricing` Component, xử lý `onClick` bằng cách submit POST form request tới API mới, bắt `checkout_url` và route trình duyệt tới URL đó bằng thẻ A hoặc JS `window.location.href`.
- [ ] Task 1: Thêm `stripe_customer_id` vào User (Backend DB)
- [ ] Subtask 1.1: Alembic migration thêm column `stripe_customer_id` (String, nullable, unique, indexed) vào `User`.
- [ ] Subtask 1.2: Tạo helper function `get_or_create_stripe_customer(user)` — nếu `stripe_customer_id` null → gọi `stripe.customers.create(email=user.email)` → lưu ID vào DB.
- [ ] Task 2: Tạo Subscription Checkout Endpoint (Backend)
- [ ] Subtask 2.1: Tạo endpoint `POST /api/v1/stripe/create-subscription-checkout`.
- [ ] Subtask 2.2: Request body: `{ "plan_id": "pro_monthly" | "pro_yearly" }`.
- [ ] Subtask 2.3: Map `plan_id` → Stripe Price ID từ env vars (`STRIPE_PRO_MONTHLY_PRICE_ID`, `STRIPE_PRO_YEARLY_PRICE_ID`). Tuyệt đối **không nhận price từ frontend** — phòng tránh giả mạo giá.
- [ ] Subtask 2.4: Gọi `stripe.checkout.sessions.create(mode='subscription', customer=stripe_customer_id, ...)`.
- [ ] Subtask 2.5: Trả về `{ "checkout_url": "https://checkout.stripe.com/..." }`.
- [ ] Task 3: Kết nối Frontend với Endpoint mới
- [ ] Subtask 3.1: Từ `pricing-section.tsx`, nút "Upgrade to Pro" gọi `POST /api/v1/stripe/create-subscription-checkout` với `plan_id`.
- [ ] Subtask 3.2: Redirect đến `checkout_url`.
- [ ] Subtask 3.3: Xử lý success return URL — hiển thị toast "Subscription activated!"
## Dev Notes
### Relevant Architecture Patterns & Constraints
- Codebase hiện có `surfsense_backend/app/routes/stripe_routes.py` nhưng ĐANG GẮN code Payment Intent cho Token "Page Purchase" One-time 1 lần. DEV cần lưu ý CẤU HÌNH LẠI để route mới sinh này phục vụ riêng cho gói Subscription hàng tháng (Gửi theo CustomerID nếu User đã bind).
- Security: Giá `stripe_price_id` bắt buộc phải map và define ở Back-End environment variable (Vd `STRIPE_PRO_PLAN_ID`), tuyệt đối không chấp nhận param `price` từ Header gửi lên (Phòng tránh giả mạo giá tiền).
### Giữ song song PAYG và Subscription?
Endpoint page purchase cũ (`create-checkout-session` với `mode='payment'`) có thể giữ nguyên. Endpoint subscription mới chạy song song. Quyết định business logic.
### Project Structure Notes
- Module thay đổi:
- `surfsense_backend/app/routes/stripe_routes.py`
- `surfsense_web/src/pages/pricing/page.tsx`
### Security
- Giá `stripe_price_id` map ở **backend env vars** (`STRIPE_PRO_MONTHLY_PRICE_ID`, `STRIPE_PRO_YEARLY_PRICE_ID`).
- Frontend chỉ gửi `plan_id` (string enum), backend resolve ra Stripe Price ID.
### Stripe Customer
Khi tạo subscription checkout, **bắt buộc** phải có `customer` parameter. Stripe dùng customer ID để quản lý recurring billing, invoices, payment methods. Nên create Stripe customer ngay khi user đăng ký hoặc lần đầu checkout.
### Webhook (xem Story 5.3)
Sau checkout, Stripe sẽ gửi `checkout.session.completed` → webhook handler cần detect `mode='subscription'` và activate subscription trên DB.
### References
- [Epic 5.2 - Subscriptions]
## Dev Agent Record
### Agent Model Used
Antigravity Claude 3.5 Sonnet Engine
### File List
- `surfsense_backend/app/routes/stripe_routes.py`
- `surfsense_backend/app/routes/stripe_routes.py` — endpoint PAYG hiện tại (tham khảo pattern)
- Stripe Subscription Checkout docs: https://stripe.com/docs/billing/subscriptions/build-subscriptions