サブスクリプションの自動更新
説明
このドキュメントでは、サブスクリプションの自動更新に関する自動化されたプロセスについて概説します。新規登録とは異なり、このフローはサブスクリプションの請求期間が更新時期に達した際に完全にStripeによって開始されます。システムは、支払い処理から失敗時の対応まで、更新のライフサイクル全体を管理するために、Stripeからの一連のWebhookに依存しています。
主に関与するWebhookは、成功した更新のための invoice.paid と、失敗時のための invoice.payment_failed です。システムはこれらのイベントをリッスンして、ユーザーのサブスクリプションステータスを更新し、アクセス期間を延長し、支払い問題が発生した場合の再試行を管理し、ユーザーの手動介入なしにサービスのシームレスな継続を保証します。
前提条件:
- アクティブなユーザーサブスクリプションが
deadline_atの日付に近づいていること。 - サブスクリプションで
auto_renewが有効になっていること。 - Stripeが更新支払いを自動的に試行するように設定されていること。
プロセスフロー図
---
config:
theme: base
layout: dagre
flowchart:
curve: linear
htmlLabels: true
themeVariables:
edgeLabelBackground: "transparent"
---
flowchart TD
%% == NODES DEFINITION ==
StripeScheduler((Stripeスケジューラ))
%% Layer components
subgraph ApiControllerLayer["API Controller層"]
WebhookController[WebhookController]
end
subgraph ApiServiceLayer["API Service層"]
SubscriptionService(SubscriptionService)
InvoiceHandler(SubscriptionInvoiceHandler)
end
subgraph DatabaseLayer["データベース層"]
SubscriptionDB[(subscriptions)]
HistoryDB[(subscription_histories)]
WebhookEventsDB[(stripe_webhook_events)]
end
subgraph ExternalServices["外部サービス"]
StripeAPI((Stripe API))
end
%% == STEPS DEFINITION ==
StepW1["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W1</span><p style='margin-top: 8px'>支払いを試行し、Webhookを送信<br/>(invoice.paid / invoice.payment_failed)</p></div>"]
StepW2["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W2</span><p style='margin-top: 8px'>Webhookの検証と記録</p></div>"]
StepW3["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W3</span><p style='margin-top: 8px'>Invoice Handlerによるイベント処理</p></div>"]
StepW4["<div style='text-align: center'><span style='display: inline-block; background-color: #ff9966 !important; color:white; width: 28px; height: 28px; line-height: 28px; border-radius: 50%; font-weight: bold'>W4</span><p style='margin-top: 8px'>サブスクリプション更新と履歴作成</p></div>"]
%% == CONNECTIONS ==
StripeScheduler --> StripeAPI
StripeAPI --- StepW1 --> WebhookController
WebhookController --- StepW2 --> WebhookEventsDB
WebhookController --- StepW3 --> SubscriptionService
SubscriptionService --> InvoiceHandler
InvoiceHandler --- StepW4
StepW4 --> SubscriptionDB
StepW4 --> HistoryDB
%% == STYLING ==
style ApiControllerLayer fill:#e6f3ff,stroke:#0066cc,stroke-width:2px
style ApiServiceLayer fill:#f0f8e6,stroke:#339933,stroke-width:2px
style DatabaseLayer fill:#ffe6cc,stroke:#ff9900,stroke-width:2px
style ExternalServices fill:#fcd9d9,stroke:#cc3333,stroke-width:2px
style StepW1 fill:transparent,stroke:transparent,stroke-width:1px
style StepW2 fill:transparent,stroke:transparent,stroke-width:1px
style StepW3 fill:transparent,stroke:transparent,stroke-width:1px
style StepW4 fill:transparent,stroke:transparent,stroke-width:1px
ユースケース
ケース1: 成功した自動更新
説明
サブスクリプションの定期的な支払いが正常に処理されると、Stripeは invoice.paid Webhookを送信します。システムはこのイベントをリッスンして、更新された期間の新しい履歴レコードを作成し、サブスクリプションの deadline_at を延長します。
シーケンス図
sequenceDiagram
participant StripeAPI as Stripe API
participant Webhook as WebhookController
participant SubSvc as SubscriptionService
participant InvoiceHandler as SubscriptionInvoiceHandler
participant DB as データベース
Note over StripeAPI,DB: 成功した自動更新フロー
StripeAPI->>Webhook: POST /webhook (invoice.paid, billing_reason: subscription_cycle)
rect rgb(255, 255, 200)
Note right of Webhook: イベントの検証と記録
Webhook->>DB: 既存イベントの確認 (べき等性)
Webhook->>DB: stripe_webhook_events レコードを作成
end
Webhook->>SubSvc: handleInvoicePaid(invoice_data)
SubSvc->>InvoiceHandler: handleInvoicePaid(invoice_data)
rect rgb(200, 255, 255)
Note right of InvoiceHandler: 更新処理
InvoiceHandler->>DB: provider_idでサブスクリプションを検索
alt サブスクリプションが有効な場合
InvoiceHandler->>DB: トランザクション開始
InvoiceHandler->>DB: subscriptionsを更新 (deadline_at = new_period_end)
InvoiceHandler->>DB: 新しいsubscription_historiesレコードを作成 (type: renewal, payment_status: paid, amount, etc.)
InvoiceHandler->>DB: トランザクションコミット
InvoiceHandler-->>SubSvc: 成功
else サブスクリプションが有効でない場合
Note over InvoiceHandler: エラーをログに記録し、アクションは行わない
InvoiceHandler-->>SubSvc: 失敗
end
end
SubSvc-->>Webhook: 成功
Webhook->>DB: stripe_webhook_eventsを更新 (status: completed)
Webhook-->>StripeAPI: 200 OK
手順
ステップ1: invoice.paid Webhookの受信
- Stripeは新しい請求サイクルの支払いを自動的に試行します。成功すると、
billing_reason: 'subscription_cycle'を伴うinvoice.paidWebhookを送信します。
ステップ2: イベントの検証と処理
WebhookControllerはイベントを検証し、SubscriptionServiceに委譲し、それがSubscriptionInvoiceHandlerを呼び出します。
ステップ3: サブスクリプションの更新と履歴の作成
SubscriptionInvoiceHandlerは、ローカルデータベースで対応するsubscriptionを特定します。- データベーストランザクション内で、2つの主要なアクションを実行します:
- メインの
subscription.deadline_atを、Webhookデータで提供された新しい期間の終了日に更新します。 - 更新期間を表すために
subscription_historiesに新しいレコードを作成します。この新しいレコードは、subscription_histories.typeがrenewalに、subscription_histories.payment_statusがpaidに設定されます。
- メインの
ケース2: 失敗した自動更新
説明
定期的な支払いが失敗した場合、Stripeは invoice.payment_failed Webhookを送信します。システムはこの失敗を記録します。Stripeによって管理される数回の失敗した試行の後、Stripeは自動的にサブスクリプションのステータスを変更し(例: past_due または canceled)、対応する customer.subscription.updated または customer.subscription.deleted Webhookを送信します。その時点で、システムはローカルのサブスクリプションステータスを更新します。
シーケンス図
sequenceDiagram
participant StripeAPI as Stripe API
participant Webhook as WebhookController
participant SubSvc as SubscriptionService
participant InvoiceHandler as SubscriptionInvoiceHandler
participant DB as データベース
Note over StripeAPI,DB: 失敗した自動更新フロー
StripeAPI->>Webhook: POST /webhook (invoice.payment_failed)
rect rgb(255, 220, 180)
Note right of Webhook: 初回失敗の記録
Webhook->>SubSvc: handleInvoicePaymentFailed(invoice_data)
SubSvc->>InvoiceHandler: handleInvoicePaymentFailed(invoice_data)
InvoiceHandler->>DB: provider_idでサブスクリプションを検索
InvoiceHandler->>DB: 新しいsubscription_historiesレコードを作成 (type: renewal, payment_status: failed, payment_attempt: 1)
end
Note over StripeAPI,StripeAPI: Stripeスマートリトライが再度支払いを試行...
StripeAPI->>Webhook: POST /webhook (invoice.payment_failed)
rect rgb(255, 220, 180)
Note right of Webhook: 再試行失敗の記録
Webhook->>SubSvc: handleInvoicePaymentFailed(invoice_data)
SubSvc->>InvoiceHandler: handleInvoicePaymentFailed(invoice_data)
InvoiceHandler->>DB: 既存の失敗した履歴レコードを検索
InvoiceHandler->>DB: subscription_historiesを更新 (payment_attempt + 1)
end
Note over StripeAPI,StripeAPI: すべての再試行が失敗した後...
StripeAPI->>Webhook: POST /webhook (customer.subscription.updated, status: canceled)
rect rgb(255, 200, 200)
Note right of Webhook: サブスクリプションのキャンセル
Webhook->>SubSvc: handleSubscriptionUpdated(subscription_data)
SubSvc->>DB: subscriptionsを更新 (status: canceled, canceled_at)
end
手順
ステップ1: invoice.payment_failed Webhookの受信
- 定期的な支払いが失敗した場合、StripeはこのWebhookを送信します。
ステップ2: 支払い失敗の記録
SubscriptionInvoiceHandlerは関連するサブスクリプションを検索します。- 請求期間内で最初の失敗である場合、
subscription_histories.typeをrenewal、subscription_histories.payment_statusをfailed、payment_attemptを1として新しいsubscription_historiesレコードを作成します。 - 同じサイクルで後続の
invoice.payment_failedイベントが発生した場合、ハンドラは既存のfailed履歴レコードを見つけ、そのpayment_attemptカウンタをインクリメントします。
ステップ3: Stripeによる再試行と最終ステータスの管理
- Stripeのスマートリトライ機能は、顧客への再請求を自動的に試みます。
- すべての再試行が尽きた後、Stripeはサブスクリプションのステータスを更新し(例:
past_dueまたはcanceled)、customer.subscription.updatedまたはcustomer.subscription.deletedWebhookを送信します。
ステップ4: 最終的なサブスクリプションステータスの更新
- アプリケーションはこれらの最終的なステータス変更Webhookをリッスンします。
SubscriptionLifeCycleHandlerはこれらを処理し、メインのsubscription.statusをcanceled(またはpast_due)に更新し、canceled_atタイムスタンプを設定して、その期間のユーザーアクセスを正式に終了させます。
関連するデータベース構造
erDiagram
users {
bigint id PK
string name "ユーザーの氏名"
string email "ユーザーのメールアドレス(ユニーク)"
string payment_provider_customer_id "Stripeからの顧客ID(null許容)"
timestamp created_at
timestamp updated_at
}
subscriptions {
bigint id PK
string slug "Stripeメタデータと連携するためのユニークな識別子"
bigint user_id FK
bigint group_id FK
bigint package_id FK
bigint package_plan_id FK
string status "unpaid, active, past_due, canceled"
string payment_provider_subscription_id "StripeサブスクリプションID"
boolean auto_renew "自動更新フラグ"
timestamp deadline_at "現在の請求期間の終了日"
timestamp canceled_at "キャンセルのタイムスタンプ"
timestamp created_at
timestamp updated_at
}
subscription_histories {
bigint id PK
bigint subscription_id FK
string status "active, inactive"
string payment_status "pending, paid, failed"
string type "new_contract, renewal, change"
string invoice_id "StripeインボイスID"
string payment_intent_id "Stripe支払いインテントID"
timestamp started_at "期間開始のタイムスタンプ"
timestamp expires_at "期間終了のタイムスタンプ"
timestamp paid_at "支払い成功のタイムスタンプ"
integer payment_attempt "このサイクルの失敗した支払い試行回数"
timestamp created_at
timestamp updated_at
}
stripe_webhook_events {
bigint id PK
string stripe_event_id "重複を防ぐためのStripeイベントID"
string event_type
enum status "pending, processing, completed, failed"
text error "処理が失敗した場合のエラーメッセージ"
timestamp created_at
timestamp updated_at
}
users ||--o{ subscriptions : has
subscriptions ||--o{ subscription_histories : has
関連APIエンドポイント
| メソッド | エンドポイント | コントローラー | 説明 |
|---|---|---|---|
| POST | /api/v1/admin/stripe/webhook | WebhookController@handleWebhook | サブスクリプションに関連するStripeからのすべての着信イベントをリッスンし、処理します。 |
エラーハンドリング
-
ログ:
- すべてのWebhook処理の失敗はアプリケーションログに記録されます。
- Webhook処理エラーは
stripe_webhook_eventsテーブルのerror列に保存されます。
-
エラー詳細:
| ステータスコード | エラーメッセージ | 説明 |
|---|---|---|
| 400 | 無効なWebhook署名。 | リクエストがStripeから発信されたものではありません。 |
| 404 | Webhookに対応するサブスクリプションが見つかりません。 | WebhookからのサブスクリプションIDがデータベース内のどのレコードとも一致しません。 |
| 500 | データベースエラー: {message} | Webhook処理中にデータベース操作が失敗しました。 |
追加の注記
- 自動化: 更新フロー全体が自動化されており、ユーザーの操作は必要ありません。
- 信頼できる唯一の情報源: サブスクリプションのステータスに関する信頼できる唯一の情報源はStripeです。ローカルデータベースは、Webhookを介して通知された状態の変更を反映するように更新されます。
- べき等性: システムは
stripe_webhook_eventsテーブルを使用して処理済みのイベントIDを追跡し、Stripeが複数回送信した場合でも各Webhookが一度だけ処理されるようにします。 - 再試行ロジック: 支払いの再試行ロジックは、Stripeの「スマートリトライ」機能によって処理されます。アプリケーションの役割は、これらの試行をログに記録し、最終的なサブスクリプションステータスの変更に対応することです。