決済はうまくいっている。ユーザーの継続課金も完了している。なのに管理表の契約終了日が変わらない、という状態がありました。
「Stripeのダッシュボードでは更新されているのに、なぜかFirebase側が古いまま」というやつです。
Webhookが届いていた
最初は「更新イベントが届いていないのでは」と疑いました。でも、ログを見るとそうではなかった。
Stripe event type: customer.subscription.updated
Processing subscription update: {
subscriptionId: 'sub_XXXXX',
current_period_end: 1737167804,
...
}
イベントは届いていた。Stripeはちゃんと通知を送っていた。それを受け取るコードが抜けていただけです。
当初の実装は、初回購入のイベント(決済完了)しか見ていなかった。継続更新のときは別のイベントが飛んでくるのに、そっちを処理するコードを入れていなかった。
更新時に飛ぶイベントは、購入時と別物
Stripeのサブスクは、イベントが用途ごとに分かれています。初回購入時と、毎月の自動更新では種類が違う。
整理するとこうなります。
初回購入のとき
↓ checkout.session.completed が飛ぶ
継続更新のとき
↓ customer.subscription.updated が飛ぶ
最初にコードを書いたとき、購入完了時のイベントだけ処理して「よし動いた」と思っていました。月次更新が走るまで、もう一種類のイベントが来ることに気づかなかった。
もう一つの詰まり: タイムスタンプの型
Webhookを受け取る処理を足しても、まだ更新されない時期がありました。
ログには更新処理が走った跡があるのに、Firebaseの値が変わらない。そこで気づいたのが、日付の型の違いです。
Stripeから来る current_period_end は、Unix秒単位の数値(例: 1737167804)です。Firebaseの Timestamp 型は、そのまま入れても合わない。ミリ秒に変換してから、Timestamp 型に直して保存する必要があります。
AIにこう頼みました。
サブスクの更新イベントを受け取ったとき、current_period_end をFirebaseに保存したい。 Stripeの値はUnix秒の整数で来るが、Firebaseの型に変換して入れるようにして。
これで値が入るようになりました。
管理表をどこで更新するか
少し手前の問題として「どのタイミングで管理表(スプシや自社DBなど)を更新するか」もあります。
よく見るパターンは、更新ごとに手動で確認して書き直している、です。月次で課金が走るたびに、ダッシュボードを開いて手で直している。件数が増えれば増えるほど、この手間は正直に積み上がります。
自動化するなら、更新Webhookを受けた時点でそのまま書き込むのが一番シンプルです。別途バッチを走らせて定期チェック、という構成も見かけますが、Webhookがあるなら使った方が素直に動きます。
AIにどう頼んだか
このあたりの処理を、私はAIに作ってもらいました。頼み方はだいたいこんな感じです。
Stripeのサブスクが更新されたとき、契約終了日をFirebaseに反映させたい。
- 更新イベント(customer.subscription.updated)を受け取ったら処理する
- 契約終了日のフィールドを、Stripeから来た値で上書きして保存する
- キャンセルされても、終了日はnullにせず最後の有効日付を保持したまま
タイムスタンプの型はFirebase側に合わせて変換して。
「どのイベントを受け取るか」「キャンセル時の扱い」「型の変換」を業務の言葉で渡すと、あとは動くものが出てきます。
詰まりやすい箇所
Stripeからは複数のイベントが来ます。今回のケースでは subscription_schedule.updated という別のイベントも発生していて、しばらく混乱しました。どちらのイベントから期限日を拾うか、先に決めてから実装する方がスムーズです。
契約終了日の更新漏れは、気づくのが遅れやすい。次の更新月まで誰も気づかなかった、というパターンになりがちです。Webhookが届いているか、Firebaseに入った日付が合っているか。更新後にさっと確認する仕組みを最初から入れておくと安心です。
