はじめに
スクールの課題でフリマアプリを作る中で、payjpを利用したクレカ登録・決済機能の実装がありました。
検索すると以前の卒業生が多数記事を書いてくれてるのでコードは見つかるのですが、何をやっているの?という所は結局公式のリファレンス読むのが一番だよねとなったので理解したことについて記載しておくものです。
この記事では
payjpを利用するってそもそもどういうこと?(APIの説明)
payjpの呼び出し方
クレカ登録の実装
登録したクレカによる決済実装
上記について記載していきます。
※現在payjpはv2バージョンを利用することが推奨されております。よりセキュアな実装として登録フォーム自体もpayjp側で用意してくれているのがv2なのですが、今回の課題はフォームの実装もこちら側のタスクとして含まれていたため、旧型のv1で実装を進めています。
とはいえ呼び出し方や使えるメソッドが異なるだけで、どちらも公式を読みながらやればあまり差がなく実装できるかと思います。
環境
ruby 2.6.5
rails 6.0.3
payjpを利用するってそもそもどういうこと?
コードを書いていくにしても、前段としてここの理解がまず大事だと思っています(あまりここに触れた記事はなかった)
payjpは、APIの1種です。
APIっていうのはググれば色々説明が出てくるんですが、**「外部向けにソフトウェアの機能を一部提供してあげるよ」**って感じです。
用意してくれてる側の指示に従いうまくAPIを呼び出すことで、自分のアプリケーションの中でそのソフトウェアの機能を一部使えることができる様になる、という仕組みですね。
具体例を言うと、例えばGogole Mapのサービスがあります。
あれもgoogleが用意してくれたAPIを利用し、指示にしたがってコードを記載することで、自分のアプリケーション上にgoogle mapを表示することが可能になるという仕組みです。
payjpもクレカに関するAPIであり、利用することでクレジットカードの登録や決済が可能になります。
APIを利用する時に考え方として個人的に大事だと思っているのが、相手に用意してもらっているという流れをちゃんと把握するということです。
上記を認識した上で実装の流れを考えると、
用意してもらったやり方で、APIを呼び出す準備をする
用意してもらったやり方で、向こうのアプリケーションとやりとりをし、処理を行う
といった感じになります。このイメージがあると、以降の作業が具体的に何をしているのか分かりやすくなると思うので重要です。
それが故、用意してくれてる側の公式ドキュメントでやり方を理解する、と言うのが一番適切なアプローチになります。公式の説明書を読む様なものなので。
payjpの呼び出し方
前提として、payjpに登録しテスト公開鍵とテスト秘密鍵を取得しましょう。
APIの認証のために必要なキーです。payjp側はこれらを持って、僕らが「利用を許されたユーザー」であることを判断しています。
登録が終わったらv1の公式リファレンスを読みましょう。
アクセスして早々、埋め込むべきスクリプトがちゃんと記載されています。
また、少し下に読み進めると、公開鍵による認証方法も書いています。
実際の記載については次項で見ていきます。
クレカ登録の実装
上記の呼び出し方も踏まえ、先に実際のコードを載せます。
その上で流れを解説していきます。
※各ファイル、説明に不要な部分の記載は適宜削除しています。
!!!
%html
%head
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%script{src: "https://js.pay.jp/v1/", type: "text/javascript"}
= csrf_meta_tags
= csp_meta_tag
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application'
.creditCreate
.creditCreate__title
%h1 クレジットカード情報入力
.creditForm
= form_with model: @credit, method: :post, id: "cardCreateForm" do |f|
.creditForm__numberfield
%label(for="cardnumber-input") カード番号
%span.creditPoint 必須
%br
= f.text_field :card_number, type: "text", class: 'cardnumber-input', id:'card-number', placeholder: "半角数字のみ", maxlength: 16
.creditslist
= image_tag "jcb.gif", class:"creditsIcon"
= image_tag "visa.gif", class:"creditsIcon"
= image_tag "master.gif", class:"creditsIcon"
= image_tag "amex.gif", class:"creditsIcon"
.creditForm__datefield
%label 有効期限
%span.creditPoint 必須
%br
= f.select :exp_month, [["01",1],["02",2],["03",3],["04",4],["05",5],["06",6],["07",7],["08",8],["09",9],["10",10],["11",11],["12",12]],{} , class: 'dateSelect', name: 'exp_month'
月
= f.select :exp_year, [["20",2020],["21",2021],["22",2022],["23",2023],["24",2024],["25",2025],["26",2026],["27",2027],["28",2028],["29",2029]],{} , class: 'dateSelect', name: 'exp_year'
年
.creditForm__securityfield
%label セキュリティコード
%span.creditPoint 必須
%br
= f.text_field :cvc, type: 'text', class: 'securityInput', id: 'cvc', placeholder: 'カード背面4桁もしくは3桁の番号', maxlength: "4"
#card_token.creditForm__submitfield
= f.submit '追加する', class: 'creditsSubmit', id: 'token_submit'
$(function() {
$('#cardCreateForm').on('submit', function(e) {
e.preventDefault()
Payjp.setPublicKey(['PAYJP_PUBLIC_KEY']);
var card = {
number: document.getElementById("card-number").value,
exp_month: document.getElementById("credit_exp_month").value,
exp_year: document.getElementById("credit_exp_year").value,
cvc: document.getElementById("cvc").value
};
if (card.number == "" || card.cvc == "") {
alert("入力もれがあります");
} else {
Payjp.createToken(card, function(status, response) {
if (status === 200 ) {
$("#card_number").removeAttr("name");
$("#cvc").removeAttr("name");
$("#exp_month").removeAttr("name");
$("#exp_year").removeAttr("name");
$("#card_token").append(
$('<input type="hidden" name="payjp-token">').val(response.id)
);
$('#cardCreateForm').get(0).submit();
alert("登録に成功しました");
} else {
alert("カード情報が正しくありません");
}
});
}
});
});
require 'payjp'
def create
Payjp.api_key = ENV['PAYJP_SECRET_KEY']
if params['payjp-token'].blank?
render :new
else
customer = Payjp::Customer.create(
email: current_user.email,
card: params['payjp-token'],
metadata: {user_id: current_user.id}
)
@credit = Credit.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
if @credit.save
redirect_to user_path(current_user.id)
else
render :new
end
end
end
以上がクレカ登録に関するコードです。
処理の流れは以下の通りです。この流れに沿って説明をしていきます。
①payjpのAPIを利用するセッティングをする
②jsファイルにて、フォーム送信時に入力内容とpajypを紐付け登録の準備をする
③コントローラーにアクションを飛ばし、payjp上で顧客データを生成・顧客データを紐づくidをテーブルに保存する
①payjpのAPIを利用するセッティングをする
application.html.hamlをみましょう
!!!
%html
%head
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%script{src: "https://js.pay.jp/v1/", type: "text/javascript"}
= csrf_meta_tags
= csp_meta_tag
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application'
scriptの部分がpayjpを呼び出す記載です。
呼び出し方は公式リファレンスに書いてあります。ただ転記するだけです。公式をみていればただ転記するだけなのです。
②jsファイルにて、フォーム送信時に入力内容とpajypを紐付け登録の準備をする
jsファイルをみましょう。
フォーム送信のhamlファイルと対応した記載になってるので、並べてみながらだと理解がしやすいと思います。
$(function() {
$('#cardCreateForm').on('submit', function(e) {
e.preventDefault()
Payjp.setPublicKey(['PAYJP_PUBLIC_KEY']);
var card = {
number: document.getElementById("card-number").value,
exp_month: document.getElementById("credit_exp_month").value,
exp_year: document.getElementById("credit_exp_year").value,
cvc: document.getElementById("cvc").value
};
if (card.number == "" || card.cvc == "") {
alert("入力もれがあります");
} else {
Payjp.createToken(card, function(status, response) {
if (status === 200 ) {
$("#card_number").removeAttr("name");
$("#cvc").removeAttr("name");
$("#exp_month").removeAttr("name");
$("#exp_year").removeAttr("name");
$("#card_token").append(
$('<input type="hidden" name="payjp-token">').val(response.id)
);
$('#cardCreateForm').get(0).submit();
alert("登録に成功しました");
} else {
alert("カード情報が正しくありません");
}
});
}
});
});
分割して解説していきます。
$(function() {
$('#cardCreateForm').on('submit', function(e) {
e.preventDefault()
jQueryで、フォームボタンをクリックした際の挙動ですと定義しています。
Payjp.setPublicKey(['PAYJP_PUBLIC_KEY']);
認証(payjpの利用登録ができていますよ!と言うこと)を示すため、登録し取得したテスト公開鍵をセットします。
これを持ってpayjp側が利用を許可してくれます。
setPublicKeyという書き方がいきなり出てきますが、これも公式リファレンスで指定された方法で記述してます。ただのコピペです。恐ることはありません。
var card = {
number: document.getElementById("card-number").value,
exp_month: document.getElementById("credit_exp_month").value,
exp_year: document.getElementById("credit_exp_year").value,
cvc: document.getElementById("cvc").value
};
JSの記載です。get element by id と言う書き方の通り、各フォーム欄のidを指定・特定することで、それぞれの入力内容を定義し、cardと言う変数に代入し定義しています。
if (card.number == "" || card.cvc == "") {
alert("入力もれがあります");
jsの記載です。定義した変数cardの、numberもしくはcvcが空だった時に受付をせずエラーを返す処理をしています。
} else {
Payjp.createToken(card, function(status, response) {
if (status === 200 ) {
$("#card_number").removeAttr("name");
$("#cvc").removeAttr("name");
$("#exp_month").removeAttr("name");
$("#exp_year").removeAttr("name");
$("#card_token").append(
$('<input type="hidden" name="payjp-token">').val(response.id)
);
$('#cardCreateForm').get(0).submit();
alert("登録に成功しました");
} else {
alert("カード情報が正しくありません");
}
});
}
});
});
カード情報を基にトークンを生成します。
これもpayjp APIの公式リファレンスを読んでください。
トークンを生成の部分を読むと、トークンはどのように生成するのかと言う文章での説明と、実際の記載方法が右側に記載されているはずです。
いきなりPayjp.create~と言う記載が出てきましたが、これはリファレンスに「この通り書いたらできるよ」って書いてくれてるだけなので、ちゃんと読めばその通りにするだけです。
ここでnumber等4つの値を渡す必要があると書かれいてがために、先ほどcard変数に4つの値を定義した訳です。順番的にはここで必要とされてるから定義している訳です。
if status === 200とは、カード情報が有効であり、正しくトークンが生成された場合にpayjp側が返してくれるステータス値になります。
なのでelseでエラーを返しているのは「正常に登録できなかった場合」を示しているわけです。
ここからさらに細かくみていきます。
$("#card_number").removeAttr("name");
$("#cvc").removeAttr("name");
$("#exp_month").removeAttr("name");
$("#exp_year").removeAttr("name");
JS,jQueryの記載です。
カードが有効だった場合、セキュリティの観点からそれぞれのフォームに入力した値を取り除いています(idで指定したフォームのname属性をremoveする、と言う処理です)
$("#card_token").append(
$('<input type="hidden" name="payjp-token">').val(response.id)
);
$('#cardCreateForm').get(0).submit();
alert("登録に成功しました");
カードが有効な際にPayjpから帰ってきたデータ(response.id)を、フォームに返す処理をappendで行っています。
type = hiddenを指定することで、あくまでユーザーからは見えないものの、フォームにデータを送っている形です。
その上で、submitすることで、Payjpから返してもらったデータをこの後controllerに送りcreateアクションを行っていく、と言うことを実現しています。
コントローラーにうまくデータを飛ばすための記載ということですね。
これを持って次にcontrollerの処理をみていくことができます。
③コントローラーにアクションを飛ばし、payjp上で顧客データを生成・顧客データを紐づくidをテーブルに保存する
require 'payjp'
def create
Payjp.api_key = ENV['PAYJP_SECRET_KEY']
if params['payjp-token'].blank?
render :new
else
customer = Payjp::Customer.create(
email: current_user.email,
card: params['payjp-token'],
metadata: {user_id: current_user.id}
)
@credit = Credit.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
if @credit.save
redirect_to user_path(current_user.id)
else
render :new
end
end
end
ここまでうまく処理ができて、最後にcontrollerでデータ作成となります。
順を追ってみていきましょう。
require 'payjp'
def create
Payjp.api_key = ENV['PAYJP_SECRET_KEY']
if params['payjp-token'].blank?
render :new
まずrequire 'payjp'でpayjpを使える様にします。
次にSECRET_KEYを指定し、こちらがpayjpの利用権を思ったユーザーであることを示します(環境変数を用いて記載しています)
その上で先ほど正常なレスポンスだった場合に送られてきたデータがparams['payjp-token']ですので、そちらがからだった場合は処理を行わずrenderする処理を記載しています。
def create
else
customer = Payjp::Customer.create(
email: current_user.email,
card: params['payjp-token'],
metadata: {user_id: current_user.id} #ここは任意
)
ここで、Payjpの顧客データを作成し、変数customerに代入しています。
いきなりこの書き方が出てきました。
何度も言う様ですが公式リファレンスを見れば全てが買いてあります。
今回行いたいのは顧客データの作成なので、リファレンスの中の顧客データの欄をみます。
すると、Payjp::Customer.createという記載方法や、その引数が書かれているはずです。
引数の内容をみていけば何をいれるべきなのかは簡単にわかるはずです。
例えばcardと言う引数はトークンIDを指定と書かれているので、先ほど送る様に設定したトークンIDを設定すればいいことがわかります。
def create
@credit = Credit.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
if @credit.save
redirect_to user_path(current_user.id)
else
render :new
end
end
ここからはRails側のテーブル処理の話です。
先ほどのリファレンスを参照すれば、Payjpの顧客が持つレスポンスデータについても確認することができます。
先ほど作成したPayjpの顧客データは変数customerに代入していたため、customer.id等で指定することで、レスポンスデータをテーブルに保存することができるわけです。
(法律上クレカデータを自分のテーブルに保存することはできないため、そのデータ自体はpayjp側に保存してもらい、そのデータを呼び出すための紐付けとしてcustomer_idやcardのdataをテーブルに保存しておくわけです。
終わりに
結構な長文になってしまったので登録のみで終わらせますが、大事なのは流れを理解し、リファレンスを読むこと、これに尽きると思います。
この登録したデータを基にカード決済を行う流れについても、リファレンスを読みながら決済にはどの様な記述が必要か?引数には何を指定するのか?を見ることでわりとすんなり実装できました。
(先ほどテーブルに紐付けたことからわかる通り、決済に必要なカードデータを呼び出して引数に渡してやれば良い、と言うのはそれほど難しくなく想像できるのではないでしょうか)
とはいえAPIに慣れていない状態だったのでQiitaの記事にも大分助けられつつの実装にはなりました。が、結論公式が最強、この言葉を実感を持って理解できる経験だったので記録しておきます。
*初学者ゆえ何かあればご指摘いただけると嬉しいです。