Payjpを使用したクレジットカード登録機能

#はじめに
某スクールのチーム開発にてpay.jpを活用したクレジットカード登録を担当させて頂きましたのでアウトプットも兼ねてまとめたいです。
ちなみにコードについてはまだまだ素人の為拙い部分も目立つかも知れない為、参考にされる方はご了承ください。

##Payjpとは何か？
クレジットカードを登録、変更、購入を行ってくれる便利なAPIです。
フリマアプリ、ECサイトでクレジットカードを使って商品の購入する時クレジットカード情報が保存されるところになります。
ちなみにカード情報そのものをECサイト上に保存することは禁止されている為ご注意下さい。
http://payjp-announce.hatenablog.com/entry/2017/11/10/182738

現在はWEBサイトに通販も交えたサイトが多く、ネットでのECサービスほぼ全てに実装されている機能で、身分証明も兼ねているため、Webエンジニアになられる方は学んでおいた方が良いかもしれません。


##前提条件
今回のPayjp登録機能実装ですが、前提条件として
・hamlでの記載（gem 'haml-rails'）
・deviseが導入済みでログインができている

その為、何もない状態から機能を実装する場合
事前準備として以下のサイトが参考になるかと思います。
https://qiita.com/takachan_coding/items/c7584f7ac165d22c5526


##1.下準備を行いましょう

#####1-1 PAY.JPのアカウントを取得しましょう。
https://pay.jp/
上記URLでPayjpのアカウントを新規登録しましょう。

#####1-2 APIを確認しましょう。
![Payjp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519320/da5d8411-aaa9-c254-e1ab-132f2d108d6d.png)

上記の[APIのダッシュボード](https://pay.jp/d/settings)で確認してみましょう。
今回はテストモードでの実装なので、テスト秘密鍵とテスト公開鍵を使用します。

#####1-3 Payjpのgemをインストールします。

```Rails:Gemfile
gem 'payjp'
```
忘れずにbundle installをしましょう。

④ payjp.jsを読み込めるようにしましょう
application.html.hamlのtitleの直下に以下の記述を行いましょう。

```application.html.haml
%script{src: "https://js.pay.jp/", type: "text/javascript"}  
```

以上で下準備は完了です。

##2.テーブルを作成しましょう。
下記コマンドをターミナル上で入力する事でPayjpのデータを保管する為のテーブルを作成できます。

```
$ rails g model Card user_id:integer customer_id:string card_id:string
```

下記のようなテーブルができていますので確認して下さい。

```db/migrate/20190400000000_create_cards.rb
class CreateCards < ActiveRecord::Migration[5.0]
  def change
    create_table :cards do |t|
      t.integer :user_id
      t.string :customer_id
      t.string :card_id
      t.timestamps
    end
  end
end
```

テーブルの説明は下記の通りになります。
・user_id ... Userテーブルのid
・customer_id ... payjpの顧客id
・card_id ... payjpのデフォルトカードid

ちなみにカード情報そのものをECサイトのDB上に保存することは禁止されていますので、カード番号、有効期限、セキュリティコードのカラムは作らないようにしましょう。

[カード情報非通過化対応のお願い](http://payjp-announce.hatenablog.com/entry/2017/11/10/182738)

##3.ビューとそれに対するCSSを2つ作成しましょう

```cards.new.html.haml
    = form_tag(cards_path, method: :post, id: 'charge-form') do |f|
      .payment-text__box
        .payment-text__contents
          %label カード番号
          %span 必須
          = text_field_tag "card_number", "", class: "number", placeholder: "半角数字のみ" ,maxlength: "16", type: "text", id: "card_number"
          %ul.card
            %li=image_tag "image-visa.png",size: "49x20" 
            %li=image_tag "image-mastercard.png",size: "34x20" 
            %li=image_tag "image-SAISON.png",size: "30x20" 
            %li=image_tag "image-JCB.png",size: "32x20" 
            %li=image_tag "image-AMERICAN.png",size: "21x20" 
            %li=image_tag "image-Diners.png",size: "32x20"
            %li=image_tag "image-DISCOVER.png",size: "32x20"  
        .payment-expiration_date
          %label 有効期限
          %span.required 必須
          .expiration_date
            %select#exp_month{name: "exp_month", type: "text"}
              %option{value: ""} --
              %option{value: "1"}01
              %option{value: "2"}02
              %option{value: "3"}03
              %option{value: "4"}04
              %option{value: "5"}05
              %option{value: "6"}06
              %option{value: "7"}07
              %option{value: "8"}08
              %option{value: "9"}09
              %option{value: "10"}10
              %option{value: "11"}11
              %option{value: "12"}12
            .exp-text 月
            %select#exp_year{name: "exp_year", type: "text"}
              %option{value: ""} --
              %option{value: "2021"}21
              %option{value: "2022"}22
              %option{value: "2023"}23
              %option{value: "2024"}24
              %option{value: "2025"}25
              %option{value: "2026"}26
              %option{value: "2027"}27
              %option{value: "2028"}28
              %option{value: "2029"}29
            .exp-text 年
        .payment-text__contents
          %label セキュリティコード
          %span 必須
          = text_field_tag "cvc", "", class: "cvc", placeholder: "カード背面3~4桁の番号", maxlength: "4", id: "cvc"
        #card_token
        = submit_tag "登録する", id: "token_submit",class:"red_btn","data-turbolinks": false
.toppage_btn
          = link_to 'トップページに戻る',root_path,class:"toppage_btn"
```

```cards.show.html.haml
%section.main-contents
  .single-main
    %section.chapter__container
      %h2.chapter__head 支払い方法
      .payment__main
        .payment__list
          .payment__list__content
            %h3.sub__head クレジットカード
            .card__payment__list
              %form.card__payment__content
              .card__number
                カード番号
                %br
                = "**** **** **** " + @default_card_information.last4
              .expire__date
                有効期限
                %br
                - exp_month = @default_card_information.exp_month.to_s
                - exp_year = @default_card_information.exp_year.to_s.slice(2,3)
                = exp_month + " / " + exp_year
                = form_tag(card_path(current_user.id), method: :delete, id:'charge-form') do
                  %input{ type: "hidden", name: "card_id", value: "" }
                  %button.delete__button 削除する
              .toppage_btn
                = link_to 'トップページに戻る',root_path,class:"toppage_btn"

```



上記のコードですが
new.html.hamlがカード登録画面
show.html.hamlがカード確認画面になります。
CSSも一応載せますが上手く反映されない可能性もありますのでご了承下さい。

```new.scss
.header-payment {
  height: 128px;
  background: #f5f5f5;
  text-align: center;
  display: block;
  box-sizing: border-box;
  font-size: 12px;
  color: #888;
  font-weight: 600;
  h1 {
    margin: 40px 0 0;
    display: inline-block;
  }
  img {
    width: 185px;
    height: 49px;
    line-height: 49px;
  }
}
.payment-box {
  font-family: 'Source Sans Pro', Helvetica , Arial, '游ゴシック体', 'YuGothic', 'メイリオ', 'Meiryo', sans-serif;
  background: #f5f5f5;
}

.payment-main {
  width: 650px;
  margin: 0 auto;
  height: 620px;
  background: #fff;
}
.payment-container {
  font-size: 24px;
  padding: 8px 24px;
  text-align: center;
  h2 {
    font-weight: bold;
    line-height: 1.4;
  }
}
.payment-text {
  margin-top: 25px;
  border-top: 1px solid #f5f5f5;
  &__box {
    max-width: 400px;
    margin: 0 auto;
    #token_submit.red_btn{
      margin-top: 30px;
      background-color: red;
      color: white;
    }
    .toppage_btn{
      height: 50px;
      color:white;
      text-align: center;
      background-color: lightblue;
      margin-top: 30px;
      padding-top: 12px;
      width: 100%;
      text-decoration: none;
      font-size: 18px;
      border-radius: 2%;
    }

  }
  &__number {
    position: relative;
    margin: 8px 0 0;
  }
  .text {
    text-align: right;
    color: #0099e8;
  i {
    display: inline-block;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: #0099e8;
    color: #fff;
    line-height: 14px;
    font-size: 12px;
    text-align: center;
  span {
    text-align: right;
    color: #0099e8;
    }
  }
}
  &__contents {
    margin: 0;
    margin: 24px 0 0;
  p {
    margin: 8px 0 0;
    line-height: 1.5;
    color: #888;
  }
  label {
    font-weight: 600;
    display: inline-block;
  }
  span {
    background: #ea352d;
    margin: 10px 0 0 10px;
    padding: 2px 4px;
    border-radius: 2px;
    color: #fff;
    font-size: 12px;
    vertical-align: top;
  }
  textarea {
    width: 100%;
    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;
  img {
    width: 49px;
    height: 20px;
    }
  }
  .password {
    margin: 16px 0 0;
  }
  .card {
    margin: 8px 0 0;
    font-size: 0;
   }
  li {
    display: inline-block;
    margin: 0 0 0 8px;
    vertical-align: middle;
   }
  }
}
.payment-expiration_date {
  margin-top: 30px;
  label {
    font-weight: 600;
    display: inline-block;
    vertical-align: top;
  }
  span.required {
    background: #ea352d;
    margin-left: 10px;
    padding: 2px 4px;
    border-radius: 2px;
    color: #fff;
    font-size: 12px;
    vertical-align: top;
    p {
      margin: 8px 0 0;
      line-height: 1.5;
      color: #888;
      font-weight: 600;
    }
  }
  .expiration_date {
    display: flex;
    margin-top: 10px;
    #exp_month{
      width: 45%;
    }
    #exp_year{
      width: 45%;
    }
    .exp-text{
      margin-left: 5px;
      margin-right: 5px;
      padding-top: 10px;
      font-size: 20px;
    }
  }
  select {
    width: calc(100% - 38px);
    z-index: 2;
    height: 48px;
    padding: 0 16px;
    border-radius: 4px;
    border: 1px solid #ccc;
    background: 0;
    font-size: 16px;
    line-height: 1.5;
    cursor: pointer;
    }
    span {
      margin: 20px 0 0;
    }
}

```

```show.scss
.main-contents {
  float: right;
  width: 700px;
  margin: 0;
  background: #fff;
  .chapter__container {
    background: #fff;
    width: 700px;
    margin: 0 auto;

    .chapter__head {
      font-size: 24px;
      padding: 8px 24px;
      border-bottom: 1px solid #f5f5f5;
      text-align: center;
      font-weight: bold;
    }
    .payment__main{
      padding: 64px;
      border-top: 1px solid #f5f5f5;

      .payment__list {
        max-width: 320px;
        margin: 0 auto;

        .sub__head {
          font-size: 16px;
          font-weight: bold;
        }
      }
      .card__payment__list{
        max-width: 320px;
        margin: 0 auto;
        padding: 24px 0;
        border-bottom: 1px solid #eee;

         .card__number {
           margin: 8px 0 0;
           font-size: 16px;
         }
         .expire__date {
           margin: 8px 0 0;
           font-size: 16px;
         }
         .delete__button {
           position: relative;
           bottom: 0px;
           padding: 4px 6px;
           border-radius: 3px;
           border: 1px solid red;
           color: white;
           background-color: red;
         }
         .toppage_btn{
          height: 50px;
          color:white;
          text-align: center;
          background-color: lightblue;
          margin-top: 30px;
          padding-top: 12px;
          width: 100%;
          text-decoration: none;
          font-size: 18px;
          border-radius: 2%;
        }
      }
    }
  }
}
```

CSSが上手く適応されれば以下の画像ができると思います
![クレジットカード登録画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519320/f456c573-8b8b-8525-533a-f3c461c61da3.png)
![カード確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519320/6c0c8751-a685-75ec-f16a-1190b4ecbb14.png)


また、トップページの好きな場所にこんな感じのlink_toメソッドを追記しましょう

```index.html.haml
= link_to new_card_path do
  クレジットカード登録
```

また、これらに対してのルーティングにつきましては後ほど説明します。

##4.コントローラーを作成しましょう。
```
$ rails g controller cards
```
作成したコントローラーの内容を変更します。

```cards.controller.rb
class CardsController < ApplicationController
  require 'payjp'
  before_action :set_card, only:[:index, :show, :destroy]

  def index
    if @card.blank?
      #登録された情報がない場合にカード登録画面に移動
      flash[:alert] = 'カードを登録してください'
      render :new
    else
      Payjp.api_key = ENV["PAYJP_SECRET_KEY"]
      customer = Payjp::Customer.retrieve(@card.customer_id)
      @default_card_information = customer.cards.retrieve(@card.card_id)
    end
  end

  def new
    @card = Card.where(user_id: current_user.id)
    redirect_to card_path(current_user.id) if @card.exists?
  end

  def create
    Payjp.api_key = ENV["PAYJP_SECRET_KEY"]
    if params['payjp-token'].blank?
      render :new
    else
      customer = Payjp::Customer.create(
        card: params['payjp-token'],
        metadata: {user_id: current_user.id}
      )
      @card = Card.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
      if @card.save
        flash.now[:notice] = "カードの登録に成功しました"
        redirect_to root_path
      else
        flash[:alert] = 'カードの登録に失敗しました'
        render :new
      end
    end
  end

  def show
    if @card.blank?
      # 登録された情報がない場合にカード登録画面に移動
      redirect_to new_card_path
    else
      Payjp.api_key = ENV["PAYJP_SECRET_KEY"]
      customer = Payjp::Customer.retrieve(@card.customer_id)
      @default_card_information = customer.cards.retrieve(@card.card_id)
    end
  end


  def destroy #PayjpとCardデータベースを削除
    if @card.blank?
      render :new
    else
      Payjp.api_key = ENV["PAYJP_SECRET_KEY"]
      customer = Payjp::Customer.retrieve(@card.customer_id)
      customer.delete
      @card.delete
    end
      flash[:notice] = 'カードが削除されました'
      redirect_to root_path
  end


  private

  def card_params
    params.require(:card).permit(:user_id, :customer_id, :card_id).merge(user_id: current_user.id)
  end

  def set_card
    @card = Card.find_by(user_id: current_user.id)
  end
end

```

コントローラ内のENV["PAYJP_SECRET_KEY"]は環境変数でテスト秘密鍵を設定し読み込みます。


##5 Payjpにデータを送る為の事前準備を行いましょう

#####5-1. 1章の下準備を行いましょうの1-2で確認しましたテストキーをターミナルに入力しましょう

```
$ vim ~/.bash_profile
```
vimの細かい使い方はこちらで確認して下さい
https://qiita.com/hide/items/5bfe5b322872c61a6896

上記コマンドを入力すると入力できる画面に入りますので
iをクリックして入力
下記画像のように[Payjpテストモードの画面](https://pay.jp/d/settings)でテスト秘密鍵とテスト公開鍵を入力して下さい。
![secretキー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/519320/9bf3d5fd-51d3-d95f-6105-4c5791992f47.png)

入力が終わりましたら

```
$ source ~/.bash_profile
```
で保存しましょう。

5-2. secret.ymlに以下の通りテスト秘密鍵とテスト公開鍵を登録しましょう。

```config/secret.yml

development:
  payjp_secret_key: <%= ENV["PAYJP_SECRET_KEY"] %>
  payjp_public_key: <%= ENV["PAYJP_PUBLIC_KEY"] %>

production:
  payjp_secret_key: <%= ENV["PAYJP_SECRET_KEY"] %>
  payjp_public_key: <%= ENV["PAYJP_PUBLIC_KEY"] %>
```

##6 Payjpにデータを送りトークンを取得しよう
提供されている[pay.jp.isのサンプル](https://payjp.hatenablog.com/entry/2017/12/05/134933)を参照し、アレンジを加えています。

jQueryを使用していますので設定をしていない人は[こちら](https://qiita.com/ngron/items/95846bd630a723e00038)を参考に設定して下さい。

```payjp.js
$(document).on('turbolinks:load', function(){

  // payjp.jsの初期化
  Payjp.setPublicKey('pk_test_fe63a9d675fba31924799f7e');
  // ボタンのイベントハンドリング
  const btn = document.getElementById("token_submit");
  btn.addEventListener('click', (e) => {
    e.preventDefault();
    // カード情報生成
    const card = {
      number: $('#card_number').val(),
      exp_month: $('#exp_month').val(),
      exp_year: $('#exp_year').val(),
      cvc: $('#cvc').val(),
    };
     // トークン生成
     Payjp.createToken(card, (status, response) => {
      if (status === 200) { //成功した場合
        $("#card_number").removeAttr("name");
        $("#exp_month").removeAttr("name");
        $("#exp_year").removeAttr("name");
        $("#cvc").removeAttr("name");
        $("#charge-form").append(
          $('<input type="hidden" name="payjp-token">').val(response.id)
        ); //取得したトークンを送信できる状態にします
        $("#charge-form").submit();
        alert("登録が完了しました"); //確認用
      } else {
        alert("カード情報が正しくありません。"); //確認用
      }
    });
  });
})
```

##7.ルートを作成しよう
今回はshow,pay,new,deleteの4つのメゾットがあるので下記のように設定しましょう。

```config/routes.rb
resources :cards, only: [:create, :new, :show, :destroy] 
```

##8.カードを登録してみよう

http://localhost:3000/cards/new にアクセスして登録できるか確認してみましょう。
その時、カード番号は必ず[テストカード](https://pay.jp/docs/testcard)で登録するようにしてください。
それ以外を打ち込んだ場合はトークンが発行できずはねられてしまいます。
また、くれぐれも自身のカード番号、セキュリティコードは入力しないようにして下さい。

カード入力の例
カード番号:4242424242424242
有効期限: 12月 22年
CVC（セキュリティコード）:222
名前:HIDE



以上でカード登録から削除まで一通り実装できました。
初めてまともな記事を投稿する事になりますので拙い文章やコードだったと思いますが
今後ともよろしくお願いします。