猶予期間と支払い再試行メカニズム

概要

このドキュメントでは、サブスクリプション支払いが失敗した際に、システムがどのように支払い失敗、猶予期間、および自動再試行メカニズムを処理するかについて説明します。

主要な概念

猶予期間

猶予期間は、支払い失敗後にユーザーがサービスアクセスを維持できるようにするローカルアプリケーションレベルのバッファ時間です。これはconfig/stripe.phpで設定可能です:

'grace_period' => 1, // 猶予期間(日数)(デフォルト: 1日)

猶予期間が設定されるタイミング:

  • 最初のinvoice.payment_failed webhookを受信したときにトリガーされます
  • サブスクリプションステータスがpast_dueに変更されます
  • grace_period_end_atが設定されます: 現在時刻 + 猶予期間日数
  • ユーザーはこの期間中、完全なサービスアクセスを維持します

目的:

  • 一時的な支払い問題(カードの有効期限切れ、残高不足など)のバッファを提供
  • ユーザーが支払い方法を更新する時間を確保
  • 即座のサービス中断を防ぐ

Stripeスマートリトライ

Stripeスマートリトライは、Stripeの組み込み自動支払い再試行メカニズムです。これはローカルの猶予期間とは完全に独立しており、完全にStripeによって管理されます。

デフォルトの再試行スケジュール:

  1. 試行1: 即座に(請求日)
  2. 試行2: 請求日の3日後
  3. 試行3: 請求日の5日後
  4. 試行4: 請求日の7日後

最終再試行失敗後:

  • Stripeは自動的にサブスクリプションステータスをcanceledまたはunpaidに更新します
  • Stripeはcustomer.subscription.updatedまたはcustomer.subscription.deleted webhookを送信します
  • ローカルシステムはサブスクリプションステータスをcanceledに更新します

システムの動作

タイムラインの例

Day 0: 支払期日
├─ Stripeが支払いを試行(試行1)
├─ 支払いが失敗
├─ Stripeが送信: invoice.payment_failed webhook
└─ システムアクション:
   ├─ subscription.status = 'past_due' に設定
   ├─ grace_period_end_at = Day 0 + 1日(設定値)に設定
   ├─ subscription_historiesを作成 (type: renewal, payment_status: failed, payment_attempt: 1)
   └─ ユーザーはサービスアクセスを維持

Day 1: 猶予期間終了(ローカル)
└─ 注: この時点でアプリケーションロジックによりユーザーアクセスが制限される可能性があります
   └─ しかし、Stripeの再試行は継続...

Day 3: Stripe再試行2
├─ Stripeが再度支払いを試行(試行2)
├─ 支払いが失敗
├─ Stripeが送信: invoice.payment_failed webhook
└─ システムアクション:
   └─ subscription_historiesを更新 (payment_attempt: 2)

Day 5: Stripe再試行3
├─ Stripeが再度支払いを試行(試行3)
├─ 支払いが失敗
├─ Stripeが送信: invoice.payment_failed webhook
└─ システムアクション:
   └─ subscription_historiesを更新 (payment_attempt: 3)

Day 7: Stripe再試行4(最終)
├─ Stripeが再度支払いを試行(試行4)
├─ 支払いが失敗
├─ Stripeが自動的にサブスクリプションをキャンセル
├─ Stripeが送信: customer.subscription.updated (status: canceled)
└─ システムアクション:
   ├─ subscription.status = 'canceled' に更新
   ├─ canceled_atタイムスタンプを設定
   └─ ユーザーアクセス終了

実装の詳細

支払いが失敗したとき(初回)

ハンドラー: SubscriptionInvoiceHandler::handleInvoicePaymentFailed()

private function updateSubscriptionToPastDue($subscription, $subscriptionHistory, $session): void
{
    if ($subscription->status->value !== SubscriptionStatus::PastDue->value) {
        // 設定に基づいて猶予期間終了を計算
        $gracePeriodDays = config('stripe.grace_period', 1);
        $gracePeriodEnd = Carbon::now()->addDays($gracePeriodDays);
        
        // サブスクリプションステータスをPastDueに更新
        $this->statusManager->updateStatus($subscription, SubscriptionStatus::PastDue->value, []);
        
        // grace_period_end_atを設定(未設定の場合)
        if (!$subscription->grace_period_end_at) {
            $subscription->update([
                'grace_period_end_at' => $gracePeriodEnd->format('Y-m-d H:i:s'),
            ]);
        }
    }
}

設定

ファイル: config/stripe.php

return [
    'slug' => 'stripe',
    'config' => [
        'key' => env('STRIPE_KEY'),
        'secret' => env('STRIPE_SECRET'),
        'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
    ],
    'currency' => env('CURRENCY'),
    'currency_locale' => env('CURRENCY_LOCALE'),
    'logger' => env('STRIPE_LOGGER'),
    
    // 支払い再試行設定(参照/ドキュメント用)
    'payment_retry' => [
        'attempts' => 3, // 初回試行後にStripeは3回再試行
        'interval' => 1, // ドキュメント化された間隔: 3, 5, 7日
    ],
    
    // サービスアクセス制限前のローカル猶予期間
    'grace_period' => 1, // 日数(デフォルト: 1)
];

重要な注記

猶予期間 vs スマートリトライ

機能 猶予期間 Stripeスマートリトライ
管理者 ローカルアプリケーション Stripe
目的 サービスアクセスバッファ 自動支払い回復
期間 設定可能(デフォルト: 1日) 固定(合計7日)
無効化可能 はい(設定) いいえ(Stripe機能)
トリガー 初回支払い失敗 毎回の支払い失敗
ユーザーへの影響 ローカルアクセス制御 サブスクリプションステータス制御

主要な考慮事項

  1. 独立したシステム:

    • 猶予期間はローカルアプリケーション機能です
    • StripeスマートリトライはStripeプラットフォーム機能です
    • これらは独立して動作します
  2. アクセス制御:

    • アプリケーションは猶予期間終了後にアクセスを制限できます
    • サブスクリプションはStripeがキャンセルするまでpast_dueステータスのままです
    • 最終的なキャンセルは、すべてのStripe再試行が失敗した後にのみ発生します
  3. 設定:

    • 猶予期間: config/stripe.phpで設定可能
    • Stripe再試行: Stripeダッシュボード設定で管理(設定ファイルではカスタマイズ不可)
  4. ベストプラクティス:

    • 猶予期間はStripeの再試行期間より短く設定してください(1日 < 7日)
    • これにより、Stripeが再試行を続けている間に、ユーザーが支払い問題を修正する時間を確保できます
    • 猶予期間が長すぎると、意図したバッファを超えて無料アクセスを提供する可能性があります

関連するWebhook

Webhookイベント トリガータイミング システムアクション
invoice.payment_failed 各再試行失敗時 - past_dueステータスの設定/更新
- grace_period_end_atの設定(初回)
- payment_attemptカウンタのインクリメント
customer.subscription.updated 最終ステータス変更時 - サブスクリプションステータスをcanceledに更新
- canceled_atタイムスタンプを設定
customer.subscription.deleted サブスクリプション削除時 - サブスクリプションステータスをcanceledに更新
- canceled_atタイムスタンプを設定

データベーススキーマ

subscriptionsテーブル

CREATE TABLE subscriptions (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    -- ... その他のフィールド ...
    status ENUM('unpaid', 'active', 'past_due', 'canceled') NOT NULL,
    deadline_at TIMESTAMP NULL,
    grace_period_end_at TIMESTAMP NULL COMMENT 'ステータスがpast_dueに変更されたときに設定',
    canceled_at TIMESTAMP NULL,
    -- ... その他のフィールド ...
);

subscription_historiesテーブル

CREATE TABLE subscription_histories (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    subscription_id BIGINT NOT NULL,
    type ENUM('new', 'renewal', 'change', 'cancel', 'resume') NOT NULL,
    payment_status ENUM('pending', 'paid', 'failed', 'refunded', 'na') NOT NULL,
    payment_attempt INT DEFAULT 0 COMMENT '失敗した支払い試行回数',
    -- ... その他のフィールド ...
);

テストシナリオ

シナリオ1: 一時的な支払い失敗(猶予期間終了前に解決)

1. Day 0で支払いが失敗 → grace_period_end_atがDay 1に設定される
2. Day 3でStripeが再試行 → 支払い成功
3. invoice.paid webhookを受信
4. システムがサブスクリプションを'active'ステータスに更新
5. ユーザーは中断なくアクセスを維持

シナリオ2: 猶予期間を超える支払い失敗

1. Day 0で支払いが失敗 → grace_period_end_atがDay 1に設定される
2. Day 1で猶予期間が終了 → アプリケーションがユーザーアクセスを制限
3. Stripeが再試行を継続(Day 3, 5, 7)
4. Day 7までにすべての再試行が失敗
5. Stripeがサブスクリプションをキャンセル
6. システムが'canceled'ステータスに更新

シナリオ3: ユーザーが猶予期間中に支払い方法を更新

1. Day 0で支払いが失敗 → grace_period_end_atがDay 1に設定される
2. Day 1にユーザーが支払い方法を更新
3. Day 3のStripe再試行が新しい支払い方法で成功
4. サブスクリプションが'active'ステータスに戻る
5. 猶予期間がクリアされる

参照