記事概要
Ruby on RailsにPAY.JPを実装し、登録済みクレジットカードで決済する方法について、まとめる
前提
- Macを使用している
- Ruby on Railsでアプリケーションを作成している
- PAY.JPのアカウントを作成済みである
- PAY.JPが提供する「テストモード」を使用する
Pay.JPについて
PAY.JPとアプリケーションのやり取り
手順 | 内容 |
---|---|
① | 入力フォームの情報をjsファイルへ送信する |
② | 入力した内容をPAY.JPに送信する →入力したカード情報(カード番号、有効期限など)が、jsファイルによってPAY.JPに送信される |
③ | PAY.JPからトークン(パスワード)が発行される |
④ | トークンをコントローラーへ送信する →jsファイルは、発行されたトークンをコントローラーへ送信する役割もある |
⑤ | トークンをPAY.JPに送信する |
⑥ | トークンを元に本人確認が出来たので、PAY.JPから顧客IDが発行される →トークンは一度しか利用できない(ワンタイムパスワードと言う)ため、顧客IDを発行することで何度でも同じカードで支払い処理が可能になる、という仕組み |
用語
envコマンド
設定されている環境変数を実行するためのコマンド
オプションを指定せずに実行すると、設定しているすべての環境変数を表示することができる
grepコマンド
指定した条件で検索をするためのコマンド
サンプルアプリ(GitHub)
手順1(環境変数を確認する)
- ターミナルで、下記コマンドを実行する
% env | grep PAYJP PAYJP_PUBLIC_KEY="設定した公開鍵の値" PAYJP_SECRET_KEY="設定した秘密鍵の値"
- PAY.JPのAPI設定を確認し、ターミナルの出力結果と一致しているかを確認する
手順2(PAY.JPが使えるように設定する)
- 「payjp」「gon」のGemをインストールする
-
app/javascript/card.js
を生成するため、ターミナルで下記コマンドを実行する% touch app/javascript/card.js
- jsファイルを読み込めるようにする
config/importmap.rb
# 最終行に追記 pin "card", to: "card.js"
app/javascript/application.js// 最終行に追記 import "card"
-
app/views/layouts/application.html.erb
にライブラリを追加し、「payjp.js」を読み込めるようにする<!DOCTYPE html> <html> <head> <title>PayjpApp</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <%= csrf_meta_tags %> <%= csp_meta_tag %> <!-- payjp.jsのライブラリを追加 --> <script type="text/javascript" src="https://js.pay.jp/v2/pay.js"></script> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> </head> <body> <%= yield %> </body> </html>
手順3(カード登録の画面を表示する)
- ルーティングを設定する
config/routes.rb
# 省略 resources :cards, only: [:new, :create] end
- cardモデルを生成するため、ターミナルで下記コマンドを実行する
% rails g model card
- cardモデル(子モデル)を、以下のように編集する
card.rb
class Card < ApplicationRecord belongs_to :user end
- userモデル(親モデル)を、以下のように編集する
user.rb
# 省略 has_one :card, dependent: :destroy # 追加 end
- マイグレーションファイルを以下のように編集する
db/migrate/**************_create_cards.rb
class CreateCards < ActiveRecord::Migration[7.1] def change create_table :cards do |t| t.string :customer_token, null: false t.references :user, foreign_key: true t.timestamps end end end
- コマンド実行し、データベースに反映する
% rails db:migrate
- cardsテーブルが、以下のようになっていることを確認する
- cardsコントローラーを生成するため、コマンドを実行する
% rails g controller cards new
- コントローラーからJavaScriptに環境変数を渡すため、以下のように編集する
app/controllers/cards_controller.rb
class CardsController < ApplicationController def new gon.public_key = ENV["PAYJP_PUBLIC_KEY"] end end
-
app/views/cards/new.html.erb
に、カード情報を入力するフォームを生成する<%= include_gon %> <h1>カード登録</h1> <%= form_with url: cards_path, id: 'charge-form', class: 'card-form',local: true do |f| %> <div class='form-wrap'> <p>カード番号</p> <div id="number-form" class="input-default"></div> </div> <div class='form-wrap'> <p>期限</p> <div id="expiry-form" class="input-default"></div> </div> <div class='form-wrap'> <p>カード背面4桁もしくは3桁の番号</p> <div id="cvc-form" class="input-default"></div> </div> <%= f.submit "登録する", class:"button", id: "button" %> <% end %>
- PAY.JPの入力フォームをJavaScriptで挿入する
app/javascript/card.js
const pay = () => { const publicKey = gon.public_key; const payjp = Payjp(publicKey); const elements = payjp.elements(); const numberElement = elements.create('cardNumber'); const expiryElement = elements.create('cardExpiry'); const cvcElement = elements.create('cardCvc'); numberElement.mount('#number-form'); expiryElement.mount('#expiry-form'); cvcElement.mount('#cvc-form'); }; window.addEventListener("turbo:load", pay);
- localhost:3000/cards/newにアクセスし、入力フォームが表示されるか確認する
手順4(フォームに入力した情報をjsファイルに送信する)
- 登録するボタンを取得し、クリックイベントを指定する
card.js
const pay = () => { const publicKey = gon.public_key; const payjp = Payjp(publicKey); const elements = payjp.elements(); const numberElement = elements.create('cardNumber'); const expiryElement = elements.create('cardExpiry'); const cvcElement = elements.create('cardCvc'); numberElement.mount('#number-form'); expiryElement.mount('#expiry-form'); cvcElement.mount('#cvc-form'); // 登録するボタンを取得 const form = document.getElementById("charge-form"); // 登録するボタンをクリックすると、イベント発火 form.addEventListener("submit", (e) => { console.log('clicked') }); }; window.addEventListener("turbo:load", pay);
- JavaScriptからサーバーサイドに値を送信させるため、フォームの送信処理を阻止(prevent)する
card.js
// 省略 // 登録するボタンをクリックすると、イベント発火 form.addEventListener("submit", (e) => { console.log('clicked') e.preventDefault(); // フォームの送信処理をキャンセル }); }; // 省略
- クレジットカードのトークンを生成する
card.js
// 省略 // 登録するボタンをクリックすると、イベント発火 form.addEventListener("submit", (e) => { // クレジットカードのトークンを生成 payjp.createToken(numberElement).then(function (response) { if (response.error) { } else { const token = response.id; const tokenObj = `<input value=${token} name='token' type="hidden">`; form.insertAdjacentHTML("beforeend", tokenObj); } }); e.preventDefault(); // フォームの送信処理をキャンセル }); // 省略
- トークンをサーバーサイドに送信する際、入力されたクレジットカードの情報を削除する
card.js
// 省略 // クレジットカードのトークンを生成 payjp.createToken(numberElement).then(function (response) { if (response.error) { } else { const token = response.id; const tokenObj = `<input value=${token} name='token' type="hidden">`; form.insertAdjacentHTML("beforeend", tokenObj); // 入力されたクレジットカード情報を削除 numberElement.clear(); expiryElement.clear(); cvcElement.clear(); // 省略
- DBにクレジットカードの情報を保存することは法律で禁止されているため、サーバーサイドに送信する必要がない
- 保存しないとしても、セキュリティの観点からparamsにカードの情報を含めることは避けるべき
- トークンをサーバーサイドに送信する
card.js
// 省略 // 入力されたクレジットカード情報を削除 numberElement.clear(); expiryElement.clear(); cvcElement.clear(); // フォーム送信 document.getElementById("charge-form").submit(); } // 省略
手順5(cardsコントローラーを実装する)
- 顧客トークンを生成する
app/controllers/cards_controller.rb
class CardsController < ApplicationController def new gon.public_key = ENV["PAYJP_PUBLIC_KEY"] end def create Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む # 顧客トークンを生成 customer = Payjp::Customer.create( description: 'test', # テストカードであることを説明 card: params[:token] # 登録しようとしているカード情報 ) end end
- 顧客トークンをもとにインスタンスを生成する
app/controllers/cards_controller.rb
# 省略 # 顧客トークンを生成 customer = Payjp::Customer.create( description: 'test', # テストカードであることを説明 card: params[:token] # 登録しようとしているカード情報 ) # 顧客トークンとログインしているユーザーを紐付けるインスタンスを生成 card = Card.new( customer_token: customer.id, # 顧客トークン user_id: current_user.id # ログインしているユーザー ) # 省略
- 具体的なカード情報(カード番号など)をそのままDBに保存することは法律上禁止されているが、トークン化された顧客情報であれば保存可能
- この顧客トークンをユーザー情報に紐付けることで、繰り返し使用できるようになる
→毎回カード情報を入力をしなくても購入可能になる
- カード登録後、トップページにリダイレクトされるようにする
app/controllers/cards_controller.rb
# 省略 # 顧客トークンとログインしているユーザーを紐付けるインスタンスを生成 card = Card.new( customer_token: customer.id, # 顧客トークン user_id: current_user.id # ログインしているユーザー ) card.save # カード情報を保存 redirect_to root_path # トップページにリダイレクト # 省略
手順6(バリデーションを設定する)
-
token
を保存するカラムがcardsテーブルに存在しないため、バリデーションを設定するためにはattr_accessor
でキーを指定する必要があるapp/models/card.rbclass Card < ApplicationRecord belongs_to :user attr_accessor :token # tokenカラムがないので、attr_accessorを使用 validates :token, presence: true end
-
token
に対して設定したバリデーションを利用し、保存段階で条件分岐するapp/controllers/cards_controller.rb# 省略 # 顧客トークンとログインしているユーザーを紐付けるインスタンスを生成 card = Card.new( token: params[:token], #カード情報 customer_token: customer.id, # 顧客トークン user_id: current_user.id # ログインしているユーザー ) if card.save redirect_to root_path # カード情報を保存した場合、トップページにリダイレクト else redirect_to action: "new" # カード情報を保存できなかった場合、カード登録画面へリダイレクト end end end
- ブラウザで確認する
- サーバーを再起動する
-
localhost:3000/cards/newにアクセスし、テストカードを使用してカード登録を行う
項目 値 カード番号 4242424242424242(16桁) CVC 123 有効期限 登録時より未来 - DBに
customer_token
が保存されていることを確認する
-
PAY.JPの顧客一覧にカードが登録されていることを確認する
手順7(マイページにカード情報を表示する)
アプリ側にはトークンしかないので、このトークンをPAY.JP側に送るのと引き換えに顧客情報(顧客ID)をPAY.JP側から受け取る(⑤と⑥)
顧客情報を受け取った後、その中に含まれているカード情報をマイページに表示する
- ログインユーザーに紐付くカード情報を取得する
app/controllers/users_controller.rb
class UsersController < ApplicationController def show Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む card = Card.find_by(user_id: current_user.id) # ログインユーザーのid情報を元に、カード情報を取得 end # 省略
- カード情報を元に顧客情報を取得する
app/controllers/users_controller.rb
class UsersController < ApplicationController def show Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む card = Card.find_by(user_id: current_user.id) # ログインユーザーのid情報を元に、カード情報を取得 customer = Payjp::Customer.retrieve(card.customer_token) # カード情報を元に、顧客情報を取得 @card = customer.cards.first # 顧客情報を元に、カード情報を取得。顧客が複数カード登録している場合、その内の「最初のカード(first)情報」を取得 end # 省略
- マイページへ画面遷移する前に、カード登録するようにする
app/controllers/users_controller.rb
class UsersController < ApplicationController def show Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む card = Card.find_by(user_id: current_user.id) # ログインユーザーのid情報を元に、カード情報を取得 # カード未登録の場合、カード登録ページへ画面遷移 redirect_to new_card_path and return unless card.present? customer = Payjp::Customer.retrieve(card.customer_token) # カード情報を元に、顧客情報を取得 @card = customer.cards.first # 顧客情報を元に、カード情報を取得。顧客が複数カード登録している場合、その内の「最初のカード(first)情報」を取得 end # 省略
- マイページにカード情報が表示されるように、ビューファイル
app/views/users/show.html.erb
を編集する<div class="AccountPage"> <div class="AccountPage__title"> <h1>マイページ</h1> </div><br> <!-- カード情報の追加 --> <div class="Card__title"> <h2>カード情報</h2> </div> <div class="Card_info"> 【カード番号】 <br> <%= "**** **** **** " + @card[:last4] %> <%# カード番号の下4桁を取得 %> <br> 【有効期限】 <br> <%= @card[:exp_month] %> <%# 有効期限の「月」を取得 %> / <%= @card[:exp_year] %> <%# 有効期限の「年」を取得 %> </div> <!-- カード情報の追加 --> <div class="Account__info"> <h2>ユーザー情報</h2> </div> <!-- 省略 -->
- ブラウザにアクセスし、マイページにカード情報が表示されることを確認する
手順8(商品購入機能を実装する)
- ルーティングを設定する
config/routes.rb
# 省略 # orderメソッド(購入機能のメソッド)に対して、id情報を伴うURIを生成する resources :items, only: :order do post 'order', on: :member end end
- 「購入するボタン」にリンクを設定するため、
app/views/items/index.html.erb
を編集する購入する商品の情報はidで判断するので、「item.id」とする<!-- 省略 --> <%= link_to '購入する', order_item_path(item.id), data: { turbo_method: :post } %> <% end %> </div>
- 購入した商品を識別するための専用テーブルを用意する
% rails g model item_order
- item_orderモデル(子モデル)を編集する
app/models/item_order.rb
class ItemOrder < ApplicationRecord belongs_to :item end
- itemモデル(親モデル)を編集する
app/models/item.rb
class Item < ApplicationRecord has_one :item_order end
- マイグレーションファイルを更新する
db/migrate/**************_create_item_orders.rb
class CreateItemOrders < ActiveRecord::Migration[7.1] def change create_table :item_orders do |t| t.references :item, foreign_key: true t.timestamps end end end
- 下記コマンド実行し、データベースに反映する
% rails db:migrate
手順9(購入処理をコントローラーに記述する)
- idを元に、購入する商品を特定する
app/controllers/items_controller.rb
class ItemsController < ApplicationController before_action :find_item, only: :order # 「find_item」を動かすアクションを限定 def index @items = Item.all # 全商品の情報を取得 end def order # 購入する時のアクションを定義 end private def find_item @item = Item.find(params[:id]) # 購入する商品を特定 end end
- ユーザーがカード登録しているかどうかで、条件分岐する
app/controllers/items_controller.rb
# 省略 def order # 購入する時のアクションを定義 # 購入ボタンを押した際に、「current_userに紐付くcardが存在(present)していなければ、カードの新規登録画面にリダイレクトして実行(return)する」 redirect_to new_card_path and return unless current_user.card.present? end # 省略
- PAY.JPに支払い情報を送れるようにする
app/controllers/items_controller.rb
# 省略 def order # 購入する時のアクションを定義 # 購入ボタンを押した際に、「current_userに紐付くcardが存在(present)していなければ、カードの新規登録画面にリダイレクトして実行(return)する」 redirect_to new_card_path and return unless current_user.card.present? Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む customer_token = current_user.card.customer_token # ログインしているユーザーの顧客トークンを定義 Payjp::Charge.create( amount: @item.price, # 商品の値段 customer: customer_token, # 顧客のトークン currency: 'jpy' # 通貨の種類(日本円) ) end # 省略
- 購入した商品がitem_orderテーブルへ保存されるようにする
app/controllers/items_controller.rb
# 省略 def order # 購入する時のアクションを定義 # 中略 # 商品のid情報を「item_id」として保存する ItemOrder.create(item_id: params[:id]) end # 省略
- 購入後のリダイレクト先を記述する
app/controllers/items_controller.rb
# 省略 # 商品のid情報を「item_id」として保存する ItemOrder.create(item_id: params[:id]) # ルートパスにリダイレクトする redirect_to root_path end # 省略
- ブラウザで確認する
- 商品を購入する
- 購入した商品がDBに保存されていることを確認する
-
PAY.JPの売上一覧に登録されていることを確認する
- 購入済みの商品は「Sold Out!!」と表示されるように、
app/views/items/index.html.erb
を編集する<%= link_to 'マイページへ行く', user_path(current_user) %> <div class="content"> <% @items.each do |item| %> <%# eachメソッドを使うことで、全商品に対して以下の処理を実行します %> <p>商品名:<%= item.name %></p> <p>値段:<%= item.price %>円</p> <% if item.item_order != nil %> <%# item_orderテーブルに保存されていたら、「Sold Out!!」と表示する %> <div class='sold-out'> <b>Sold Out!!</b> </div> <% else %> <%# item_orderテーブルに保存されていなければ、購入ボタンを表示する %> <%= link_to '購入する', order_item_path(item.id), data: { turbo_method: :post } %> <% end %> <% end %> </div>
- ブラウザにて、購入済み商品は「Sold Out!!」と表示されていることを確認する