Help us understand the problem. What is going on with this article?

【Ruby on Rails で簡単!】PAY.JPを利用したクレジットカード決済の導入

Image from Gyazo

何かサービスを作る際に、決済機能を導入したいはずです。
今回はPAY.JPを利用した決済機能を案内していきます。

PAY.JPの導入準備

スクリプトの記述

PAY.JPを使うためのスクリプトを記述します。
下記をコピーしてください。

スクリプト
%script{src: "https://js.pay.jp/", type: "text/javascript"}

コピーしたら、application.html.hamlに貼り付けましょう!!

application.html.haml
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %title payjptest
    %script{src: "https://js.pay.jp/", type: "text/javascript"}
    -# このscriptを記載
    = csrf_meta_tags
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
  %body
    = yield

PAY.JPの登録

PAY.JPのアカウントを作成しましょう!
https://pay.jp/

APIキーを取得します

アカウントを作成してログインし、下記の場所のAPIキーを確認しましょう!

Image from Gyazo

Appにgem 'payjp'を追加

Gemfile
# PAY.JPのgem
gem 'payjp'

# 環境変数を簡単に定義できるENVファイルを対応させるgem
gem 'dotenv-rails'

追加したら

ターミナル
$ bundle install
$ rails s 

再起動しないとgemもscriptも読み込まれないので、エラーがおきます。

環境変数を利用して、APIキーをAppに登録

gem 'dotenv-rails'をインストールできたので、.envファイルを作成しましょう

app > .env の場所に作成します。

Image from Gyazo

.gitignoreの上だと思えば簡単です。

秘密鍵をGithubにコミットしてしまうとAPIキーを世に公開してしまうので、.gitignoreに.envを記述します

gitignore
/.env

ここ本当に重要なので、注意してくださいね!

では、APIキーを記述します

.env
PAYJP_PRIVATE_KEY     = 'sk_test_111111111111111111111111'
PAYJP_KEY             = 'pk_test_111111111111111111111111'

記載したら、Githubのコミットに表示されていないか?確認します。
コミットに表示されてたら、ヤバイです。
危険です。.gitignoreを再確認してください。

ここまでで準備完了です。

素材作成(view):ここは自身で記述すると良いかと思います。

ここはviewですが、必要なければ飛ばしてください。

クレジットカードを登録するためのviewを作成します。
下記はformの部分テンプレートですが下記のような

スクリプト

記述
#{asset_path 'creditcards/master-card.svg'}

Image from Gyazo

master-card.svgの画像素材がないとエラーがおきますので、
コピペするなら素材を集めてください。

form

formの部分テンプレート
= form_with url:creditcards_path, method: :post, html: { name: "inputForm" },class:"form" do |f|
  .form__upper
    .form__upper__group
      = f.label :カード番号
      %span.form-require 必須
      = f.text_field :card_number, name: "card_number", id:"card_number", type: "text", placeholder: '半角数字のみ', class: 'input-default', maxlength: "16"
      %ol
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/visa.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/master-card.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/saison-card.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/jcb.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/american_express.svg'}", width:"35", height:"20", class:"american_express"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/dinersclub.svg'}", width:"35", height:"20"}
        %li
          %object{type: "image/svg+xml", data: "#{asset_path 'creditcards/discover.svg'}", width:"35", height:"20"}
    .form__upper__group.exp
      .name
        = f.label :有効期限
        %span.form-require 必須
      = f.select :exp_year, [["19",2019],["20",2020],["21",2021],["22",2022],["23",2023],["24",2024],["25",2025],["26",2026],["27",2027],["28",2028],["29",2029]], {}, class: 'input-default harf', name: "exp_year", id:"exp_year"
      %span= 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: 'input-default harf', name: "exp_month", id:"exp_month"
      %span.form__upper__group
      = f.label :セキュリティーコード
      %span.form-require 必須
      = f.text_field :cvc, name: "cvc", id:"cvc", class:"cvc", type: "text", placeholder: 'カード背面4桁もしくは3桁の番号', class: 'input-default', maxlength: "16"
    .form__upper__group
      %p.about
        = fa_icon 'question-circle'
        %span
        = link_to 'カード裏面の番号とは?', root_path, class:'about__registered'
    .form__upper__group
      = f.submit '次へ進む', class: 'btn-default', id: "charge-form"

data: "#{asset_path 'creditcards/visa.svg'}"は、
【 app > asset > images > creditcards > visa.svg 】を読み込む
という設定になります。

scss
.form{
  &__upper{
    margin: 0 auto;
    max-width: 343px;
    p {
      text-align: center;
    }
    &__group {
      font-size: 14px;
      color: #333;
      &:not( :first-child ){
        margin-top: 32px;
      }
      label{
        font-weight: 600;
      }
      ol{
        display:flex;
        li {
          margin: 5px 8px 0 0;
        }
      }
      .form-require {
        background-color: $green;
        color: #fff;
        font-size: 12px;
        margin: 0 0 0 8px;
        padding: 2px 4px;
        border-radius: 2px;
        vertical-align:top;
        &-optional {
          background-color: gray;
          color: #fff;
          font-size: 12px;
          margin: 0 0 0 8px;
          padding: 2px 4px;
          border-radius: 2px;
          vertical-align:top;
        }
      }
      .input-default{
        width: 90%;
        margin: 8px 0 0;
        height: 48px;
        padding: 10px 16px 8px;
        border-radius: 4px;
        border: 1px solid #ccc;
        background: #fff;
        line-height: 1.5;
        font-size: 16px;
        &.harf{
          width: calc(40% - 6px);
          margin: 8px 8px 0 0;
          height: 45px;
          border-radius: 4px;
          border: 1px solid #ccc;
          background: #fff;
          line-height: 1.5;
          font-size: 16px;
          &.exp{
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
          }
        }
        &-select{
          width: 76px;
          margin-top: 8px;
          height: 45px;
          border-radius: 4px;
          border: 1px solid #ccc;
          background: #fff;
          line-height: 1.5;
          font-size: 16px;
        }
      }
      h3 {
        font-size: 16px;
        font-weight: bold;
      }
      .attention{
        margin: 8px 0 0;
      }
      .agree{
        text-align: center;
      }
      a {
        color: #0099e8;
        text-decoration: none;
      }
      span {
        margin: 0 2px;
      }
      .about{
        text-align: right;
        &__registered{
          color: #0099e8;
          text-decoration: none;
        }
        .fa-chevron-right {
          color: #0099e8;
        }
        .fa-question-circle {
          color: #0099e8;
          font-size: 1rem
        }
      }
    }
    .form-info-text {
      color: #888;
      margin-top: 8px;
      font-size: 14px;
    }
  }
  &__bottom {
    margin: 0 auto;
    max-width: 343px;
    .registance {
      text-align: right;
    }
  }
  .btn-default {
    width: 100%;
    height: 50px;
    background-color: $green;
    color: #FFFFFF;
    font-size: 15px;
    cursor: pointer;
  }
  .btn-registration{
    color: #fff;
    border-radius: 4px;
    width: 50%;
    line-height: 48px;
    border: 1px solid transparent;
    text-align: center;
    margin: 0 auto;
    position: relative;
    i{
      font-size:20px;
      position: absolute;
      top:13.5px;
      left:15px;
    }
    .about__registered {
      color: #FFFFFF;
      text-decoration: none;
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      cursor: pointer;
    }
    &.email{
      background-color: $green;
    }
    &.facebook{
      background-color:#385184;
    }
    &.google{
      background-color:#FFFFFF;
      color: black;
      background: #fff image-url('google.svg') 
      no-repeat 3px top;
      border: #979797 solid 1px;
    }
    &:not( :first-child ){
      margin-top: 16px;
    }
  }
}
new.html.haml
= render 'shared/main-header'
.container-fluid
  .row.py-5.w-100
    = render 'shared/mypage-side'
    .mypage-main.col-9
      %h2.header_title
        支払い方法
      .single-container
        .rgs-main__section
          = render "shared/creditcard-form"
        .payment-explain
          = fa_icon 'chevron-right', class:"arrows"
          = link_to '支払い方法について', '#'

モデル作成

ターミナル
$ rails g model creditcard

creditcardsテーブル

Column Type Options
user_id references foreign_key: true, null: false
payjp_id string null: false

Association

  • belongs_to :user

ということでマイグレーションファイルは下記になります。

migrationファイル
class CreateCreditcards < ActiveRecord::Migration[5.2]
  def change
    create_table :creditcards do |t|
      t.references :user,  foreign_key: true, null: false
      t.string :payjp_id, null: false
      t.timestamps
    end
  end
end

ここは他の記事と異なります。

  • user_id: AppのUser-ID
  • payjp_id: PAYJPのUser-ID

他の記事だとカード用のカラムも作成していますが、PAYJPのアカウントから引っ張りだせばいいので不要です。

本題のjQueryです。

Payjp.js
$(document).on('turbolinks:load',function(){
  // PAY.JPの公開鍵をセットします。
  Payjp.setPublicKey('pk_test_111111111111111111');

  //formのsubmitを止めるために, クレジットカード登録のformを定義します。
  var form = $(".form");

  $("#charge-form").click(function() {
    // submitが完了する前に、formを止めます。
    form.find("input[type=submit]").prop("disabled", true);
    // submitを止められたので、PAY.JPの登録に必要な処理をします。

    // formで入力された、カード情報を取得します。
    var card = {
      number: $("#card_number").val(),
      cvc: $("#cvc").val(),
      exp_month: $("#exp_month").val(),
      exp_year: $("#exp_year").val(),
    };

    // PAYJPに登録するためのトークン作成
    Payjp.createToken(card, function(status, response) {
      if (response.error){
        // エラーがある場合処理しない。
        form.find('.payment-errors').text(response.error.message);
        form.find('button').prop('disabled', false);
      }   
      else {
        // エラーなく問題なく進めた場合
        // formで取得したカード情報を削除して、Appにカード情報を残さない。
        $("#card_number").removeAttr("name");
        $("#cvc").removeAttr("name");
        $("#exp_month").removeAttr("name");
        $("#exp_year").removeAttr("name");
        var token = response.id;
        form.append($('<input type="hidden" name="payjpToken" />').val(token));
        form.get(0).submit();
      };
    });
  });
});

コントローラーの記述

コントローラー(new&create)の作成

creditcards_controller.rb
class CreditcardsController < ApplicationController
  require "payjp"
  before_action :set_card

  def new
    # cardがすでに登録済みの場合、indexのページに戻します。
    @card = Creditcard.where(user_id: current_user.id).first
    redirect_to action: "index" if @card.present?    
  end

  def create
    # PAY.JPの秘密鍵をセット(環境変数)
    Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]

    # jsで作成したpayjpTokenがちゃんと入っているか?
    if params['payjpToken'].blank?
      # トークンが空なら戻す
      render "new"
    else
      # トークンがちゃんとあれば進めて、PAY.JPに登録されるユーザーを作成します。
      customer = Payjp::Customer.create(
        description: 'test',
        email: current_user.email,
        card: params['payjpToken'],
        metadata: {user_id: current_user.id}
      )

      # PAY.JPのユーザーが作成できたので、creditcardモデルを登録します。
      @card = Creditcard.new(user_id: current_user.id, payjp_id: customer.id)
      if @card.save
        redirect_to action: "index", notice:"支払い情報の登録が完了しました"
      else
        render 'new'
      end
    end
  end

  private
  def set_card
    @card = Creditcard.where(user_id: current_user.id).first if Creditcard.where(user_id: current_user.id).present?
  end
end

コントローラーの追記(index、destory)

creditcards_controller.rb
class CreditcardsController < ApplicationController
  require "payjp"
  before_action :set_card

  def index
    # すでにクレジットカードが登録しているか?
    if @card.present?
      # 登録している場合,PAY.JPからカード情報を取得する
      # PAY.JPの秘密鍵をセットする。
      Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
      # PAY.JPから顧客情報を取得する。
      customer = Payjp::Customer.retrieve(@card.payjp_id)
      # PAY.JPの顧客情報から、デフォルトで使うクレジットカードを取得する。
      @card_info = customer.cards.retrieve(customer.default_card)
      # クレジットカード情報から表示させたい情報を定義する。
      # クレジットカードの画像を表示するために、カード会社を取得
      @card_brand = @card_info.brand
      # クレジットカードの有効期限を取得
      @exp_month = @card_info.exp_month.to_s
      @exp_year = @card_info.exp_year.to_s.slice(2,3) 

      # クレジットカード会社を取得したので、カード会社の画像をviewに表示させるため、ファイルを指定する。
      case @card_brand
      when "Visa"
        @card_image = "visa.svg"
      when "JCB"
        @card_image = "jcb.svg"
      when "MasterCard"
        @card_image = "master-card.svg"
      when "American Express"
        @card_image = "american_express.svg"
      when "Diners Club"
        @card_image = "dinersclub.svg"
      when "Discover"
        @card_image = "discover.svg"
      end
    end
  end

  def new
    @card = Creditcard.where(user_id: current_user.id).first
    redirect_to action: "index" if @card.present?    
  end

  def create
    Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
    if params['payjpToken'].blank?
      render "new"
    else
      customer = Payjp::Customer.create(
        description: 'test',
        email: current_user.email,
        card: params['payjpToken'],
        metadata: {user_id: current_user.id}
      )
      @card = Creditcard.new(user_id: current_user.id, payjp_id: customer.id)
      if @card.save
        if request.referer&.include?("/registrations/step5")
          redirect_to controller: 'registrations', action: "step6"
        else
          redirect_to action: "index", notice:"支払い情報の登録が完了しました"
        end
      else
        render 'new'
      end
    end
  end

  def destroy     
    # 今回はクレジットカードを削除するだけでなく、PAY.JPの顧客情報も削除する。これによりcreateメソッドが複雑にならない。
    # PAY.JPの秘密鍵をセットして、PAY.JPから情報をする。
    Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
    # PAY.JPの顧客情報を取得
    customer = Payjp::Customer.retrieve(@card.payjp_id)
    customer.delete # PAY.JPの顧客情報を削除
    if @card.destroy # App上でもクレジットカードを削除
      redirect_to action: "index", notice: "削除しました"
    else
      redirect_to action: "index", alert: "削除できませんでした"
    end
  end

  private
  def set_card
    @card = Creditcard.where(user_id: current_user.id).first if Creditcard.where(user_id: current_user.id).present?
  end
end

登録したカード情報を表示

Image from Gyazo
index.html.hamlに記述します。

index.html.haml
.mypage-main.col-9
      %h2.header_title
        支払い方法
      .single-container
        %section.creditcard_section
          %h3 クレジットカード一覧
          - if @card.present?
            .container
              .creditcard-info
                = image_tag "creditcards/#{@card_image}",width:'34',height:'20', alt:'master-card'
                %p.creditcard-info__number
                  = "**** **** **** " + @card_info.last4 #クレジットカードの下4桁を表示
                %p.creditcard-info__period 
                = @exp_month + " / " + @exp_year
                = button_to "削除する", creditcard_path(@card), method: :delete, class:"creditcard-info__delete"
          - else
            .new-card
              = link_to new_creditcard_path, class:"new-card-btn" do
                %i.far.fa-credit-card 
                クレジットカードを追加する

クレジットカードがない場合は下記の表示にさせます
Image from Gyazo

購入処理を追加

creditcards_controller.rb
def buy
    @product = Product.find(params[:product_id])
    # すでに購入されていないか?
    if @product.buyer.present? 
      redirect_back(fallback_location: root_path) 
    elsif @card.blank?
      # カード情報がなければ、買えないから戻す
      redirect_to action: "new"
      flash[:alert] = '購入にはクレジットカード登録が必要です'
    else
      # 購入者もいないし、クレジットカードもあるし、決済処理に移行
      Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
      # 請求を発行
      Payjp::Charge.create(
      amount: @product.price,
      customer: @card.customer_id,
      currency: 'jpy',
      )
      # 売り切れなので、productの情報をアップデートして売り切れにします。
      if @product.update(buyer_id: current_user.id)
        flash[:notice] = '購入しました。'
        redirect_to controller: 'products', action: 'show', id: @product.id
      else
        flash[:alert] = '購入に失敗しました。'
        redirect_to controller: 'products', action: 'show', id: @product.id
      end
    end
  end

あとはボタンを押したら、buyアクションが動くようにすれば、完了です!!

以上です
お疲れ様です。

参考リンク

新規登録時にクレジットカード登録

購入処理

テストカード

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした