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

@ -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``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``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``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