【Rails/PAYJP】ユーザーとクレジットカード情報を紐付ける方法

プログラミングスクールの最終課題でオリジナルアプリを作成するにあたり、
ユーザーとクレジットカード情報を紐付ける方法を学習したので、アウトプットしていきます。
Qiitaへの投稿に不慣れなため、稚拙な部分もありますがご容赦ください。

前提条件
・Rubyバージョン2.6.5
・Railsバージョン6.0.0
・`gem 'devise'`によるユーザー管理機能を作成済み。
・公開鍵と秘密鍵の環境変数を設定済み。

# ユーザーとカード情報を紐付ける流れ

①環境変数の確認
②PAY.JPを使えるように設定
③カード登録画面の実装
④jsファイルの実装
⑤cardsコントローラーの実装とバリデーションの設定
⑥登録しているカード情報の詳細ページを作成

# ①環境変数が設定されているか確認する

セキュリティ上、公開鍵・秘密鍵の情報をコードに直接記述できないため、環境変数に設定する必要があります。
ターミナルで環境変数が設定されているか確認します。

```
% env | grep PAYJP
PAYJP_PUBLIC_KEY="設定した公開鍵の値"
PAYJP_SECRET_KEY="設定した秘密鍵の値"
```

設定した値が表示されていればOKです。

# ②PAY.JPが使えるように設定する

**＜PAY.JPのgemをインストール＞**
まずはGemfileを編集します。

```
gem 'payjp'  (ファイルの一番下に追記する)
```

編集後、`bundle install`を実行します。

```
bundle install
```

**＜jsファイルを生成＞**
次にJavaScriptのコードを記述するファイルを生成します。
今回は「card.js」というファイル名にしました。

```
% touch app/javascript/card.js
```
**＜生成したcard.jsを読み込めるようにする＞**

`application.js`を以下のように編集します。

```app/javascript/packs/application.js
//省略

require("@rails/ujs").start()
// require("turbolinks").start()    //コメントアウトする
require("@rails/activestorage").start()
require("channels")
require("../card")    //追加

//省略
```
jsファイルはapplication.jsによって読み込まれます。
`app/JavaScript`フォルダ内の位置関係を見てみると、`application.js`から見て`card.js`は1つ上の階層に位置していることが分かります。
そのため、1つ上の階層を意味する「../」を記述しています。
<br>
**＜payjp.jsライブラリの読み込み＞**
カード情報をトークン化してくれるとても便利なライブラリです。

```app/views/layouts/application.html.erb
<%# 省略 %>

  <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
  <%= javascript_pack_tag 'application' %>
  <script src="https://js.pay.jp/" , type="text/javascript"></script> # この一行を追加
 </head>

<%# 省略 %>
```
これでPAY.JPを使える準備は整いました。

# ③カード登録画面の実装

**＜ルーティングを設定する＞**

```config/routes.rb
Rails.application.routes.draw do
  #省略
  resources :cards, only: [:new, :create, :show]
end
```

※ステップ⑥「登録しているカード情報の詳細ページを作成」が必要ない方は、`showアクション`の記述は不要です。

**＜cardモデルを生成する＞**

```
% rails g model card
```

**＜モデルにアソシエーションを記載する＞**

```app/models/card.rb
class Card < ApplicationRecord
  belongs_to :user
end
```

```app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_one :card, dependent: :destroy #追加
end
```

**「has_one」**とは、アソシエーションが１対１の時に使われます。
注意点としては、親モデル側に「has_one」を、子モデル側に「belongs_to」を記述することです。

**「dependent: :destroyオプション」**とは、親モデルが削除された時、それに紐付ている子モデルも一緒に削除されるというオプションです。
<br>
**＜マイグレーションファイルを編集する＞**

```db/migrate/**************_create_cards.rb
class CreateCards < ActiveRecord::Migration[6.0]
  def change
    create_table :cards do |t|
      t.string :customer_token, null: false  #追加
      t.references :user,       foreign_key: true  #追加
      t.timestamps
    end
  end
end
```

編集後は`rails db:migrate`を実行し、データベースに変更を反映します。
<br>
**＜cardsコントローラーを生成する＞**

```	
% rails g controller cards
```

**＜カード情報の入力フォームを記述する＞**

```app/views/cards/new.html.erb
<%= form_with url: cards_path, id: 'charge-form', class: 'card-form', local: true do |f| %>

  <div class="Cardpage">
    <h1>カード登録</h1><br>
  </div>
  <div class="form-wrap">
    <%= f.label :number, "【カード番号】" %>
    <%= f.text_field :number, class:"number", placeholder:"カード番号(半角英数字)", maxlength:"16" %>
  </div>
  <div class="form-wrap">
    <%= f.label :cvc, "【CVC】" %>
    <%= f.text_field :cvc, class:"cvc", placeholder:"カード背面にある3桁の番号", maxlength:"4" %>
  </div>
  <div class="form-wrap">
  <br>
    <p>【有効期限】</p>
    <div class="input-expiration-date-wrap">
      <%= f.text_field :exp_month, class: "exp-month", placeholder:"例）3" %>
      <p>月</p>
      <%= f.text_field :exp_year, class: "year-month", placeholder:"例）24" %>
      <p>年</p>
    </div>
  </div>
  <%= f.submit "登録する", class:"button", id:"button" %>

<% end %>
```

登録ボタンがクリックされると「createアクション」が動きます。
(`rails routes`でパスを確認できます)
また「id: 'charge-form'」は、後に実装するjsファイルで重要な役割を持ちます。

ここで一旦サーバーを再起動後、[http://localhost:3000/cards/new](http://localhost:3000/cards/new)にアクセスして画面を確認しておきましょう。(cvcのplaceholderが見切れています。各々CSSを調整してください)

# ④jsファイルの実装

**＜ jsファイルで環境変数を読み込めるようにする＞**
webpackerの設定ファイルを用いて、Railsで設定した環境変数を呼び出します。

```
% touch config/initializers/webpacker.rb
```

```config/initializers/webpacker.rb
Webpacker::Compiler.env["PAYJP_PUBLIC_KEY"] = ENV["PAYJP_PUBLIC_KEY"]
```

**＜card.jsのコードを完成させる＞**

```app/javascript/card.js
const pay = () => {
  Payjp.setPublicKey(process.env.PAYJP_PUBLIC_KEY);  //環境変数を読み込む
    const submit = document.getElementById("button"); 
    submit.addEventListener("click", (e) => {        // イベント発火
    e.preventDefault();
    
    // カード情報の取得先を設定
    const formResult = document.getElementById("charge-form");
    const formData = new FormData(formResult);

    // カードオブジェクトを生成(入力されたカード情報を「定数card」にまとめる)
    const card = {                         
      number: formData.get("number"),
      cvc: formData.get("cvc"),
      exp_month: formData.get("exp_month"),
      exp_year: `20${formData.get("exp_year")}`,
    };

    // カードオブジェクトをPAY.JPに送信
    Payjp.createToken(card, (status, response) => {
      if (status === 200) {
        const token = response.id;
        const renderDom = document.getElementById("charge-form");  //idを元に要素を取得
        const tokenObj = `<input value=${token} name='card_token' type="hidden">`;  //paramsの中にトークンを含める
        renderDom.insertAdjacentHTML("beforeend", tokenObj);  //フォームの一番最後に要素を追加
      }

      // カード情報がparamsに含まれないようにする
      document.getElementById("number").removeAttribute("name");
      document.getElementById("cvc").removeAttribute("name");
      document.getElementById("exp_month").removeAttribute("name");
      document.getElementById("exp_year").removeAttribute("name");

      document.getElementById("charge-form").submit();  // トークンをコントローラーに送信
    });
  });
};

window.addEventListener("load", pay);  // 定義した「定数pay」を起動
```

# ⑤cardsコントローラーの実装とバリデーションの設定
ここでは「Customerオブジェクト」というものを利用します。

**Customerオブジェクト**
PAY.JP側であらかじめ用意されている顧客を管理するためのオブジェクト。
「**Payjp::Customer.create**」と記述することで利用出来る。

このステップにおける流れを、先に整理しておきます。
1.顧客トークンを生成する
2.顧客トークンをもとにインスタンスを生成する
3.card_tokenに対するバリデーションを設定する
4.保存段階で条件分岐させる
5.テストカードを用いて挙動を確認する
<br>
**＜1.顧客トークンを生成する＞**

```app/controllers/cards_controller.rb
class CardsController < ApplicationController

  def new
  end

  def create
    Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む
    customer = Payjp::Customer.create(
      description: 'test',                  # テストカードであることを説明
      card: params[:card_token]             # 登録しようとしているカード情報
    )
  end
end
```
※`params[:card_token]`は、card.jsで追加したinput要素の**name属性**です。

**＜2.顧客トークンをもとにインスタンスを生成する＞**

```app/controllers/cards_controller.rb
#省略

  def create
    Payjp.api_key = ENV["PAYJP_SECRET_KEY"] 
    customer = Payjp::Customer.create(
      description: 'test', 
      card: params[:card_token] 
    )
    card = Card.new(   # 顧客トークンとログインしているユーザーを紐付けるインスタンスを生成
      customer_token: customer.id, # 顧客トークン
      user_id: current_user.id     # ログインしているユーザー
    )
  end  
end
```
具体的なカード情報（カード番号など）をそのままデータベースに保存することは法律上禁止されていますが、トークン化された顧客情報であれば保存できます。

この顧客トークンをユーザー情報に紐付けることで、繰り返し使用できるようになり、毎回カード情報を入力をしなくても購入が可能になります。

**＜3.card_tokenに対するバリデーションを設定＞**

```app/models/card.rb
class Card < ApplicationRecord
  belongs_to :user
  attr_accessor :card_token
  validates :card_token, presence: true
end
```

入力されたカード情報が正しくないときは、card_tokenそのものがコントローラー側に送られません。
そのため、card_tokenに対するバリデーションを設定し、保存段階で条件分岐をするようにします。

しかしcard_tokenを保存するカラムは、cardsテーブルに存在しません。
こういった「モデルに対応するテーブルのカラム名**以外の属性を扱いたい**」場合は、
`attr_accessor`を用いてキーを追加する必要があります。
※今回は`params[:card_token]`を値に取る、`card_tokenキー`を追加しました。

**＜4.保存段階で条件分岐させる＞**

```app/controllers/cards_controller.rb
#省略

    card = Card.new( 
      card_token: params[:card_token],  #カード情報
      customer_token: customer.id, 
      user_id: current_user.id 
    )
    if card.save
      redirect_to root_path
    else
      redirect_to action: "new" # カード登録画面へリダイレクト
    end
  end
end
```
cardインスタンス生成時に、attr_accessorで追加したキー`card_token`を加えます。
そしてインスタンスの保存状況に応じて、リダイレクト先を指定します。

**＜5.テストカードを用いて挙動を確認する＞**
サーバーを再起動後、[http://localhost:3000/cards/new](http://localhost:3000/cards/new)にアクセスし、テストカード(練習用の仮想カード)を用いてカード登録をしてみましょう。
【テストカード】
カード番号：4242424242424242（16桁）
CVC：123
有効期限：登録時より未来

カード登録後、以下のことが確認できれば、「ユーザーとクレジットカード情報の紐付け」は完了です。
・Sequel Proで、データベースに情報が保存されていること
・PAY.JPへログインし、"顧客"の項目にてテストカードの登録がされていること

# ⑥登録しているカード情報の詳細ページを作成
先程登録したカード情報をユーザーが確認できるように表示します。
必要ない方は、このステップは飛ばしてください。

まず、現在サーバー側にはトークンしかないため、このトークンをPAY.JP側に送るのと引き換えに顧客情報（顧客ID）をPAY.JP側から受け取る必要があります。
そして顧客情報を受け取った後、その中に含まれているカード情報を表示します。

ここからの流れを先に確認しておきましょう。
1.ユーザー詳細のビューに、カード情報を表示するリンクを記述する
2.cardsコントローラーで、ユーザーに紐付くカード情報を取得する
3.そのカード情報を元に顧客情報を取得する
4.カード情報が表示されるようにビューを編集する
<br>
**＜1.ユーザー詳細ページに、カード情報を表示するリンクを設定する＞**
※usersコントローラーのshowアクション内で、`@user = User.find(params[:id])`を定義していることが前提です。

```app/views/users/show.html.erb
  <% if user_signed_in? && current_user.id == @user.id %>
    <% if @user.card %>
      <%= link_to "登録しているクレジットカード情報を確認する", card_path(@user) %>
    <% else %>
      <%= link_to "クレジットカードを登録する", new_card_path %>
    <% end %>
  <% end %>
```
「ログイン済み」かつ「ログイン中ユーザーのマイページ」であればリンクを表示します。
そして`if @user.card`とし、「ユーザーに紐付くカード情報が存在するかどうか」で表示先を分岐させています。
・紐付くカードが存在する→`cards/show.html.erb`へ
・紐付くカードが存在しない→`cards/new.html.erb`へ

**＜2.cardsコントローラーで、ユーザーに紐付くカード情報を取得する＞**

```app/controllers/cards_controller.rb
class CardsController < ApplicationController

  def show
    @user = User.find(params[:id])

    Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む
    card = Card.find_by(user_id: @user.id) # ユーザーのid情報を元に、カード情報を取得
  end

#省略
```

**＜3.カード情報を元に顧客情報を取得する＞**

```app/controllers/cards_controller.rb
class CardsController < ApplicationController

  def show
    @user = User.find(params[:id])

    Payjp.api_key = ENV["PAYJP_SECRET_KEY"]
    card = Card.find_by(user_id: @user.id) 

    customer = Payjp::Customer.retrieve(card.customer_token) # 一行上のカード情報を元に、顧客情報を取得
    @card = customer.cards.first
  end

#省略
```

※一行上で定義したカード情報のcustomer_tokenカラムを元に、Payjp側から顧客情報を取得。

**＜4.カード情報が表示されるようにビューを編集する＞**

```app/views/cards/show.html.erb
  <div class="card-title">
    <h2>カード情報</h2>
  </div>
  <div class="card-info">
    【カード番号】
    <br>
    <%= "**** **** **** " + @card[:last4] %> (カード番号の下4桁を取得)
    <br>
    【有効期限】
    <br>
    <%= @card[:exp_month] %> (有効期限の「月」を取得)
    /
    <%= @card[:exp_year] %>  (有効期限の「年」を取得)
  </div>

  <%= link_to "マイページへ戻る", user_path(@user.id) %>
```
これで登録しているカード情報の詳細ページを作成できました。
<br>
以上で「ユーザーとクレジットカード情報の紐付け」及び「登録したカード情報の表示」は完了です！お疲れ様でした！
ご指摘等あれば、ご教授頂けますと幸いです。