mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-26 01:06:23 +02:00
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:
parent
a368dbcb08
commit
e7382b26de
5 changed files with 268 additions and 149 deletions
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
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,51 +10,71 @@ so that tôi có thể dùng trực tiếp và chi phí sử dụng được tr
|
|||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Hệ thống cung cấp Dropdown chọn `LLM Model` trong giao diện Chat.
|
||||
2. Tuyệt đối không hiển thị ô nhập "Your API Key" ở Frontend (Xóa bỏ logic Bring-Your-Own-Key cũ nếu có).
|
||||
3. Hệ thống Backend tính toán chi phí (Token * Đơn giá của Model).
|
||||
4. Hệ thống kiểm tra Quota hàng tháng của người dùng; nếu vượt quá (Quota Exceeded), trả về lỗi 403/429 báo "Hãy nâng cấp gói".
|
||||
1. Hệ thống cung cấp Dropdown chọn `LLM Model` trong giao diện Chat — danh sách model do hệ thống quản lý.
|
||||
2. Tuyệt đối không hiển thị ô nhập "Your API Key" ở Frontend khi `DEPLOYMENT_MODE=hosted` (PRD: "Tuyệt đối không hỗ trợ chức năng User tự nhập LLM API Key riêng nhằm kiểm soát chất lượng và doanh thu").
|
||||
3. Hệ thống Backend tính toán chi phí (Token × Đơn giá của Model) và trừ vào quota subscription.
|
||||
4. Hệ thống kiểm tra Quota hàng tháng của người dùng; nếu vượt quá (Quota Exceeded), trả về lỗi 402/429 báo "Hãy nâng cấp gói".
|
||||
|
||||
## As-Is (Code hiện tại — cần thay đổi)
|
||||
|
||||
| Component | Hiện trạng | File |
|
||||
|-----------|-----------|------|
|
||||
| Frontend Model Selector | BYOK: user chọn từ `NewLLMConfig` do mình tự tạo (tự nhập API key) | `surfsense_web/components/new-chat/model-selector.tsx` |
|
||||
| Frontend LLM Config UI | Cho user nhập API key, chọn provider, model name | `surfsense_web/app/dashboard/[search_space_id]/llm-configs/` |
|
||||
| Backend LLM Config | `NewLLMConfig` table lưu `api_key`, `llm_model_name`, `provider` per-user | `surfsense_backend/app/db.py` |
|
||||
| Backend Chat Streaming | Dùng API key từ user's `NewLLMConfig` để gọi LLM | Chat routes / RAG engine |
|
||||
| Quota cho LLM | **Không tồn tại** — chỉ có `pages_limit`/`pages_used` cho document ETL | `surfsense_backend/app/db.py` |
|
||||
| PageLimitService | Đã implement đầy đủ cho document quota — có thể dùng làm pattern | `surfsense_backend/app/services/page_limit_service.py` |
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Task 1: Dọn dẹp Kiến trúc Client cũ (Frontend)
|
||||
- [ ] Subtask 1.1: Gỡ bỏ flow nhập `api_key` tự do ở giao diện LLM Configs (`surfsense_web/app/dashboard/[search_space_id]/llm-configs`), khoá cứng các tuỳ chọn chọn Provider.
|
||||
- [ ] Subtask 1.2: Triển khai Component `ModelSelector` trên UI Chat (hoặc sửa đổi UI chọn LLM Config cũ sang danh sách LLM thương mại).
|
||||
- [ ] Task 2: Cập nhật Schema & Table Quản lý Quota (Backend - `db.py`)
|
||||
- [ ] Subtask 2.1: Bổ sung column `token_balance` (Integer) vào model `User` (hoặc tạo table `UserSubscription`).
|
||||
- [ ] Subtask 2.2: Bổ sung ENUM `LLM_MODEL` vào config db.
|
||||
- [ ] Task 3: Tích hợp logic Trừ Quota vào RAG Engine (Backend - `rag_engine` / `chat_session_api`)
|
||||
- [ ] Subtask 3.1: Ở API Endpoint Streaming, kiểm tra `token_balance` trước khi khởi tạo luồng SSE. Nếu <= 0, trả HTTP 402/429.
|
||||
- [ ] Subtask 3.2: Dùng `tiktoken` hoặc đếm chữ ước tính số tokens Generate để UPDATE trừ lùi vào Database sau mỗi chu kỳ trả lời xong.
|
||||
- [ ] Task 4: Hiển thị Banner nâng cấp (Frontend)
|
||||
- [ ] Subtask 4.1: Bắt lỗi 402/429 từ SSE, render Alert UI Upgrade.
|
||||
- [ ] Task 1: Tạo System Model Catalog (Backend)
|
||||
- [ ] Subtask 1.1: Tạo config/table `SystemLLMModel` với fields: `model_id`, `provider` (openai/anthropic), `model_name`, `display_name`, `cost_per_1k_input_tokens`, `cost_per_1k_output_tokens`, `tier_required` (free/pro). Có thể dùng Enum hoặc DB table.
|
||||
- [ ] Subtask 1.2: Backend đọc API keys từ env vars (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`) — không lưu vào DB per-user.
|
||||
- [ ] Subtask 1.3: Tạo endpoint `GET /api/v1/models` trả danh sách models khả dụng (filtered theo subscription tier của user).
|
||||
|
||||
- [ ] Task 2: Cập nhật Chat Streaming để dùng System Keys (Backend)
|
||||
- [ ] Subtask 2.1: Sửa RAG engine / chat streaming endpoint để nhận `model_id` thay vì `llm_config_id`.
|
||||
- [ ] Subtask 2.2: Resolve API key từ env vars dựa trên `provider` của model, không từ user's `NewLLMConfig`.
|
||||
- [ ] Subtask 2.3: Sau khi stream xong, đếm tokens (dùng `tiktoken` cho OpenAI hoặc response metadata) → gọi atomic UPDATE `token_balance = token_balance - cost` (tránh race condition khi mở 2 tab chat đồng thời).
|
||||
|
||||
- [ ] Task 3: Thêm Token Quota vào User Model (Backend DB)
|
||||
- [ ] Subtask 3.1: Alembic migration thêm columns vào `User`: `monthly_token_limit` (Integer), `tokens_used_this_month` (Integer), `token_reset_date` (Date), `subscription_status` (Enum: free/active/canceled/past_due), `plan_id` (String).
|
||||
- [ ] Subtask 3.2: Logic reset `tokens_used_this_month = 0` khi đến `token_reset_date` (middleware hoặc webhook trigger khi subscription renews).
|
||||
|
||||
- [ ] Task 4: Quota Check trước khi gọi LLM (Backend — Fail-fast)
|
||||
- [ ] Subtask 4.1: Trước khi gọi LLM trong SSE stream, check `tokens_used_this_month < monthly_token_limit`. Nếu vượt → raise HTTPException 402 "Token quota exceeded. Upgrade your plan."
|
||||
- [ ] Subtask 4.2: (Optional) Ước tính input tokens trước khi gọi để pre-check.
|
||||
|
||||
- [ ] Task 5: Frontend — System Model Selector (thay thế BYOK)
|
||||
- [ ] Subtask 5.1: Tạo component `SystemModelSelector` — fetch `GET /api/v1/models`, hiển thị dropdown với model name + cost indicator.
|
||||
- [ ] Subtask 5.2: Conditional rendering: nếu `NEXT_PUBLIC_DEPLOYMENT_MODE=hosted` → dùng `SystemModelSelector`; nếu `self-hosted` → giữ BYOK hiện tại.
|
||||
- [ ] Subtask 5.3: Ẩn/disable trang `llm-configs` (nhập API key) khi ở hosted mode.
|
||||
|
||||
- [ ] Task 6: Frontend — Upgrade Prompt khi hết quota
|
||||
- [ ] Subtask 6.1: Bắt lỗi 402 từ SSE stream, hiển thị modal "Bạn đã hết token quota. Nâng cấp gói tại /pricing".
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Relevant Architecture Patterns & Constraints
|
||||
- **Billing Pivot Constraint:** Đây là cốt lõi của quá trình chuyển đổi sang nền tảng Thương mại (SaaS SaaS Commercialization). CẤM các logic Hardcode API key người dùng truyền vào, tất cả Server Auth phải lấy từ biến môi trường `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`.
|
||||
- **Concurrency & Database Locks:** Việc trừ Token balance phải chịu sự cạnh tranh cao (ví dụ mở 2 tab chat đồng thời). Yêu cầu cân nhắc dùng `session.execute(update(User).where(...).values(token_balance=User.token_balance - cost))` để tận dụng Database atomic locks (tránh Race conditions).
|
||||
- **RAG Endpoint:** `Surfsense` stream thông qua giao thức SSE ở `/api/v1/chat/stream`. Hãy thêm interceptor kiểm tra số dư ngay đầu route (Fail-fast).
|
||||
### Deployment Mode
|
||||
Dùng `NEXT_PUBLIC_DEPLOYMENT_MODE` để phân biệt:
|
||||
- `self-hosted`: Giữ BYOK (user tự quản lý API keys) — không billing
|
||||
- `hosted`: System model catalog + token billing + subscription enforcement
|
||||
|
||||
### Project Structure Notes
|
||||
- Module thay đổi:
|
||||
- `surfsense_backend/app/db.py`
|
||||
- `surfsense_backend/app/routes/chat_routes.py` (hoặc nơi implement SSE)
|
||||
- `surfsense_web/src/components/chat/`
|
||||
- Phải đảm bảo DB schema migration (bằng Alembic) khi có thuộc tính mới `token_balance` ở User.
|
||||
- Endpoint `/api/v1/chat/stream` hiện đang phụ thuộc vào `NewLLMConfig`. Cần chỉnh sửa kiến trúc để ánh xạ ID Model do người dùng chọn sang Backend Config cố định của hệ thống.
|
||||
### Concurrency & Race Conditions
|
||||
```python
|
||||
# Atomic update — tránh race condition khi 2 tab chat đồng thời
|
||||
await session.execute(
|
||||
update(User).where(User.id == user_id)
|
||||
.values(tokens_used_this_month=User.tokens_used_this_month + tokens_consumed)
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern Reference
|
||||
Tham khảo `PageLimitService` (`surfsense_backend/app/services/page_limit_service.py`) — đã implement đầy đủ check + update + estimate cho page quota. Có thể tạo `TokenQuotaService` tương tự.
|
||||
|
||||
### References
|
||||
- [Epic 3 - RAG Engine Requirements]: Epic `3.5`.
|
||||
- [Constraint from BMad Rule]: "All billing must handle quota limits based on the user's current subscription".
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Antigravity Claude 3.5 Sonnet Engine (Context: 120k)
|
||||
|
||||
### File List
|
||||
- `surfsense_backend/app/db.py`
|
||||
- `surfsense_backend/app/routes/chat_routes.py`
|
||||
- `surfsense_web/app/dashboard/[search_space_id]/llm-configs/...`
|
||||
- `surfsense_web/components/chat/...`
|
||||
- `surfsense_backend/app/db.py` — User model, NewLLMConfig
|
||||
- `surfsense_web/components/new-chat/model-selector.tsx` — BYOK (cần thay)
|
||||
- `surfsense_backend/app/services/page_limit_service.py` — pattern tham khảo
|
||||
- Endpoint SSE hiện tại: `/api/v1/chat/stream`
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
# Story 5.1: Giao diện Bảng giá & Lựa chọn Gói Cước (Pricing Plan Selection UI)
|
||||
# Story 5.1: Giao diện Bảng giá & Lựa chọn Gói Cước (Pricing & Plan Selection UI)
|
||||
|
||||
Status: ready-for-dev
|
||||
|
||||
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
||||
|
||||
## Story
|
||||
|
||||
As a Khách hàng tiềm năng,
|
||||
|
|
@ -17,35 +15,47 @@ so that tôi biết chính xác số lượng file/tin nhắn mình nhận đư
|
|||
3. Kèm theo hiệu ứng hover mượt mà cho các Pricing Cards (<150ms delay).
|
||||
4. Phân bổ ít nhất 2 gói cước (Free, Pro) gắn liền với Limit.
|
||||
|
||||
## As-Is (Code hiện tại)
|
||||
|
||||
| Component | Hiện trạng | File |
|
||||
|-----------|-----------|------|
|
||||
| Pricing Page | **Đã tồn tại** — route `/pricing` | `surfsense_web/app/(home)/pricing/page.tsx` |
|
||||
| Pricing Section | **Đã tồn tại** — 3 tiers (FREE / PAY AS YOU GO / ENTERPRISE) | `surfsense_web/components/pricing/pricing-section.tsx` |
|
||||
| Pricing Data | Static `demoPlans` constant — Free=500 pages, PAYG=$1/1000 pages, Enterprise=Contact | `pricing-section.tsx` lines 2–59 |
|
||||
| CTA Buttons | Free="Get Started" → `/login`, PAYG="Get Started" → `/login`, Enterprise="Contact Sales" → `/contact` | `pricing-section.tsx` |
|
||||
| Monthly/Yearly Toggle | **Không có** — chỉ có `price` và `yearlyPrice` fields nhưng chưa có toggle UI | |
|
||||
|
||||
**Gap:** UI hiện tại mô hình PAYG (mua page packs 1 lần). Cần chuyển sang **Subscription tiers** (Free/Pro/Team monthly/yearly) theo PRD.
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Task 1: Dựng Page Route Pricing (Frontend)
|
||||
- [ ] Subtask 1.1: Tạo Component `/src/pages/pricing/page.tsx` (app router tuỳ cấu hình Next.js hoặc Vite App).
|
||||
- [ ] Subtask 1.2: Sử dụng thư viện `framer-motion` (nếu có sẵn) hoặc Tailwind Utilities (`transition-all duration-150`) để thoả mãn Animation criteria.
|
||||
- [ ] Task 2: Data cấu hình Static Pricing (Frontend)
|
||||
- [ ] Subtask 2.1: Cấu trúc Object Constant cho Gói "Free" (Limits: 10 docs, 50 LLM messages/day).
|
||||
- [ ] Subtask 2.2: Cấu trúc Object Constant cho Gói "Pro" (Limits: 100 docs, 1000 LLM messages/day).
|
||||
- [ ] Task 3: Liên kết Nút "Upgrade" (Frontend)
|
||||
- [ ] Subtask 3.1: Nút nâng cấp sẽ chèn hàm mock gọi `/api/v1/stripe/checkout` (Endpoint này sẽ được làm chi tiết ở story 5.2).
|
||||
- [ ] Task 1: Thiết kế lại Pricing Tiers (Frontend)
|
||||
- [ ] Subtask 1.1: Cập nhật `demoPlans` constant → đổi sang subscription tiers:
|
||||
- **Free**: 500 pages ETL, 50 LLM messages/day, basic models (GPT-3.5), $0/mo
|
||||
- **Pro**: 5,000 pages ETL, 1,000 LLM messages/day, premium models (GPT-4, Claude), $X/mo hoặc $Y/year
|
||||
- **Team/Enterprise**: Unlimited, custom pricing, SSO, audit logs
|
||||
- [ ] Subtask 1.2: Thêm Monthly/Yearly toggle switch — hiển thị `price` vs `yearlyPrice` tương ứng.
|
||||
|
||||
- [ ] Task 2: Kết nối nút CTA với Stripe Checkout (Frontend)
|
||||
- [ ] Subtask 2.1: Nút "Get Started" cho tier Free → redirect `/login` (giữ nguyên).
|
||||
- [ ] Subtask 2.2: Nút "Upgrade to Pro" → gọi `POST /api/v1/stripe/create-subscription-checkout` (Story 5.2) với `plan_id` tương ứng. Nếu user chưa login → redirect `/login` trước.
|
||||
- [ ] Subtask 2.3: Nút "Contact Sales" cho Enterprise → giữ nguyên `/contact`.
|
||||
|
||||
- [ ] Task 3: Graceful degradation khi Offline
|
||||
- [ ] Subtask 3.1: Pricing data dùng static constant (load được offline). Disable nút "Upgrade" khi offline để tránh lỗi network request.
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Relevant Architecture Patterns & Constraints
|
||||
- **State Management & Data Retrieval:** Vì UI Pricing khá tĩnh, hãy triển khai bằng cấu trúc Const Typescript thay vì gọi DB ở màn đầu tiên nhằm tối ưu tốc độ load.
|
||||
- Chú ý đến Graceful degradation: Nếu user Offline, màn pricing vẫn load được Static Data, nhưng disable nút "Purchase" để tránh lỗi Network Request.
|
||||
### Giữ lại hay xóa PAYG?
|
||||
Quyết định kinh doanh: PAYG (mua page packs 1 lần) có thể tồn tại song song với subscription, hoặc bị thay thế hoàn toàn. Nếu giữ PAYG → thêm 1 tier "Pay As You Go" bên cạnh Free/Pro. Nếu thay → xóa PAYG flow cũ.
|
||||
|
||||
### Project Structure Notes
|
||||
- Module thay đổi:
|
||||
- `surfsense_web/src/pages/pricing/page.tsx` (hoặc tương đương tuỳ thư mục routes).
|
||||
- `surfsense_web/src/constants/billing.ts` (Lưu định mức cước).
|
||||
### Stripe Price IDs
|
||||
Mỗi tier subscription cần 1 Stripe Price ID (tạo trên Stripe Dashboard):
|
||||
- `STRIPE_FREE_PRICE_ID` (optional — free tier không cần checkout)
|
||||
- `STRIPE_PRO_MONTHLY_PRICE_ID`
|
||||
- `STRIPE_PRO_YEARLY_PRICE_ID`
|
||||
→ Lưu vào env vars backend, KHÔNG hardcode trong frontend.
|
||||
|
||||
### References
|
||||
- [Epic 5 - Commercialization & Account Limits].
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Antigravity Claude 3.5 Sonnet Engine
|
||||
|
||||
### File List
|
||||
- `surfsense_web/src/pages/pricing...`
|
||||
- `surfsense_web/components/pricing/pricing-section.tsx` — pricing UI hiện tại
|
||||
- `surfsense_web/app/(home)/pricing/page.tsx` — pricing page route
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
Status: ready-for-dev
|
||||
|
||||
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
||||
|
||||
## Story
|
||||
|
||||
As a Kỹ sư Hệ thống,
|
||||
|
|
@ -12,38 +10,70 @@ so that database được cập nhật trạng thái Subscription của user (Ac
|
|||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Backend bắt được Event Type qua HTTP POST.
|
||||
2. Kiểm tra chính xác Webhook-Signature tránh Event giả.
|
||||
3. Update trạng thái (Status, Expiry date, Plan_id) vào User record tương ứng trên Database Postgres.
|
||||
1. Backend bắt được Event Type qua HTTP POST và verify Webhook-Signature.
|
||||
2. Xử lý các event subscription: `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`.
|
||||
3. Update trạng thái (`subscription_status`, `plan_id`, `monthly_token_limit`, `token_reset_date`) vào User record tương ứng trên Database.
|
||||
4. Reset `tokens_used_this_month = 0` khi subscription renews (billing cycle mới).
|
||||
|
||||
## As-Is (Code hiện tại)
|
||||
|
||||
| Component | Hiện trạng | File |
|
||||
|-----------|-----------|------|
|
||||
| Webhook Endpoint | **Đã tồn tại** — `POST /api/v1/stripe/webhook` với signature verification | `stripe_routes.py` line ~280 |
|
||||
| Event Handlers | Chỉ xử lý PAYG events: `checkout.session.completed/expired/failed` → update `PagePurchase` | `stripe_routes.py` |
|
||||
| Idempotency | **Đã có** cho page purchases — `_get_or_create_purchase_from_checkout_session()` | `stripe_routes.py` |
|
||||
| Signature Verify | **Đã có** — dùng `stripe.Webhook.construct_event()` | `stripe_routes.py` line ~303 |
|
||||
| User ↔ Stripe | **Không có** `stripe_customer_id` trên User (sẽ thêm ở Story 5.2) | `db.py` |
|
||||
| Subscription Fields | **Không có** `subscription_status`, `plan_id` trên User | `db.py` |
|
||||
|
||||
**Gap:** Webhook infrastructure đã vững (signature verify, error handling). Cần thêm **subscription event handlers** bên cạnh PAYG handlers hiện tại.
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Task 1: Dựng Webhook Route
|
||||
- [ ] Subtask 1.1: Tạo Route `/api/v1/stripe/webhook` (đã có route cũ dành cho Page Purchase, xem ở `stripe_routes.py` line 281).
|
||||
- [ ] Subtask 1.2: Code logic giải mãi Signature.
|
||||
- [ ] Task 2: Listen Subscription Events
|
||||
- [ ] Subtask 2.1: Phân tích Webhook Event Type. Lắng nghe ít nhất 2 Event cơ bản: `customer.subscription.updated` và `customer.subscription.deleted`. Xử lý và fetch customer ID để map với User nội bộ (có thể dùng `stripe_customer_id` lưu trên bảng `users`).
|
||||
- [ ] Task 3: Database User Updates
|
||||
- [ ] Subtask 3.1: Viết hàm DB handler gọi tới DB để ghi đè `subscription_status` = 'active', set `plan_id`, và cập nhật `token_balance` hàng tháng khi có trigger chu kỳ mới. Cập nhật `users.py` controller.
|
||||
- [ ] Task 1: Thêm Subscription Fields vào User Model (Backend DB)
|
||||
- [ ] Subtask 1.1: Alembic migration thêm columns (nếu chưa có từ Story 3.5):
|
||||
- `subscription_status` — Enum: `free`, `active`, `canceled`, `past_due` (default: `free`)
|
||||
- `plan_id` — String nullable (e.g. `pro_monthly`, `pro_yearly`)
|
||||
- `stripe_subscription_id` — String nullable, indexed
|
||||
- `subscription_current_period_end` — DateTime nullable (để biết khi nào renewal)
|
||||
|
||||
- [ ] Task 2: Thêm Subscription Event Handlers vào Webhook (Backend)
|
||||
- [ ] Subtask 2.1: Mở rộng webhook handler — thêm routing cho:
|
||||
- `customer.subscription.created` → activate subscription
|
||||
- `customer.subscription.updated` → update status/plan (handle upgrade/downgrade)
|
||||
- `customer.subscription.deleted` → set status=`canceled`, downgrade limits
|
||||
- `invoice.payment_succeeded` → reset `tokens_used_this_month = 0` (billing cycle mới)
|
||||
- `invoice.payment_failed` → set status=`past_due`
|
||||
- [ ] Subtask 2.2: Tạo helper function `_handle_subscription_event(event, db_session)`:
|
||||
- Extract `customer` ID từ event → query User by `stripe_customer_id`
|
||||
- Update `subscription_status`, `plan_id`, `monthly_token_limit` theo plan
|
||||
- Update `subscription_current_period_end`
|
||||
- [ ] Subtask 2.3: Plan → Limits mapping (config):
|
||||
```python
|
||||
PLAN_LIMITS = {
|
||||
"free": {"monthly_token_limit": 50_000, "pages_limit": 500},
|
||||
"pro_monthly": {"monthly_token_limit": 1_000_000, "pages_limit": 5000},
|
||||
"pro_yearly": {"monthly_token_limit": 1_000_000, "pages_limit": 5000},
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Task 3: Xử lý `checkout.session.completed` cho Subscription mode
|
||||
- [ ] Subtask 3.1: Trong handler `checkout.session.completed` hiện tại, thêm check: nếu `session.mode == 'subscription'` → activate subscription thay vì grant pages.
|
||||
- [ ] Subtask 3.2: Giữ logic PAYG cũ cho `session.mode == 'payment'`.
|
||||
|
||||
- [ ] Task 4: Idempotency cho Subscription Events
|
||||
- [ ] Subtask 4.1: Dùng `stripe_subscription_id` + event timestamp để tránh duplicate processing.
|
||||
- [ ] Subtask 4.2: Log tất cả webhook events để debug.
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Relevant Architecture Patterns & Constraints
|
||||
- **Security Check:** Webhook API endpoint `MUST` parse raw body using `await request.body()`. Nếu FastAPI parse ra Pydantic Object TRƯỚC chữ ký signature thì thư viện Stripe auth sẽ báo lỗi văng Exception.
|
||||
- **Race Condition in DB:** Do event `checkout.session.completed` và `customer.subscription.created` có thể call webhook cục bộ gần như đồng thời, phải code check Upsert (Ví dụ: set timestamp check updatedAt để tránh data đè lên nhau).
|
||||
### Security — Raw Body Parsing
|
||||
Webhook endpoint **PHẢI** parse raw body bằng `await request.body()` TRƯỚC khi Pydantic parse. Nếu FastAPI parse thành Pydantic object trước → Stripe signature verify sẽ fail. Code hiện tại đã xử lý đúng.
|
||||
|
||||
### Project Structure Notes
|
||||
- Module thay đổi:
|
||||
- `surfsense_backend/app/routes/stripe_routes.py`
|
||||
- `surfsense_backend/app/db.py`
|
||||
### Race Condition
|
||||
`checkout.session.completed` và `customer.subscription.created` có thể fire gần như đồng thời. Dùng `stripe_subscription_id` unique constraint hoặc `updatedAt` timestamp check để tránh data đè lên nhau.
|
||||
|
||||
### References
|
||||
- [Epic 5.3 - Webhook Sync]
|
||||
|
||||
## 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` — webhook handler hiện tại
|
||||
- `surfsense_backend/app/db.py` — User model
|
||||
- Stripe Subscription Events: https://stripe.com/docs/billing/subscriptions/webhooks
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
Status: ready-for-dev
|
||||
|
||||
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
||||
|
||||
## Story
|
||||
|
||||
As a Kỹ sư Hệ thống,
|
||||
|
|
@ -12,37 +10,81 @@ so that mô hình kinh doanh không bị lỗ do chi phí LLM và Storage, áp d
|
|||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Endpoint Upload (Document Parser) và Endpoint Chat (RAG AI) sẽ query Database để check `Subscription_Status` và `document_count`, `token_balance`.
|
||||
2. Nếu Document count >= Limit hệ thống từ chối Upload và trả lỗi `402 Payment Required` hoặc `403`.
|
||||
3. UI hiển thị Toast Error hoặc một Modal up-sell Upgrade to Pro.
|
||||
1. Endpoint Upload (Document) kiểm tra `pages_used` vs `pages_limit` (dựa trên subscription tier) → từ chối nếu vượt.
|
||||
2. Endpoint Chat (RAG AI) kiểm tra `tokens_used_this_month` vs `monthly_token_limit` → từ chối nếu vượt (đã cover ở Story 3.5 Task 4).
|
||||
3. API trả lỗi `402 Payment Required` khi quota exceeded.
|
||||
4. UI hiển thị Toast Error hoặc Modal up-sell "Upgrade to Pro".
|
||||
5. Dashboard hiển thị quota indicator (pages used / limit, tokens used / limit).
|
||||
|
||||
## As-Is (Code hiện tại)
|
||||
|
||||
| Component | Hiện trạng | File |
|
||||
|-----------|-----------|------|
|
||||
| Page Quota Service | **Đã implement đầy đủ** — `check_page_limit()`, `update_page_usage()`, `get_page_usage()`, `estimate_pages_*()` | `surfsense_backend/app/services/page_limit_service.py` |
|
||||
| Enforcement trong Celery | **Đã có** — check quota trước khi process document | `document_tasks.py` |
|
||||
| Enforcement trong Connectors | **Đã có** — Google Drive, OneDrive, Dropbox indexers check `remaining_quota` | `connector_indexers/*.py` |
|
||||
| Enforcement tại HTTP API | **Chưa có** — document upload route không check quota trước khi enqueue task | routes/ |
|
||||
| Token Quota Service | **Chưa có** — sẽ tạo ở Story 3.5 | |
|
||||
| Frontend Quota UI | **Chưa có** | |
|
||||
| Frontend Error Handling | **Chưa có** — không bắt 402 từ upload/chat | |
|
||||
|
||||
**Gap chính:**
|
||||
1. Thiếu quota pre-check tại HTTP layer (upload route) — user phải đợi Celery task fail mới biết bị reject
|
||||
2. Thiếu frontend feedback khi quota exceeded
|
||||
3. Thiếu quota indicator trong dashboard
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Task 1: Check Limits ở Route `/api/v1/documents`
|
||||
- [ ] Subtask 1.1: Tạo function Utils `check_upload_quota()` query lấy Package Plan của JWT User. Đếm số object hiện có trong DB (Count records trong table `documents` nơi `user_id = X`).
|
||||
- [ ] Subtask 1.2: Nếu đạt MAX_LIMIT (ví dụ 10 đối với FREE), Raise HTTPException 402/403.
|
||||
- [ ] Task 2: Refine Frontend Validation
|
||||
- [ ] Subtask 2.1: Ở component `DocumentUploader`, handle catch Error response `402`. Bật Toast thông báo "Bạn đã hết File Limit miễn phí".
|
||||
- [ ] Task 1: Thêm Page Quota Pre-check vào Document Upload Route (Backend)
|
||||
- [ ] Subtask 1.1: Tìm document upload route (search trong `surfsense_backend/app/routes/`).
|
||||
- [ ] Subtask 1.2: Inject `PageLimitService`, gọi `estimate_pages_from_metadata(filename, file_size)` rồi `check_page_limit(user_id, estimated_pages)`.
|
||||
- [ ] Subtask 1.3: Nếu `PageLimitExceededError` → raise `HTTPException(402, detail={"error": "page_quota_exceeded", "pages_used": X, "pages_limit": Y, "message": "..."})`.
|
||||
- [ ] Subtask 1.4: Giữ nguyên enforcement trong Celery tasks (double-check layer cho trường hợp estimate sai).
|
||||
|
||||
- [ ] Task 2: Plan-based Limits (Backend)
|
||||
- [ ] Subtask 2.1: Tạo config mapping `plan_id` → limits (liên kết với Story 5.3):
|
||||
```python
|
||||
PLAN_LIMITS = {
|
||||
"free": {"pages_limit": 500, "monthly_token_limit": 50_000, "max_docs": 10},
|
||||
"pro": {"pages_limit": 5000, "monthly_token_limit": 1_000_000, "max_docs": 100},
|
||||
}
|
||||
```
|
||||
- [ ] Subtask 2.2: Khi subscription activate/update (webhook Story 5.3), update `pages_limit` và `monthly_token_limit` theo plan mới.
|
||||
|
||||
- [ ] Task 3: Frontend — Bắt lỗi Quota Exceeded
|
||||
- [ ] Subtask 3.1: Document upload component — bắt HTTP 402 response, phân biệt `page_quota_exceeded` vs `token_quota_exceeded`.
|
||||
- [ ] Subtask 3.2: Hiển thị toast/modal: "Bạn đã dùng X/Y pages. Nâng cấp lên Pro để tiếp tục." với link đến `/pricing`.
|
||||
- [ ] Subtask 3.3: Chat component — bắt HTTP 402 từ SSE stream (tương tự Story 3.5 Task 6).
|
||||
|
||||
- [ ] Task 4: Frontend — Quota Indicator (Dashboard)
|
||||
- [ ] Subtask 4.1: Expose `pages_used`, `pages_limit`, `tokens_used_this_month`, `monthly_token_limit` qua user endpoint hoặc Zero sync.
|
||||
- [ ] Subtask 4.2: Tạo component `QuotaIndicator` — hiển thị 2 progress bars (Pages, Tokens) trong sidebar/header.
|
||||
- [ ] Subtask 4.3: Warning state (amber) khi usage > 80%, critical state (red) khi > 95%.
|
||||
|
||||
- [ ] Task 5: Anti-spam Rate Limiting (Optional — nếu cần)
|
||||
- [ ] Subtask 5.1: Thêm rate limit cho chat endpoint (e.g. max 60 requests/hour cho free tier).
|
||||
- [ ] Subtask 5.2: Dùng Redis counter hoặc FastAPI dependency.
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Relevant Architecture Patterns & Constraints
|
||||
- Yêu cầu NFR Architecture: KHÔNG HARDCODE số file LIMIT vào trong code API mà phải load Enum hoặc Database row. Nên dùng một `config module` tĩnh hoặc Map Object dựa trên `plan_id` enum để check.
|
||||
- Nếu User Plan là `PRO`, `MAX_LIMIT` là 100.
|
||||
- Khúc check Chat Token Balance đã được cover một phần ở Story 3.5, nên Story 5.4 tập trung vào `File Upload Rate Limit` và `Chat Frequency Rate Limit` (tránh crawler/spam theo giờ).
|
||||
### PageLimitService — đã sẵn sàng, không cần viết lại
|
||||
`page_limit_service.py` đã có đầy đủ:
|
||||
- `check_page_limit(user_id, estimated_pages)` → raises `PageLimitExceededError`
|
||||
- `update_page_usage(user_id, pages_to_add)`
|
||||
- `estimate_pages_before_processing(file_path)`
|
||||
- `estimate_pages_from_metadata(filename, file_size)` (pure function, dùng cho HTTP layer)
|
||||
|
||||
### Project Structure Notes
|
||||
- Module thay đổi:
|
||||
- `surfsense_backend/app/routes/documents_routes.py` (hoặc endpoints xử lý Upload mới nhất).
|
||||
- `surfsense_web/src/components/UploadDocument.tsx`
|
||||
### Enforcement Layers
|
||||
```
|
||||
HTTP Upload Route → PageLimitService.check_page_limit() [FAST — pre-check]
|
||||
↓ (nếu pass)
|
||||
Celery Task → PageLimitService.check_page_limit() [ACCURATE — sau khi parse xong]
|
||||
↓ (nếu pass)
|
||||
PageLimitService.update_page_usage() [COMMIT — tăng pages_used]
|
||||
```
|
||||
|
||||
### References
|
||||
- [Epic 5.4 - Quota Check]
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Antigravity Claude 3.5 Sonnet Engine
|
||||
|
||||
### File List
|
||||
- `surfsense_backend/app/routes/documents_routes.py`
|
||||
- `surfsense_backend/app/services/page_limit_service.py` — page quota (đọc, ít sửa)
|
||||
- `surfsense_backend/app/tasks/celery_tasks/document_tasks.py` — enforcement pattern hiện tại
|
||||
- `surfsense_backend/app/routes/` — document upload route (cần tìm)
|
||||
- Frontend upload component (cần tìm)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue