某スクールにおいて、チーム開発で、フリーマーケットアプリを作成中であり、使用した技術について公開しています。
※初学者のため、ミスや認識違いが多々あると思いますがご了承ください。
フリマアプリにおいて、PAY.JPを用いたクレジットカード決済機能を実装しました!!
|内容 | url |
|:-----------------|------------------:|:-----------------------------:|
| 第1回 |導入編 | https://qiita.com/sho_U/items/ee0831844a0afd6e976f|
| 第2回 |運用編(クレジットカード登録) | https://qiita.com/sho_U/items/0a127e6f0904e3e9132b |
| 第3回| 運用編(購入)|https://qiita.com/sho_U/items/64ca57b7a1f72bf7079f|
今回の記事は運用編(クレジットカード登録)となります。
それでは、実際にPAY.JPを用いてクレジットカード機能を運用できるように実装して行きたいと思います。
###①マイグレーションの作成
class CreateCards < ActiveRecord::Migration[5.2]
def change
create_table :cards do |t|
t.integer :user_id
t.string :customer_id
t.string :card_id
t.timestamps
end
end
end
※user_idは自動生成され、またcard_idとcustomer_idは登録時にpay.jpから返される値ですので(入力フォームから入力する値ではない)、null: falseは現状つけていません。が、良からぬバグを防ぐためにつけた方がいいかもです。
user_id :ログインユーザ
costomer_id : 顧客ID情報
card_id : カードID情報
上記の構造を見てもらったらわかるのですが、クレジットカードの情報に必要そうな、**「カード番号」や「有効期限」**といったカラムがごっそり抜け落ちています。こういった情報はpay.jp側で保管し、毎回、顧客idとカードidで紐づいたクレジットカードを引っ張り出してきて処理することにより安全に決済を行います。
###②ルーティングの設定
resources :cards, only:[:index, :new, :create,:destroy,:show] do
member do
post 'pay'
end
end
indexは、「カード一覧表示」
newは、「カード新規作成」
createは、「カード登録」
destroyは、「カード削除」
showは、「商品購入決定」
payは、「購入」
のそれぞれのアクションに遷移するように組んでいます。
payはpathに商品IDを組み込めるように member でネストしています。この辺の詳細は以下の記事を参考にしていただければ詳しく記載しています。
フリマアプリにおける商品詳細ページのコメント機能(第2回)〜ルーティング編〜
https://qiita.com/sho_U/items/5c829b3060be2cce919a
###③カード新規登録機能
まずは、ビューを確認します。
%p クレジットード情報入力
= form_with model: @card, url: cards_path, local: true, html: { name: 'inputForm' } do |f|
%p カード番号
= f.text_field :card_number, id: "card_number",class: "card_form",maxlength: '16'
%p 有効期限
= f.collection_select :exp_month , Month.all, :id, :name ,class: "card-check-box"
= f.collection_select :exp_year, Year.all, :id, :name, class: "card-check-box"
%p セキュリティーコード
= f.password_field :cvc, id: 'cvc',class: "card_form" ,maxlength: '6'
#card_token.card-btn
= f.submit '登録', id: 'token_submit',class: 'card-btn'
※装飾的なクラスは全て省いています。有効期限のセレクトボックスにはactivehashを用いています。
この時生成されるhtmlは以下の通りです。
<p>クレジットード情報入力</p>
<form name="inputForm" action="/cards" accept-charset="UTF-8" method="post">
<input name="utf8" type="hidden" value="✓">
<input type="hidden" name="authenticity_token" value="oHo8ZvsveZiFsXISmtlib/cgjY5SAgxclt1rR7CH3B5Cd30wiKEwhrICaqKjZmmH03viJMwlFiEKHYHD5s0YLg==">
<p>カード番号</p>
<input id="card_number" class="card_form" maxlength="16" size="16" type="text" name="card_number">
<p>有効期限</p>
<select name="exp_month" id="exp_month"><option value="0">month</option>
<option value="1">01</option>
<option value="2">02</option>
<option value="3">03</option>
<option value="4">04</option>
<option value="5">05</option>
・
・
//略
</select>
<select name="exp_year" id="exp_year"><option value="0">year</option>
<option value="2020">20</option>
<option value="2021">21</option>
<option value="2023">23</option>
<option value="2024">24</option>
<option value="2025">25</option>
<option value="2026">26</option></select>
<p>セキュリティーコード</p>
<input id="cvc" class="card_form" maxlength="6" size="6" type="password" name="cvc">
<div class="card-btn" id="card_token">
<input type="submit" name="commit" value="登録" id="token_submit" class="card-btn" data-disable-with="登録">
</div>
</form>
form_withを用いてフォームを作成しています。
inputタグで、「card_number(カード番号)」、「exp_month(有効期限(月))」、「exp_year(有効期限(年))」、「card_form(セキュリティーコード)」を送信してることがわかります。
これらはデーターベースに保存せず、PAY.JPに送り届けます。
では、実際各項目が入力されて、「登録ボタン」が押下された場合の動作を確認します。
簡単な流れとしては
①**「登録ボタン」が押されたらjqueryが発火。ブラウザのデフォルトの動き(createアクションへの遷移)を停止する。
②入力されたカード情報をPAYJPに送る。
③PAYJPから返却されたカードトークンを受け取る。
④返却されたカードトークンのidを値とした、隠しフォームをhtmlにinputタグを追加する。
⑤止めていたデフォルトの動きを解除し、cardsコントローラーのcreateアクションへ遷移する。
(隠しフォームの値をコントローラーへ送る)
⑥createアクションで、カードトークンを用いて登録したカードと現在のユーザー**を結びつける。
(「customer_id」と「card_id]を作成し、「user_id」と共にcardsテーブルに格納)
※温もりを重視して手書きとさせていただいています。
上記の手順で、ユーザーとクレジットカードを安全に結び付けて、カードを登録します。
登録さえすれば、ユーザーとカードは紐づくので、購入等の際に、クレジットカード番号情報等がネットワーク上で通信されることはなくなります。
では、実際のjqueryを見ていきます。
document.addEventListener(
"DOMContentLoaded", e => {
if (document.getElementById("token_submit") != null) {
// "token_submit"というidをもつhtmlがあるページか?つまりカード作成ページか
Payjp.setPublicKey("pk_test_******公開鍵******");
let btn = document.getElementById("token_submit"); // 送信ボタンをbtnに格納
btn.addEventListener("click", e => { // 送信ボタンがクリックされたとき
e.preventDefault(); // デフォルトのブラウザの動きをいったんとめる(createアクションへの遷移を)
let card = { // cardに入力された値をハッシュで格納
number: document.getElementById("card_number").value,
cvc: document.getElementById("cvc").value,
exp_month: document.getElementById("exp_month").value,
exp_year: document.getElementById("exp_year").value
};
Payjp.createToken(card, (status, response) => {
// カード情報をpayjpに送りカードトークンを(response.id)を受け取る。
if (status === 200) { // 正常な値の場合
$("#card_number").removeAttr("name");
$("#cvc").removeAttr("name");
$("#exp_month").removeAttr("name");
$("#exp_year").removeAttr("name");
// name属性を削除することにより、dataベースに送るのを防ぐ。
$("#card_token").append(
$('<input type="hidden" name="payjp-token">').val(response.id)
// <input type="hidden" name="payjp-token" value= response.id>が#card_tokenに追加される。
);
document.inputForm.submit(); // inputFormのsubmitを発動。(上記で停止していた)
alert("登録が完了しました");
} else {
alert("カード情報が正しくありません。");
}
});
});
}
},
false
);
解説します。
document.addEventListener(
"DOMContentLoaded", e => {
addEventListenerについて
対象要素.addEventListener( 種類, 関数, false )
対象要素に対し、第一引数の種類の動作が成された時、イベント(第二引数の関数)を実行します。第三引数のfalseはイベントの受け取るタイミングを指定しているようです。
参考:https://snaka72.hatenadiary.org/entry/20100925/1285404467
今回の場合は対象要素は、「document」 種類は、「"DOMContentLoaded"」
イベントはアロー関数の処理となります。
つまり、webページが読み込まれた時("DOMContentLoaded")、**読み込まれたページのdocumentオブジェクト(document)**に対し、アロー関数の処理を実施します。
ではアロー関数の処理を見ていきます。
if (document.getElementById("token_submit") != null) {
// "token_submit"というidをもつhtmlがあるページか?つまりカード作成ページか
Payjp.setPublicKey("pk_test_******公開鍵******");
let btn = document.getElementById("token_submit"); // 送信ボタンをbtnに格納
if (document.getElementById("token_submit") != null) {
で、読み込まれたdocumentにtoken_submitというidを持つDOMがあるかで条件分岐しています。
上記のhtmlを確認すると、
<input type="submit" name="commit" value="登録" id="token_submit" class="card-btn" data-disable-with="登録">
クレジットカードの新規登録ページには、「token_submit」というidをもつDOMが存在しています。
つまり、読み込まれたページが**「カード新規登録ページの場合か否か」**を判断しています。
読み込まれたページが**「カード新規登録ページの場合」公開鍵をセットします。
そして、#token_submitを、「変数btn」**に格納します。
btn.addEventListener("click", e => { // 送信ボタンがクリックされたとき
e.preventDefault(); // デフォルトのブラウザの動きをいったんとめる(createアクションへの遷移を)
btnがクリックされたら(「登録ボタン」がクリックれたら)ブラウザのデフォルトの動きを止めます。
let card = { // cardに入力された値をハッシュで格納
number: document.getElementById("card_number").value,
cvc: document.getElementById("cvc").value,
exp_month: document.getElementById("exp_month").value,
exp_year: document.getElementById("exp_year").value
};
各フォームに入力された値を、ハッシュオブジェクトcardに格納します。
Payjp.createToken(card, (status, response) => {
// カード情報をpayjpに送りカードトークンを(response.id)を受け取る。
ハッシュオブジェクトcardを元に、createTokenメソッドを用いて**カードトークン(response)と通信状況(status)**をpayjpから受け取ります。受け取ったresponseとstatusを引数にさらに、アロー関数を実行します。
if (status === 200) { // 正常な値の場合
$("#card_number").removeAttr("name");
$("#cvc").removeAttr("name");
$("#exp_month").removeAttr("name");
$("#exp_year").removeAttr("name");
// name属性を削除することにより、dataベースに送るのを防ぐ。
通信状況が正常の場合は、各入力フォームからname属性を除去します。これはなぜかというと、今回カード番号といったセキュアな情報はデータベースに保存しないのでデータを飛ばす必要がありません。
(おそらくデータが飛んでもエラーは起きないが、もしデータが飛ぶとログに**「こんなカード番号のデータが送られましたよ」と残るためセキュリティ上危ない)
payjp.jsではカードトークン番号response.idさえ取得できれば良いので、response.idを取得できたら他の情報は削りresponse.id**のみ送る形とするためです。
$("#card_token").append(
$('<input type="hidden" name="payjp-token">').val(response.id)
// <input type="hidden" name="payjp-token" value= response.id>が#card_tokenに追加される。
);
document.inputForm.submit(); // inputFormのsubmitを発動。(上記で停止していた)
alert("登録が完了しました");
#card_tokenに値がresponse.id(作成されたトークンのid)隠しフォームを追加して、停止していたinputForm.submit();を発動させ、cardsコントローラーのcreateアクションにparamsを送信します。
では、次にコントローラーを見てみます。
class CardsController < ApplicationController
require 'payjp'
before_action :set_api_key
def create
if params['payjp-token'].blank?
redirect_to action: "new"
# トークンが取得出来てなければループ
else
user_id = current_user.id
customer = Payjp::Customer.create(
card: params['payjp-token']
# params['payjp-token'](response.id)からcustomerを作成
)
@card = Card.new(user_id: user_id, customer_id: customer.id, card_id: customer.default_card)
if @card.save
flash[:notice] = '登録しました'
redirect_to "/"
else
flash[:alert] = '登録できませんでした'
redirect_to action: "new"
end
end
end
def set_api_key
Payjp.api_key = Rails.application.credentials[:payjp][:PAYJP_PRIVATE_KEY]
end
解説します。
まず、before_actionで秘密鍵をcredential.yamlから取得し、Payjpと照合します。
if params['payjp-token'].blank?
redirect_to action: "new"
隠しデーターとして送ったはずの**params['payjp-token']**が空だった場合は、newアクションへリダイレクトする。
user_id = current_user.id
customer = Payjp::Customer.create(card: params['payjp-token'])
# params['payjp-token'](response.id)からcustomerを作成)
params['payjp-token']が格納されたいた場合は、
params['payjp-token'](response.id)から、顧客データーを作成し「変数customer」に格納します。
変数customerにはjson形式で、顧客IDや、カード番号等の情報が格納されています。
@card = Card.new(user_id: user_id, customer_id: customer.id, card_id: customer.default_card)
現在のログインユーザーと、payjp側で作成した顧客データーを紐づけて変数@cardに格納します。
if @card.save
flash[:notice] = '登録しました'
redirect_to "/"
正しく保存できた場合は、ルートパスにリダイレクトします。
これで、データーベースに保存したカード情報には、ユーザーとpayjp側の顧客データーと結びついたデーターが保存されました。
正しく処理されていた場合は、payjpの顧客データーに新たに追加されます。
##次回は、PAY.JPでのクレジットカード決済機能について 〜運用編(購入)〜 となります。
https://qiita.com/sho_U/items/64ca57b7a1f72bf7079f