102
130

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rails で Payjp を使って決済システムを導入する

Last updated at Posted at 2018-03-15

はじめに

サービスEC のサービスを作る機会があり、MVP リリース時には BASE を使っていたのですが、
API の通信数に上限があったり、かゆいところに全然手が届かなかったりしたので決済システムを導入することにしました。
Stripe と迷っており、他のサービスで既に Stripe は使っていたのですが、Payjp もやれることはほとんど変わらなかったので今回は 試しにPayjp を使うことにしました。

やりたいこと

今回 Payjp では単発決済の導入をしました。
Stripe 同様定額課金なんかも実装はできます。

実装

Payjp の導入

API キーの取得

スクリーンショット 2018-03-15 18.55.47.png
Payjp のアカウントを作成し、左カラムにある API のリンクを踏むと Payjp で使える API キーの情報が表示されます。
手元の環境やテスト環境ではテスト用の鍵を使って、本番では本番用の鍵を使うのがよいかと思います。

決済

まずは View に決済用のフォームを作成します。

app/views/items/show.html.haml
= form_tag action: :pay, method: :post do
  :plain
    <script type="text/javascript" src="https://checkout.pay.jp" class="payjp-button" data-key="公開鍵"></script>

上記コードで決済のボタンが出力され、ボタンを押すと決済フォームが出てくるようになります。
スクリーンショット 2018-03-15 19.08.57.png
そしてそれぞれの値を入力し、トークンを作成を押すと、コントローラ内で実際に決済処理をします。
入力値はテストの場合は

  • カード番号 → 4242424242424242(正常に決済される VISA のテストカード番号)
  • 有効期限 → 未来
  • CVC → 数字3桁(数字は何でもよいです)
  • カード名義 → こちらも何でもよい
app/controllers/items_controller.rb
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 で実装

実装

モデル

まずはモデルにリレーションと簡単なバリデーションを実装(本当は正規表現など使った方がいいと思います)。

app/models/item.rb
class Item < ApplicationRecord
  has_many item_payments
  
  validates :stock, presence: true
  validates :price, presence: true
end
app/models/item_payment.rb
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 というカスタムバリデーションを作っています。

コントローラ
app/controllers/items_controller.rb
class ItemsController < ApplicationController
  def show
    @item = Item.find(params[:id])
  end
end
app/controllers/items/item_payments_controller.rb
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 を拾って、カードに関するエラーが出た場合の処理を加えています。その他の例外はこちらをご覧ください。

ビュー
app/views/items/show.html
= 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 が正しくできたときのレスポンス書いただけです)。

spec/support/payjp_mock.rb
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 は上記のような処理だけで簡単に決済が導入できてめちゃくちゃ便利です。とはいえ、テスト書けないのはしんどいのでちゃんとしてほしいです。
またカード情報だけで処理できてしまう分、自動で決済完了メールが送られないなどあるのでその辺は加味しないといけません。

その分全部ドキュメントからダッシュボードまで日本語でなのでその辺はありがたいなと。
自分たちの場合は返金処理をするのがカスタマーセンターなので日本語だとありがたい。。

最後に

コード等ざっと書いたので間違っていたらすみません、、

102
130
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
102
130

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?