2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails】登録済みクレジットカード情報で決済を行う方法について

Posted at

記事概要

Ruby on RailsにPAY.JPを実装し、登録済みクレジットカードで決済する方法について、まとめる

前提

  • Macを使用している
  • Ruby on Railsでアプリケーションを作成している
  • PAY.JPのアカウントを作成済みである
  • PAY.JPが提供する「テストモード」を使用する

Pay.JPについて

PAY.JPとアプリケーションのやり取り

Image from Gyazo

手順 内容
入力フォームの情報をjsファイルへ送信する
入力した内容をPAY.JPに送信する
→入力したカード情報(カード番号、有効期限など)が、jsファイルによってPAY.JPに送信される
PAY.JPからトークン(パスワード)が発行される
トークンをコントローラーへ送信する
→jsファイルは、発行されたトークンをコントローラーへ送信する役割もある
トークンをPAY.JPに送信する
トークンを元に本人確認が出来たので、PAY.JPから顧客IDが発行される
→トークンは一度しか利用できない(ワンタイムパスワードと言う)ため、顧客IDを発行することで何度でも同じカードで支払い処理が可能になる、という仕組み

用語

envコマンド

設定されている環境変数を実行するためのコマンド
オプションを指定せずに実行すると、設定しているすべての環境変数を表示することができる

grepコマンド

指定した条件で検索をするためのコマンド

サンプルアプリ(GitHub)

手順1(環境変数を確認する)

  1. ターミナルで、下記コマンドを実行する
    % env | grep PAYJP
    PAYJP_PUBLIC_KEY="設定した公開鍵の値"
    PAYJP_SECRET_KEY="設定した秘密鍵の値"
    
  2. PAY.JPのAPI設定を確認し、ターミナルの出力結果と一致しているかを確認する

手順2(PAY.JPが使えるように設定する)

  1. payjp」「gon」のGemをインストールする
  2. app/javascript/card.jsを生成するため、ターミナルで下記コマンドを実行する
    % touch app/javascript/card.js
    
  3. jsファイルを読み込めるようにする
    config/importmap.rb
    # 最終行に追記
    pin "card", to: "card.js"
    
    app/javascript/application.js
    // 最終行に追記
    import "card"
    
  4. app/views/layouts/application.html.erbにライブラリを追加し、「payjp.js」を読み込めるようにする
    <!DOCTYPE html>
    <html>
      <head>
        <title>PayjpApp</title>
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <%= csrf_meta_tags %>
        <%= csp_meta_tag %>
    
        <!-- payjp.jsのライブラリを追加 -->
        <script type="text/javascript" src="https://js.pay.jp/v2/pay.js"></script>
        
        <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
        <%= javascript_importmap_tags %>
      </head>
    
      <body>
        <%= yield %>
      </body>
    </html>
    

手順3(カード登録の画面を表示する)

Image from Gyazo

  1. ルーティングを設定する
    config/routes.rb
      # 省略
      resources :cards, only: [:new, :create]
    end
    
  2. cardモデルを生成するため、ターミナルで下記コマンドを実行する
    % rails g model card
    
  3. cardモデル(子モデル)を、以下のように編集する
    card.rb
    class Card < ApplicationRecord
      belongs_to :user
    end
    
  4. userモデル(親モデル)を、以下のように編集する
    user.rb
      # 省略
      
      has_one :card, dependent: :destroy # 追加
    end
    
  5. マイグレーションファイルを以下のように編集する
    db/migrate/**************_create_cards.rb
    class CreateCards < ActiveRecord::Migration[7.1]
      def change
        create_table :cards do |t|
          t.string :customer_token, null: false
          t.references :user,       foreign_key: true
          t.timestamps
        end
      end
    end
    
  6. コマンド実行し、データベースに反映する
    % rails db:migrate
    
  7. cardsテーブルが、以下のようになっていることを確認する
    Image from Gyazo
  8. cardsコントローラーを生成するため、コマンドを実行する
    % rails g controller cards new
    
  9. コントローラーからJavaScriptに環境変数を渡すため、以下のように編集する
    app/controllers/cards_controller.rb
    class CardsController < ApplicationController
      def new
        gon.public_key = ENV["PAYJP_PUBLIC_KEY"]
      end
    end
    
  10. app/views/cards/new.html.erbに、カード情報を入力するフォームを生成する
    <%= include_gon %>
    
    <h1>カード登録</h1>
    <%= form_with  url: cards_path, id: 'charge-form', class: 'card-form',local: true do |f| %>
      <div class='form-wrap'>
        <p>カード番号</p>
        <div id="number-form" class="input-default"></div>
      </div>
      <div class='form-wrap'>
        <p>期限</p>
        <div id="expiry-form" class="input-default"></div>
      </div>
      <div class='form-wrap'>
        <p>カード背面4桁もしくは3桁の番号</p>
        <div id="cvc-form" class="input-default"></div>
      </div>
      <%= f.submit "登録する", class:"button", id: "button" %>
    <% end %>
    
  11. PAY.JPの入力フォームをJavaScriptで挿入する
    app/javascript/card.js
    const pay = () => {
        const publicKey = gon.public_key;
        const payjp = Payjp(publicKey);
        const elements = payjp.elements();
        const numberElement = elements.create('cardNumber');
        const expiryElement = elements.create('cardExpiry');
        const cvcElement = elements.create('cardCvc');
    
        numberElement.mount('#number-form');
        expiryElement.mount('#expiry-form');
        cvcElement.mount('#cvc-form');
    };
    
    window.addEventListener("turbo:load", pay);
    
  12. localhost:3000/cards/newにアクセスし、入力フォームが表示されるか確認する

手順4(フォームに入力した情報をjsファイルに送信する)

Image from Gyazo

  1. 登録するボタンを取得し、クリックイベントを指定する
    card.js
    const pay = () => {
        const publicKey = gon.public_key;
        const payjp = Payjp(publicKey);
        const elements = payjp.elements();
        const numberElement = elements.create('cardNumber');
        const expiryElement = elements.create('cardExpiry');
        const cvcElement = elements.create('cardCvc');
    
        numberElement.mount('#number-form');
        expiryElement.mount('#expiry-form');
        cvcElement.mount('#cvc-form');
    
        // 登録するボタンを取得
        const form = document.getElementById("charge-form");
    
        // 登録するボタンをクリックすると、イベント発火
        form.addEventListener("submit", (e) => {
            console.log('clicked')
        });
    };
    
    window.addEventListener("turbo:load", pay);
    
  2. JavaScriptからサーバーサイドに値を送信させるため、フォームの送信処理を阻止(prevent)する
    card.js
        // 省略
        
        // 登録するボタンをクリックすると、イベント発火
        form.addEventListener("submit", (e) => {
            console.log('clicked')
            e.preventDefault(); // フォームの送信処理をキャンセル
        });
    };
    
    // 省略
    
  3. クレジットカードのトークンを生成する
    card.js
    // 省略
    
    // 登録するボタンをクリックすると、イベント発火
    form.addEventListener("submit", (e) => {
        // クレジットカードのトークンを生成
        payjp.createToken(numberElement).then(function (response) {
            if (response.error) {
            } else {
                const token = response.id;
                const tokenObj = `<input value=${token} name='token' type="hidden">`;
                form.insertAdjacentHTML("beforeend", tokenObj);
            }
        });
        e.preventDefault(); // フォームの送信処理をキャンセル
    });
    
    // 省略
    
  4. トークンをサーバーサイドに送信する際、入力されたクレジットカードの情報を削除する
    card.js
    // 省略
    
    // クレジットカードのトークンを生成
    payjp.createToken(numberElement).then(function (response) {
        if (response.error) {
        } else {
            const token = response.id;
            const tokenObj = `<input value=${token} name='token' type="hidden">`;
            form.insertAdjacentHTML("beforeend", tokenObj);
    
            // 入力されたクレジットカード情報を削除
            numberElement.clear();
            expiryElement.clear();
            cvcElement.clear();
            
            // 省略
    
    • DBにクレジットカードの情報を保存することは法律で禁止されているため、サーバーサイドに送信する必要がない
    • 保存しないとしても、セキュリティの観点からparamsにカードの情報を含めることは避けるべき
  5. トークンをサーバーサイドに送信する
    card.js
        // 省略
        
        // 入力されたクレジットカード情報を削除
        numberElement.clear();
        expiryElement.clear();
        cvcElement.clear();
    
        // フォーム送信
        document.getElementById("charge-form").submit();
    }
    
    // 省略
    

手順5(cardsコントローラーを実装する)

Image from Gyazo

  1. 顧客トークンを生成する
    app/controllers/cards_controller.rb
    class CardsController < ApplicationController
      def new
        gon.public_key = ENV["PAYJP_PUBLIC_KEY"]
      end
    
      def create
        Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む
    
        # 顧客トークンを生成
        customer = Payjp::Customer.create(
          description: 'test', # テストカードであることを説明
          card: params[:token] # 登録しようとしているカード情報
        )
      end
    end
    
  2. 顧客トークンをもとにインスタンスを生成する
    app/controllers/cards_controller.rb
    # 省略
    
    # 顧客トークンを生成
    customer = Payjp::Customer.create(
      description: 'test', # テストカードであることを説明
      card: params[:token] # 登録しようとしているカード情報
    )
    
    # 顧客トークンとログインしているユーザーを紐付けるインスタンスを生成
    card = Card.new(
      customer_token: customer.id, # 顧客トークン
      user_id: current_user.id # ログインしているユーザー
    )
    
    # 省略
    
    • 具体的なカード情報(カード番号など)をそのままDBに保存することは法律上禁止されているが、トークン化された顧客情報であれば保存可能
    • この顧客トークンをユーザー情報に紐付けることで、繰り返し使用できるようになる
      →毎回カード情報を入力をしなくても購入可能になる
  3. カード登録後、トップページにリダイレクトされるようにする
    app/controllers/cards_controller.rb
    # 省略
    
    # 顧客トークンとログインしているユーザーを紐付けるインスタンスを生成
    card = Card.new(
      customer_token: customer.id, # 顧客トークン
      user_id: current_user.id # ログインしているユーザー
    )
    
    card.save # カード情報を保存
    redirect_to root_path # トップページにリダイレクト
    
    # 省略
    

手順6(バリデーションを設定する)

Image from Gyazo

  1. tokenを保存するカラムがcardsテーブルに存在しないため、バリデーションを設定するためにはattr_accessorでキーを指定する必要がある
    app/models/card.rb
    class Card < ApplicationRecord
      belongs_to :user
      attr_accessor :token # tokenカラムがないので、attr_accessorを使用
      validates :token, presence: true
    end
    
  2. tokenに対して設定したバリデーションを利用し、保存段階で条件分岐する
    app/controllers/cards_controller.rb
    # 省略
    
        # 顧客トークンとログインしているユーザーを紐付けるインスタンスを生成
        card = Card.new(
          token: params[: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
    
  3. ブラウザで確認する
    1. サーバーを再起動する
    2. localhost:3000/cards/newにアクセスし、テストカードを使用してカード登録を行う
      Image from Gyazo
      項目
      カード番号 4242424242424242(16桁)
      CVC 123
      有効期限 登録時より未来
    3. DBにcustomer_tokenが保存されていることを確認する
      Image from Gyazo
    4. PAY.JPの顧客一覧にカードが登録されていることを確認する
      Image from Gyazo

手順7(マイページにカード情報を表示する)

Image from Gyazo

アプリ側にはトークンしかないので、このトークンをPAY.JP側に送るのと引き換えに顧客情報(顧客ID)をPAY.JP側から受け取る(⑤と⑥)
顧客情報を受け取った後、その中に含まれているカード情報をマイページに表示する

  1. ログインユーザーに紐付くカード情報を取得する
    app/controllers/users_controller.rb
    class UsersController < ApplicationController
      def show
        Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む
        card = Card.find_by(user_id: current_user.id) # ログインユーザーのid情報を元に、カード情報を取得
      end
      
      # 省略
    
  2. カード情報を元に顧客情報を取得する
    app/controllers/users_controller.rb
    class UsersController < ApplicationController
      def show
        Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む
        card = Card.find_by(user_id: current_user.id) # ログインユーザーのid情報を元に、カード情報を取得
    
        customer = Payjp::Customer.retrieve(card.customer_token) # カード情報を元に、顧客情報を取得
        @card = customer.cards.first # 顧客情報を元に、カード情報を取得。顧客が複数カード登録している場合、その内の「最初のカード(first)情報」を取得
      end
      
      # 省略
    
  3. マイページへ画面遷移する前に、カード登録するようにする
    app/controllers/users_controller.rb
    class UsersController < ApplicationController
      def show
        Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む
        card = Card.find_by(user_id: current_user.id) # ログインユーザーのid情報を元に、カード情報を取得
    
        # カード未登録の場合、カード登録ページへ画面遷移
        redirect_to new_card_path and return unless card.present?
    
        customer = Payjp::Customer.retrieve(card.customer_token) # カード情報を元に、顧客情報を取得
        @card = customer.cards.first # 顧客情報を元に、カード情報を取得。顧客が複数カード登録している場合、その内の「最初のカード(first)情報」を取得
      end
      
      # 省略
    
  4. マイページにカード情報が表示されるように、ビューファイルapp/views/users/show.html.erbを編集する
    <div class="AccountPage">
      <div class="AccountPage__title">
        <h1>マイページ</h1>
      </div><br>
    
      <!-- カード情報の追加 -->
      <div class="Card__title">
        <h2>カード情報</h2>
      </div>
      <div class="Card_info">
        【カード番号】
        <br>
        <%= "**** **** **** " + @card[:last4] %>
        <%# カード番号の下4桁を取得 %>
    
        <br>
        【有効期限】
        <br>
        <%= @card[:exp_month] %>
        <%# 有効期限の「月」を取得 %>
        /
        <%= @card[:exp_year] %>
        <%# 有効期限の「年」を取得 %>
      </div>
      <!-- カード情報の追加 -->
      
      <div class="Account__info">
        <h2>ユーザー情報</h2>
      </div>
    
      <!-- 省略 -->
    
  5. ブラウザにアクセスし、マイページにカード情報が表示されることを確認する
    Image from Gyazo

手順8(商品購入機能を実装する)

  1. ルーティングを設定する
    config/routes.rb
      # 省略
      
      # orderメソッド(購入機能のメソッド)に対して、id情報を伴うURIを生成する
      resources :items, only: :order do
        post 'order', on: :member
      end
    end
    
  2. 「購入するボタン」にリンクを設定するため、app/views/items/index.html.erbを編集する
        <!-- 省略 -->
        
        <%= link_to '購入する', order_item_path(item.id), data: { turbo_method: :post } %>
      <% end %>
    </div>
    
    購入する商品の情報はidで判断するので、「item.id」とする
  3. 購入した商品を識別するための専用テーブルを用意する
    % rails g model item_order
    
  4. item_orderモデル(子モデル)を編集する
    app/models/item_order.rb
    class ItemOrder < ApplicationRecord
      belongs_to :item
    end
    
  5. itemモデル(親モデル)を編集する
    app/models/item.rb
    class Item < ApplicationRecord
      has_one :item_order
    end
    
  6. マイグレーションファイルを更新する
    db/migrate/**************_create_item_orders.rb
    class CreateItemOrders < ActiveRecord::Migration[7.1]
      def change
        create_table :item_orders do |t|
          t.references :item, foreign_key: true
          t.timestamps
        end
      end
    end
    
  7. 下記コマンド実行し、データベースに反映する
    % rails db:migrate
    

手順9(購入処理をコントローラーに記述する)

  1. idを元に、購入する商品を特定する
    app/controllers/items_controller.rb
    class ItemsController < ApplicationController
      before_action :find_item, only: :order  # 「find_item」を動かすアクションを限定
    
      def index
        @items = Item.all  # 全商品の情報を取得
      end
    
      def order # 購入する時のアクションを定義
      end
    
      private
    
      def find_item
        @item = Item.find(params[:id]) # 購入する商品を特定
      end
    end
    
  2. ユーザーがカード登録しているかどうかで、条件分岐する
    app/controllers/items_controller.rb
    # 省略
    
    def order # 購入する時のアクションを定義
      # 購入ボタンを押した際に、「current_userに紐付くcardが存在(present)していなければ、カードの新規登録画面にリダイレクトして実行(return)する」
      redirect_to new_card_path and return unless current_user.card.present?
    end
    
    # 省略
    
  3. PAY.JPに支払い情報を送れるようにする
    app/controllers/items_controller.rb
    # 省略
    
    def order # 購入する時のアクションを定義
      # 購入ボタンを押した際に、「current_userに紐付くcardが存在(present)していなければ、カードの新規登録画面にリダイレクトして実行(return)する」
      redirect_to new_card_path and return unless current_user.card.present?
      
      Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 環境変数を読み込む
      customer_token = current_user.card.customer_token # ログインしているユーザーの顧客トークンを定義
      Payjp::Charge.create(
        amount: @item.price, # 商品の値段
        customer: customer_token, # 顧客のトークン
        currency: 'jpy' # 通貨の種類(日本円)
      )
    end
    
    # 省略
    
  4. 購入した商品がitem_orderテーブルへ保存されるようにする
    app/controllers/items_controller.rb
    # 省略
    
    def order # 購入する時のアクションを定義
      # 中略
      
      # 商品のid情報を「item_id」として保存する
      ItemOrder.create(item_id: params[:id]) 
    end
    
    # 省略
    
  5. 購入後のリダイレクト先を記述する
    app/controllers/items_controller.rb
      # 省略
      
      # 商品のid情報を「item_id」として保存する
      ItemOrder.create(item_id: params[:id]) 
    
      # ルートパスにリダイレクトする
      redirect_to root_path
    end
    
    # 省略
    
  6. ブラウザで確認する
    1. 商品を購入する
    2. 購入した商品がDBに保存されていることを確認する
      Image from Gyazo
    3. PAY.JPの売上一覧に登録されていることを確認する
      Image from Gyazo
  7. 購入済みの商品は「Sold Out!!」と表示されるように、app/views/items/index.html.erbを編集する
    <%= link_to 'マイページへ行く', user_path(current_user) %>
    <div class="content">
      <% @items.each do |item| %> 
        <%# eachメソッドを使うことで、全商品に対して以下の処理を実行します %>
        <p>商品名:<%= item.name %></p>
        <p>値段:<%= item.price %></p>
        
        <% if item.item_order != nil %> 
          <%# item_orderテーブルに保存されていたら、「Sold Out!!」と表示する %>
          <div class='sold-out'>
            <b>Sold Out!!</b>
          </div>
        <% else %>
          <%# item_orderテーブルに保存されていなければ、購入ボタンを表示する %>
          <%= link_to '購入する', order_item_path(item.id), data: { turbo_method: :post } %>
        <% end %>
      <% end %>
    </div>
    
  8. ブラウザにて、購入済み商品は「Sold Out!!」と表示されていることを確認する
    Image from Gyazo
2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?