Railsで決済機能を実装する際、決済サービスの候補としてあがるのはだいたいStripeかPay.jpだと思う。
業務で定期講読する商品を扱うことになりPay.jpを使って実装を行なったが、Payjpの定期購入について解説した記事が少なかったので解説してみたいと思う。
本記事では前段として、単発購入機能を作成することでPayjpの導入にしたいと思う。
ドキュメント
決済の流れ
2018年6月に施行された改正割賦販売法により、事業会社は顧客のカード情報を自社で保有する機器・ネットワーク内で保存、処理、通過することが出来なくなった。したがって、顧客のカード情報をそのまま自社のサーバには送信させず、Pay.jpが用意しているチェックアウトを用いてPay.jp側に送信し、そのレスポンスをもってカード情報のトークンをサーバに送るという流れになる。

導入
payjp-rubyという、Pay.jpが用意してくれている便利なgemがあるのでそれをインストールする。
# Gemfile gem 'payjp'
$ bundle install
フロントエンド
チェックアウトという、デザインされた決済フォーム、カード情報のバリデーション、カード情報のトークン化を行うフォームを生成するライブラリを用意してくれているので、これをそのまま使う。
<%# app/views/charges/new.html.erb %>
<script
type="text/javascript"
src="https://checkout.pay.jp/"
class="payjp-button"
data-key="#{ENV['PAYJP_PUBLIC_KEY']}"
data-submit-text="購入する"
data-text="カードを入力"
>
</script>
PAYJP_PUBLIC_KEYはPay.jpの管理画面で取得出来る公開鍵のこと。秘密鍵と合わせて取得しておくように。
サーバサイド
上記で入力されたカード情報はトークン化されサーバサイドへ送られてくる。params[:payjp_token]でトークンを取得出来るので、単発決済の場合はこれをそのまま用いることで決済を完了させられる。
また、決済履歴を残しておく必要があるので、PaymentHistoryというモデルを用意しておく。
# app/models/payment_history
# == Schema Information
#
# Table name: orders
#
# id :uuid not null, primary key
# amount :integer default(0), not null
# error_detail :string
# error_message :string
# status(決済ステータス) :integer default("before_payment"), not null
# created_at :datetime not null
# updated_at :datetime not null
# charge_id(Payjp決済ID) :string default(""), not null
# user_id :uuid not null
#
# Indexes
#
# index_orders_on_user_id (user_id)
class PaymentHistory
belongs_to :user
enum status: {
before_payment: 0, # 未決済
completed: 1, # 決済完了
failed: 2, # 決済失敗
}
end
コントローラーに決済処理を書いていく。payjpgemを入れてあるおかげでPay.jp用のクライアントを自分で用意する必要はない。
ここでは決済だけを使うので、Payjp::Chargeを使う。返り値はレスポンスになるので、変数に格納しておく。
決済金額や決済IDはメソッドで取得出来る。
# app/controllers/charges_controller.rb
class PaymentHistoriesController < ApplicationController
def create
payment_history = current_user.payment_histories.create(status: :before_payment)
response = Payjp::Charge.create(
amount: params[:amount], # 税込み金額を決済させる
card: params[:payjp_token], # カードにトークンを指定させることで単発決済は要件を満たせる
currency: 'jpy' # 通貨を指定する必要があるが、現在は日本円のみ対応
)
payment_history.update!(
status: :completed,
amount: response.amount,
charge_id: response.id
)
render json: { message: '決済完了' }, status: 200
end
end
また、当然外部サービスとの連携なのでエラーハンドリングを行い、ログを残す必要がある。
Pay.jpではエラーは全てPayjp::PayjpErrorで返却されるようになっているので、最初にこれをrescueし、その次にStandardErrorをrescueする。
Pay.jpのエラーコードはe.json_body[:error]で取得出来る。
# app/controllers/charges_controller.rb
class PaymentHistoriesController < ApplicationController
def create
payment_history = current_user.payment_histories.create(status: :before_payment)
response = Payjp::Charge.create(
amount: params[:amount], # 税込み金額を決済させる
card: params[:payjp_token], # カードにトークンを指定させることで単発決済は要件を満たせる
currency: 'jpy' # 通貨を指定する必要があるが、現在は日本円のみ対応
)
payment_history.update!(
status: :completed,
amount: response.amount,
charge_id: response.id
)
render json: { message: '決済完了' }, status: 200
rescue Payjp::PayjpError => e
err = e.json_body[:error]
payment_history.update!(
status: :failed,
error_message: err[:code]
)
render json: { message: '決済失敗' }, status: 400
rescue StandardError => e
payment_history.update!(
status: :failed,
error_message: 'failed_payment',
error_detail: '何らかの理由で決済に失敗しました'
)
render json: { message: '決済失敗' }, status: 500
end
end
当然、エラーコードだけだと何のエラーなのか判別出来ないので、Pay.jpのドキュメントを参照し、エラーコードに対応したメッセージを格納出来るようにする。
# app/controllers/charges_controller.rb
class ChargesController < ApplicationController
PAYJP_ERROR_CODE = {
'invalid_number' => 'カード番号が不正です',
'invalid_cvc' => 'CVCが不正です',
'invalid_expiration_date' => '有効期限年、または月が不正です',
'incorrect_card_data' => 'カード番号、有効期限、CVCのいずれかが不正です',
'invalid_expiry_month' => '有効期限月が不正です',
'invalid_expiry_year' => '有効期限年が不正です',
'expired_card' => '有効期限切れです',
'card_declined' => 'カード会社によって拒否されたカードです',
'processing_error' => '決済ネットワーク上でエラーが発生しました',
'missing_card' => '顧客がカードを保持していない',
'unacceptable_brand' => '対象のカードブランドが許可されていません'
}.freeze
def create
payment_history = current_user.payment_histories.create(status: :before_payment)
response = Payjp::Charge.create(
amount: params[:amount], # 税込み金額を決済させる
card: params[:payjp_token], # カードにトークンを指定させることで単発決済は要件を満たせる
currency: 'jpy' # 通貨を指定する必要があるが、現在は日本円のみ対応
)
payment_history.update!(
status: :completed,
amount: response.amount,
charge_id: response.id
)
render json: { message: '決済完了' }, status: 200
rescue Payjp::PayjpError => e
err = e.json_body[:error]
payment_history.update!(
status: :failed,
error_message: err[:code],
error_detail: PAYJP_ERROR_CODE[err[:code]]
)
render json: { message: PAYJP_ERROR_CODE[err[:code]] }, status: 400
rescue StandardError => e
payment_history.update!(
status: :failed,
error_message: 'failed_payment',
error_detail: '何らかの理由で決済に失敗しました'
)
render json: { message: '何らかの理由で決済に失敗しました' }, status: 500
end
end
まとめ
以上で単発決済機能を実装することが出来た。
上記のコントローラーはかなりファットになってしまっているので、自分で実装する際はスリムにするように心がけて欲しい。
今回でPay.jpについて簡単な導入が出来たので、次回は定期決済機能を実装していこうと思う。