rails7でstripeのcheckoutを実装

目次

マーケター、エンジニアを月1時間からジョインできるプラットフォーム

airteamは月1時間からマーケターやエンジニアに相談できるプラットフォーム。 雇うのはハードル高いけどプロをチームに入れたい。そんな経営者のためのサービスです。 相談にのる方も募集しています。

タスクなしだから月一時間からジョイン可能

作業はなくオンライン相談メイン。 月1時間からさっと経験者に継続的に相談できます。

多様な経験者を雇用するより何倍も早くチームに

あらゆるジャンルの経験者がいるので あなたのチームのノウハウの選択肢が広がります。

NDAはすでに締結済み、契約もスムーズ

契約の煩雑なやりとりはなく、NDAはすでに締結済み、書面のやりとりはありません。

rails7でstripeのcheckoutにて決済を実装しました。詰まったところや設計をまとめました。

やりたいこと

今回の実装内容は下記

  • rails7で決済を実装したい
  • フォームなどはつくりたくないのでstripeのcheckout機能で決済ページはすでに構築済みのものを使う
  • クレジットカード決済
  • 単発決済
  • webhookで決済完了をstripeから受け取りdbに反映

全体の流れ

  • stripeにてアカウント作成、商品登録
  • gemの導入
  • config/initializers/stripe.rbにStripeのAPIキーを設定
  • 決済ボタンをおすとticket_payment viewへ遷移。そのページは表示させず、javascriptでcheckoutへredirect
  • sucsess viewへ遷移
  • 決済後にサーバーにwebhookから受け取った情報をpayment_historyに反映

stripeのcheckoutとは?

stripeのcheckoutは決済画面がすでにできていて、そこに遷移させることで決済できる仕組みです。

Checkout の仕組み

https://stripe.com/docs/payments/checkout/how-checkout-works

決済の結果をwebhookで受け取って、dbに反映させることも可能。もっと簡単なものにPayment Linksがあるんですが、こちらはただリンクを生成し、決済するもので決済の結果をサービスのdbに反映させる必要がある場合はこちらのcheckoutがおすすめです。

stripeの商品登録

stripe管理画面にてまずは商品を作成します。

商品タブから商品を追加ボタンをクリック

商品情報を設定します。

ここでprice_idを発行します。

gemの導入

gemfile

gem 'stripe'

上記でstripe gemを導入

APIキーの設定

APIキー周りを設定。config/initializers/stripe.rbにstripe.rbファイルを作成し、下記を記載。

config/initializers/stripe.rb

Stripe.api_key = ENV['STRIPE_SECRET_KEY']

checkoutページへの遷移

次にcheckoutページへの遷移を実装します。

https://stripe.com/docs/checkout/quickstart

stripeのクイックスタートでは

  • postで/create-checkout-sessionにリクエスト
  • create-checkout-sessionメソッドからredirect

ですが、railsのturbo link周りの仕様でうまくいかなかったので

  • ticket_payment viewに遷移
  • javascriptでredirect

で実装しました。

ticket_paymentの作成、遷移

  • ticket_payment viewを作成します。
  • そこへgetで遷移させます。

ticket_payment viewに下記を記載。

<script>
  var stripe = Stripe('pk_test_'); // ()内には公開可能キーを記述
  stripe.redirectToCheckout({
    sessionId: '<%= @session.id %>'
  }).then(function (result) {
  });
</script>

controller

def ticket_payment
    @session = Stripe::Checkout::Session.create({
      customer: current_user.stripe_customer_id,
      line_items: [{
        price: 'price_',
        quantity: params[:ticket_count].to_i
      }],
      mode: 'payment',
      success_url: request.base_url + '/admin/ticket_payment_success',
      cancel_url: request.base_url + '/admin/setting'
    })
  end

controllerのメソッドは上記。priceにstripeの設定画面で登録した商品のidを入力。

決済後にサーバーにwebhookから受け取った情報をpayment_historyに反映

決済後にwebhookから受け取った情報を反映させます。今回反映させるのは

  • 購入したチケット数を反映
  • 決済履歴を残す

後者はstripeで確認すればいいといえば良いのですが、サービス内で購入数などをのちのち表示できたほうが良いなと思い、一応実装。

webhook controllerを下記で作成。

class WebhookController < ActionController::API

  def webhook_event
    webhook_secret = ENV["WEBHOOK_SECRET"]
    payload = request.body.read
    if !webhook_secret.empty?
      # Retrieve the event by verifying the signature using the raw body and secret if webhook signing is configured.
      sig_header = request.env['HTTP_STRIPE_SIGNATURE']
      event = nil

      begin
        event = Stripe::Webhook.construct_event(
          payload, sig_header, webhook_secret
        )
      rescue JSON::ParserError => e
        # Invalid payload
        status 400
        return
      rescue Stripe::SignatureVerificationError => e
        # Invalid signature
        puts '⚠️  Webhook signature verification failed.'
        status 400
        return
      end
    else
      data = JSON.parse(payload, symbolize_names: true)
      event = Stripe::Event.construct_from(data)
    end
    # Get the type of webhook event sent
    event_type = event['type']
    data = event['data']
    data_object = data['object']

    case event.type
    when 'checkout.session.completed'
      event_checkout_session_completed(data)
    else
      puts "Unhandled event type: \#{event.type}"
      puts event.type
    end
    render status: 200, json: { status: 200, message: "Success" }
  end

  private

  def event_checkout_session_completed(data)
    session = data.object
    user = User.find_by(stripe_customer_id: session.customer)
    begin
      ActiveRecord::Base.transaction do
        stripe_session = Stripe::Checkout::Session.retrieve({
          id: session.id,
          expand: ['line_items']
        })
      
        # Get the total quantity of items purchased
        total_quantity = stripe_session.line_items.data.sum(&:quantity)

        amount_total = session.amount_total

        # チケット購入履歴を作成
        payment_history = PaymentHistory.create(amount: amount_total)
        payment_history.save!

        # チケットカウントを増やす
        user.increment!(:ticket_count, total_quantity)
      end
    rescue => e
      puts e
    end
  end

end

webhook_eventメソッド

  • 最初に、環境変数からWebhookの秘密鍵(WEBHOOK_SECRET)を取得します。
  • 続いて、リクエストボディを取得し、それが空でないかどうかをチェックします。
  • Webhookの秘密鍵が空でない場合(つまり、Webhookの署名検証が必要な場合)、ヘッダからStripeの署名を取得し、それとペイロード、秘密鍵を使ってイベントの検証と構築を行います。
  • 何らかの理由でペイロードが無効(JSON::ParserError)または署名が無効(Stripe::SignatureVerificationError)な場合、それをログに出力し、400のHTTPステータスコードを返して処理を終了します。
  • 秘密鍵が空の場合、ペイロードをJSONオブジェクトに変換し、そのデータからStripeのイベントオブジェクトを構築します。
  • イベントタイプとデータを抽出し、チェックアウトセッションが完了したかどうかを確認します(checkout.session.completed)。その場合、専用のメソッド(event_checkout_session_completed)を呼び出します。
  • 処理が完了したら、200のHTTPステータスコードと”Success”メッセージを含むJSONレスポンスを返します。

event_checkout_session_completedメソッド

のプライベートメソッドは、チェックアウトセッションの完了イベントを処理します。

  • セッションデータを取得し、そのセッションから顧客情報を取り出します。顧客情報を元に、データベースからユーザーを検索します。
  • トランザクションを開始し、エラーが発生した場合はそれをキャッチします。
  • Stripe APIを使用して、ラインアイテム情報を含むチェックアウトセッションを取得します。
  • ラインアイテムから購入した合計数量を算出します。
  • 支払い(payment_intent)と合計金額をログに出力します。
  • 支払い履歴(PaymentHistory)オブジェクトを作成し、保存します。
  • ユーザーが購入したチケット数を、ユーザーのチケットカウントに追加します。

このコントローラーは、StripeのWebhookを利用して購入情報を処理し、ユーザーの購入履歴とチケット数を更新する役割を果たしています。

total_quantity = stripe_session.line_items.data.sum(&:quantity)

Rubyにおける.sum(&:quantity)は、Enumerableモジュールのsumメソッドを利用しています。

まず、&:quantityはRubyのシンボルをProcオブジェクトに変換する省略形です。これは{ |item| item.quantity }と同等で、それぞれのアイテムに対してquantityメソッドを呼び出します。つまり、&:quantityは各アイテムからquantity属性を取得するための手段です。

そして、sumメソッドはその取得した属性(この場合はquantity)の合計を計算します。

なので、stripe_session.line_items.data.sum(&:quantity)はstripe_sessionのline_itemsデータ(配列と想定)のそれぞれのアイテムからquantityを取り出し、その総和(つまり、全アイテムのquantityの合計)を計算します。

具体的には、購入した商品の個数の合計を計算していると考えられます。