はじめに
本 Chapter では、決済処理を実装していきます。
前 Chapter で、カート機能を実装しました。カートに追加された商品を Stripe を使って購入する処理を実装します。
モデル・テーブルを作成しよう
Order
最初に Order モデル・orders テーブルの作成を行います。
コンテナが立ち上がっていることが確認できましたら、ecommerce_web コンテナに入ります。
$ docker-compose run --rm web bash
Creating ecommerce_web_run ... done
ruby@884097f5536a:/app$
無事コンテナに入ることができましたら、下記コマンドを実行してください。
$ rails g model Order name postal_code prefecture address1 address2 postage:integer billing_amount:integer status:integer customer:references
顧客と注文テーブルは1対多の関係にあるため、references 型 を使って外部キー制約のついたカラムを作成しています。
上記コマンドを実行するとマイグレーションファイルが生成されるはずです。
db/migrate/xxxxxxxxxxxxxx_create_orders.rb
を開いて下記コードを記述してください。
(x には数字が入ります)
class CreateOrders < ActiveRecord::Migration[7.0]
def change
create_table :orders do |t|
t.string :name, null: false
t.string :postal_code, null: false
t.string :prefecture, null: false
t.string :address1, null: false
t.string :address2
t.integer :postage, null: false
t.integer :billing_amount, null: false
t.integer :status, null: false, default: 0
t.references :customer, null: false, foreign_key: true
t.timestamps
end
end
end
address2 カラム以外には、null: false
制約を付与しました。
address2 カラムは Stripe の決済画面で入力が任意のため null を許可しています。
また、status
は、enum で管理しますので、integer 型であること、デフォルト値を設定している点に注意です。
マイグレーションファイルの編集が完了したら、下記コマンドを実行して orders テーブルを作成しましょう。
$ rails db:migrate
db/schema.rb
に 下記のように orders テーブルが追加されていれば OK です。
create_table "orders", force: :cascade do |t|
t.string "name", null: false
t.string "postal_code", null: false
t.string "prefecture", null: false
t.string "address1", null: false
t.string "address2"
t.integer "postage", null: false
t.integer "billing_amount", null: false
t.integer "status", default: 0, null: false
t.bigint "customer_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["customer_id"], name: "index_orders_on_customer_id"
end
続いて、アソシエーションの記述を行います。
app/models/order.rb
を開いてください。
マイグレーションファイルを作成する際に、references 型を使ったため、既に下記コードが記述されているはずです。
class Order < ApplicationRecord
belongs_to :customer
end
ただし、has_many
の記述はされていないため、手動で追加しましょう。
app/models/customer.rb
を開いて下記コードを追加してください。
class Customer < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
with_options presence: true do
validates :name
validates :status
end
enum status: {
normal: 0,
withdrawn: 1,
banned: 2
}
has_many :cart_items, dependent: :destroy
+ has_many :orders, dependent: :destroy
end
これで、下記のようなアソシエーションが実現できました。
続いて、order モデルに enum の設定を行いましょう。
app/models/order.rb
を開いて、下記コードを記述してください。
class Order < ApplicationRecord
belongs_to :customer
+ enum status: {
+ waiting_payment: 0,
+ confirm_payment: 1,
+ shipped: 2,
+ out_of_delivery: 3,
+ delivered: 4
+ }
end
注文ステータスとは、
- waiting_payment(入金待ち)
- confirm_payment(入金確認)
- shipped(出荷済み)
- out_for_delivery(配送中)
- delivered(配達済み)
という五つの状態のいずれかです。デフォルト値は、0(waiting_payment)と設定しましたね。
OrderDetail
次に OrderDetail モデル・order_details テーブルの作成を行います。
下記コマンドを実行してください。
$ rails g model OrderDetail price:integer quantity:integer order:references product:references
上記コマンドを実行するとマイグレーションファイルが生成されるはずです。
db/migrate/xxxxxxxxxxxxxx_create_order_details.rb
を開いて下記コードを記述してください。
(x には数字が入ります)
class CreateOrderDetails < ActiveRecord::Migration[7.0]
def change
create_table :order_details do |t|
t.integer :price, null: false
t.integer :quantity, null: false
t.references :order, null: false, foreign_key: true
t.references :product, null: false, foreign_key: true
t.timestamps
end
end
end
price カラムと quantity カラムに null: false
を付与しました。
マイグレーションファイルの編集が完了したら、下記コマンドを実行して order_details テーブルを作成しましょう。
$ rails db:migrate
db/schema.rb
に 下記のように order_details テーブルが追加されていれば OK です。
create_table "order_details", force: :cascade do |t|
t.integer "price", null: false
t.integer "quantity", null: false
t.bigint "order_id", null: false
t.bigint "product_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["order_id"], name: "index_order_details_on_order_id"
t.index ["product_id"], name: "index_order_details_on_product_id"
end
続いて、アソシエーションの記述を行います。
app/models/order_detail.rb
を開いてください。
マイグレーションファイルを作成する際に、references 型を使ったため、既に下記コードが記述されているはずです。
class OrderDetail < ApplicationRecord
belongs_to :order
belongs_to :product
end
ただし、has_many
の記述はされていないため、手動で追加しましょう。
app/models/order.rb
を開いて下記コードを追加してください。
class Order < ApplicationRecord
belongs_to :customer
enum status: {
waiting_payment: 0,
confirm_payment: 1,
shipped: 2,
out_of_delivery: 3,
delivered: 4
}
+ has_many :order_details, dependent: :destroy
end
同様に app/models/product.rb
を開いて下記コードを追加してください。
class Product < ApplicationRecord
with_options presence: true do
validates :name
validates :description
validates :price
validates :stock
validates :image
end
has_one_attached :image
scope :price_high_to_low, -> { order(price: :desc) }
scope :price_low_to_high, -> { order(price: :asc) }
has_many :cart_items, dependent: :destroy
+ has_many :order_details, dependent: :destroy
end
これで、下記のようなアソシエーションが実現できました。
これで作成しなければならないモデル・テーブルは以上です 🎉
これより先の Chapter では、コントローラとビューのみを作成していきます。
Stripe のセットアップをしよう
続いて、Stripe のセットアップを行います。
アプリケーションの中で Stripe を使うためには、API キーが必要になるため、取得しましょう。
まずは、Stripe に新規登録する必要があります。
新規登録が完了しましたら、ホームにアクセスしてください。
上記画面の右下に「公開可能キー」と「シークレットキー」があります。この二つの値をアプリケーションに登録します。
また、テスト環境であることを確認してください。
キーの値は機密情報です。
そのため今回は、機密情報を暗号化し、安全に保護する Credentials という機能を使います。Credentials を使えば、復号化キーを紛失、もしくは盗難にあわない限り、機密情報を保護することができます。
下記コマンドを実行して、Credentials を作成しましょう。
$ EDITOR="vi" bin/rails credentials:edit -e development
EDITOR="vi"
はエディタを、-e development
は環境を指定しています。上記コマンドを実行すると、以下の二つのファイルが生成されるはずです。
-
config/credentials/development.key
(秘密鍵) -
config/credentials/development.yml.enc
(公開鍵)
現在、下記のようにエディタが立ち上がっているはずです。
続いて、「i」キーを押して、インサートモードに入り、下記コードを記述してください。
# aws:
# access_key_id: 123
# secret_access_key: 345
stripe:
publishable_key: pk_test_xxxxx
secret_key: sk_test_xxxxx
publishable_key
には 公開可能キー を、secret_key
には シークレットキー を設定してください。
キーが設定できたら、「esc」を押して、「wq!」で保存しましょう。Credentials を閉じると、下記のようなログが表示されているはずです。
ruby@bb885d66e366:/app$ EDITOR="vi" bin/rails credentials:edit -e development
Adding config/credentials/development.key to store the encryption key: f670674081f46cc683cb9594dca3f10b
Save this in a password manager your team can access.
If you lose the key, no one, including you, can access anything encrypted with it.
create config/credentials/development.key
Ignoring config/credentials/development.key so it won't end up in Git history:
append .gitignore
File encrypted and saved.
ruby@bb885d66e366:/app$
それでは、キーが設定できたかどうか確認してみましょう。下記コマンドを実行してコンソールにログインしてください。
$ rails c
コンソールに入りましたら、publishable_key
と secret_key
が設定できているかを確認します。
irb(main):001:0> Rails.application.credentials.dig(:stripe, :publishable_key)
=> "pk_test_xxxxx"
irb(main):002:0> Rails.application.credentials.dig(:stripe, :secret_key)
=> "sk_test_xxxxx"
Credentials を読み込む際には dig メソッドを使いました。
コマンドを実行して、Stripe のキーが返ってくれば正しくキーが設定されています。
あとは、サーバを起動した際に Credentials に設定した Stripe のキーを読み込むだけです。
config/initializers/stripe.rb
を作成して下記コードを記述しましょう。
$ touch config/initializers/stripe.rb
Stripe.api_key = Rails.application.credentials.dig(:stripe, :secret_key)
Stripe.api_version = '2022-11-15'
Stripe.api_version
には、最新のバージョンを指定してください。
バージョンについては、下記のドキュメントをご覧ください。
記述が完了しましたら、サーバを再起動 してください。
サーバ起動時にエラーが吐かれず、正常にコンテナが立ち上がれば OK です。
Stripe のセットアップは以上です。
これで Stripe の決済処理が使えるようになっています 🎉
決済処理を実装しよう
これより Stripe を使った決済処理を実装していきます。
処理の流れは下記ドキュメントに記載されている通りです。
- 顧客が商品を購入する準備ができると、Stripe の新しい Checkout セッションを作成する。
- Checkout セッションは、顧客を Stripe がオンラインで提供する決済ページにリダイレクトさせる URL を提供する。
- 顧客は決済ページに決済情報を入力し、取引を完了させる。(今回は、注文の宛名、住所、クレジットカード情報を入力)
- 取引終了後、Webhook は
checkout.session.completed
イベントを使用して注文のフルフィルメントを実行する。 - Webhook のイベントを検知し、注文情報をデータベースに登録する。
- 顧客を注文完了ページにリダイレクトさせる。
処理の流れの中で、Webhook という単語が登場しました。
Webhook とは、Stripe からイベントを受信する HTTP エンドポイントです。
Stripe は HTTPS を使用して、これらの通知を JSON ペイロードとしてアプリに送信します。次に、これらの通知を使用して、バックエンドシステムで操作を実行することができるのです。
今回は、checkout.session.completed
をトリガーにして、注文情報をデータベースに登録します。
上記の決済処理の流れを覚えておいてください。
この流れ通りにコントローラを作成していきます。
CheckoutsController を作ろう
まずは、Stripe の新しい Checkout セッションを作成するためのコントローラを作成します。
下記コマンドを実行してください。
$ rails g controller customer/checkouts --no-helper --skip-template-engine
CheckoutsController を作成する際に、--skip-template-engine
オプションを付与しているのは、決済画面は Stripe が用意するものを使うためです。ビューは不要ということですね。
続いて、ルーティングの設定を行います。
config/routes.rb
を開いて下記コードを追加してください。
Rails.application.routes.draw do
devise_for :admins, controllers: {
sessions: 'admin/sessions'
}
devise_for :customers, controllers: {
sessions: 'customer/sessions',
registrations: 'customer/registrations'
}
root to: 'pages#home'
namespace :admin do
resources :products, only: %i[index show new create edit update]
end
scope module: :customer do
resources :products, only: %i[index show]
resources :cart_items, only: %i[index create destroy] do
member do
patch 'increase'
patch 'decrease'
end
end
+ resources :checkouts, only: [:create]
end
get '/up/', to: 'up#index', as: :up
get '/up/databases', to: 'up#databases', as: :up_databases
end
CheckoutsController は、create アクションのみとなります。
生成されたパスを見てみましょう。
$ rails routes | grep checkouts
下記のようなパスが出力されれば OK です。
checkouts POST /checkouts(.:format) customer/checkouts#create
では次に、コントローラの各アクションの中身を記述していきます。
app/controllers/customer/checkouts_controller.rb
を開いて下記コードを記述してください。
class Customer::CheckoutsController < ApplicationController
before_action :authenticate_customer!
def create
line_items = current_customer.line_items_checkout
session = create_session(line_items)
# Allow redirection to the host that is different to the current host
redirect_to session.url, allow_other_host: true
end
private
def create_session(line_items)
Stripe::Checkout::Session.create(
client_reference_id: current_customer.id,
customer_email: current_customer.email,
mode: 'payment',
payment_method_types: ['card'],
line_items:,
shipping_address_collection: {
allowed_countries: ['JP']
},
shipping_options: [
{
shipping_rate_data: {
type: 'fixed_amount',
fixed_amount: {
amount: 500,
currency: 'jpy'
},
display_name: 'Single rate'
}
}
],
success_url: root_url,
cancel_url: "#{root_url}cart_items"
)
end
end
Stripe 特有の書き方となりますので、一行一行確認していきます。
まず、create アクション内の一行目ですが、current_customer
に対して、line_items_checkout
というメソッドを実行しています。現在、このメソッドは定義されていないので、定義しましょう。app/models/customer.rb
を開いて下記コードを記述してください。
class Customer < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
with_options presence: true do
validates :name
validates :status
end
enum status: {
normal: 0,
withdrawn: 1,
banned: 2
}
has_many :cart_items, dependent: :destroy
has_many :orders, dependent: :destroy
+ def line_items_checkout
+ cart_items.map do |cart_item|
+ {
+ quantity: cart_item.quantity,
+ price_data: {
+ currency: 'jpy',
+ unit_amount: cart_item.product.price,
+ product_data: {
+ name: cart_item.product.name,
+ metadata: {
+ product_id: cart_item.product_id
+ }
+ }
+ }
+ }
+ end
+ end
end
上記で定義した、line_items_checkout
は
{
quantity: cart_item.quantity,
price_data: {
currency: 'jpy',
unit_amount: cart_item.product.price,
product_data: {
name: cart_item.product.name,
metadata: {
product_id: cart_item.product_id
}
}
}
}
というカート内商品の情報を配列で返すメソッドです。なぜこのような配列を作成しているかというと、Checkout セッションを作成する際に必要だからです。オブジェクトのプロパティは、Stripe 側で指定されています。
line_items_checkout
メソッドが定義できたところで、CheckoutsController に戻りましょう。
create アクションの二行目で、Checkout セッションを作成しています。
create_session
メソッドは、CheckoutsController の private 以下に定義されていますね。
def create_session(line_items)
Stripe::Checkout::Session.create(
client_reference_id: current_customer.id,
customer_email: current_customer.email,
mode: 'payment',
payment_method_types: ['card'],
line_items:,
shipping_address_collection: {
allowed_countries: ['JP']
},
shipping_options: [
{
shipping_rate_data: {
type: 'fixed_amount',
fixed_amount: {
amount: 500,
currency: 'jpy'
},
display_name: '全国一律'
}
}
],
success_url: root_url,
cancel_url: "#{root_url}cart_items"
)
end
Checkout セッションを作成する際に登場するオブジェクトのプロパティの説明は下記の通りです。
キー名 | 説明 |
---|---|
client_reference_id | チェックアウトセッションを参照するための一意の文字列。今回は、顧客のIDとしました。 |
customer_email | 顧客のメールアドレス |
mode | チェックアウトセッションのモード(payment ・setup ・subscription があります) |
payment_method_types | 受け入れることができる支払い方法の種類のリスト |
line_items | 顧客が購入した品目。line_items_checkout で作成しましたね。 |
shipping_address_collection.allowed_countries | 配送先住所として入力できる国 |
shipping_options.shipping_rate_data | 配送オプションの配送料作成に渡されるパラメータ |
success_url | 支払いが成功した後に、リダイレクトされるURL |
cancel_url | 決済をキャンセルした後に、リダイレクトされるURL |
Stripe はドキュメントが非常に丁寧ですので、オブジェクトのプロパティについては下記も参考にしてください。
そして、create アクションの三行目では、Stirpe が用意する決済画面へのリンクにリダイレクトしています。
このとき、allow_other_host: true
としているのは、現在のホストと異なるホストへのリダイレクトを許可するためです。
これで、CheckoutsController の create アクションに関する説明は終了です。
決済を行ってみよう
それでは、決済を行うためのボタンを用意して、実際にカート内商品を購入してみましょう。
app/views/customer/cart_items/index.html.erb
を開いて、請求金額の表示を囲っている div タグの後に、「Checkout ボタン」を追加してください。
<div class="flex justify-between my-6">
<span class="font-semibold text-lg uppercase">total</span>
<span class="font-semibold text-lg"><%= number_to_currency(@total + POSTAGE, unit: "¥", strip_insignificant_zeros: true) %></span>
</div>
<%= button_to checkouts_path, data: { turbo: false }, class:'w-full cursor-pointer focus:outline-none text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2' do %>
Checkout
<% end %>
これで、カート内商品一覧画面に、決済処理を行うためのボタンが表示されるはずです。
http://localhost:8000/cart_items にアクセスして確認しましょう。
無事ボタンが表示されたら、クリックして Stripe の決済画面へ遷移するか試してみてください。
カート内商品の合計金額・送料が正確に反映されていれば成功です。🎉
こうして Stripe の画面がうまく表示できると感動しますよね!
それでは、住所とクレジットカード情報を入力しましょう。
住所は適当で構いません。ただし、クレジットカードはテスト環境で使用可能なものを使ってください。今回は、
- カード番号:4242 4242 4242 4242
- 有効期限:現在時刻より未来の年月
- セキュリティーコード:任意の三桁
を入力します。詳しくは、下記ドキュメントをご覧ください。
下記のように必要事項を入力したら、「支払う」ボタンを押してください。
「それ以降の住所」については、空欄でも構いません。
「支払う」ボタンを押下した後に、ルートパスにリダイレクトされれば決済が成功しているはずです。
(決済処理が終了した後にリダイレクトされる画面は、後ほど変更します。)
https://dashboard.stripe.com/test/payments にアクセスして、支払いデータが生成されているかどうか確認してみましょう。
たしかに一件レコードが追加されていますね。クリックして詳細を見てみましょう。
支払い内容の詳細が記載されています。
配送先の住所、どんな商品を何個購入したのか、送料はいくらだったのか等が正確に記載されていますね。Stripe による決済処理成功です 🎉
しかし、注文情報はデータベースに保存されていません。カート内商品も決済が完了したら、削除する必要があります。次節では、Stripe の決済情報をもとにアプリケーションのデータベースに反映させる処理を記述していきます。
WebhooksController を作ろう
Stripe は、イベントが発生すると、Webhook を使用してアプリケーションに通知します。
今回は、Stripe から送信される checkout.session.completed
をトリガーにして、注文情報をデータベースに登録します。
そこで、Stripe からイベントを受信する HTTP エンドポイントを作成しましょう。
下記コマンドを実行して、コントローラを生成します。
$ rails g controller customer/webhooks --no-helper --skip-template-engine
CheckoutsController 同様、--skip-template-engine
オプションを付与しているのは、HTTP エンドポイントの作成にビューは不要だからです。
続いて、ルーティングの設定を行います。
config/routes.rb
を開いて下記コードを追加してください。
Rails.application.routes.draw do
devise_for :admins, controllers: {
sessions: 'admin/sessions'
}
devise_for :customers, controllers: {
sessions: 'customer/sessions',
registrations: 'customer/registrations'
}
root to: 'pages#home'
namespace :admin do
resources :products, only: %i[index show new create edit update]
end
scope module: :customer do
resources :products, only: %i[index show]
resources :cart_items, only: %i[index create destroy] do
member do
patch 'increase'
patch 'decrease'
end
end
resources :checkouts, only: [:create]
+ resources :webhooks, only: [:create]
end
get '/up/', to: 'up#index', as: :up
get '/up/databases', to: 'up#databases', as: :up_databases
end
WebhooksController は、create アクションのみとなります。
生成されたパスを見てみましょう。
$ rails routes | grep webhooks
下記のようなパスが出力されれば OK です。
webhooks POST /webhooks(.:format) customer/webhooks#create
では次に、コントローラの各アクションの中身を記述していきます。
といっても、HTTP エンドポイントの作成については、Stripe が例を用意してくれているので、そちらを参考にしましょう。下記 URL にアクセスしてください。
すると、下記のような画面が現れるはずです。
上記画面に表示されている endpoint_secret
は後ほど Credentials に登録します。
# This is your Stripe CLI webhook secret for testing your endpoint locally.
endpoint_secret = 'whsec_xxxx'
エンドポイントのサンプル中の post '/webhook' do
の中身を少し変更して、app/controllers/customer/webhooks_controller.rb
に記述します。
class Customer::WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
endpoint_secret = Rails.application.credentials.dig(:stripe, :endpoint_secret)
event = nil
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, endpoint_secret
)
rescue JSON::ParserError => e
# Invalid payload
p e
status 400
return
rescue Stripe::SignatureVerificationError => e
# Invalid signature
p e
status 400
return
end
case event.type
when 'checkout.session.completed'
# ... execute a process
end
end
end
endpoint_secret
を Credentials から取得している点、'checkout.session.completed'
イベントを受け取っている点が異なります。
それでは、https://dashboard.stripe.com/webhooks/create?endpoint_location=local で取得した endpoint_secret
を Credentials に登録しましょう。下記コマンドを実行してください。
$ EDITOR="vi" bin/rails credentials:edit -e development
エディタが立ち上がったら、「i」キーを押して、インサートモードに入り、下記コードを記述してください。
# aws:
# access_key_id: 123
# secret_access_key: 345
stripe:
publishable_key: pk_test_xxxxx
secret_key: sk_test_xxxxx
+ endpoint_secret: whsec_xxxxx
キーが設定できたら、「esc」を押して、「wq!」で保存しましょう。
それでは、キーが設定できたかどうか確認してみましょう。下記コマンドを実行してコンソールにログインしてください。
$ rails c
コンソールに入りましたら、endpoint_secret
が設定できているかを確認します。
irb(main):001:0> Rails.application.credentials.dig(:stripe, :endpoint_secret)
=> "whsec_xxxxx"
Credentials を読み込む際に dig メソッドを使っています。
コマンドを実行して、endpoint_secret の値が返ってくれば OK です。
それでは、WebhooksController の create アクションの説明に戻りましょう。
begin
から始まるブロックは、例外処理を記述しています。
begin
の処理でエラーが発生した場合に、rescue
を実行していますね。
今回は、json を parse できなかったとき、Stripe の署名が無効のときに 400 エラーを返すようにしています。
そして最後に、checkout.session.completed
イベントを受け取った際の処理を記述するということですね。この checkout.session.completed
イベントには、Session オブジェクトと呼ばれるものが含まれ、顧客とその支払いに関する詳細を取得することができます。
checkout.session.completed
イベントを受け取ったということは、Stripe による決済処理が正常に完了したことを意味しますので、checkout.session.completed
イベントを受け取った後に、注文情報をデータベースに登録します。
それでは、when 'checkout.session.completed'
の中身を記述していきましょう。
まずは、下記コードを記述してください。
case event.type
when 'checkout.session.completed'
session = event.data.object # sessionの取得
customer = Customer.find(session.client_reference_id)
return unless customer # 顧客が存在するかどうか確認
# トランザクション処理開始
ApplicationRecord.transaction do
order = create_order(session) # sessionを元にordersテーブルにデータを挿入
end
# トランザクション処理終了
redirect_to session.success_url
end
解説をコメントとして記述しています。
ポイントは、ApplicationRecord.transaction do
の部分ですね。
こちらは、トランザクション といって、複数の処理をまとめて大きな1つの処理として扱うための機能です。
今回は、注文情報をデータベースに格納する際に、orders テーブルと order_details テーブルにデータを挿入します。(さらに、cart_items テーブルからデータを削除します。)
このように、複数のリソースにまたがった変更 をひとまとまりで扱うときに トランザクション が役立ちます。
ApplicationRecord.transaction do
で囲むことで、処理の1つで例外が発生したら、複数の処理を巻き戻すことができます。すなわち、「orders テーブルへのデータの挿入に成功したが、order_details テーブルへのデータの挿入に失敗した」という事象を防ぐことができるのです。
後ほど、order_details テーブルにデータを挿入する処理を ApplicationRecord.transaction do
内に追加します。
では、create_order
メソッドを定義しましょう。
class Customer::WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
endpoint_secret = Rails.application.credentials.dig(:stripe, :endpoint_secret)
event = nil
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, endpoint_secret
)
rescue JSON::ParserError => e
# Invalid payload
p e
status 400
return
rescue Stripe::SignatureVerificationError => e
# Invalid signature
p e
status 400
return
end
case event.type
when 'checkout.session.completed'
session = event.data.object # sessionの取得
customer = Customer.find(session.client_reference_id)
return unless customer # 顧客が存在するかどうか確認
ApplicationRecord.transaction do # トランザクション処理開始
order = create_order(session) # sessionを元にordersテーブルにデータを挿入
end # トランザクション処理終了
redirect_to session.success_url
end
end
+ private
+ def create_order(session)
+ Order.create!({
+ customer_id: session.client_reference_id,
+ name: session.shipping.name,
+ postal_code: session.shipping.address.postal_code,
+ prefecture: session.shipping.address.state,
+ address1: session.shipping.address.line1,
+ address2: session.shipping.address.line2,
+ postage: session.shipping_options[0].shipping_amount,
+ billing_amount: session.amount_total,
+ status: 'confirm_payment'
+ })
+ end
end
create_order
メソッドは、引数として受け取った session
を元に各カラムにデータを格納して、ActiveRecord メソッド create!
を実行しています。
create
ではなく、create!
としているのは、レコードの保存に失敗した場合に、例外 を発生させる必要があるからです。例外 を発生させることで、トランザクションにより処理を巻き戻すこと(rollback)ができます。
event.data.object
で取得できる session
オブジェクトの中身は下記の通りです。
{
"id": "cs_test_b18XGkF4PObIlcROc2P3x49hLUUAT1TTM29fIioKx7xFQUk9Pf2vM9MXTs",
"object": "checkout.session",
"after_expiration": null,
"allow_promotion_codes": null,
"amount_subtotal": 47100,
"amount_total": 47600,
"automatic_tax": {"enabled":false,"status":null},
"billing_address_collection": null,
"cancel_url": "http://localhost:8000//cart_items",
"client_reference_id": "1",
"consent": null,
"consent_collection": null,
"created": 1669802452,
"currency": "jpy",
"custom_text": {"shipping_address":null,"submit":null},
"customer": null,
"customer_creation": "if_required",
"customer_details": {"address":{"city":null,"country":"JP","line1":"龍ケ崎市","line2":null,"postal_code":"301-0000","state":"茨城県"},"email":"yamada.taro@gmail.com","name":"山田太郎","phone":null,"tax_exempt":"none","tax_ids":[]},
"customer_email": "yamada.taro@gmail.com",
"expires_at": 1669888852,
"livemode": false,
"locale": null,
"metadata": {},
"mode": "payment",
"payment_intent": "pi_3M9n6IF4dCXP6zuk1jIznnJd",
"payment_link": null,
"payment_method_collection": "always",
"payment_method_options": {},
"payment_method_types": [
"card"
],
"payment_status": "paid",
"phone_number_collection": {"enabled":false},
"recovered_from": null,
"setup_intent": null,
"shipping": {"address":{"city":"","country":"JP","line1":"港区芝公園","line2":null,"postal_code":"105-0011","state":"東京都"},"name":"山田太郎"},
"shipping_address_collection": {"allowed_countries":["JP"]},
"shipping_options": [
{"shipping_amount":500,"shipping_rate":"shr_1M9n60F4dCXP6zukH1cXKtjo"}
],
"shipping_rate": "shr_1M9n60F4dCXP6zukH1cXKtjo",
"status": "complete",
"submit_type": null,
"subscription": null,
"success_url": "http://localhost:8000/",
"total_details": {"amount_discount":0,"amount_shipping":500,"amount_tax":0},
"url": null
}
支払いの詳細が含まれています。
session
オブジェクトには、orders テーブルなのレコードを作成するために必要なデータが全て揃っていますね。
また、'checkout.session.completed'
イベントを受信した時点で、決済は完了していますので、status カラムには、'confirm_payment'
を格納しています。
続いて、どの商品を何個購入したのかを記録するための order_details を新たに作成しましょう。
そのためには line_items の情報が必要になります。
Checkout セッションを作成する際に、line_items というオブジェクトの配列を作成しましたね。
session
オブジェクトからそれらを取り出し、order_details を作成することにします。
どのように取り出すかというと、下記のようなコードを記述します。
Stripe::Checkout::Session.retrieve({ id: session.id, expand: ['line_items'] })
expand
に、プロパティ名を配列で渡すと、指定したオブジェクトを取得することができます。
今回は、'line_items'
を expand
に渡すことで、line_items オブジェクトが取得できるというわけです。
expand
については下記ドキュメントも参考にしてください。
それでは、app/controllers/customer/webhooks_controller.rb
に下記コードを追記しましょう。
class Customer::WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
endpoint_secret = Rails.application.credentials.dig(:stripe, :endpoint_secret)
event = nil
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, endpoint_secret
)
rescue JSON::ParserError => e
# Invalid payload
p e
status 400
return
rescue Stripe::SignatureVerificationError => e
# Invalid signature
p e
status 400
return
end
case event.type
when 'checkout.session.completed'
session = event.data.object # sessionの取得
customer = Customer.find(session.client_reference_id)
return unless customer # 顧客が存在するかどうか確認
# トランザクション処理開始
ApplicationRecord.transaction do
order = create_order(session) # sessionを元にordersテーブルにデータを挿入
+ session_with_expand = Stripe::Checkout::Session.retrieve({ id: session.id, expand: ['line_items'] })
+ session_with_expand.line_items.data.each do |line_item|
+ create_order_details(order, line_item) # 取り出したline_itemをorder_detailsテーブルに登録
+ end
end
# トランザクション処理終了
redirect_to session.success_url
end
end
private
def create_order(session)
Order.create!({
customer_id: session.client_reference_id,
name: session.shipping.name,
postal_code: session.shipping.address.postal_code,
prefecture: session.shipping.address.state,
address1: session.shipping.address.line1,
address2: session.shipping.address.line2,
postage: session.shipping_options[0].shipping_amount,
billing_amount: session.amount_total,
status: 'confirm_payment'
})
end
end
session_with_expand
には、下記のような値を持つ line_items
というキーが追加されます。
data
というキーの値は配列になっていますので、それを each メソッドによって一つずつ取り出しています。
{
"object": "list",
"data": [
{
"id": "li_1M9p2sF4dCXP6zuk7x6TGmkl",
略...,
"quantity": 1
},
{
"id": "li_1M9p2sF4dCXP6zukHrwyhmhK",
略...,
"quantity": 2
}
],
"has_more": false,
"url": "/v1/checkout/sessions/cs_test_b1w0gxjgkEcfZ7HVdoZ8etvGkSmagne7GhthCskEaZBSa9ZHDNz4nsK1kb/line_items"
}
それでは、最後に create_order_details
メソッドを定義しましょう。
class Customer::WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
endpoint_secret = Rails.application.credentials.dig(:stripe, :endpoint_secret)
event = nil
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, endpoint_secret
)
rescue JSON::ParserError => e
# Invalid payload
p e
status 400
return
rescue Stripe::SignatureVerificationError => e
# Invalid signature
p e
status 400
return
end
case event.type
when 'checkout.session.completed'
session = event.data.object # sessionの取得
customer = Customer.find(session.client_reference_id)
return unless customer # 顧客が存在するかどうか確認
# トランザクション処理開始
ApplicationRecord.transaction do
order = create_order(session) # sessionを元にordersテーブルにデータを挿入
session_with_expand = Stripe::Checkout::Session.retrieve({ id: session.id, expand: ['line_items'] })
session_with_expand.line_items.data.each do |line_item|
create_order_details(order, line_item) # 取り出したline_itemをorder_detailsテーブルに登録
end
end
# トランザクション処理終了
redirect_to session.success_url
end
end
private
def create_order(session)
Order.create!({
customer_id: session.client_reference_id,
name: session.shipping.name,
postal_code: session.shipping.address.postal_code,
prefecture: session.shipping.address.state,
address1: session.shipping.address.line1,
address2: session.shipping.address.line2,
postage: session.shipping_options[0].shipping_amount,
billing_amount: session.amount_total,
status: 'confirm_payment'
})
end
+ def create_order_details(order, line_item)
+ product = Stripe::Product.retrieve(line_item.price.product)
+ purchased_product = Product.find(product.metadata.product_id)
+ raise ActiveRecord::RecordNotFound if purchased_product.nil?
+
+ order_detail = order.order_details.create!({
+ product_id: purchased_product.id,
+ price: line_item.price.unit_amount,
+ quantity: line_item.quantity
+ })
+ end
end
このメソッドが最後の関門です。一行一行確認していきましょう 🚀
まず、一行目の Stripe::Product.retrieve(line_item.price.product)
は、Stripe に登録された商品を取得しています。
Stripe の Checkout サマリーをみると、購入された商品が表示されていますね。
商品をクリックすると、詳細画面に遷移します。
少し下にスクロールすると、メタデータ という項目があるはずです。
このメタデータに登録されている product_id
を取得するために、Stripe::Product.retrieve(line_item.price.product)
という記述を行なっています。
下記のように記述することで、product テーブルから購入された商品のデータを取得することができるのです。
product = Stripe::Product.retrieve(line_item.price.product)
purchased_product = Product.find(product.metadata.product_id)
購入された商品を取得して、purchased_product
という変数に格納しました。あとは、
order
purchased_product
line_item
を使って、order_details を作成するだけです。
order_detail = order.order_details.create!({
product_id: purchased_product.id,
price: line_item.price.unit_amount,
quantity: line_item.quantity
})
上記のように、order.order_details.create!
とすることで、直近で生成された order
に紐づいた order_details を作成することができます。つまり、order_id カラムに order
の id
が自動的に格納されるということです。
これで、order_details が作成できました。注文情報に関する処理は全て終了したのですが、やることがあと二つ残っています。
一つは、購入された商品の在庫数の更新です。購入された数だけ、商品の在庫数を減らす必要があります。
もう一つは、顧客のカート内商品を空にする処理です。購入が完了したらカート内商品は全て削除しなくてはなりません。
下記コードを追加してください。
class Customer::WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
endpoint_secret = Rails.application.credentials.dig(:stripe, :endpoint_secret)
event = nil
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, endpoint_secret
)
rescue JSON::ParserError => e
# Invalid payload
p e
status 400
return
rescue Stripe::SignatureVerificationError => e
# Invalid signature
p e
status 400
return
end
case event.type
when 'checkout.session.completed'
session = event.data.object # sessionの取得
customer = Customer.find(session.client_reference_id)
return unless customer # 顧客が存在するかどうか確認
# トランザクション処理開始
ApplicationRecord.transaction do
order = create_order(session) # sessionを元にordersテーブルにデータを挿入
session_with_expand = Stripe::Checkout::Session.retrieve({ id: session.id, expand: ['line_items'] })
session_with_expand.line_items.data.each do |line_item|
create_order_details(order, line_item) # 取り出したline_itemをorder_detailsテーブルに登録
end
end
# トランザクション処理終了
+ customer.cart_items.destroy_all # 顧客のカート内商品を全て削除
redirect_to session.success_url
end
end
private
def create_order(session)
Order.create!({
customer_id: session.client_reference_id,
name: session.shipping.name,
postal_code: session.shipping.address.postal_code,
prefecture: session.shipping.address.state,
address1: session.shipping.address.line1,
address2: session.shipping.address.line2,
postage: session.shipping_options[0].shipping_amount,
billing_amount: session.amount_total,
status: 'confirm_payment'
})
end
def create_order_details(order, line_item)
product = Stripe::Product.retrieve(line_item.price.product)
purchased_product = Product.find(product.metadata.product_id)
raise ActiveRecord::RecordNotFound if purchased_product.nil?
order_detail = order.order_details.create!({
product_id: purchased_product.id,
price: line_item.price.unit_amount,
quantity: line_item.quantity
})
+ purchased_product.update!(stock: (purchased_product.stock - order_detail.quantity)) # 購入された商品の在庫数の更新
end
end
下記の二行を適切な箇所に追加しました。
customer.cart_items.destroy_all
purchased_product.update!(stock: (purchased_product.stock - order_detail.quantity))
カート内商品の削除は、トランザクションが成功したとき、商品の在庫数の更新は order_details を新たに作成したときに実行しています。
これで、決済処理のロジックを全て実装し終わりました 🎉
次節で動作確認を行います。
決済処理の動作確認をしよう
それでは、決済処理の動作確認をしましょう。
その前に、Stripe のイベントを先ほど作成した Webhook に送信する設定を行います。
設定の手順は下記の通りです。
- Stripe CLI をダウンロードする
- Stripe アカウントにログインする
- Stripe のイベントを先ほど作成した Webhook に送信する設定を行う
最初に、下記ドキュメントを参照しながら、Stripe CLI をインストールしてください。
その後、下記コマンドを実行して Stripe アカウントにログインします。
このコマンドは Docker コンテナではなく、ローカル で実行してください。
$ stripe login
上記コマンドを実行して Enter を押すと、下記画面に遷移するはずです。
アカウントのパスワードを入力すると、ログインが完了します。
下記のようなログが出力されれば OK です。
Your pairing code is: xxxxx-xxxxx-xxxxx-xxxxx
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser or visit https://dashboard.stripe.com/stripecli/confirm_auth?t=xxxxx (^C to quit)
> Done! The Stripe CLI is configured for FarStep with account id acct_xxxxx
Please note: this key will expire after 90 days, at which point you'll need to re-authenticate.
ログインが完了したら、下記コマンドを実行して、Stripe のイベントを先ほど作成した Webhook に送信する設定を行ってください。
$ stripe listen --forward-to localhost:8000/webhooks
上記コマンドを実行すると下記のようなログが出力されます。
A newer version of the Stripe CLI is available, please update to: v1.13.5
> Ready! You are using Stripe API Version [2020-08-27]. Your webhook signing secret is whsec_xxxxx (^C to quit)
これで、Stripe は WebhooksController の create アクションにイベントを送信するようになります。
この設定の仕方は下記ドキュメントにも記載されていますので参考にしてください。
では、実際に商品を購入してみましょう。
決済が完了した後、エラーが吐かれず、カート内商品が全て削除されれば OK です。
また、stripe listen --forward-to localhost:8000/webhooks
を実行したターミナルをご覧ください。Stripe から下記のようなイベントが送信されているはずです。
2022-12-01 02:27:27 --> charge.succeeded [evt_3M9u48F4dCXP6zuk0e9WiDkx]
2022-12-01 02:27:27 <-- [204] POST http://localhost:8000/webhooks [evt_3M9u48F4dCXP6zuk0e9WiDkx]
2022-12-01 02:27:27 --> payment_intent.succeeded [evt_3M9u48F4dCXP6zuk05jwARSK]
2022-12-01 02:27:27 <-- [204] POST http://localhost:8000/webhooks [evt_3M9u48F4dCXP6zuk05jwARSK]
2022-12-01 02:27:27 --> payment_intent.created [evt_3M9u48F4dCXP6zuk0FgAJHV2]
2022-12-01 02:27:27 <-- [204] POST http://localhost:8000/webhooks [evt_3M9u48F4dCXP6zuk0FgAJHV2]
2022-12-01 02:27:27 --> checkout.session.completed [evt_1M9u4BF4dCXP6zukZCNmglz2]
2022-12-01 02:27:28 <-- [302] POST http://localhost:8000/webhooks [evt_1M9u4BF4dCXP6zukZCNmglz2]
最後に、checkout.session.completed
イベントを受信していますね。そして、
INSERT INTO "orders"
INSERT INTO "order_details"
UPDATE "products"
DELETE FROM "cart_items"
という SQL 文が発行されているか確認してください。
カートにいれた商品の個数によって、発行される SQL 文の数は変わります。
特にエラーが吐かれていなければ決済処理は正常に動作しています 🎉
エラーが出ている場合は、エラー文をよく読んで原因となるコードを探してみてください。
もしも、下記のようなエラーが吐かれた場合は、Credentials の endpoint_secret
が正しく読み込まれていない可能性が高いです。
ArgumentError (secret should be a string):
サーバを再起動するとこのエラーは解消されるはずです。
最後に、RuboCop を実行し、必要であればコードを修正しましょう。
$ rubocop
今回は、RuboCop の静的解析で指摘事項が七つありました。
Inspecting 57 files
...........C.......C..........C..........................
Offenses:
app/controllers/customer/checkouts_controller.rb:13:3: C: Metrics/MethodLength: Method has too many lines. [24/10]
def create_session(line_items) ...
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
app/controllers/customer/webhooks_controller.rb:4:3: C: Metrics/AbcSize: Assignment Branch Condition size for create is too high. [<12, 31, 4> 33.48/17]
def create ...
^^^^^^^^^^
app/controllers/customer/webhooks_controller.rb:4:3: C: Metrics/MethodLength: Method has too many lines. [32/10]
def create ...
^^^^^^^^^^
app/controllers/customer/webhooks_controller.rb:48:3: C: Metrics/AbcSize: Assignment Branch Condition size for create_order is too high. [<0, 20, 0> 20/17]
def create_order(session) ...
^^^^^^^^^^^^^^^^^^^^^^^^^
app/controllers/customer/webhooks_controller.rb:48:3: C: Metrics/MethodLength: Method has too many lines. [11/10]
def create_order(session) ...
^^^^^^^^^^^^^^^^^^^^^^^^^
app/controllers/customer/webhooks_controller.rb:62:3: C: Metrics/AbcSize: Assignment Branch Condition size for create_order_details is too high. [<3, 18, 1> 18.28/17]
def create_order_details(order, line_item) ...
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
app/models/customer.rb:18:3: C: Metrics/MethodLength: Method has too many lines. [15/10]
def line_items_checkout ...
^^^^^^^^^^^^^^^^^^^^^^^
57 files inspected, 7 offenses detected
しかも、どれも自動修正はできないようです。指摘事項の種類は下記の二種類です。
- Metrics/MethodLength: Method has too many lines.
- Metrics/AbcSize: Assignment Branch Condition size for create is too high.
一つ目は、メソッドの行数が長いと指摘されています。RuboCop で定められているメソッドの行数の制限はデフォルトで 10 行です(コメントは対象外)。10 行はあまりにも少ないので、この制限を緩和しましょう。
.rubocop.yml
を開いて、一番下に下記コードを記述してください。
Metrics/MethodLength:
Max: 40
今回は、メソッドの行数を 40 行まで許容することにしました。
さて、二つ目は、メソッドの複雑度が高いと指摘されています。RuboCop で定められているメソッド複雑度の指標の制限はデフォルトで 17 行です。17 という値はあまりにも厳しいため、この制限を緩和しましょう。
.rubocop.yml
を開いて、一番下に下記コードを記述してください。
Metrics/AbcSize:
Max: 40
今回は、メソッドの複雑度を 40 まで許容することにしました。
Rubocop の制限値を編集したところで、再度静的解析を行ってみましょう。
$ rubocop
今度は、RuboCop の静的解析を全て PASS したようです。
Inspecting 57 files
.........................................................
57 files inspected, no offenses detected
今回は、WebhooksController の処理の書き方を Stripe の公式ドキュメントを参考にしたため、制限を緩めるという方法をとりました。しかし、Rubocop はあくまで、システムの品質を向上させるためのツールです。Offenses が出ないようにすることが目的ではありません。Rubocop の制限値はアプリケーションに合わせて適切な値に編集してください。
それでは、コミットしておきましょう。
$ git add . && git commit -m "Implementation of payment processing"
おわりに
お疲れ様でした。
本 Chapter では、Stripe を使った決済処理を実装しました。
Stripe はドキュメントが非常に充実していますので、何かカスタマイズしたい場合は、是非ドキュメントを参照してください。
次の Chapter では、注文情報を閲覧する機能を実装していきます。