はじめに
サービスEC のサービスを作る機会があり、MVP リリース時には BASE を使っていたのですが、
API の通信数に上限があったり、かゆいところに全然手が届かなかったりしたので決済システムを導入することにしました。
Stripe と迷っており、他のサービスで既に Stripe は使っていたのですが、Payjp もやれることはほとんど変わらなかったので今回は 試しにPayjp を使うことにしました。
やりたいこと
今回 Payjp では単発決済の導入をしました。
Stripe 同様定額課金なんかも実装はできます。
実装
Payjp の導入
API キーの取得
Payjp のアカウントを作成し、左カラムにある API
のリンクを踏むと Payjp で使える API キーの情報が表示されます。
手元の環境やテスト環境ではテスト用の鍵を使って、本番では本番用の鍵を使うのがよいかと思います。
決済
まずは View に決済用のフォームを作成します。
= form_tag action: :pay, method: :post do
:plain
<script type="text/javascript" src="https://checkout.pay.jp" class="payjp-button" data-key="公開鍵"></script>
上記コードで決済のボタンが出力され、ボタンを押すと決済フォームが出てくるようになります。
そしてそれぞれの値を入力し、トークンを作成を押すと、コントローラ内で実際に決済処理をします。
入力値はテストの場合は
- カード番号 → 4242424242424242(正常に決済される VISA のテストカード番号)
- 有効期限 → 未来
- CVC → 数字3桁(数字は何でもよいです)
- カード名義 → こちらも何でもよい
def pay
Payjp.api_key = 'Payjp の画面から取得した秘密鍵'
Payjp::Charge.create(
amount: 3500, # 決済する値段
card: params['payjp-token'],
currency: 'jpy'
)
end
先程取得した API の秘密鍵を Payjp.api_key = '秘密鍵'
で設定し、
Payjp::Charge.cretae
するだけで決済が完了します。
問題
めちゃくちゃ簡単!ではあるのですが、さすがにこれだと実運用できないので修正が必要です。
実運用するには在庫の管理や、それにともなうトランザクション処理、キャンセルや返金となった場合など色々と加味しなければならないのですが、ここでは
- Item に在庫があって購入されたら数が減る
- 同時に Item を更新できないようにブロックする
- 返金処理をために Payjp の「決済の ID」、「名前」、「メールアドレス」を保存する
をしたいと思います。
実運用のための実装
前提
- Item モデルが在庫、金額を持つ(在庫:stock、金額:price)
- ItemPayment モデルが「決済 ID」、「名前」、「メールアドレス」、「購入数」を持つ(決済 ID:charge_id、名前:name、メールアドレス:email、購入数:purchase_amount)
- Item が複数の ItemPayment と紐づく
- フォームは simpler_form で実装
実装
モデル
まずはモデルにリレーションと簡単なバリデーションを実装(本当は正規表現など使った方がいいと思います)。
class Item < ApplicationRecord
has_many item_payments
validates :stock, presence: true
validates :price, presence: true
end
class ItemPayment < ApplicationRecord
belongs_to :item
validates :name, presence: true
validates :email, presence: true
validates :purchase_amount, presence: true, numericality: { only_integer: true, greater_than: 0 }
validate :validate_purchase_amount_for_stock
private
def validate_purchase_amount_for_stock
if self.purchase_amount.present?
stock = self.item.stock
errors.add(:purchase_amount, "購入できるのは#{stock}個までです") if self.purchase_amount > stock
end
end
end
ItemPayment のバリデーションには、在庫以上の個数を購入できないように validate_purchase_amount_for_stock
というカスタムバリデーションを作っています。
コントローラ
class ItemsController < ApplicationController
def show
@item = Item.find(params[:id])
end
end
class ItemPaymentsController < ApplicationController
def create
@item = Item.find(params[:item_id])
@item_payment = @item.item_payments.build(item_payment_parmas)
@item.with_lock do
Payjp.api_key = 'Payjp の秘密鍵'
amount = @item.price * @item_payment.purchase_amount
charge = Payjp::Charge.create(currency: 'jpy', amount: amount, card: params['payjp-token'])
@item_payment.charge_id = charge['id']
respond_to do |format|
if @item_payment.save
# ここに決済完了メール送る処理書くとよいと思います。
format.html { redirect_to item_path(@item), notice: '購入しました。' }
end
end
end
rescue Payjp::CardError
respond_to do |format|
format.html { redirect_to item_path(@item), notice: 'カードエラーが発生しました' }
end
end
private
def item_payment_params
params.require(:item_payment).permit(:name, :email, :purchase_amount)
end
end
with_lock
を使って同時に在庫を更新できないようにブロックして、あとは必要な情報を保存するようにしています。
また例外処理としてここでは Payjp::CardError
を拾って、カードに関するエラーが出た場合の処理を加えています。その他の例外はこちらをご覧ください。
ビュー
= simple_form_for @item_payment, url: item_item_payments_path do |f|
= f.input :name
= f.input :email
= f.input :purchase_amount
:plain
<script type="text/javascript" src="https://checkout.pay.jp" class="payjp-button" data-key="公開鍵"></script>
これで決済と、その他ユーザーの情報を ItemPayment 内に保存できるます、多分。
ちなみに返金処理も API を使ってできるのですが、今回は Payjp の管理画面上で行う運用にしました。
そのため charge_id を保存して、返金する際は charge_id を元に Payjp 内で決済を検索して返金処理をできるようにしています(決済の検索が charge_id か顧客の ID でないとできなかったため)。
自動テスト(RSpec)
簡単に決済を導入できる Payjp なのですが、公式にはテスト用のモックが用意されておらずちゃんとしたテストが書けない。。
また自動テストで Payjp の API を叩くのはダメみたいです。。(https://muut.com/i/payjp/general:n8jubya6r7gxcp4398dp9d697ed)
決済周りでテストはちゃんと書きたいので是非とも用意してほしい。。
仕方なし今回は慰め程度に簡単なモックを書いときます(Payjp::Charge.create
が正しくできたときのレスポンス書いただけです)。
modle PayjpMock
def self.prepare_valid_charge
{
"amount": 3500,
"amount_refunded": 0,
"captured": true,
"captured_at": 1433127983,
"card": {
"address_city": nil,
"address_line1": nil,
"address_line2": nil,
"address_state": nil,
"address_zip": nil,
"address_zip_check": "unchecked",
"brand": "Visa",
"country": nil,
"created": 1433127983,
"customer": nil,
"cvc_check": "unchecked",
"exp_month": 2,
"exp_year": 2020,
"fingerprint": "e1d8225886e3a7211127df751c86787f",
"id": "car_d0e44730f83b0a19ba6caee04160",
"last4": "4242",
"name": nil,
"object": "card"
},
"created": 1433127983,
"currency": "jpy",
"customer": nil,
"description": nil,
"expired_at": nil,
"failure_code": nil,
"failure_message": nil,
"id": "ch_fa990a4c10672a93053a774730b0a",
"livemode": false,
"metadata": nil,
"object": "charge",
"paid": true,
"refund_reason": nil,
"refunded": false,
"subscription": nil
}
end
end
使うときは、
allow(Payjp::Charge).to receive(:create).and_return(PayjpMock.prepare_valid_charge)
を記載した後に Payjp::Charge.create
が走れば上記のレスポンスが返ります。
その他カードエラーなどの例外処理に関しては、
allow(Payjp::Charge).to receive(:create).and_raise(Payjp::CardError.new('', {}, 402))
とかすると例外が返るようになります。
とはいえちゃんとテストとして機能するには不十分すぎるのでどなたかいい方法教えてください。
まとめ
Payjp は上記のような処理だけで簡単に決済が導入できてめちゃくちゃ便利です。とはいえ、テスト書けないのはしんどいのでちゃんとしてほしいです。
またカード情報だけで処理できてしまう分、自動で決済完了メールが送られないなどあるのでその辺は加味しないといけません。
その分全部ドキュメントからダッシュボードまで日本語でなのでその辺はありがたいなと。
自分たちの場合は返金処理をするのがカスタマーセンターなので日本語だとありがたい。。
最後に
コード等ざっと書いたので間違っていたらすみません、、