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>
4.7 KiB
Story 5.3: Webhook & Cập nhật Trạng thái Gói cước (Stripe Webhook Sync)
Status: ready-for-dev
Story
As a Kỹ sư Hệ thống, I want backend tự động hứng Webhook từ Stripe mỗi khi có thanh toán thành công, gia hạn, hoặc hủy gói, so that database được cập nhật trạng thái Subscription của user (Active/Canceled) mà không cần can thiệp thủ công.
Acceptance Criteria
- Backend bắt được Event Type qua HTTP POST và verify Webhook-Signature.
- Xử lý các event subscription:
customer.subscription.created,customer.subscription.updated,customer.subscription.deleted. - 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. - Reset
tokens_used_this_month = 0khi 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: 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, indexedsubscription_current_period_end— DateTime nullable (để biết khi nào renewal)
- Subtask 1.1: Alembic migration thêm columns (nếu chưa có từ Story 3.5):
-
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 subscriptioncustomer.subscription.updated→ update status/plan (handle upgrade/downgrade)customer.subscription.deleted→ set status=canceled, downgrade limitsinvoice.payment_succeeded→ resettokens_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
customerID từ event → query User bystripe_customer_id - Update
subscription_status,plan_id,monthly_token_limittheo plan - Update
subscription_current_period_end
- Extract
- Subtask 2.3: Plan → Limits mapping (config):
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}, }
- Subtask 2.1: Mở rộng webhook handler — thêm routing cho:
-
Task 3: Xử lý
checkout.session.completedcho Subscription mode- Subtask 3.1: Trong handler
checkout.session.completedhiện tại, thêm check: nếusession.mode == 'subscription'→ activate subscription thay vì grant pages. - Subtask 3.2: Giữ logic PAYG cũ cho
session.mode == 'payment'.
- Subtask 3.1: Trong handler
-
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.
- Subtask 4.1: Dùng
Dev Notes
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.
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
surfsense_backend/app/routes/stripe_routes.py— webhook handler hiện tạisurfsense_backend/app/db.py— User model- Stripe Subscription Events: https://stripe.com/docs/billing/subscriptions/webhooks