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

[![Image from Gyazo](https://i.gyazo.com/58d2388bc26e3d6418898b12f8ba9f70.png)](https://gyazo.com/58d2388bc26e3d6418898b12f8ba9f70)

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

## PAY.JPの導入準備

### スクリプトの記述

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

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

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

```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](https://i.gyazo.com/a34feea58123f38a66c1a8960cf01474.png)](https://gyazo.com/a34feea58123f38a66c1a8960cf01474)

### Appにgem 'payjp'を追加

```ruby: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](https://i.gyazo.com/ae185eb2685d442e6c13e258fdf4e8a5.png)](https://gyazo.com/ae185eb2685d442e6c13e258fdf4e8a5)

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

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

```ruby:gitignore
/.env
```

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

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

```ruby:.env
PAYJP_PRIVATE_KEY     = 'sk_test_111111111111111111111111'
PAYJP_KEY             = 'pk_test_111111111111111111111111'
```

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

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




## 素材作成（view）：ここは自身で記述すると良いかと思います。

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

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

### スクリプト
```haml:記述
#{asset_path 'creditcards/master-card.svg'}
```

[![Image from Gyazo](https://i.gyazo.com/58d2388bc26e3d6418898b12f8ba9f70.png)](https://gyazo.com/58d2388bc26e3d6418898b12f8ba9f70)

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


### form

```haml: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: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;
    }
  }
}
```

```haml: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

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

```ruby: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です。

```js: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](https://i.gyazo.com/672d0283d74561b53bfca8fd72585373.png)](https://gyazo.com/672d0283d74561b53bfca8fd72585373)
index.html.hamlに記述します。

```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 #クレジットカードの下４桁を表示
                %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](https://i.gyazo.com/afadb920ca44aed2b394eb1200c774c1.png)](https://gyazo.com/afadb920ca44aed2b394eb1200c774c1)



### 購入処理を追加

```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アクションが動くようにすれば、完了です！！

以上です
お疲れ様です。


## 参考リンク
- [Payjpでクレジットカード登録と削除機能を実装する(Rails)](https://qiita.com/takachan_coding/items/f7e70794b9ca03b559dd)
- [Rails で Payjp を使って決済システムを導入する](https://qiita.com/hirotakasasaki/items/794c920016ac7c33da74)
- [PAY.JPの公式ガイド](https://pay.jp/docs/api/?ruby#charge-%E6%94%AF%E6%89%95%E3%81%84)
- [https://pay.jp/docs/payjs](https://pay.jp/docs/payjs)

- [https://qiita.com/kubbo0211/items/0030c00cea8461b1a498](https://qiita.com/kubbo0211/items/0030c00cea8461b1a498)

- [【Payjp】データベースと顧客情報を紐付ける方法](https://qiita.com/kubbo0211/items/ef142f5af97af90293a4)

- [【Rails5】簡単便利！PAY.JPでクレジットカードのオンライン決済機能を導入！](https://qiita.com/emincoring/items/ce29dbbd182aa3c49c6b)

- [PAY.JPのカスタムフォームを修正する【Rails】](https://qiita.com/tech_kazuki/items/3f127785f7002ebf7d97)
- [viの基本的な使い方](https://rat.cis.k.hosei.ac.jp/article/rat/serverliteracy/2004/vi.html)

### 新規登録時にクレジットカード登録
- [ユーザー登録と同時に生成](https://teratail.com/questions/112)
- [コントローラーでrender先を変える]
(https://qiita.com/taka_571/items/9b1c82d8fcc602df8a1a)

### 購入処理
- [buyページにProduct idを持っていく]
(https://qiita.com/hirokihello/items/fa82863ab10a3052d2ff)
- [購入処理　公式](https://pay.jp/docs/api/#%E6%94%AF%E6%89%95%E3%81%84%E3%82%92%E4%BD%9C%E6%88%90)
- [購入処理](https://qiita.com/Masataka_Sugi/items/57b68eb19de0c014ef12)
- [@product.idをどうやってもっていくか？](https://teratail.com/questions/103667)

### テストカード
- [テストカード](https://pay.jp/docs/testcard)