全国オンライン対応受付 平日 9:00-18:00
お問い合わせ

開発記録 / 1cto.jp制作記 / Vol.23 1cto.jp制作記 Vol.2

記事 02

Stripeのサブスクを5回テストして学んだこと

これが最悪の状態だ。ユーザーにとって理不尽で、対応に追われるのはこちらだ。サブスクリプション決済は単発購入より状態が多い。テストを重ねて、その複雑さを自分で確認した。

2026-06-07 公開

「払ったのに読めない」

これが最悪の状態だ。ユーザーにとって理不尽で、対応に追われるのはこちらだ。サブスクリプション決済は単発購入より状態が多い。テストを重ねて、その複雑さを自分で確認した。


サブスクの状態遷移

Stripeのサブスクは主に4つの状態を持つ。

| 状態 | 意味 ||---|---|| active | 有効(サービス利用可) || past_due | 支払い失敗(猶予期間) || canceled | 解約済み || incomplete | 初回支払い未完了 |

1cto.jpでは「activeなら全記事閲覧可」という単純なルールで実装した。


テスト1:基本的な購入フロー

テストカードで月額980円のサブスクを購入した。Stripeダッシュボードでサブスクがactiveになり、Webhookが届いてDBのsubscriptionsテーブルにレコードが作られることを確認した。この時点では問題なく動いた。


テスト2:解約フロー

サブスクを解約した。Stripeには2種類の解約方法がある。

即時解約。解約するとすぐにcanceledになる。

期間終了時に解約。cancel_at_period_end: trueを設定すると、現在の支払い期間が終わる日まではactiveのまま継続し、その後canceledになる。これが一般的な「次回更新をしない」という解約方法だ。

1cto.jpでは後者を採用した。ユーザーが解約しても残り期間は読めるべきだという判断だ。

cancel_at_period_endがtrueになっても、Stripeのサブスク状態はまだactiveのままだ。マイページの表示で「次回更新なし(〇月〇日まで利用可)」という表示を出すには、DBにcancel_at_period_endフラグを保存してUI側で使う必要があった。


テスト3:Webhookの遅延問題

サブスク購入後にすぐ記事にアクセスしようとすると、「購入済みのはずなのにアクセスできない」という状態になることがあった。

原因はWebhookの遅延だ。Stripe Checkoutで決済が完了してsuccess画面にリダイレクトされた時点では、Webhookがまだ届いていないことがある。DBにはまだサブスクの記録がない。

対策として、アクセス制御にStripeフォールバックを実装した。

def can_access_article(user_id: int, article: Article) -> bool:
    sub = get_active_subscription(user_id)
    if sub:
        return True

    user = get_user(user_id)
    stripe_subs = stripe.Subscription.list(customer=user.stripe_customer_id, status='active')
    if stripe_subs.data:
        return True

    return False

DBが更新されていなくても、Stripeに直接問い合わせることで即座にアクセスを許可できる。


テスト4:解約後の再購入

解約後に再びサブスクを購入した。新しいサブスクがactiveになり、正常に動いた。

ただし、フォールバック実装ではStripe Customer IDでサブスクを検索しているため、複数の顧客IDが存在する場合に片方しか検索できない可能性がある。この問題はコスト対効果の観点で「許容」とした。発生した場合はStripeダッシュボードから手動で確認できる。


テスト5:支払い期限後のアクセス拒否

Stripeのテスト環境には「クロックを進める」機能がある。これを使ってサブスクの更新日を過ぎさせた。

更新日を過ぎるとcustomer.subscription.updatedというWebhookが届き、サブスクのステータスが変わる。このイベントでDBのstatus(とcurrent_period_end)を更新するハンドラを実装した。canceledになった後はアクセスが拒否される。この動作を確認した。


5回のテストで確認したこと

Webhookの到達遅延は必ず考慮する。Checkoutのsuccess後すぐにサービスを使えるようにするには、Stripeへの直接問い合わせフォールバックが必要だ。

cancel_at_period_endとstatusは別物だ。解約予約しても、期間終了まではstatusがactiveのままだ。UIには「解約予約中」として表示する必要がある。

Webhookのべき等性を確保する。同じWebhookが2回届いてもDBが重複しないよう、subscriptions.stripe_subscription_idのUNIQUE制約とupsert処理を入れる。

テストカードで5回試したのは1日の仕事だった。しかしこの1日が「本番で払ったのに読めない」という状況を防いでいる。


*次回は「Webhookが届かなくてもサービスを止めないフォールバック設計」*

*シンプルシステム株式会社 代表 伊藤勝彦*