SurfSense/_bmad-output/implementation-artifacts/5-3-stripe-webhook-sync.md
Vonic e7382b26de 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>
2026-04-14 14:19:57 +07:00

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

  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ạiPOST /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, 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):
      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

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.completedcustomer.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