Rails7+Stripeで決済機能を実装してみた
バージョン
- Ruby: 3.1.4
- Rails: 7.1.5
- データベース: MySQL (mysql2 ~> 0.5)
開発したアプリの概要
今回は決済機能を作ってみたいと思い、個人開発を行ったのでアウトプットしていきます
作成したアプリは僕が現役神主であることから『御祈祷予約アプリ』です
以下の流れで御祈祷予約を行うようにしています
- ログイン(devise)
↓ - トップ画面
↓ - 新規御祈祷予約画面
↓ - 予約内容確認画面
↓ - Stripe決済画面
↓ - 予約一覧画面
決済処理までの流れ
stripeのライブラリをインストールしておきます。
gem 'stripe'
1. ログイン後、トップ画面の『新規予約ボタン』を押下
.container
h1.page-title 神社御祈祷予約システム
.action-cards
.card
h2.card-title 新規御祈祷予約
p.card-text ご希望の御祈祷の予約を承ります。
= link_to "新規予約", new_reservation_path, class: "button button-primary"
2.以下のメソッドが呼び出されて新規御祈祷予約画面が表示される
def new
@reservation = current_user.reservations.build
# キャンセルされた場合の通知
flash.now[:alert] = "決済がキャンセルされました。再度お試しください。" if params[:canceled]
end
.reservation-container
h2 新規御祈祷予約
= form_with(model: @reservation, local: true) do |f|
- if @reservation.errors.any?
.error-messages
ul
- @reservation.errors.full_messages.each do |message|
li = message
.form-group
= f.label :prayer_type_id, "御祈祷の種類"
= f.collection_select :prayer_type_id, PrayerType.active, :id, :name, { prompt: "選択してください" }, class: "form-select"
.form-actions
= f.submit "予約内容を確認する", class: "button button-primary"
= content_for :scripts do
javascript:
window.stripeKey = "#{Rails.configuration.stripe[:publishable_key]}";
3.新規御祈祷予約画面の項目を埋めていき『予約内容を確認する』ボタン押下で確認画面に遷移
def create
# フォームから送信された予約情報を保存
@reservation = current_user.reservations.build(reservation_params)
# フォームの検証
if @reservation.valid?
# 有効な場合は決済ページに遷移するためのフォームを表示
render :confirm
else
render :new, status: :unprocessable_entity
end
end
.reservation-container
h2 予約内容の確認
.reservation-details
p
strong 御祈祷の種類:
= @reservation.prayer_type.name
p
strong 備考:
= @reservation.note
.payment-actions
= form_tag checkouts_path, method: :post do
= hidden_field_tag :prayer_type_id, @reservation.prayer_type_id
= hidden_field_tag :reserved_date, @reservation.reserved_date
= hidden_field_tag :time_slot_id, @reservation.time_slot_id
= hidden_field_tag :number_of_people, @reservation.number_of_people
= hidden_field_tag :note, @reservation.note
= submit_tag '決済に進む', class: 'button button-primary', data: { turbo: false, turbo_confirm: '決済画面に移動しますか?' }
= link_to '修正する', new_reservation_path(reservation: { prayer_type_id: @reservation.prayer_type_id, reserved_date: @reservation.reserved_date, time_slot_id: @reservation.time_slot_id, number_of_people: @reservation.number_of_people, note: @reservation.note }), class: 'button button-secondary'
4.決済に進むボタン押下で以下のコントローラーにリクエストが送られてStripeの決済画面に遷移します
class CheckoutsController < ApplicationController
before_action :authenticate_user!
def create
# 予約情報をセッションに保存
session[:reservation_params] = {
prayer_type_id: params[:prayer_type_id],
reserved_date: params[:reserved_date],
time_slot_id: params[:time_slot_id],
number_of_people: params[:number_of_people],
note: params[:note]
}
# プレビュー用に一時オブジェクトを作成
prayer_type = PrayerType.find(params[:prayer_type_id])
time_slot = TimeSlot.find(params[:time_slot_id])
# チェックアウトセッションの作成
checkout_session = create_session(prayer_type, time_slot, params)
# Stripeの決済ページにリダイレクト
# 作成されたStripeのチェックアウトセッションURLへユーザーをリダイレクト
# allow_other_host: trueは外部ドメイン(Stripe)へのリダイレクトを許可するパラメータ
redirect_to checkout_session.url, allow_other_host: true
end
private
def create_session(prayer_type, time_slot, params)
# Stripeセッションを作成
# セッションはStripeサーバー上で安全に保管され、決済が完了するまで有効です
# 決済情報(金額、商品説明など)と予約情報をStripe側で関連付けて管理できます
# Stripe::Checkout::Session.createメソッド
Stripe::Checkout::Session.create({
# 顧客情報:ユーザーIDとメールアドレス
client_reference_id: current_user.id,
customer_email: current_user.email,
# 決済モード:支払いのみ
mode: 'payment',
# 支払い方法:クレジットカードのみ
payment_method_types: ['card'],
# 商品情報:1件の商品
line_items: [{
quantity: 1,
price_data: {
currency: 'jpy',
unit_amount: prayer_type.price,
product_data: {
name: prayer_type.name,
description: "#{params[:reserved_date]} #{time_slot.formatted_time}",
metadata: {
prayer_type_id: prayer_type.id,
time_slot_id: time_slot.id
}
}
}
}],
# メタデータ作成
metadata: {
prayer_type_id: prayer_type.id,
reserved_date: params[:reserved_date],
time_slot_id: time_slot.id,
number_of_people: params[:number_of_people],
note: params[:note],
user_id: current_user.id
},
# 決済成功時のリダイレクト先
success_url: "#{root_url}reservations?success=true",
# 決済キャンセル時のリダイレクト先
cancel_url: "#{root_url}reservations/new?canceled=true"
})
end
end
5. 決済完了後の処理
決済処理が完了すると以下のURLにリダイレクトされます。
success_url: "#{root_url}reservations?success=true",
また今回はStripeのWebhookを設定しました。
決済処理が完了するとStripeから以下のイベントがlocalhost:3001/webhooks/stripe
に自動で送られてくるように設定しました。
- 決済の課金処理が成功 (
charge.succeeded
) - 支払い意図が成功 (
payment_intent.succeeded
) - チェックアウトセッションが完了 (
checkout.session.completed
) - 新しい支払い意図が作成 (
payment_intent.created
)
localhost:3001/webhooks/stripe
に上記のイベントが送られてくると、以下のコントローラーで予約完了の新しいレコードが作成されるようにしました。
class WebhooksController < ApplicationController
# CSRFトークン検証をスキップ(外部サービスからのリクエストのため)
skip_before_action :verify_authenticity_token
def stripe
# リクエストボディを読み込む
request_body = request.body.read
# Stripeの署名ヘッダーを取得
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
# Stripeのエンドポイントシークレットを取得
endpoint_secret = ENV['STRIPE_WEBHOOK_SECRET']
begin
# Stripeのイベントを解析
# Stripe::Webhook.construct_eventメソッド
event = Stripe::Webhook.construct_event(
request_body, sig_header, endpoint_secret
)
rescue JSON::ParserError => e
# JSON解析エラー時の処理
render json: {error: e.message}, status: 400
return
rescue Stripe::SignatureVerificationError => e
# 署名検証エラー時の処理
render json: {error: e.message}, status: 400
return
end
# イベント処理
if event.type == 'checkout.session.completed'
# 決済が完了した場合の処理
handle_checkout_session_completed(event.data.object)
end
# リクエストを受け取ったことを確認
render json: {received: true}
end
private
# 決済が完了した場合の処理
def handle_checkout_session_completed(session)
# セッションからメタデータを取得
metadata = session.metadata
# 予約レコードを作成
reservation = Reservation.create!(
user_id: metadata.user_id,
prayer_type_id: metadata.prayer_type_id,
reserved_date: metadata.reserved_date,
time_slot_id: metadata.time_slot_id,
number_of_people: metadata.number_of_people,
note: metadata.note,
status: 'confirmed',
payment_intent_id: session.payment_intent
)
end
end
上記の処理を含め決済処理が完了したら、予約一覧画面にリダイレクトされ、予約のレコードが作成されます。
まとめ
今回はstripeを用いた決済とstripeのwebhookを用いて決済処理を実装していきました。
stripeのライブラリを用いることで決済画面を作成することなく実装することができました。
stripe便利なのでぜひ使ってみてください!!
重要なのはstripeのメソッドを使うことです。
def create_checkout_session(prayer_type, time_slot, params)
# このメソッド
Stripe::Checkout::Session.create({
# 顧客情報:ユーザーIDとメールアドレス
client_reference_id: current_user.id,
customer_email: current_user.email,
# 決済モード:支払いのみ
mode: 'payment',
# 支払い方法:クレジットカードのみ
payment_method_types: ['card'],
# 商品情報:1件の商品
line_items: [{
quantity: 1,
price_data: {
currency: 'jpy',
unit_amount: prayer_type.price,
product_data: {
name: prayer_type.name,
description: "#{params[:reserved_date]} #{time_slot.formatted_time}",
metadata: {
prayer_type_id: prayer_type.id,
time_slot_id: time_slot.id
}
}
}
}],
# 予約情報のメタデータ作成
metadata: {
prayer_type_id: prayer_type.id,
reserved_date: params[:reserved_date],
time_slot_id: time_slot.id,
number_of_people: params[:number_of_people],
note: params[:note],
user_id: current_user.id
},
# 決済成功時のリダイレクト先
success_url: "#{root_url}reservations?success=true",
# 決済キャンセル時のリダイレクト先
cancel_url: "#{root_url}reservations/new?canceled=true"
})
end
# Stripeのイベントを解析
event = Stripe::Webhook.construct_event(
request_body, sig_header, endpoint_secret
)