0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails Stripe Webhook用のモデルを作る

0
Posted at

背景

  • Stripeを利用したWebhookの受信処理を実装する機会があった
  • 「なぜわざわざWebhook用のモデルを作るのか?コントローラーで直接処理すればよいのでは?」と疑問に思ったので調べた
  • 調べてみると、モデル化には明確な理由があったので残しておく

Stripe Webhookとは

Webhookとは

特定のイベントが発生したときに、別のサービスやアプリケーションへリアルタイムで通知を送る仕組み

通常のAPIが「こちらから問い合わせる(ポーリング)」のに対して、Webhookは「あちらから通知が届く」という逆方向の通信になります

Stripe Webhookとは

Stripe上で決済完了やサブスクリプション更新などのイベントが発生した際、自動的に自社サーバーへHTTP POSTリクエストで通知する仕組みです

例えば、以下のようなイベントが通知されます

  • checkout.session.completed(決済完了)
  • customer.subscription.updated(サブスクリプション更新)
  • invoice.payment_failed(請求の支払い失敗)

コード

Webhook用のモデルを作成し、受信したイベントを保存→非同期処理する実装例

マイグレーション

class CreateStripeWebhooks < ActiveRecord::Migration[7.1]
  def change
    create_table :stripe_webhooks do |t|
      t.string :event_id, null: false
      t.json :body, null: false

      t.timestamps
    end

    add_index :stripe_webhooks, :event_id, unique: true
  end
end

モデル

class StripeWebhook < ApplicationRecord
  validates :event_id, presence: true, uniqueness: true

  after_create_commit :process_later

  private

  def process_later
    StripeWebhookProcessJob.perform_later(id)
  end
end

コントローラー

class StripeWebhooksController < ApplicationController

  def create
    if StripeWebhook.exists?(event_id: @event.id)
      head :ok
      return
    end

    StripeWebhook.create!(event_id: @event.id, body: @event)
    head :ok
  end
end

ジョブ

class StripeWebhookProcessJob < ApplicationJob
  def perform(stripe_webhook_id)
    stripe_webhook = StripeWebhook.find(stripe_webhook_id)

    case stripe_webhook.body['type']
    when 'checkout.session.completed'
      # 決済完了時の処理
    when 'customer.subscription.updated'
      # サブスクリプション更新時の処理
    end
  end
end

処理の流れ

  1. Stripeからエンドポイントに対してHTTP POSTでイベントが送信される
  2. 同一イベントが既に存在する場合は200 OKを返して終了する
  3. 受信したイベントをstripe_webhooksテーブルに保存し、200 OKを返す
  4. after_create_commitActiveJobが起動し、バックグラウンドでイベントを処理する

Webhookをモデル化した方がいい理由

1. 信頼性の高い非同期処理(データの欠損を防ぐ)

WebhookはHTTP POSTで送られてきますが、その瞬間にサーバーがダウンしていたり、処理が失敗したりすると、イベントデータは消失してしまいます

stripe_webhooksテーブルにイベント情報を保存することで、処理に失敗した場合でも後から再処理(リトライ)が可能になります。決済の失敗やサブスクリプションの更新漏れといった致命的な問題を防げます

なお、Stripeは本番環境では指数バックオフで最大3日間イベントの再送を行いますが、3日間すべて失敗し続けるとエンドポイントが自動的に無効化されます。自社DB側にイベントを保存していれば、Stripe側のリトライに依存せず自前でリカバリーできます

2. 冪等性(べきとうせい)の担保

Stripeは、Webhookが正常に処理されたと認識できるまで、同じイベントを何度も再送してきます

受信したイベントのidevt_で始まる一意の識別子)をテーブルに保存し、ユニーク制約をかけておくことで、同じイベントの二重処理を防止できます

例えば、1回の支払いに対して誤って2回分のサブスクリプションを登録してしまう、といったバグの防止につながります

上記のコード例では、exists?で重複チェックを行い、既に存在する場合は200 OKを返して処理をスキップすることで、Stripe側のリトライを正常に停止させています

3. 非同期処理によるパフォーマンス向上

Webhookの処理が重い場合、サーバーから即座にレスポンスを返さないとStripe側でタイムアウトとみなされます

コントローラーでは「イベントをデータベースに保存して200 OKを返す」ことだけ行い、実際の処理(メール送信、プラン更新など)はafter_create_commitからActiveJobで非同期に実行します

これによってレスポンスを早めています

4. 監査・デバッグの容易さ

「いつ、どのイベントが届いたか」がデータベースに履歴として残ります

顧客から「支払ったのにサービスが使えない」と問い合わせを受けた際、stripe_webhooksテーブルを見れば「イベントは届いたが処理に失敗した」のか「そもそもイベントが届いていない」のかがすぐに特定できます

5. データの一貫性(ローカルコピーの維持)

Stripe側の状態(サブスクリプション状況など)を自社DBに同期しておくことで、Stripe APIへの問い合わせを最小限に抑えられます

画面表示のたびにStripe APIを叩く必要がなくなるため、レスポンス速度の向上やAPIレート制限の回避にもつながります

感想

  • シンプルにWebhook用のモデルがあった方がコードの見通しがよくなるなぁというのはかなり大きい理由
  • コントローラーは「受け取って保存して返す」だけ、モデルは「イベントの種類に応じて処理する」だけ、と責務が明確に分かれるので、後から見ても何をやっているかが分かりやすくなります

参考URL

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?