月額サブスク決済失敗時に term_to が延長されない問題
STG 環境での網羅的テスト計画 ― バグ再現 → 修正検証 → 影響範囲確認
テスト環境・前提条件
stg.shop.mirrorfit.jp
本田さんから共有済みの toC アカウント
DB 直接参照 / MirrorFit コンソール / GKE ログ
STG ダッシュボード(テストモード)/ Stripe CLI
テスト対象テーブル
事前準備
- テスト対象ユーザーの user_id と stripe_customer_id を控える
- user_subscriptions の現在のレコードを SELECT で記録しておく
- Stripe Dashboard > Webhooks で現在の endpoint 設定をスクリーンショット保存
- stg 用アカウントでミラーフィットアプリにログインできるか確認NOW
- そのアカウントが既にサブスク契約中かどうか確認する
- API バージョン不整合のコード修正が必要か確認する
webhook:
2020-08-27(旧形式)/ コード:2025-03-31.basil(新形式)
テスト用カード
有効期限: 任意の未来日(例: 12/30)、CVC: 任意の3桁(例: 123)
API バージョン不整合について(既知の課題)
Stripe には 2つの API バージョンが存在する。コード側(アプリ → Stripe の API 呼び出し時)と、webhook エンドポイント側(Stripe → アプリへのイベント送信時)で、それぞれ別のバージョンが使われる。
同じ invoice オブジェクトでも、取得方法によってフィールド構造が異なる:
handleInvoicePaymentFailed(559行)は webhook ペイロードをそのまま使うが、新形式のフィールドで読もうとしている。旧形式のペイロードが届くため subscriptionId が undefined になる。
一方 handlePaymentIntentFailed(364行)は Stripe API で invoice を retrieve するため、コード側のバージョンでレスポンスが返る。こちらは正常に取得できる。
subscriptionId が取れないため、term_to が current_period_end + 1ヶ月(正確な値)ではなく now + 1ヶ月(フォールバック値)になる。致命的ではないが正確ではない。この問題は Phase 1〜3 とは別に検証する。
- Stripe Dashboard > Webhooks で、STG endpoint に invoice.payment_failed が登録されていないことを確認
- テストユーザーの支払い方法をカード 4000000000000341 に変更
- 決済失敗をトリガー A: toC アプリからサブスク申し込み → テストカードで決済 B: Stripe Dashboard > Subscriptions > Actions > Create upcoming invoice C: Stripe CLI で stripe trigger invoice.payment_failed
- Stripe Dashboard > Events で発火したイベントを確認
- invoice.payment_failed イベントが発火しているが webhook 未配信
- payment_intent.payment_failed が発火している可能性あり
term_toが変更されていないことfailed_at,retry_countが更新されていないこと
- invoice.payment_failed の処理ログが存在しないこと(webhook 未登録のため届かない)
- payment_intent.payment_failed が受信されている → invoice retrieve 成功 →
shouldHandleFailure = false→ handleSubscriptionPaymentFailed が呼ばれていないこと
2025-03-31.basil でレスポンスが返るため、invoice.parent.subscription_details.subscription から subscriptionId を取得でき、shouldHandleFailure = false になる(invoice.payment_failed 側に処理を委譲する設計)。しかし invoice.payment_failed 自体が届かないため、どちらのハンドラでも term_to は更新されない。- Stripe Dashboard > Webhooks に
invoice.payment_failedを追加(これのみ。コード変更なし)
- テストユーザーに有効なサブスクがあることを確認(term_to が未来日、failed_at が NULL)
- 支払い方法を失敗カード 4000000000000341 に設定
- 決済失敗をトリガー
- Stripe Dashboard で invoice.payment_failed の配信が 200 OK か確認
- handleInvoicePaymentFailed が実行されている(invoice.payment_failed が届いている)
- handleSubscriptionPaymentFailed が呼ばれている
term_toが更新されているfailed_atが現在日時付近に設定されているretry_count= 1payment_failed_codeにエラーコードが入っているlast_webhook_event_idに evt_xxx が入っている
status= 'failure'
- ユーザーのサブスクが有効なまま表示される(term_to が延長済みのため)
subscriptionId が取れず term_to = now + 1ヶ月(フォールバック値)になる可能性がある。current_period_end + 1ヶ月との差異は「別検証: API バージョン」で扱う。ここでは term_to が更新されること自体を確認する。- テスト 2-1 の失敗状態(retry_count = 1)から開始
- 再度決済失敗をトリガー
retry_countが増加(前回 +1 以上、最大 4)last_webhook_event_idが新しい event ID に更新failed_atは最初に設定された値のまま変わらないterm_toが再計算されている
- テスト 2-1 or 2-2 実施後、last_webhook_event_id を控える
- Stripe Dashboard > Events から同じイベントを Resend(または stripe events resend evt_xxx)
retry_count、term_to、failed_atが一切変化しない
- 重複イベントとしてスキップされたログが出力されている
- テスト 2-1 or 2-2 の失敗状態から開始
- 支払い方法を成功カード 4242424242424242 に変更
- 決済成功をトリガー
- payment_intent.succeeded が発火することを確認
failed_at= NULLretry_count= NULLpayment_failed_code= NULLlast_webhook_event_id= NULL
status= 'success'
- テスト 2-4 で決済成功した後、または正常なサブスクがある状態
- 次の月次請求を成功させる(Stripe Dashboard で手動トリガー)
- invoice.paid イベントが発火
term_toが invoice の period.end に基づいて更新されている- 新規レコードが作成されず既存レコードが更新されている(extendSubscriptionPeriod パス)
- extendSubscriptionPeriod が呼ばれているログ
- テストユーザーの user_subscriptions レコードを全て削除(またはレコードなしの別ユーザーを使用)
- 決済失敗をトリガー
- 新規レコードが INSERT されている
term_from= Stripe の current_period_startterm_to= current_period_end + 1ヶ月failed_at,retry_count,payment_failed_codeが設定されているplan_typeが Stripe の price metadata から正しく判定されている
- createSubscriptionRecordOnFirstFailure が実行されたログ
- 決済失敗をトリガー
- Stripe Dashboard で両イベントの発火を確認
- GKE ログで両ハンドラの実行順序を確認
- handlePaymentIntentFailed: shouldHandleFailure = false → handleSubscriptionPaymentFailed を呼んでいない
- handleInvoicePaymentFailed: handleSubscriptionPaymentFailed を呼んでいる
term_toが1回だけ更新されている(二重更新なし)
- テストユーザーで toC アプリからサブスクを新規購入
- Stripe Checkout でカード 4242424242424242 を入力して完了
- checkout.session.completed が 200 OK で配信
- orders テーブルにレコード作成
- user_subscriptions に新規レコード作成
- ユーザーのサブスク状態が正しく反映
- サブスクが期限切れ、またはレコードなしのユーザーで決済成功
- invoice.paid イベント発火
- isNewSubscriptionStart = true のパスで新規レコード作成
plan_typeが正しい
is_withdrawal= falsewithdrawal_date= NULLis_device_enabled= true
- Stripe Dashboard から過去の charge に対して返金を実行
- charge.refunded イベント発火
- charge.refunded が 200 OK で配信
- handleChargeRefunded がエラーなく処理されている
- Stripe Dashboard > Send test webhook で checkout.session.expired を送信
- checkout.session.expired が 200 OK で配信
- handleCheckoutSessionExpired がエラーなく処理されている
- 失敗情報がない通常状態で決済成功させる
failed_at等が既に NULL の場合、resetSubscriptionPaymentFailure がエラーなく通過する
status= 'success'
Stripe CLI or Dashboard の Send test webhook で invoice.payment_failed を送信し、attempt_count を以下の値に変えてテスト
| attempt_count | retry_count | |
|---|---|---|
| 0 | → | 1 |
| 1 | → | 1 |
| 4 | → | 4 |
| 5 | → | 4 |
| なし (undefined) | → | 1 |
invoice.parent.subscription_details.subscriptionpaymentIntent.subscriptionpaymentIntent.metadata.subscription_idpaymentIntent.metadata.subscriptionId
- 通常の決済失敗で (1) が使われることをログで確認
- Stripe CLI で invoice なしの payment_intent.payment_failed を送信しフォールバックが動作するかログで確認
- 通常パス: invoice 取得成功 → shouldHandleFailure = false
- フォールバックパス: invoice 取得失敗 → shouldHandleFailure = true、フォールバックで subscriptionId 取得
何が起きているか
webhook エンドポイントの API バージョンが 2020-08-27 のため、Stripe から送られる webhook ペイロードは旧形式。handleInvoicePaymentFailed のコード(559行)は新形式の invoice.parent.subscription_details.subscription で subscriptionId を取得しようとするが、旧形式ペイロードではこのフィールドが存在しない(旧形式では invoice.subscription)。
結果として subscriptionId が undefined → Stripe subscription を retrieve できない → term_to が current_period_end + 1ヶ月(正確な値)ではなく now + 1ヶ月(フォールバック値)になる。
なお handlePaymentIntentFailed(364行)は Stripe API で invoice を retrieve するため、コード側の API バージョン 2025-03-31.basil でレスポンスが返り、こちらは正常に取得できる。問題は webhook ペイロードを直接使う handleInvoicePaymentFailed のみ。
解消方法の選択肢
Stripe ダッシュボードで webhook エンドポイントの API バージョンを 2025-03-31.basil に更新する。ペイロードが新形式になり、コード修正不要。
確認事項: 全イベントのペイロード形式が変わるため、既存ハンドラ(handleInvoicePaid の data.lines.data 等)が新形式でも同じ構造かを Stripe changelog で事前確認。
559行で旧形式 invoice.subscription も参照するフォールバックを追加する。webhook 設定はそのまま。
コード変更 + デプロイが必要。webhook 設定との不整合は残るが、影響範囲は限定的。
- handleInvoicePaymentFailed で
subscriptionIdが undefined になっているか - handleSubscriptionPaymentFailed で Stripe subscription の retrieve がスキップされているか
term_toの値がnow + 1ヶ月(フォールバック)かcurrent_period_end + 1ヶ月(正確)かを、Stripe ダッシュボードの current_period_end と突き合わせて判定
- V-1 のデータをリセット(failed_at, retry_count 等を NULL に戻す、または別ユーザーを使用)
- 決済失敗をトリガー
- handleInvoicePaymentFailed で
subscriptionIdが正しく取得できている - handleSubscriptionPaymentFailed で Stripe subscription の retrieve が成功している
term_to= Stripe のcurrent_period_end + 1ヶ月(正確な値)
テスト結果記録テンプレート
完了基準
- P1修正前にバグが再現できること(term_to が延長されない)
- P2テスト 2-1 〜 2-7 が全て OK(webhook 追加のみで term_to が延長される)
- P3テスト 3-1 〜 3-7 が全て OK(既存ハンドラにリグレッションなし)
- VV-1 で API バージョン不整合の影響を確認し、V-2 で対応後の正常動作を確認
- ALL全フェーズ完了後、GKE ログに予期しないエラーがないことを最終確認