Tài Liệu Tham Khảo Stripe Webhook Events
Mô Tả
Tài liệu này cung cấp tài liệu tham khảo toàn diện cho tất cả các Stripe webhook event mà hệ thống xử lý, bao gồm mục đích, cấu trúc dữ liệu, logic xử lý và kết quả mong đợi. Sử dụng làm hướng dẫn tham khảo nhanh khi debug hoặc mở rộng chức năng webhook.
Tổng Quan Xử Lý Event
graph TD
A[Stripe Event] --> B{Loại Event}
B -->|checkout.session.*| C[SubscriptionSessionHandler]
B -->|customer.subscription.*| D[SubscriptionLifeCycleHandler]
B -->|invoice.*| E[SubscriptionInvoiceHandler]
B -->|payment_intent.succeeded| F[PaymentIntentHandler]
C --> G[(Database)]
D --> G
E --> G
F --> G
style A fill:#fcd9d9,stroke:#cc3333
style C fill:#f0f8e6,stroke:#339933
style D fill:#f0f8e6,stroke:#339933
style E fill:#f0f8e6,stroke:#339933
style F fill:#f0f8e6,stroke:#339933
style G fill:#ffe6cc,stroke:#ff9900
Checkout Events
checkout.session.completed
Mục đích: Kích hoạt subscription mới sau thanh toán thành công qua Stripe Checkout.
Khi nào trigger:
- User hoàn tất thanh toán trong Stripe Checkout
- Cho cả gói miễn phí và trả phí
Handler: SubscriptionSessionHandler::handleSessionCompleted()
Các trường dữ liệu chính:
{
"id": "cs_test_...",
"object": "checkout.session",
"mode": "subscription",
"subscription": "sub_...",
"invoice": "in_...",
"customer": "cus_...",
"metadata": {
"subscription_slug": "sub_abc123"
},
"amount_total": 1000,
"currency": "jpy",
"payment_status": "paid"
}
Logic xử lý:
- Tìm subscription local bằng
metadata.subscription_slug - Xác minh subscription status là
unpaid - Lock subscription record
- Cập nhật subscription:
status:unpaid→activepayment_provider_subscription_id: Stripe subscription IDdeadline_at: Thời điểm kết thúc từ Stripe
- Cập nhật subscription_histories:
payment_status:pending→paidinvoice_id,payment_intent_id,paid_at
Kết quả mong đợi:
- Subscription được kích hoạt
- User có quyền truy cập dịch vụ
- History record được đánh dấu là đã thanh toán
Event liên quan:
invoice.paid(bỏ qua cho subscription mới để tránh xử lý trùng lặp)
Subscription Lifecycle Events
customer.subscription.updated
Mục đích: Xử lý tất cả các event thay đổi subscription (đổi gói, hủy, thay đổi trạng thái).
Khi nào trigger:
- Thay đổi gói (upgrade/downgrade)
- Lên lịch hủy
- Khôi phục hủy
- Thay đổi trạng thái (active → past_due, v.v.)
- Gia hạn chu kỳ
- Tạm dừng/tiếp tục subscription
Handler: SubscriptionLifeCycleHandler::handleSubscriptionUpdated()
Các trường dữ liệu chính:
{
"id": "sub_...",
"object": "subscription",
"status": "active",
"items": {
"data": [{
"plan": {
"id": "price_...",
"product": "prod_...",
"interval": "month"
}
}]
},
"current_period_start": 1702857600,
"current_period_end": 1705536000,
"cancel_at_period_end": false,
"canceled_at": null,
"previous_attributes": {
"items": { /* gói cũ */ },
"cancel_at_period_end": true
}
}
Logic xử lý (Cây quyết định):
1. Là incomplete_expired? → handleIncompleteExpired()
2. Là kích hoạt miễn phí? → handleFreeSubscriptionActivation()
3. Là hủy đã lên lịch? → handleScheduledCancellation()
4. Là quá hạn? → handlePastDue()
5. Là tạm dừng? → handleSubscriptionPause()
6. Là tiếp tục từ tạm dừng? → handleSubscriptionResumeFromPause()
7. Là tiếp tục từ hủy? → handleSubscriptionResume()
8. Là áp dụng thay đổi gói đã lên lịch? → handleScheduledPlanChangeApplication()
9. Là thay đổi gói? → handlePlanChange()
- Thay đổi ngay? → handleImmediatePlanChange()
- Thay đổi đã lên lịch? → handleScheduledPlanChange()
10. Là tự động gia hạn? → handleAutoRenewal()
11. Là reset chu kỳ thanh toán? → handleBillingCycleReset()
12. Mặc định: handleRegularUpdate()
Các tình huống:
| Tình huống | Tiêu chí phát hiện | Hành động |
|---|---|---|
| Hủy đã lên lịch | cancel_at_period_end: true |
Cập nhật status thành pending_cancellation, tạo history hủy |
| Khôi phục hủy | previous: cancel_at_period_end=true, current: false |
Trở về active, xóa history hủy |
| Thay đổi gói ngay | Plan ID thay đổi + period start gần đây (<2 phút) | Tạo change history, cập nhật package_plan_id |
| Áp dụng thay đổi gói đã lên lịch | scheduled_plan_id khớp gói hiện tại + period thay đổi |
Hoàn tất change history |
| Tự động gia hạn | Period thay đổi, plan không đổi, status=active | Tạo renewal history (pending, hoàn tất bởi invoice.paid) |
| Quá hạn | status: 'past_due' |
Cập nhật status, kéo dài grace period |
Kết quả mong đợi:
- Subscription status được cập nhật trong database
- History record được tạo hoặc cập nhật
- Quyền truy cập của user được điều chỉnh phù hợp
customer.subscription.deleted
Mục đích: Xử lý hủy subscription cuối cùng.
Khi nào trigger:
- Subscription bị hủy ngay lập tức
- Chu kỳ hủy đã lên lịch kết thúc
- Thanh toán thất bại hết số lần retry
Handler: SubscriptionLifeCycleHandler::handleSubscriptionDeleted()
Các trường dữ liệu chính:
{
"id": "sub_...",
"object": "subscription",
"status": "canceled",
"canceled_at": 1702857600,
"current_period_end": 1705536000,
"cancellation_details": {
"reason": "cancellation_requested",
"comment": "User yêu cầu hủy",
"feedback": "too_expensive"
}
}
Logic xử lý:
- Tìm subscription local bằng
provider_subscription_id - Cập nhật subscription:
status: →canceledcanceled_at: Timestamp từ Stripecanceled_reason: Từ cancellation_details
- Tạo history record
immediate_cancellation - Xóa pending invoice items (nếu có)
Kết quả mong đợi:
- Subscription bị hủy trong database
- Quyền truy cập của user bị chấm dứt (hoặc tiếp tục đến grace_period_end_at)
- History hủy được tạo
Invoice Events
invoice.paid
Mục đích: Xử lý thanh toán invoice thành công (gia hạn, đổi gói).
Khi nào trigger:
- Thanh toán định kỳ thành công
- Thanh toán đổi gói thành công
- Thanh toán subscription ban đầu thành công (bỏ qua, xử lý bởi checkout.session.completed)
Handler: SubscriptionInvoiceHandler::handleInvoicePaid()
Các trường dữ liệu chính:
{
"id": "in_...",
"object": "invoice",
"subscription": "sub_...",
"billing_reason": "subscription_cycle",
"amount_paid": 1000,
"amount_due": 1000,
"currency": "jpy",
"payment_intent": "pi_...",
"period_start": 1702857600,
"period_end": 1705536000,
"lines": {
"data": [{
"plan": { "id": "price_...", "interval": "month" },
"amount": 1000
}]
}
}
Logic xử lý (Dựa trên billing_reason):
1. subscription_create:
- Bỏ qua - Đã xử lý bởi
checkout.session.completed - Ngăn tạo history trùng lặp
2. subscription_cycle (Tự động gia hạn):
- Tìm subscription bằng
provider_subscription_id - Tạo history record mới:
type:renewalpayment_status:paidinvoice_id,payment_intent_id,paid_at
- Cập nhật
deadline_atcủa subscription
3. subscription_update (Đổi gói):
- Kiểm tra change history đang pending
- Nếu tìm thấy (luồng bình thường):
- Cập nhật history với chi tiết thanh toán
- Đặt
payment_status:paid
- Nếu KHÔNG tìm thấy (race condition - invoice.paid đến trước):
- Gọi
createChangeHistoryFromInvoice() - Trích xuất
old_plan_idtừ credit line (amount < 0) - Trích xuất
new_plan_idtừ charge line (amount > 0) hoặc Stripe API - Tạo history record đầy đủ
- Gọi
Kết quả mong đợi:
- Gia hạn: Chu kỳ history mới được tạo, deadline được kéo dài
- Đổi gói: Thanh toán được ghi nhận, thay đổi được hoàn tất
invoice.payment_failed
Mục đích: Xử lý thanh toán định kỳ thất bại.
Khi nào trigger:
- Thanh toán tự động thất bại
- Không đủ tiền, thẻ hết hạn, v.v.
Handler: SubscriptionInvoiceHandler::handleInvoicePaymentFailed()
Các trường dữ liệu chính:
{
"id": "in_...",
"object": "invoice",
"subscription": "sub_...",
"billing_reason": "subscription_cycle",
"attempt_count": 1,
"amount_due": 1000,
"next_payment_attempt": 1703030400
}
Logic xử lý:
- Tìm subscription bằng
provider_subscription_id - Tìm history record mới nhất
- Nếu history mới nhất là
paid(lần thất bại đầu tiên):- Tạo history mới:
type:renewalpayment_status:failedpayment_attempt: 1
- Tạo history mới:
- Nếu history mới nhất là
failed(retry thất bại):- Cập nhật history:
payment_attempt: + 1
- Cập nhật history:
Lưu ý:
- Thay đổi subscription status (
active→past_due→canceled) được xử lý bởi eventcustomer.subscription.updated/deleted - Event này chỉ theo dõi các lần thử thanh toán
Kết quả mong đợi:
- Thanh toán thất bại được ghi log
- Số lần thử thanh toán được tăng
- Chờ logic retry của Stripe hoặc thay đổi status cuối cùng
Payment Intent Events
payment_intent.succeeded
Mục đích: Cập nhật history record với payment intent ID để theo dõi.
Khi nào trigger:
- Bất kỳ thanh toán nào được xử lý thành công
Handler: PaymentIntentHandler::handlePaymentIntentSucceeded()
Các trường dữ liệu chính:
{
"id": "pi_...",
"object": "payment_intent",
"invoice": "in_...",
"amount": 1000,
"status": "succeeded"
}
Logic xử lý:
- Tìm history bằng
invoice_idvớipayment_intent_id IS NULL - Cập nhật history:
payment_intent_id=pi_...
Lưu ý: Chủ yếu để đảm bảo tính toàn vẹn dữ liệu, vì invoice.paid đã điền hầu hết thông tin thanh toán.
Các Pattern Xử Lý Event
Pattern 1: Idempotency
Tất cả event đều được kiểm tra với bảng stripe_webhook_events:
if ($webhookEvent->status === 'completed') {
return '200 OK - Đã xử lý rồi';
}
if ($webhookEvent->status === 'processing') {
return '200 OK - Đang xử lý';
}
// Chỉ xử lý nếu mới hoặc thất bại
Pattern 2: Xử Lý Race Condition
Event đổi gói có thể đến theo bất kỳ thứ tự nào:
Tình huống A:
1. customer.subscription.updated → Tạo history pending
2. invoice.paid → Hoàn tất history với thanh toán
Tình huống B:
1. invoice.paid → Tạo history đầy đủ từ dữ liệu invoice
2. customer.subscription.updated → Phát hiện history đã có, bỏ qua tạo
Triển khai:
- Kiểm tra history hiện có bằng
subscription_id + type + started_at(dung sai ±5 giây) - Nếu tìm thấy thì cập nhật, nếu không thì tạo từ dữ liệu có sẵn
Pattern 3: An Toàn Transaction
Tất cả thay đổi database được wrap trong transaction:
return $this->wrapTransaction(function () use (...) {
// Tất cả thao tác DB là atomic
$this->updateSubscription(...);
$this->createHistory(...);
});
Pattern 4: An Toàn Null
Tất cả truy cập dữ liệu webhook sử dụng phương thức an toàn:
// Tốt
$planId = data_get($session, 'items.data.0.plan.id');
$billingReason = $session->billing_reason ?? null;
// Không tốt
$planId = $session->items['data'][0]['plan']['id']; // Có thể crash
Debug Vấn Đề Webhook
Kiểm Tra Trạng Thái Xử Lý
SELECT
stripe_event_id,
event_type,
status,
error,
created_at,
processed_at
FROM stripe_webhook_events
WHERE event_type LIKE '%subscription%'
ORDER BY created_at DESC
LIMIT 20;
Tìm Event Thất Bại
SELECT * FROM stripe_webhook_events
WHERE status = 'failed'
ORDER BY created_at DESC;
Xác Minh Tạo History
SELECT
sh.id,
sh.type,
sh.payment_status,
sh.invoice_id,
sh.started_at,
sh.created_at
FROM subscription_histories sh
WHERE sh.subscription_id = :subscription_id
ORDER BY sh.created_at DESC;
Log Cần Kiểm Tra
storage/logs/stripe-{date}.log- Tất cả xử lý webhookstorage/logs/laravel.log- Lỗi ứng dụng chung
Bảng Tham Khảo Nhanh
| Event | Billing Reason | Phương thức Handler | Tạo History? | Cập nhật Subscription? |
|---|---|---|---|---|
checkout.session.completed |
N/A | handleSessionCompleted() |
Không (cập nhật có sẵn) | Có |
invoice.paid |
subscription_create |
(bỏ qua) | Không | Không |
invoice.paid |
subscription_cycle |
handleAutoRenewalInvoice() |
Có (renewal) | Có (deadline) |
invoice.paid |
subscription_update |
handlePlanChangeInvoice() |
Có/Cập nhật (change) | Có (deadline) |
invoice.payment_failed |
Bất kỳ | handleInvoicePaymentFailed() |
Có/Cập nhật (failed) | Không |
customer.subscription.updated |
N/A | handleSubscriptionUpdated() |
Thay đổi | Có |
customer.subscription.deleted |
N/A | handleSubscriptionDeleted() |
Có (cancellation) | Có |
payment_intent.succeeded |
N/A | handlePaymentIntentSucceeded() |
Không (cập nhật có sẵn) | Không |
Trạng thái tài liệu: Tài liệu tham khảo đầy đủ cho tất cả webhook event