【Rails】PAY.JPを用いた購入機能の実装について

購入機能の実装が一段落したので関連する機能およびエラーについてアウトプットしていきたいと思います。
今回はPAY.JPを用いた購入機能の実装について。
ネットでもリアルでもクレジットカードが使えない場面というのは今やほとんどないですよね。(ラーメン屋さんとかは券売機使ってるから現金のみのところも多い気がしますが)
スタンダードな決済機能、しっかりと実装していきましょう!


なぜオープンAPIを使うのか?

そもそもの前提の話をまず記録しておきます。
簡単な話、カード会社と直接連携しようとすると発生する以下2点の問題が解決できるからオープンAPIを使用します。

  • 会社ごとに事務的手続きをして連携しなければならない
  • 金銭の授受に関わるため、一定のセキュリティー基準をクリアしないといけない

尚、カード情報を直接アプリケーションに送ってサーバーにデータを保存・処理を行うことが2018年6月以降、改正割賦販売法によって禁じられています。
したがって、トークン(セキュリティを担保するために用いられる、一回のみ使用可能なパスワード)を使用して処理することで決済をおこないます。

f:id:programmingnuoh:20210327122140j:plain
PAY.JPを用いた決済機能の流れ


実装の手順

実装の流れとしては、クライアントサイド→クライアントサイド(トークン送付)→サーバーサイド(決済機能)です。

・クライアントサイド実装・
①turbolinksの確認
今回は手作業でJavaScriptを記述してフォーム送信処理等を実装していくので、turbolinksは使用しません。
そのため、以下2ファイルの記述を確認していきます。

  • app/views/layouts/application.html.erb

    <%# 省略 %>
        <%= stylesheet_link_tag 'application', media: 'all'  %>
        <%= javascript_pack_tag 'application' %>
    <%# 省略 %>
    

  • app/javascript/packs/application.js

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

    turbolinksのチートシートコチラ

    ②クライアントサイドでPAY.JPのAPIを使用するために必要なJavaScriptを読み込む
    ビューファイルに以下の一文を記述し、JavaScriptを読み込ませます。

    <head>
      <title>Furima</title>
      <%= csrf_meta_tags %>
      <%= csp_meta_tag %>
      <script type="text/javascript" src="https://js.pay.jp/v1/"></script> //追記
      <%= stylesheet_link_tag 'application', media: 'all' %>
      <%= javascript_pack_tag 'application' %>
    </head>
    

    トークン化を行うJavaScriptファイルを作成
    app/javascriptディレクトリに直接JavaScriptファイルを生成。
    また、application.jsにrequire("xxx")を記述し、JSファイルが読み込まれるようにします(xxxがファイル名、階層があるときはそれも記述しよう)。
    手作業で生成したファイルに、以下を記述。

    const pay = () => {
      Payjp.setPublicKey(process.env.PAYJP_PUBLIC_KEY); //②
      const form = document.getElementById("charge-form"); //①~
      form.addEventListener("submit", (e) => {
        e.preventDefault();               //~①
    
        const formResult = document.getElementById("charge-form"); //③~
        const formData = new FormData(formResult);
    
        const card = {
          number: formData.get("order[number]"),
          cvc: formData.get("order[cvc]"),
          exp_month: formData.get("order[exp_month]"),
          exp_year: `20${formData.get("order[exp_year]")}`,
        };                                                //~③
    
         Payjp.createToken(card, (status, response) => { //④~
          if (status == 200) {
            const token = response.id; //~④
         }
        )};
      });
    };
    
    window.addEventListener("load", pay);
    

  • ①charge-formの部分は、フォーム全体を取得できるような記述をしています。
    また、この機能では送信時にイベント発火させたいため、"submit"を指定しています。
    各記述ごと(load時、submit時など)でconsole.logの記述をおこない、それぞれが正しく機能しているか確認しましょう。

  • ②PAY.JPのテスト公開鍵を取得、設定。
    PAY.JPの自身のアカウントページから公開鍵を確認します。
    ⚠︎自身の鍵情報は絶対に外に漏らさないように!GitHub上にもPushしないように注意⚠︎

    f:id:programmingnuoh:20210327125506p:plain
    アカウントページ、APIから確認。
    先の生成したJSファイル内に以下を追記し、公開鍵情報を読み込ませます。
    (すでに環境変数に定義しているため、環境変数が記述されています。)

  • トークン化の処理を行う
    FormDataとは、フォームに記載された値を取得できるオブジェクト。
    今回はcharge-formでフォーム全体を指定し、そこで入力された値をformDataとして生成します。
    また、cardで各フォームの情報を取得します。
    get以降の()内は各フォームのname属性を指定するのですが、必ず検証ツールから取得しましょう。
    exp_year(有効期限の年の部分)は20xx年になるように記述することも忘れずに!

  • ④カードの情報をトークン化する
    createTokenの第1引数にcard(取得したカード情報)、第2引数にstatus,responseをアロー関数(functionの代わりに() =>を用いて関数を定義)を用いて定義しています。
    statusにはHTTPのステータスコードresponseにはそのレスポンスの内容が入ります。
    変数tokenresponse.idを入れることで、トークンの値を取得できます。


    トークン送付に関するクライアントサイドの実装・
    トークン情報をフォームに追加・送信

    const pay = () => {
      Payjp.setPublicKey(process.env.PAYJP_PUBLIC_KEY);
      const form = document.getElementById("charge-form");
      form.addEventListener("submit", (e) => {
        e.preventDefault();
    
        const formResult = document.getElementById("charge-form");
        const formData = new FormData(formResult);
    
        const card = {
          number: formData.get("order[number]"),
          cvc: formData.get("order[cvc]"),
          exp_month: formData.get("order[exp_month]"),
          exp_year: `20${formData.get("order[exp_year]")}`,
        };
    
        Payjp.createToken(card, (status, response) => {
          if (status == 200) {
            const token = response.id;
            const renderDom = document.getElementById("charge-form"); //①~
            const tokenObj = `<input value=${token} name='token' type="hidden">`;
            renderDom.insertAdjacentHTML("beforeend", tokenObj);
          }                                                    //~①
    
          document.getElementById("card-number").removeAttribute("name"); //②~
          document.getElementById("card-exp-month").removeAttribute("name");
          document.getElementById("card-exp-year").removeAttribute("name");
          document.getElementById("card-cvc").removeAttribute("name"); //~②
    
          document.getElementById("charge-form").submit(); //③
        });
      });
    };
    window.addEventListener("load", pay);
    

  • トークンの情報をフォームに追加する
    トークン情報を送信できるフォームをinput要素で追加していきます。
    ここでのname属性はparamsで取得できるように設定しましょう。
    一度debugger;で処理を確かめてから、input要素をtype="hidden"で隠します。

  • ②クレジットカードの情報を削除
  • ③フォームの情報をサーバーサイドへ送る
    e.preventDefault();Railsの送信処理をキャンセルしているため、JavaScriptから送信するような記述を行います。


    ・サーバーサイドにおける実装・
    ①ストロングパラメーターにトークンの情報を追加

    def order_params
        params.require(:order).permit(:post_code, :prefecture_id, :city, :address, :building, :phone_number).merge(user_id: current_user.id, item_id: params[:item_id], token:params[:token])
    end
    

    ②モデルにトークンの情報を追加

    class Order < ApplicationRecord
      attr_accessor :token
     
    ~~省略~~
    

    attr_accessorメソッドとは、記述したクラスにゲッターとセッターを定義してくれるもの。
    モデルに対応するテーブルのカラム名以外を扱いたい時に使用する。
    ③PAY.JPによる決済処理

  • GEM 'payjp'を導入する
  • コントローラーに決済処理を記述

    def pay_item
        Payjp.api_key = ENV["PAYJP_SECRET_KEY"] //①
        Payjp::Charge.create( 
          amount: @item.price,
          card: order_params[:token],
          currency: 'jpy'
        )                     //②
    end
    

  • 秘密鍵の情報をインスタンスに代入
  • ②決済処理の記述
    Gemが提供しているPayjp::Charge.createというクラス及びクラスメソッドを使用して値段、カード情報、通貨単位の情報を保存する。

    ④バリデーションの設定
    空の情報は送信できないよう設定。

    validates :token, presence:true
    

    まとめ

    JavaScriptの記述にまだ慣れていないので、とっても難しく感じています
    どのname属性、IDなどの値を取得するのかなど、一つ一つの処理の意味を考えていけば自ずとわかるはず…
    なのでまずは流れを確認しながら、繰り返し復習していきたいと思います!