SurfSense/_bmad-output/implementation-artifacts/5-2-stripe-payment-integration.md
Vonic 07a4bc3fc3 feat(story-5.2): add Stripe subscription checkout with session verification
Add POST /api/v1/stripe/create-subscription-checkout endpoint with
get_or_create_stripe_customer (SELECT FOR UPDATE), plan_id→price_id
mapping from env vars, active subscription guard (409), and
session_id in success URL. Add GET /verify-checkout-session endpoint
for server-side payment verification. Add /subscription-success
frontend page with loading/verified/failed states.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 23:58:57 +07:00

6.5 KiB

Story 5.2: Tích hợp Stripe Subscription Checkout (Stripe Payment Integration)

Status: done

Story

As a Người dùng, I want bấm "Nâng cấp" và được chuyển tới trang thanh toán an toàn, so that tôi có thể điền thông tin thẻ tín dụng mà không sợ bị lộ dữ liệu trên máy chủ của SurfSense.

Acceptance Criteria

  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ạiget_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: Thêm stripe_customer_id vào User (Backend DB)

    • Subtask 1.1: Alembic migration thêm column stripe_customer_id — đã có trong migration 124.
    • Subtask 1.2: Tạo helper function get_or_create_stripe_customer(user) — SELECT FOR UPDATE, tạo Stripe customer nếu chưa có, persist ID.
  • 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" } — validated bằng PlanId enum.
    • Subtask 2.3: Map plan_id → Stripe Price ID từ env vars (STRIPE_PRO_MONTHLY_PRICE_ID, STRIPE_PRO_YEARLY_PRICE_ID). Frontend không gửi price.
    • 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: pricing-section.tsx đã gọi endpoint với plan_id — done trong Story 5.1.
    • Subtask 3.2: Redirect đến checkout_url — done trong Story 5.1.
    • Subtask 3.3: /subscription-success page tạo mới — invalidates user query + toast "Subscription activated!"

Dev Notes

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.

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

Dev Agent Record

Implementation Notes

  • Migration 124 đã có stripe_customer_idstripe_subscription_id từ Story 3.5 — không cần migration mới.
  • get_or_create_stripe_customer: dùng SELECT FOR UPDATE để tránh duplicate customer khi concurrent requests.
  • _get_price_id_for_plan: map PlanId enum → env var STRIPE_PRO_MONTHLY_PRICE_ID / STRIPE_PRO_YEARLY_PRICE_ID. Frontend không gửi price ID.
  • Endpoint POST /create-subscription-checkout: mode='subscription', customer=stripe_customer_id, success_url=/subscription-success, cancel_url=/pricing.
  • PlanId enum trong schemas/stripe.py đảm bảo frontend chỉ gửi giá trị hợp lệ.
  • Frontend success page /subscription-success: toast "Subscription activated!" + invalidate user query.

Completion Notes

Tất cả tasks/subtasks hoàn thành. AC 1-3 đều được đáp ứng.

File List

  • surfsense_backend/app/config/__init__.py — added STRIPE_PRO_MONTHLY_PRICE_ID, STRIPE_PRO_YEARLY_PRICE_ID
  • surfsense_backend/app/schemas/stripe.py — added PlanId enum, CreateSubscriptionCheckoutRequest, CreateSubscriptionCheckoutResponse
  • surfsense_backend/app/routes/stripe_routes.py — added _get_subscription_success_url, _get_price_id_for_plan, get_or_create_stripe_customer, POST /create-subscription-checkout
  • surfsense_web/app/subscription-success/page.tsx — new success page with toast + user query invalidation

Review Findings

  • [Review][Decision] Success page verify payment server-side — added GET /verify-checkout-session endpoint + frontend verify flow
  • [Review][Patch] Success URL includes ?session_id={CHECKOUT_SESSION_ID} template variable [stripe_routes.py:89]
  • [Review][Patch] Duplicate NEXT_FRONTEND_URL check removed — refactored to _get_subscription_urls() [stripe_routes.py:82]
  • [Review][Patch] Added active subscription guard (409 Conflict) before creating checkout [stripe_routes.py:370]
  • [Review][Patch] Toast only fires once via useRef flag + only after verified [subscription-success/page.tsx]
  • [Review][Defer] Webhook không xử lý subscription-mode checkout — deferred to Story 5.3
  • [Review][Defer] Không có handler cho subscription lifecycle events — deferred to Story 5.3
  • [Review][Defer] Orphan Stripe customer nếu commit fail sau API call — deferred, low probability

Change Log

  • 2026-04-14: Implement subscription checkout endpoint with Stripe customer creation and success page.