サブスクリプションの自動更新

説明

このドキュメントでは、サブスクリプションの自動更新に関する自動化されたプロセスについて概説します。新規登録とは異なり、このフローはサブスクリプションの請求期間が更新時期に達した際に完全に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.paid Webhookを送信します。

ステップ2: イベントの検証と処理

  • WebhookControllerはイベントを検証し、SubscriptionServiceに委譲し、それがSubscriptionInvoiceHandlerを呼び出します。

ステップ3: サブスクリプションの更新と履歴の作成

  • SubscriptionInvoiceHandlerは、ローカルデータベースで対応するsubscriptionを特定します。
  • データベーストランザクション内で、2つの主要なアクションを実行します:
    • メインの subscription.deadline_at を、Webhookデータで提供された新しい期間の終了日に更新します。
    • 更新期間を表すためにsubscription_histories新しいレコードを作成します。この新しいレコードは、subscription_histories.typerenewal に、subscription_histories.payment_statuspaid に設定されます。

ケース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.typerenewalsubscription_histories.payment_statusfailedpayment_attempt を1として新しいsubscription_historiesレコードを作成します。
  • 同じサイクルで後続のinvoice.payment_failedイベントが発生した場合、ハンドラは既存のfailed履歴レコードを見つけ、そのpayment_attemptカウンタをインクリメントします。

ステップ3: Stripeによる再試行と最終ステータスの管理

  • Stripeのスマートリトライ機能は、顧客への再請求を自動的に試みます。
  • すべての再試行が尽きた後、Stripeはサブスクリプションのステータスを更新し(例: past_dueまたはcanceled)、customer.subscription.updatedまたはcustomer.subscription.deleted Webhookを送信します。

ステップ4: 最終的なサブスクリプションステータスの更新

  • アプリケーションはこれらの最終的なステータス変更Webhookをリッスンします。SubscriptionLifeCycleHandlerはこれらを処理し、メインの subscription.statuscanceled(または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の「スマートリトライ」機能によって処理されます。アプリケーションの役割は、これらの試行をログに記録し、最終的なサブスクリプションステータスの変更に対応することです。