RailsでStripeAPIを利用して送金システムを実装する

掲題のことをしたので実装までの過程を記録に残そうと思います。 Stripeを用いてRailsで送金、という趣旨の記事はあまり見つからなかったので、後々どなたかの役に立てれば幸いです。

尚、本記事は「テストデータでの送金」までしか扱っておらず、本番環境での送金はもう少々別途で実装する必要があります。ご了承ください。

ユーザ間での送金完了までにする必要のあること

送金完了までで、大まかに以下の点を行う必要があります。

  • カード情報ページの表示
  • 送信元ユーザのカード情報の登録
  • 送信先ユーザのカード情報を含めた個人情報の登録
  • 送金処理

それぞれの工程を、必要なコードと一緒に以下から詳細に説明していきます。

下準備

ここはそんなに本題ではないのでさらっと書きます、、、

多いですが、下準備としてしていただく必要のあることは以下の事項です。

  • ここからStripeのユーザ登録

  • カード情報登録用のコントローラとモデルを作成

    • 以下の例ではコントローラ名はcards_controller、モデル名はCard
    • コントローラのアクションで必要なのはnewとcreate
    • モデルの要素にはuser_idとcustomer_idを入れておく
  • 送金用のコントローラを作成
    • 以下の例ではコントローラ名はgifts_controller
    • コントローラのアクションで必要なのはnewとcreate
  • ビューの作成
    • cardsとgiftsともにnew.html.erbが必要
  • "stripe"と"gon"というgemのインストール
    • それぞれのgemの詳細は下の方で説明

カード情報入力ページを表示するまで

カード情報入力ページを表示するまでにどのようなコードを準備する必要があるのか説明します。

①application.html.erbで外部のStripe.jsを読み込む
<!-- application.html.erb -->

<head>
 <script src="https://js.stripe.com/v3/"></script>
</head>

application.html.erbにこの1文を入れることにより、外部にあるStripeサーバと通信できるようになります。 このサーバはStripe.jsを提供してくれるStripeサーバとなります(別種のStripeサーバについては後述します)。

ちなみにカード情報や送金に関するページだけでなく、Application.html.erbを使って全てのページで読み込んでいるのは、セキュリティ保護の面で公式からそう推奨されているからです。全ページでStripe.jsを読み込むことで、Stripe側が、ユーザがWebサイトを閲覧する際に不正行為の可能性がある不審な動作を検出してくれるとのこと。なるほど。

ただこれに関しては一旦「決済関連のページ以外からも決済に関する攻撃が可能」ということだと解釈をしましたが、それが例えばどのような形で行われるのかなどはまだイメージがついていないです、、、

②ローカルのStripe.jsを作成
// Stripe.js

// 画面の読み込み終了時に以下のコードを動かす
$(window).on("load", function(){

    // gonにより取得したテスト用公開鍵を変数に入れる
    const key = gon.stripe_public_key;

    // 公開鍵を用いてStripeオブジェクトを生成
    // 外部側のStripe.jsから取得できる全ての要素はこのオブジェクトの中に入っている
    const stripe = Stripe(key);

    // Stripeオブジェクトからelementsオブジェクトを生成
    // elementsオブジェクトは、カード情報を入力するフォームを生成するためのUIコンポーネント
    const elements = stripe.elements();

    // カード情報入力フォームのUIを整えるための変数
    const style = {
      base: {
        fontSize: '17px'
      }
    };

    // 上からカード番号、有効期限、cvcの入力スペースを作成している
    // mountでDOM上にそれぞれの入力スペース用要素をアタッチしている
    const cardNumber = elements.create('cardNumber', {style: style});
    cardNumber.mount('#card-number');
    const cardExpiry = elements.create('cardExpiry',{style: style});
    cardExpiry.mount('#card-expiry');
    const cardCvc = elements.create('cardCvc', {style: style, placeholder: ''});
    cardCvc.mount('#card-cvc');

});

ひとつひとつのコードの意味は大体コメントアウトをしているのでそこを参照ください。 ざっくり、このコードの意義はカード情報入力フォーム用のUIコンポーネントを用意することです。 ビュー側でidがcard-numberなどの要素を書いてあげることで、ページにStripeサーバから与えられた入力フォームが表示されます。 テスト用の公開鍵はgonというgemを利用して取得しています。gonの詳細は次で説明します。

③card_controller.rbのnewアクションの中身を書く
#card_controller.rb

class CardsController < ApplicationController

  def new
    # あらかじめ環境変数に入れておいたテスト用公開鍵を、gonの変数にセット
    gon.stripe_public_key = ENV['STRIPE_PUBLISHABLE_KEY']
    card = Card.where(user_id: current_user.id)
    # カードのデータが既にDBに存在していた場合は指定したページにリダイレクト
    redirect_to new_post_gift_url(id: params[:id]) if card.exists?
  end

  def create

  end

end

newアクションの中身で特筆すべき箇所は gon.stripe_public_key = ENV['STRIPE_PUBLISHABLE_KEY'] です。

まず左辺から説明すると・・・

このgonってなんなん?という方に向けて、gonとはRailsJavaScriptを連携させるためのgemです。 これを使うとコントローラ内でセットした変数をJavaScriptで使用することができます。

続いて右辺について・・・

ENV['STRIPE_PUBLISHABLE_KEY'] では環境変数を取り出しています。あらかじめdotenv-railsなどで STRIPE_PUBLISHABLE_KEY という環境変数にテスト用公開鍵をセットしておく必要があります。変数名はもちろんこの名前でなくても構いません。 StripeAPI用の公開鍵と秘密鍵は下準備で登録してもらったStripeサイトのダッシュボードにある「APIキーの取得」の項目で確認できます。

つまりこの行は「環境変数にセットしたテスト用公開鍵をgonの変数にセットしている」コードになります。 これによりStripe.jsでgonの変数を使用することができ、結果として公開鍵を直書きするのを避けることができます。

④cards/new.html.erbの中身を書く
<!-- cards/new.html.erb -->

<% provide(:button_text, '登録する') %>

<!-- ローカルのstripe.jsの読み込み -->
<%= javascript_include_tag 'stripe.js' %>

<div class="container">
  <div class="row">
    <div class="col-md-4 offset-md-4">
    <h2 class="card_header">カード情報の登録</h2>
      <div class="card">
        <!-- cards_controllerのcreateアクションにformを送信 -->
        <%= form_with url: post_cards_path(id: params[:id]), method: :post, id: "payment-form", local: true do |f| %>
          <div class="card-element card-number">カード番号</div>
          <!-- カード番号入力スペース用の要素が中にアタッチされるdiv -->
          <div class="stripe-input" id="card-number"></div>
          <div class="card-element">有効期限</div>
          <!-- 有効期限入力スペース用の要素が中にアタッチされるdiv -->
          <div class="stripe-input" id="card-expiry"></div>
          <div class="card-element">CVC</div>
          <!-- cvc入力スペース用の要素が中にアタッチされるdiv -->
          <div class="stripe-input" id="card-cvc"></div>
          <div class="form-row">
            <div id="card-errors" role="alert"></div>
          </div>
          <%= f.submit yield(:button_text), class: 'btn btn-primary', id: 'info_submit' %>
        <% end %>
      </div>
    </div>
  </div>
</div>

②で作成したローカルのStripe.jsをまず読み込みます。 それにより、既にローカルのStripe.jsにおいて「card-numberというIDのdivの中にカード番号入力スペース用の要素をアタッチします」といったコードを、カード番号、有効期限、cvcについてそれぞれ書いているので、その3つの入力フォームを表示することができます。

ここまでのコードが準備できたらカード情報入力ページを表示することができます。データの流れは以下です。

f:id:rinda_1994:20210308071301p:plain

カード情報入力ページのリンクを踏むと、外部のStripe.jsにより与えられた入力フォームが表示されるという流れです。

ちなみにカード情報入力画面自体は上記の諸々のコードを書くことで表示できるのですが、素の状態だとかなり味気ないのでフォームを以下のCSSで整えます。

.card {
  border: none;
  margin-top: 20px;

  .stripe-input {
    padding:8px;
    margin-bottom:5px;
    border: 1px solid #BBBBBB;
    border-radius:5px;
  }

  #card-number {
    width: 220px;
  }

  #card-expiry {
    width: 90px;
  }

  #card-cvc {
    width: 70px;
    margin-bottom: 20px;
  }
}

手元に表示されるであろうカード情報入力画面は以下です。

f:id:rinda_1994:20210308211509p:plain

送信元ユーザのStripeアカウント情報をDBに登録するまで

送信元ユーザのStripeアカウント情報をDBに登録するまでにどのようなコードを準備する必要があるのか説明します。

①ローカルのstripe.jsに追記
// stripe.js

$(window).on("load", function(){

    const key = gon.stripe_public_key;

    const stripe = Stripe(key);

    const elements = stripe.elements();

    const style = {
      base: {
        fontSize: '17px'
      }
    };

    const cardNumber = elements.create('cardNumber', {style: style});
    cardNumber.mount('#card-number');
    const cardExpiry = elements.create('cardExpiry',{style: style});
    cardExpiry.mount('#card-expiry');
    const cardCvc = elements.create('cardCvc', {style: style, placeholder: ''});
    cardCvc.mount('#card-cvc');

    // 項目入力時のエラー処理
    cardNumber.addEventListener('change', function(event) {
      const displayError = document.getElementById('card-errors');
      if (event.error) {
        displayError.textContent = event.error.message;
      } else {
        displayError.textContent = '';
      }
    });

    const form = document.getElementById('payment-form');
    form.addEventListener('submit', function(event) {
      event.preventDefault();

      // トークンを作成
      stripe.createToken(cardNumber).then(function(result) {
        if (result.error) {
          // カード情報を登録しようとした結果、内容が不適切であればエラーメッセージを表示
          var errorElement = document.getElementById('card-errors');
          errorElement.textContent = result.error.message;
        } else {
          // カード情報が適切な内容であれば、トークンをフォームに入れてWebサーバに送信
          alert('カード情報を登録しました');
          stripeTokenHandler(result.token);
        }
      });
    });

    function stripeTokenHandler(token) {
      // トークンをフォームに内包する
      const form = document.getElementById('payment-form');
      const hiddenInput = document.createElement('input');
      hiddenInput.setAttribute('type', 'hidden');
      hiddenInput.setAttribute('name', 'stripeToken');
      hiddenInput.setAttribute('value', token.id);
      form.appendChild(hiddenInput);

      //フォームをWebサーバに送信
      form.submit();
    }
});

追記箇所により、入力されたカード情報からトークンを生成し、自身のWebサーバに送信できるようになります。 このようにカード情報をトークンという形にすることで、自身(もしくは自社)のサーバにユーザの具体的なカード情報が渡らないようにします。

これはStripeをはじめ、Payjpなどどの決済サービスを使う際もマストとされている要件なようです。 クレジットカードの不正利用があまりに多いことから、サービスのサーバからカード情報が漏洩することを防ぐ意図で取られている措置みたいです。

②card_controller.rbのcreateアクションの中身を書く
#card_controller.rb

class CardsController < ApplicationController
  # stripeAPI用のメソッドなどを使用できるようにする
  require "stripe"

  def new
    gon.stripe_public_key = ENV['STRIPE_PUBLISHABLE_KEY']
    card = Card.where(user_id: current_user.id)
    redirect_to new_post_gift_url(id: params[:id]) if card.exists?
  end

  def create
    # あらかじめ環境変数に入れておいたテスト用秘密鍵をセット
    Stripe.api_key = ENV['STRIPE_SECRET_KEY']
    # トークンが生成されていなかった場合は何もせずリダイレクト
    if params['stripeToken'].blank?
      redirect_to posts_url
    else
      # 送金元ユーザのStripeアカウントを生成
      sender = Stripe::Customer.create({
        # nameとemailは必須ではないが分かりやすくするために載せている
        name: current_user.name,
        email: current_user.email,
        source: params['stripeToken'],
      })
      # Cardテーブルに送金元ユーザのこのアプリでのIDと、StripeアカウントでのIDを保存
      @card = Card.new(
        user_id: current_user.id,
        customer_id: sender.id,
      )
      if @card.save
        redirect_to new_post_gift_url(id: params[:id])
      else
        redirect_to posts_url
      end
    end
  end

end

追記箇所により、送金元ユーザのStripeアカウント作成および、Cardテーブルに送金元ユーザのアプリ内でのIDとStripeアカウントでのIDを保存しています。

①にてformにトークンを乗せているので、トークンはparamsで取得可能です。 そのトークンをもとにStripe::Customer.createで送金元ユーザのStripeアカウントを作成します。 そしてそのStripeアカウントのIDとアプリ内でのユーザIDを一緒にCardテーブルに保存しています。

送金元ユーザのStripeアカウントが正常に登録しているかどうかは、Stripeサイトのダッシュボードの「顧客」の項目を見ると確認できます。

StripeアカウントIDとアプリ内でのユーザIDをDB(Cardテーブル)に保存するまでのデータの流れはこんな感じです。

f:id:rinda_1994:20210308234618p:plain

送金が完了するまで

送金が完了するまでにどのようなコードを準備する必要があるのか説明します。

①gift_controller.rbの中身を書く
#gift_controller.rb

class GiftsController < ApplicationController
  require 'stripe'

  def new
  end

  def create
    Stripe.api_key = ENV['STRIPE_SECRET_KEY']
    amount = 100
    card_sender = Card.find_by(user_id: current_user.id)

    begin

      # 送金先ユーザのStripeアカウント作成
      reciever = Stripe::Account.create({
        type: 'custom',
        country: 'JP',
        business_type: 'individual',
        capabilities: {
          card_payments: {
            requested: true,
          },
          transfers: {
            requested: true,
          },
        },
      })

      # 証明写真の面裏をStripeサーバにアップロード
      front = Stripe::File.create({
        purpose: 'identity_document',
        file: File.new("app/assets/images/inu.png"),
      })

      back = Stripe::File.create({
        purpose: 'identity_document',
        file: File.new("app/assets/images/neko.png"),
      })

      # 送金先ユーザがStripe側から承認を受けるのに必要な情報を追加登録
      stripe_account = Stripe::Account.update(
        reciever.id,
        individual: {
          address_kanji: {
            :postal_code => "150-0013",
            :state => "東京",
            :city => "渋谷区",
            :town => "恵比寿",
            :line1 => "1-1-1",
            :line2 => "テストビルディング101号",
          },
          address_kana: {
            :postal_code => "150-0013",
            :state => "とうきょうと",
            :city => "しぶやく",
            :town => "えびす",
            :line1 => "1-1-1",
            :line2 => "てすとびるでぃんぐ101ごう",
          },
          :first_name_kanji => "太郎",
          :last_name_kanji => "田中",
          :first_name_kana => "たろう",
          :last_name_kana => "たなか",
          dob: {
            :day => "1",
            :month => "1",
            :year => "2000",
          },
          verification: {
            document: {
              :front => front.id,
              :back => back.id,
            }
          },
          :gender => "male",
          :phone => "+819012345678",
        },
        tos_acceptance: {
          :date => Time.now.to_i,
          :ip => "192.168.0.1",
        },
        external_account: {
          :object => 'bank_account',
          :country => 'jp',
          :currency => 'jpy',
          :routing_number => '1100000', #銀行コード+支店コード
          :account_number => '0001234',
          :account_holder_name => 'トクテスト(カ',
        }
      )

      # 送金処理
      @gift = Stripe::Charge.create({
        amount: amount,
        currency: "jpy",
        customer: @card_sender.customer_id,
        transfer_data: {
          destination: reciever.id,
        }
      })

    # stripe関連でエラーが起こった場合
    rescue Stripe::CardError => e
      flash[:error] = "#決済(stripe)でエラーが発生しました。{e.message}"
      render :new

    rescue Stripe::InvalidRequestError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(InvalidRequestError)#{e.message}"
      render :new

    rescue Stripe::AuthenticationError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(AuthenticationError)#{e.message}"
      render :new

    rescue Stripe::APIConnectionError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(APIConnectionError)#{e.message}"
      render :new

    rescue Stripe::StripeError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(StripeError)#{e.message}"
      render :new

    # stripe関連以外でエラーが起こった場合
    rescue => e
      flash.now[:error] = "エラーが発生しました#{e.message}"
      render :new
    end

    # 送金が完了したら指定のパスにリダイレクト
    redirect_to posts_path

  end

end

送金用のコントローラはかなりボリューミーです。

していることとしては、まず Stripe::Account.create で送金先ユーザのStripeアカウントを登録します。 次に送金先ユーザのStripeアカウントがStripe運営から承認を受けるにあたり、Stripe::Account.updateで必要な情報を漏れなく追加登録します。 そしてStripe::Charge.createで、送金元ユーザのStripeアカウントIDと送金先ユーザのStripeアカウントIDをもとに送金処理を行います。

送金が完了するまでのデータの流れはこんな感じです。

f:id:rinda_1994:20210309010527p:plain

送金が正常に完了しているか、および送金先ユーザのStripeアカウントが正常に作成できたかは、それぞれStripeサイトのダッシュボードの「支払い」と「Connectアカウント」の項目を見ると確認できます。

承認にあたって必要な項目は上記の通り、指名、住所、電話番号、口座番号などかなり多いです。これはテストデータの送金なので適当にコード側で全部記入しても項目さえ埋めていれば承認が通りますが、本番環境だと人による審査が当然入ると思うので、どこかでユーザに上記の個人情報すべてを入力してもらう動線を用意しておかなければなりません。

また送金先ユーザのStripeアカウントにはタイプがあり、今回利用しているのはCustomというタイプになります。タイプは他にStandardとExpressがあり、それらはOAuthという、複数のWebアプリを連携させる仕組みを用いてアカウントの認証を進める必要があります。興味がある方はこのページとかから調べてみてください。

それと筆者のアプリは100円固定で送金としていますが、ユーザ側で金額を決めてもらうには別途フォームを用意する必要がありますね。

まとめてないまとめ

長くなりました。不正確な情報があればご指摘いただけると幸いです。

参考