Rails フリマアプリ payjpを使った購入機能の実装

# なにこれ
某スクールのカリキュラムでフリマアプリを開発しました。
そこで購入機能を実装したので、payjpの導入〜商品購入までの流れを備忘録として書きます。
細かく解説はしないで、ざっと流れを説明する記事です。
コードに縦棒が混じってますが、ご了承下さい。

#payjpをインストール
`gem 'payjp'`をbundle installします

# ユーザーのカード情報を保存するためのcardsテーブルを作成

## なぜこのテーブルを作るのか
payjpはセキュリティの観点で、payjp側でカード番号などを管理する仕組みになっており、
開発者側の環境では暗号化されてます。
暗号化されたカード情報は保存する必要があるためです。

#
``` db/migrate/20200615032616_create_cards.rb
| class CreateCards < ActiveRecord::Migration[5.2] |
|:--|
|   def change |
|     create_table :cards do |t| |
|       t.references :user ,foreign_key: true, null: false |
|       t.string :customer_id, null: false |
|       t.string :card_id, null: false |
|       t.timestamps |
|     end |
|   end |
| end |
```

次にアソシエーションを組みます。
#
```app/models/card.rb 
| class Card < ApplicationRecord |
|:--|
|   belongs_to :user, optional: true |
| end |

```
ルーティングを設定します。
今回はカード一覧、カード新規登録、カード作成、カード削除をするので、4つ設定します。
商品が購入した時に呼び出されるアクションが欲しいので、posts（商品投稿）にpayアクションを追加しています。
pay/:idで「どの番号の商品か」と言うのを分からせてます。
例）post.id[1]で購入が押されたらパラメーターでpost.id[1]が送られる
それで該当商品を購入済み状態に変更できます！
#
```routes.rb
|   resources :cards, only: [:index, :new, :create, :destroy] |
|   resources :posts do |
|     collection do |
|       post 'pay/:id'=>   'posts#pay' |
|     end |
|   end |
```
次にcredentials.yml.encにpayjpのシークレットキーの情報を追加します。
credentials.ymlの説明は省略します。

credentialsを編集できるようにするためには、どのエディタで編集するのかをあらかじめ設定する必要があります。

まずは、ターミナルからVSCodeを起動できるよう設定を行います。

VSCodeで、「Command + Shift + P」を同時に押してコマンドパレットを開きます。

続いて、「shell」と入力しましょう。
メニューに、「PATH内に'code'コマンドをインストールします」という項目が表示されるので、それをクリックします。

この操作を行うことで、ターミナルから「code」と打つことでVSCodeを起動できるようになりました。
以下のコマンドでcredentials.ymlを開きます。
#
```
EDITOR='code --wait' rails credentials:edit
```
開いたら以下の内容を記述。
SK_test_XXXXXXの内容は、payjpに会員登録後に「API」という項目から確認できます。
#
```credentials.yml
payjp:
  PAYJP_SECRET_KEY: sk_test_XXXXXXXXXXXXXXXXX
```
追加したらタブの✗を押して保存します。
control + K だと保存されないです。

cardsコントローラーを作成、編集します。
#
```app/controllers/cards_controller.rb
class CardsController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :set_payjp_key, except: :new

  def index
    if current_user.card.present?
      @cards = Card.where(user_id: current_user.id)
    end
  end

  def new
  end

  def create
    if params[:payjp_token]
      customer = Payjp::Customer.create(card: params[:payjp_token])
      @card = Card.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
      @card.save
      redirect_to cards_path
    end
  end

  def destroy
    card = Card.find(params[:id])
    customer = Payjp::Customer.retrieve(card.customer_id)
    customer.delete
    card.delete
    redirect_to cards_path
  end
  
  private

  def set_payjp_key
    Payjp.api_key = Rails.application.credentials.payjp[:PAYJP_SECRET_KEY]
  end

end

```
createアクションの`if params[:payjp_token]`は後述しますが、
パラメーターで[:payjp_token]というデータを送っております。
`customer = Payjp::Customer.create(card: params[:payjp_token])`
でpayjpのサーバーと通信してるみたいです。
通信した結果を変数customerに代入してる。といった流れです。
`@card = Card.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)`
これでカードの新規登録を行います。内容は暗号化されたものです

privateの中のset_payjp_keyメソッドとは？
`Payjp.api_key = Rails.application.credentials.payjp[:PAYJP_SECRET_KEY]`
この記述で、Payjp.api_keyという変数？に、先程追加したcredentials.ymlの中身を呼び出してます。
この記述がないと、createアクションでpayjpのサーバーを呼び出すことができず、エラーが起きます。

beforeアクションで`skip_before_action :verify_authenticity_token`とあります。
これは超重要で、端的に言うと「これがないとpayjpにハッシュ（データ）を送ることができません。」

サーバーサイドの準備は終わったので、次にビューを触っていきます。
application.html.hamlにscriptを追加してpayjpを読み込ませます。
#
```app/views/layouts/application.html.haml
   %body 
     %script{src: "https://js.pay.jp/", type: "text/javascript"} 
     = render 'layouts/notifications'
     = yield
```

次にカードの新規登録ページを作成します。
クラスは個人で変更して下さい。
%formを使っている理由は、payjpのjsを呼び出すためです。
form_withを使うやり方が分かりませんでした。
#
```app/views/cards/new.html.haml 
.show-main__registration-field
  %form.card-form{method: :post, action: "/cards", id: "chargeForm"}
    .show-main__registration-field--text
      クレジットカード情報登録
    .show-main__registration-field__container
      .show-main__registration-field__container__number
        %label.show-main__registration-field__container__number--text
          カード番号
        %span.form-require
          必須
        %input{type: "text", placeholder: "半角数字のみ", class: "show-main__registration-field__container__number--input", id: "card-num-input"}
      .show-main__registration-field__container__expiration
        %label.show-main__registration-field__container__expiration--text
          有効期限
        %span.form-require
          必須
        .show-main__registration-field__container__expiration--input
          %select#month-select
            %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
          %select#year-select
            %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
      .show-main__registration-field__container__security
        %label.show-main__registration-field__container__security--text
          セキュリティコード
        %span.form-require
          必須
        %input{class: "show-main__registration-field__container__security--input", id: "security-code-input",  name: "security-code", type: "text", placeholder: "カード背面4桁もしくは3桁の番号"}
        .show-main__registration-field__container__security--information
          %span.show-main__registration-field__container__security--information-link
            =link_to root_path do
              %i.fas.fa-question-circle
              カードの裏面の番号とは？
      %input#add-card-btn{type: 'submit', value: "クレジットカードの登録", class: "show-main__registration-field__container--submit"}

```

先程formで送信したデータをcontrollerに送れる形式である`payjp_token`に変更する処理をjsでします。
このあたりは正直公式のコピペなので、解説できないです。
「こういうもの」としか受け取れないです。
パブリックキーはがっつり本文に書いちゃってokです。
正しいテストカードの情報じゃないと`if (status === 200) {`によってエラーが出るようになってます。

ちなみに、カードのテスト番号は、以下です
カードナンバー【4242424242424242】
有効期限【12/21】
名前【YUI ARAGAKI】
#
```card-form.js
$(function () {

  // パブリックキーを書いてpayjpと通信できる状態にする
  Payjp.setPublicKey('pk_test_62873b159b61e41f8452494b');
  // 投稿ボタンを定義
  const card_btn = $('#add-card-btn');

  if (card_btn != null) {
    // 投稿ボタンが押されたら発火
    card_btn.click(function (e) {
      e.preventDefault();
      $(function () {
        // カードの情報を定数cardに代入。
        const card = {
          number: $('#card-num-input').val(),
          exp_month: $('#month-select').val(),
          exp_year: $('#year-select').val(),
          cvc: $('#security-code-input').val(),
        }
        form = $("#chargeForm")
        // この記述でtokenを呼び出してます。
        Payjp.createToken(card, function (status, response) {
          if (status === 200) {  //成功した場合。statusが200だと通信成功らしいです。
            // inputをappendしています valueは生成したデータで。
            form.append($('<input name="payjp_token" type="hidden">').val(response.id));
            // これでcreateアクションが呼ばれます。
            form.submit();
            // これが出たらカード情報が登録されます
            alert("カード情報を登録しました");
          } else {
            // カード情報が正しくないor入力漏れがあるとこちらが表示されます
            alert("正しいカード情報を入力してください");
          }
        })
      });
    });
  }

});
```

次にカード一覧を作成します。
これも公式を移したので解説する部分は少ないです。
payjpは自分でコードを書いたりしないで、公式に頼るのが正解だと思いました。
#
```app/views/cards/index.html.haml
  = render @cards
```

#
```app/views/cards/_card.html.haml
      = link_to "削除する", card_path(card), method: :delete, class: 'btn', data: { confirm: '削除してよろしいですか？' }
        
      - customer = Payjp::Customer.retrieve(card.customer_id)
      - @default_card_information = customer.cards.retrieve(card.card_id)
      = "**** **** **** " + @default_card_information.last4
      - 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
```

次は商品詳細ページでpayjpを使った購入リンクを作成します。
postsコントローラーのpayアクションを呼び出す記述をしています。
scriptは公式。
#
```app/views/posts/show.html.haml
   = form_tag(action: :pay, method: :post) do
      %script.payjp-button{:src => "https://checkout.pay.jp", :type => "text/javascript" ,"data-text" => "購入する" ,"data-key" => "pk_test_62873b159b61e41f8452494b"}
```
コントローラーを記述をします！
cahrgeまで全て公式に書いてあります。
@post.updateでboolean型に指定したpurchasedというカラムをtrueにしてます。
つまり、購入済みの状態にしています。
#
```posts_controller.rb
  def pay
    @post = Post.find(params[:id])
    Payjp.api_key = Rails.application.credentials.payjp[:PAYJP_SECRET_KEY]
    charge = Payjp::Charge.create(
    amount: @post.price,
    card: params['payjp-token'],
    currency: 'jpy'
    )
    @post.update(purchased: true)
    redirect_to root_path, notice: '購入しました！'
  end
```

以上でpayjpのカード登録〜商品購入までの流れは終了です！

レビュー機能をつけるなら、カラムの設定とかテーブル追加したり色々やると思いますが、
自分たちのチームは必須実装を終わらせると言う目的だったので、そこらへんは触ってないです！
追加実装までやるチームはすごいと思いました。