「スタンダードを使っている人がプレミアムに上がったとき、スタンダードをそのまま放置しない」。サブスクを2段階で設計すると、必ずここを考えることになります。
放置してしまうと、ユーザーは両方のプランを同時に課金される状態になる。それが分かった時点でクレームになるし、返金も発生する。発覚するのがだいたい数週間後というのも厄介です。
状態管理表で二重課金を防ぐ設計
このケースでは、ユーザーのプラン状態を管理するスプレッドシートを置いていました。スタンダードの状態がC列、プレミアムの状態がH列。それぞれ active か cancelled か expired で持っている。
決済が完了したときに飛んでくる通知(Webhook、外部からの通知を受け取る仕組み)を受けて、GAS(Googleのスクリプト実行環境)で処理する形でした。
AIへの頼み方はこんな感じです。
プレミアムプランの購入が完了したとき、管理表のC列を見てスタンダードがactiveだったら自動でキャンセルして。 Stripeの解約APIを呼んだあと、管理表のC列も「cancelled」に書き換えてほしい。 キャンセルが終わったら、LINEで「スタンダードプランは自動的にキャンセルされました」と一緒に通知して。
これで仕組みとしては動きます。ただ、ここから先が詰まります。
最初に詰まったこと: メッセージがリロードのたびに出る
解約通知を画面に出す実装を入れたら、「スタンダードプランはキャンセルされました」というメッセージがリロードするたびに毎回表示されるようになりました。
原因は単純で、ページを読み込むたびに管理表のC列を見てキャンセル済みかどうかを確認するロジックが走っていたからです。ステータスが cancelled のままなので、毎回該当してしまう。
直し方は、ブラウザのローカルストレージに「この通知は表示済み」というフラグを持たせて、一度表示したら二度目は出ないようにするものでした。ログインし直したらフラグをリセットするセットにして落ち着きました。
次に詰まったこと: 購入してないのに処理がずっと流れる
しばらくテストしていると、購入を一回しかしていないのに、同じ処理がループしているログが出てきました。
最初は誰かが操作しているのかと思いましたが、違いました。StripeのWebhookには、エンドポイントが正常に応答しなかったときに自動でリトライする仕組みがあります。応答として302(リダイレクトコード)が返っていたため、Stripeが「届かなかった」と判断して何度も送り直していた。
これとは別に、処理を見返したら同じ関数が2箇所に定義されていることに気づきました。同じ名前の関数が重複していると、呼ばれるたびに両方走ることがある。整理して一本にマージしました。
一番根が深かった問題: LINEとStripeを同じ窓口で受け取っていた
本当の原因はもう一つ別のところにありました。
このシステムは、LINEからの通知とStripeからの通知を、同じ受け口(doPost関数)で受け取っていました。LINEには「LINE-Signature」というヘッダーが入っていて、Stripeには「Stripe-Signature」が入っています。それぞれで検証して処理を分けるべきなのですが、GASではWebhookのヘッダーを取得するのが難しい事情があります。
結果として、Stripeのイベントが来たときもLINE側の処理が動いてしまっていた。プレミアム購入完了のWebhookが届くたびに、LINEのメッセージ送信ロジックも一緒に走り「ご購入ありがとうございます!新しいプラン(standard)が有効になりました。」という変な通知がLINEに届く、という症状もここから来ていました。
GASのヘッダー取得制限については、署名検証自体は別の構成で対応する方向を取りました。詰まり所として覚えておくと後で楽です。
AIへの頼み方のコツ
この件で感じたことを一つ。
「プレミアム購入時にスタンダードを自動キャンセルして」だけ頼むと、Stripeのキャンセル処理は書いてくれても、リトライへの備えや重複関数は出てきません。
リトライで同じ処理が複数回走らないよう、イベントIDで処理済み判定を入れて。 同じ関数が重複していないか確認してから実装して。
この一行を最初から入れておくと、後から詰まる確率がだいぶ下がります。何が起きやすいかを知っていないと頼めない、という限界はありますが、一度経験すると次から自然に入るようになります。
必要なものの整理
同じことをやるために要るのは3つです。
- Stripe(決済サービス)でサブスクリプションを管理していること
- 購入完了時にWebhookで通知を受け取れる受け口(GASやサーバー)
- ユーザーのプラン状態を記録している管理表
状態管理表のどの列を見るか、キャンセル後にどこに書き直すか、その設計が先に決まっていると、AIへの依頼がずっと具体的になります。「C列がactiveのときだけ動かして」と言えると、動くものが一発で返ってきやすい。
