💰

Stripe × Google サービスで「集金業務」を自動化した話

に公開

ボランティアで500名規模の組織のIT部門を担当しているが、ある日「集金がすべて手作業で行われている」と知り、思わず目を疑った。年会費制とはいえ、年度初めは人海戦術、途中入会者の対応も手作業。牧歌的な作業マニュアルがそこにあった。そこで、集金・督促・集計をすべて自動化するシステムを構築することにした。

エンドユーザーは毎年交代する会計担当者であり、ITスキルもバラバラ。 したがって、インターフェースは極限までシンプルにした。請求書発行はスプレッドシートに対象者のIDを入力するだけ、未払いリストも自動で氏名・金額・メールアドレス・支払いURLが出力される。

つまり「タッチポイントはスプレッドシートだけ」で完結する仕組みである。


システム構成図

下記に決済システム構成の概要図を添付している。

  1. 請求書発行リストよりトリガー発行
  2. GAS経由でBigQueryのinvoiceテーブルに請求書データ格納
  3. タイマーで請求書発行url付きのメール送信/未払いリスト更新
  4. ユーザーがurlをクリック
  5. CloudRunのエンドポイントからSecret確認しStripeAPI経由で請求書発行
  6. ユーザーは遷移したStripeの請求書画面より支払いを実施
  7. Stripe側がCloudRunのエンドポイントへ支払い済みのwebhookを実行
  8. BigQueryテーブルおよびスプレッドシートに支払い済み情報更新

sytem


システム構成要素

Cloud Run(チェックアウトサービス)

Stripeの秘密キーなどの機密情報はすべて環境変数で管理し、Secret Manager経由で安全に参照している。支払いリンクはHMAC署名+有効期限付きリンクによって守られ、不正アクセスを防止。さらにBigQueryにはハッシュ化された監査データ(IPやidempotencyキー)を保存し、再実行防止とトレースも可能にしている。


GASパイプライン

GASはスプレッドシートを入口として動作し、環境(test/live)やtermをシートから読み込む。テンプレートメールに署名付きリンクを埋め込んで送信し、送信履歴は comms データセットに記録。期限を過ぎたものはWebhook経由でアラートを飛ばし、指定のスプレッドシートに自動出力する。いわば「未払い検知の番人」。


BigQuery

既存のユーザーデータから請求書データの作成や支払い確認用の台帳として機能する。ユニークIDから請求書発行に必要な情報を取得し、リンククリック時には invoice_id から支払い履歴を確認する。MERGE 構文により「存在すれば更新、なければ挿入」を行い、請求書は常に1件のみ保持される。環境ごとにデータセットを分離しているため、コード変更なしで切り替え可能。


学び

決済システムはその性質上、各所に気を付けるべきポイントがあった。以下に主だったポイントを記載する。

HMAC署名と有効期限付きリンク

支払いリンクはユーザー識別情報を含むため、改ざんされると別人の請求にすり替わる可能性がある。
そこでCloud Runはリンク生成時にHMAC署名を付与している。HMAC(Hash-based Message Authentication Code)は、共有シークレットを用いてメッセージ内容を署名する仕組みである。invoice_iduser_id・有効期限をまとめて署名し、Cloud Run側で検証。万が一URLの一部が書き換えられても即座に無効化される。もちろん署名キーはSecret Managerで厳重に保管している。


Stripe SessionとInvoiceの違い

当初はStripe側で各ユーザー用に請求書を発行し、メールでURLを送る方式を採用していた。しかし、リンクが24時間で期限切れになるという落とし穴にハマった。調べてみると、Stripeには「Session」と「Invoice」という2種類の支払いオブジェクトが存在していた。

  • Invoiceを使う場合

    • 支払いリンクは無期限で利用可能
    • 支払い状況も自動で更新され、ダッシュボードで確認できる
    • 顧客への督促もStripe側で自動化可能
    • ただし外部連携や独自署名など、細かい制御は難しい
  • Sessionを使う場合

    • 支払いページを一時的に作成する軽量オブジェクト(通常24時間有効)
    • 開発側で自由に制御でき、HMAC署名付きリンクやBigQuery連携も可能

Invoiceは管理機能が充実しているが、Stripeの顧客DBと自前の台帳が二重管理になる。しかもInvoice機能そのものにも追加手数料がかかる。

そこで今回は、柔軟性と連携性を優先してSession方式を採用した。Stripeは「決済窓口」、BigQuery+Cloud Runが「請求管理システム」という明確な役割分担である。


Cloud Runが作る一意な支払いリンク

ユーザーがリンクを開くたびに、Cloud RunがStripeへSession作成を依頼する。ただし重複請求を避けるため、stripe_idempotency_prefix:invoice_id を冪等キーとして付与している。Stripeは同じキーのリクエストを受けると、最初の結果だけを返す仕組みを持っている。つまり、同じ請求書に対して何度ボタンを押しても課金は一度きりという安心設計になっている。


BigQueryが担う「請求の台帳」

invoice_id はBigQuery上で請求状態を一意に管理するキーである。支払い完了前は「未払い」、WebhookでStripeの支払い通知を受け取ると「支払い済み」に更新される。繰り返し実行しても請求データは上書き更新のみで、重複しない。MERGE 構文を用いたこの設計により、常に最新の正しい状態が維持されている。


リンク再発行と有効期限

Stripe Sessionは有効期限を延長できないため、Cloud Runが同じ請求情報をもとに新しいSessionを再発行する。技術的には「再生成」だが、実務的には「同じ請求書リンクを再発行」として扱う。
これにより、期限切れリンクでも常に有効な支払い導線を維持できる。


Cloud RunのWebhook受信と自動連携

Cloud RunはStripeのWebhook受信口としても機能している。

Stripeが送信する支払い完了イベントを受け取り、metadata に含まれる invoice_idfamily_id をキーにBigQueryを更新。その結果、Google Workspace側では支払い状況をリアルタイムで把握できる。未払いリストの自動抽出、督促メール、Chat通知まで全自動。「誰がまだ払っていないのか」を探す作業は、もはや過去の遺産。


Test環境

Stripeにはテスト用のSandbox環境があり、本番とテストで独立した api_keywebhook_secret を使用できる。

そこでGAS側も同じように環境を切り替えできるように設計。 live/test のスクリプトプロパティを切り替えるだけで環境変更が可能となっている。BigQueryは finance / finance_test に分けて構築し、同一スキーマで安全に検証できる。Cloud Runのコンテナは同一イメージを使用し、Secret Manager経由でキーを切り替えるだけ。コード修正ゼロでデプロイ先を切り替えられる。

動作検証

本システムは Cloud Run の完全サーバレス構成で動作しているため、初回アクセス時(コールドスタート)にリクエスト遅延が発生する可能性がある。そのため、実際に負荷テストを実施した。

テストでは、Python(aiohttp)スクリプトを用いて以下の条件でリクエストを送信した:

  • スパイク試験:同時 50 リクエスト
  • 分散試験:20 秒間に 30 リクエスト(ジッター付き)

いずれも コールドスタート状態からのアクセスだったが、すべてのリクエストが正常応答し、タイムアウトや 5xx エラーは発生しなかった。Cloud Run の無料枠では「コールドスタートからのリクエスト」が課金対象になるが、今回の規模(500名程度の会員組織)では実運用上の問題は見られないと思われる。


まとめ

BigQueryが「台帳」、Cloud Runが「制御と連携」、Stripeが「決済代行」という三層構造で動作している。invoice_id が台帳側の一意性を、idempotency_key がStripe側の一意性を担保し、
さらに HMAC署名付きリンクが安全性を保証する。未払いチェックからSession発行、Webhook連携までが自動で流れ、同じリンクを何度開いても正確で安全な一回払いが実現される。

上記の実装の多くは生成AIを活用して構築したが、StripeのSession/Invoiceの違いやAPI冪等性などの決済関連のアーキテクチャを経験できたのは非常によい糧となった。

参考

Discussion