15
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Stripeで定額制マーケットプレイスを実装する(rails)

Last updated at Posted at 2019-03-23

名称未設定.001.jpeg

オンラインサロンや、月額駐車場のような定額制のマーケットプレイスを実装します。
前回、「RailsでStripe Connectを使ってみた」を投稿したのですが、この定額制の場合、そこにあるようなConnectだけでは一筋縄にいかないということが分かりました。定額制を採用するためには、「Stripe Billing」を別途で実装する必要があるため、StripeのGUIをそのまま使えるStandardアカウントでは実装できず、Customアカウントを使う必要が出てきました。

これが意外と厄介で、決済の責任がプラットフォーム側に依存されるようになるため、利用規約やセキュリティ面など気をつけなければならないことがたくさん出てきます。

決済〜入金までの流れ

  1. 決済フォームを作成する(Stripe Element)
  2. 本人確認フォームを作成する
  3. 銀行口座フォームを作成する
  4. 定額商品を作成する
  5. 顧客が定額商品を契約する
  6. プラットフォームからアカウントに送金される
  7. アカウントの口座に入金される

なかなか壮大なワークフローとなりました。
特に、7) プラットフォームからアカウントに送金される部分は想定しておらず、もっと簡単に実装できるかと思っていましたが、Webhookを使う必要があると分かりました。
個人的に、Webhookを使うのは初めてだったので、これはスタックしそうと構えていましたがngrockという便利ツールのおかげでスムーズに進みました。

決済フォームを作成する(Stripe Element)

決済フォームを作成する際にはStripe Elementを使うのがおすすめです。
基本的にクレジットカード情報はサーバー側で持たないため、Stripeのカスタマートークンを受け取るカラムのみをユーザーに追加してあげとけば問題ありません。

payments/new.html.erb
<script src="https://js.stripe.com/v3/"></script>

<%= form_for(@user, html: {class:'add-card', id: 'payment-form'}) do |f| %>
  <div class="block">
    <%= f.hidden_field :stripe_temporary_token, id: 'stripe_temporary_token' %>
    <input type="hidden" name="token" />          
    <div class="group">
      <label>
        <span>クレジットカード番号</span>
        <div id="card-number-element" class="field"></div>
      </label>
      <label>
        <span>有効期限</span>
        <div id="card-expiry-element" class="field"></div>
      </label>
      <label>
        <span>CVC</span>
        <div id="card-cvc-element" class="field"></div>
      </label>
    </div>
    <button type="submit" class="btn btnSubmit" id="submit-card">Submit</button>
    <div class="outcome">
      <div class="error"></div>
      <div class="success"></div>
    </div>
  </div>
<% end %>

JS部分は下記のようになります。

elements.js
var stripe = Stripe(gon.stripe_key);
var elements = stripe.elements();

var style = {
  base: {
    iconColor: "#666EE8",
    color: "#31325F",
    lineHeight: "40px",
    fontWeight: 300,
    fontFamily: "Helvetica Neue",
    fontSize: "15px",

    "::placeholder": {
      color: "#CFD7E0"
    }
  }
};

var cardNumberElement = elements.create("cardNumber", {
  style: style
});
if (document.getElementById("card-number-element")) {
  cardNumberElement.mount("#card-number-element");
}

var cardExpiryElement = elements.create("cardExpiry", {
  style: style
});
if (document.getElementById("card-expiry-element")) {
  cardExpiryElement.mount("#card-expiry-element");
}

var cardCvcElement = elements.create("cardCvc", {
  style: style
});
if (document.getElementById("card-cvc-element")) {
  cardCvcElement.mount("#card-cvc-element");
}

function setOutcome(result) {
  console.log("result", result);

  var successElement = document.querySelector(".success");
  var errorElement = document.querySelector(".error");
  successElement.classList.remove("visible");
  errorElement.classList.remove("visible");

  if (result.token) {
    // In this example, we're simply displaying the token
    successElement.querySelector(".token").textContent = result.token.id;
    successElement.classList.add("visible");

    // In a real integration, you'd submit the form with the token to your backend server
    //var form = document.querySelector('form');
    //form.querySelector('input[name="token"]').setAttribute('value', result.token.id);
    //form.submit();
  } else if (result.error) {
    errorElement.textContent = result.error.message;
    errorElement.classList.add("visible");
  }
}

cardNumberElement.on("change", function(event) {
  setOutcome(event);
});

cardExpiryElement.on("change", function(event) {
  setOutcome(event);
});

cardCvcElement.on("change", function(event) {
  setOutcome(event);
});

// Handle credit card form submission:
var paymentForm = document.getElementById("payment-form");
$("#submit-card").on("click", function(event) {
  event.preventDefault();
  stripe.createToken(cardNumberElement).then(function(result) {
    console.log("result", result);

    if (result.error) {
      // Inform the user if there was an error
      var errorElement = document.getElementById("card-errors");
      errorElement.textContent = result.error.message;
    } else {
      // Send the token to your server
      stripeTokenHandler(result.token);
    }
  });
});

これでUser情報が書き換わる時に、Stripeカスタマートークンを作成します。

users_controller.rb
・・・
if @user.stripe_temporary_token.present?
  customer = Stripe::Customer.create(
    email: @user.email,
    source: @user.stripe_temporary_token
  )
  @user.update_attribute(:stripe_customer_id, customer.id)
  flash[:success] = "クレジットカード情報が登録されました"
end
・・・

本人確認フォームを作成する

売り手側が入金を受け取るためには本人確認書類をStripeに提出する必要があります。個人で受け取る場合と、法人で受け取る場合にて入力する内容が異なります。ドキュメントを読んで確認していきましょう。

国内で本人確認するために必要な情報は下記を参考にしていきます。
https://stripe.com/docs/connect/required-verification-information

本人確認の画像を送る際には、JSを使って直接Stripeに送信しトークンを取得する必要があります。

accounts/new.html.erb
・・・
<div class="mainItem">
  <div class="label">
    <p>本人確認書類</p>
  </div>
  <div class="mainForm">
    <div class="inputFile">
      <div
        class="preview"
        style="background-image: url('../assets/img/verification.png')"
      >
        <input accept="image/*" id="id-file" name="id-file" type="file" />
      </div>
      <p class="btn_upload">
        アップロードする
      </p>
    </div>
  </div>
</div>
・・・
verification.js
// Handle verification from submission:
var verifyForm = document.getElementById("verify-form");
$("#submit-account").on("click", function(event) {
  event.preventDefault();
  this.innerHTML =
    "<i class='fa fa-spinner fa-spin'></i> アカウント情報を登録しています...";
  this.className += " disabled";

  function() {
    // Handle a file upload
    var data = new FormData();
    data.append("file", document.querySelector("#id-file").files[0]);
    data.append("purpose", "identity_document");

    $.ajax({
      url: "https://uploads.stripe.com/v1/files",
      data: data,
      headers: {
        Authorization: `Bearer ${gon.stripe_key}`
      },
      cache: false,
      contentType: false,
      processData: false,
      type: "POST"
    }).done(function(response) {
      var fileData = response.id;
      $("input[id=stripe_file]").val(fileData);
      verifyForm.submit();
    });
  });
});

※ コントローラー部分は決済フォームで実装したものとほぼ同じですので省略させていただきます。

銀行口座フォームを作成する

続いて、売り手側が入金を受け取る銀行口座を登録していくフォームを作成していきます。
こちらは本人確認書類で作成したStripe側のアカウントを呼び起こして、そこに銀行口座の情報を追加してあげるように実装を進めていきます。

こちらも本人確認書類と同じで、下記URLを参考にして必要な情報を取得します。
https://stripe.com/docs/payouts

本人確認書類や決済フォームと同じようにJSによってStripeと交信し、取得したトークンを既存のユーザーアカウントに追加します。

user_controllers.rb
・・・
if @user.stripe_bank.present?
  stripe_account = Stripe::Account.retrieve(current_user.stripe_account_id)
  stripe_account.external_account = @user.stripe_bank
  stripe_account.save
end
・・・

定額商品を作成する

このあたりは少し我流もあったのですが、定額商品には「商品」と「プラン」の二つの概念があります。例えば、一つの会社がAとB製品を持っているのであれば、それぞれにプランを持つことになりますよね。今回に関していえば、手入力でStripe側の商品をあらかじめ作っておいて、そこに紐づくプランをユーザーがアプリケーション側でプランを作成するたびに増えていくようにしました。
(これが、ユーザーが商品に対して幾つものプランを用意することを想定する場合は商品ごとユーザーが作る必要があるかもしれませんが、今回はユーザーは一つのプランに対して一つの価格設定を前提とします。)
スクリーンショット 2019-03-21 16.11.39.png

作成した商品のIDはクレデンシャルに書き込んで保存しておきます。
あと、プラン側はアプリケーション側が作成された時に一緒に作成するようにコントローラー側で仕込むだけです。

planc_controller,rb
  def create
    @community = current_user.plans.create(create_params)
    @community.community_id = "plan_#{@plan.id}"
    if @community.save
      Stripe::Plan.create(
        product:      Rails.application.credentials[Rails.env.to_sym][:stripe][:product_id],
        id:           @plan.community_id,
        currency:     'jpy',
        interval:     'month',
        nickname:     @plan.name,
        amount:       @plan.price,
      )
      flash[:success] = "We'll let you know when your community is approved."
      redirect_to current_user
    else
      flash[:alert] = "Faild to add a community."
      redirect_to current_user
    end
  end

顧客が定額商品を契約する

クレジットカード情報を持ったユーザーが定額商品を契約します。
これによって、ユーザーとプラットフォーム側が定期支払いによって契約が交わされることになります。
顧客情報と、プラン情報だけでなく、メタ情報としてプランを作成したアカウントのIDと、プラットフォーム側の手数料比率をここでいれておくのがポイントです。

subscriptions_controller.rb
・・・
def create
  @subscription = current_user.subscriptions.create(create_params)
  if @subscription.save
    @plan_id = "plan_#{@subscription.community.id}"
    stripe_subscription = Stripe::Subscription.create(
      customer: @subscription.user.stripe_customer_id,
      plan: @community_id,
      metadata: {
        destination: @subscription.user.stripe_account_id,
        commission_fee: "20"
      }
    )
    @subscription.stripe_subscription_id = stripe_subscription.id
    @subscription.save

    redirect_to current_user
  else
    redirect_to current_user
  end
end
・・・

プラットフォームからアカウントに送金される

顧客から受け取った金額をそのまま、アカウントに送金する必要があります。
そのためには、StripeのWebhookを使って、毎月なり支払いがある度に都度、アカウント側へ送金を行なっていきます。ngrockというサービスがWebhookのローカルでのテストでは非常に便利です。
Stripeにおけるngrockの設定は下記を参照にしてください。
https://qiita.com/kakipo/items/5d7325902965e74b3091

ngrokを設定したあとは、Stripeダッシュボードのテスト画面から配信ができるようになります。
ngrok.jpg

支払いが行われる度に、プラットフォームからアカウントへ自動送金処理を実装していきます。

Stripe.rb
Rails.configuration.stripe = {
  :publishable_key          => Rails.application.credentials[Rails.env.to_sym][:stripe][:publishable_key],
  :secret_key               => Rails.application.credentials[Rails.env.to_sym][:stripe][:secret_key],
  :account_signing_secret   => Rails.application.credentials[Rails.env.to_sym][:stripe][:account_signing_secret],
  :connect_signing_secret   => Rails.application.credentials[Rails.env.to_sym][:stripe][:connect_signing_secret]
}

Stripe.api_key = Rails.configuration.stripe[:secret_key]
StripeEvent.signing_secrets = [
  Rails.configuration.stripe[:account_signing_secret],
  Rails.configuration.stripe[:connect_signing_secret],
]

StripeEvent.configure do |events|
  # 請求に成功した場合の処理
  events.subscribe(
    'invoice.payment_succeed',
    Events::InvoicePaymentSucceed.new
  )
end
services/invoice_payment_succed.rb
class Events::InvoicePaymentSucceed
  def call(event)
    source = event.data.object

    # 送金処理を行なう
    subscription = source.lines.data[0]
    metadata = subscription.metadata
    commissionPercentage = metadata.commission_fee.to_i / 100
    commissionAmount = subscription.amount * commissionPercentage
    destinationAmount = subscription.amount - commissionAmount
    destinationAccount = metadata.destination

    transfer = Stripe::Transfer.create({
      amount: destinationAmount,
      currency: 'jpy',
      destination: destinationAccount
    })
  end
end

アカウントの口座に入金される

入金はStripeダッシュボード上より、手動入金と、自動入金(月に何日、週に何曜日)など選べるみたいです。
10,000円以上残高貯まらないとできないなど、実装でカスタマイズはできるみたいです。
今回はサブスクリプションなんで、一ヶ月定期でいいんじゃないかなと思って、とりわけ実装はしませんでした。
スクリーンショット 2019-03-22 19.11.17.png

やってみると、意外にも壮大になってしまいました。。
お金を取り扱うあたり、慎重にならなきゃいけないところもありますよね。銀行口座や、本人確認書類のバリデーションもフロント側で処理できるといいですよね。なんか良いライブラリあったら、教えて欲しいです。

実際、これやり切るのに、Stripeご担当者さまに幾度となく問い合わせしました。
すぐに、優しく応答してくれて、サービスも会社も素晴らしいなと感動でした!

15
20
1

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
15
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?