🐶

【Stripeコンビニ決済】RailsでWebhookハンドラを実装する

2023/06/29に公開

こんちには、株式会社スペースマーケットでエンジニアをしているtchmrです。

先日公開したStripeコンビニ決済の記事で割愛したWebhookハンドラについて書きたいと思います。

https://zenn.dev/spacemarket/articles/494e9643b9812f

Stripeには任意のエンドポイントに対して特定のイベントを送信するWebhook機能があります。
これを利用することでStripe内のデータ更新結果を自社のアプリケーションに取り込むといったことができます。

本記事では以下の例をご紹介します。

購入者がコンビニで支払を完了 → Stripe(支払いステータス更新) → Webhook連携 → 自社アプリケーション(支払いステータス更新)

StripeのWebhookハンドラを作成する

大まかな手順は以下のとおりです。

  1. StripeでWebhookの設定を行う
  2. ルーティング設定を行う
  3. ルーティングに対応した処理を記述する
    3-1. コントローラ作成
    3-2. サービスクラス作成

1. StripeでWebhookの設定を行う

https://stripe.com/docs/development/dashboard/register-webhook?locale=ja-JP
まずは、Stripeのダッシュボード(あるいはAPI)からWebhookの設定を行う必要があります。

ここでの留意点は以下のとおりです。

  • 本番/テスト環境それぞれで独立したWebhookを作成することができます。
    • Stripeダッシュボードで本番/テスト環境を切り替えられます
  • リッスン対象のアカウント
    • 連結アカウントではなく本体アカウントの方を選択します。
  • リッスン対象のイベント
    • charge.created(支払情報の作成イベント)
      • payment_intent.createdも検討しましたが、連携されるevent情報からPIに紐づくTransferの情報が上手く取得できなかったため上記を選択しました。この点が特に問題がないアプリケーションならこちらを採用しても良いかもしれません。

2. ルーティング設定を行う

1.でWebhook設定時にエンドポイントを設定しています。
これに対応するルーティングをアプリケーション側で作成します。

3. 処理を記述する

コントローラの作成

対応するコントローラを作成していきます。
以下、作成にあたり意識したいポイントです。

  • コントローラは見通しの良い状態にしておく
    • Webhookごとにコントローラを分離
      • 複数のWebhookを扱う場合でも独立性、可読性を高く保ちやすいです。
    • 処理はサービスクラスに切り出す
      • コントローラではサービスを呼び出すだけとすることでシンプルな作りにしやすいです。

サンプルコード

class Webhook::KonbiniPaymentSuccessController
  # コンビニ決済 支払成功イベントWebhook処理
  def update
    result = ::Konbini::UpdateStatusAfterPaymentSuccessService.call(event: @event, object: @object)
    if result.present?
      render json: { message: result[:message] }, status: result[:status] and return
    end

    render json: { message: '正常に処理が完了しました。' }, status: 200
  rescue StandardError => e
    Rails.logger.error(e.message)
    render json: { message: e.message }, status: 500
  end
  
  ...省略
end

jsonをrenderすることでStripeダッシュボードのWebhookログにレスポンスを表示することができます。

サービスクラスの作成

連携されたWebhookを元にステータスを更新する処理を記述します。

class Konbini::UpdateStatusAfterPaymentSuccessService
  attr_accessor :event
  attr_accessor :object

  def self.call(event:, object:)
    new(event: event, object: object).call
  end

  def initialize(event:, object:)
    self.event = event
    self.object = object
  end

  # コンビニ支払い 支払成功時の更新処理(Webhook受信時)
  def call
    # リクエストの検証(実装省略)
    validated_result = validate_event_data
    if validated_result.present?
      return { message: validated_result[:message], status: validated_result[:status] }
    end

    konbini_payment = KonbiniPayment.find_by(payment_intent_id: object[:payment_intent])
    return return { message: 'リソースが存在しません', status: 404 } if konbini_payment.blank?

    reservation = konbini_payment.reservation
    reservation.status = '支払済'
    konbini_payment.status = '支払済'

    reservation.save!
    stripe_konbini_payment.save!

    nil
  end
  
  private
  
  def validate_event_data
    # 必要に応じて入力値の検証を実施
    ...
  end
end

テスト

テスト環境では、コンビニ支払い用のPaymentIntentを作成して一定時間(デフォルト3分)経過するとコンビニでの支払いが完了したこととなり上記で設定したWebhookが連携されます。
メールアドレスに特定の文字列を含むことで即時支払等にすることもできます。
https://stripe.com/docs/payments/konbini/accept-a-payment?platform=web#web-test-integration

また、連携したWebhookはStripeダッシュボード上から閲覧や再送することができるのでテストの際に役立ちます。

最後に

スペースマーケットでは一緒に働いてくれる仲間を募集中です。
少しでも興味を持っていただけたらぜひ採用ページをご覧ください。

スペースマーケット Engineer Blog

Discussion