「払ったのに読めない」
これが最悪の状態だ。ユーザーにとって理不尽で、対応に追われるのはこちらだ。サブスクリプション決済は単発購入より状態が多い。テストを重ねて、その複雑さを自分で確認した。
サブスクの状態遷移
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が届かなくてもサービスを止めないフォールバック設計」*
*シンプルシステム株式会社 代表 伊藤勝彦*