mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 08:46:22 +02:00
Revert "docs: rewrite story 3.5 and epic 5 stories to match actual codebase"
This reverts commit 04fb9eec0f.
This commit is contained in:
parent
04fb9eec0f
commit
a368dbcb08
5 changed files with 132 additions and 274 deletions
|
|
@ -1,85 +1,49 @@
|
|||
# Story 5.3: Hardening Stripe Webhook & Purchase History
|
||||
# Story 5.3: Webhook & Cập nhật Trạng thái Gói cước (Stripe Webhook Sync)
|
||||
|
||||
Status: ready-for-dev
|
||||
|
||||
## Context / Correction Note
|
||||
> **⚠️ Story gốc bị sai hướng.** Story gốc mô tả xử lý `customer.subscription.*` events để sync subscription status — đây là mô hình subscription SaaS, không phải mô hình thực tế của SurfSense. Webhook handler **đã tồn tại** và xử lý đúng event `checkout.session.completed` cho PAYG page packs. Không cần `stripe_customer_id` hay `subscription_status` vì SurfSense không dùng subscription.
|
||||
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
||||
|
||||
## Story
|
||||
|
||||
As a Kỹ sư Hệ thống,
|
||||
I want webhook handler xử lý đầy đủ các trường hợp edge-case của Stripe payment lifecycle,
|
||||
so that mọi giao dịch đều được ghi nhận chính xác và pages luôn được cộng đúng.
|
||||
|
||||
## Actual Architecture (as-is)
|
||||
|
||||
**Đã implement và đúng:**
|
||||
- `POST /api/v1/stripe/webhook` — verify Stripe-Signature, xử lý events:
|
||||
- `checkout.session.completed` → `_fulfill_completed_purchase()` → tăng `pages_limit`
|
||||
- `checkout.session.async_payment_succeeded` → fulfill
|
||||
- `checkout.session.async_payment_failed` → `_mark_purchase_failed()`
|
||||
- `checkout.session.expired` → mark failed
|
||||
- Idempotency guard: `_get_or_create_purchase_from_checkout_session()` dùng DB để tránh double-grant
|
||||
- `GET /api/v1/stripe/purchases` — lịch sử mua hàng của user
|
||||
- `GET /api/v1/stripe/status` — check Stripe config status
|
||||
|
||||
**Không cần (và không nên thêm):**
|
||||
- `customer.subscription.*` events — SurfSense không dùng subscription
|
||||
- `stripe_customer_id` field trên User — không cần cho PAYG flow
|
||||
- `subscription_status` / `plan_id` columns — không liên quan đến PAYG
|
||||
|
||||
**Còn thiếu:**
|
||||
- Webhook chưa handle `payment_intent.payment_failed` (nếu payment thất bại sau khi session tạo)
|
||||
- Chưa có notification/email khi purchase thành công (nice-to-have)
|
||||
- Frontend `/purchases` page chưa có UI hiển thị lịch sử mua
|
||||
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. Khi Stripe gửi `checkout.session.completed`, `PagePurchase.status = COMPLETED` và `user.pages_limit` tăng đúng.
|
||||
2. Nếu cùng 1 webhook event gửi 2 lần (Stripe retry), hệ thống chỉ grant pages 1 lần (idempotency).
|
||||
3. Khi Stripe gửi `checkout.session.expired` hoặc `checkout.session.async_payment_failed`, `PagePurchase.status = FAILED`, `pages_limit` không thay đổi.
|
||||
4. Endpoint `GET /api/v1/stripe/purchases` trả về danh sách purchase history đúng cho user hiện tại.
|
||||
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.
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Task 1: Verify idempotency của webhook handler
|
||||
- [ ] Subtask 1.1: Đọc `_get_or_create_purchase_from_checkout_session()` — đảm bảo có DB-level lock (SELECT FOR UPDATE hoặc unique constraint) để tránh race condition khi Stripe retry.
|
||||
- [ ] Subtask 1.2: Viết unit test simulate webhook event gửi 2 lần, assert `pages_limit` chỉ tăng 1 lần.
|
||||
- [ ] Task 2: Thêm Purchase History UI (Frontend)
|
||||
- [ ] Subtask 2.1: Tạo page hoặc section trong Dashboard hiển thị danh sách `PagePurchase` từ `GET /api/v1/stripe/purchases`.
|
||||
- [ ] Subtask 2.2: Hiển thị: ngày mua, số pages, trạng thái (Completed/Failed/Pending), số tiền.
|
||||
- [ ] Task 3: Xử lý `payment_intent.payment_failed` (defensive)
|
||||
- [ ] Subtask 3.1: Thêm case xử lý event `payment_intent.payment_failed` trong webhook handler — tìm `PagePurchase` qua `stripe_payment_intent_id` và mark failed.
|
||||
- [ ] 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.
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Existing Webhook Handler Structure
|
||||
```python
|
||||
@router.post("/webhook")
|
||||
async def stripe_webhook(request, db_session):
|
||||
# 1. Verify Stripe-Signature
|
||||
# 2. Parse event
|
||||
# 3. Route by event type:
|
||||
event_handlers = {
|
||||
"checkout.session.completed": _fulfill_completed_purchase,
|
||||
"checkout.session.async_payment_succeeded": _fulfill_completed_purchase,
|
||||
"checkout.session.expired": _mark_purchase_failed,
|
||||
"checkout.session.async_payment_failed": _mark_purchase_failed,
|
||||
}
|
||||
```
|
||||
### 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).
|
||||
|
||||
### Idempotency Pattern (đã có)
|
||||
`_get_or_create_purchase_from_checkout_session()` query theo `stripe_checkout_session_id` — nếu đã COMPLETED thì skip, tránh double-grant.
|
||||
### Project Structure Notes
|
||||
- Module thay đổi:
|
||||
- `surfsense_backend/app/routes/stripe_routes.py`
|
||||
- `surfsense_backend/app/db.py`
|
||||
|
||||
### References
|
||||
- `surfsense_backend/app/routes/stripe_routes.py` (lines ~86–345)
|
||||
- `surfsense_backend/app/db.py` (class `PagePurchase`, `PagePurchaseStatus`)
|
||||
- [Epic 5.3 - Webhook Sync]
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
_TBD_
|
||||
Antigravity Claude 3.5 Sonnet Engine
|
||||
|
||||
### File List
|
||||
- `surfsense_backend/app/routes/stripe_routes.py`
|
||||
- Frontend purchase history component (new)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue