26
26

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.

RailsのdeviseとStripe(とBootstrap)の組み合わせで、ただただユーザー登録させたユーザーに定期更新購読させるだけのappを作りました②

Last updated at Posted at 2018-09-04

前書き

初心者向けに自分なりの言葉で柔らかく書いたつもりです。非エンジニアですが、メモ目的とアウトプットの恩恵のために記事にしました。間違っているところなどありましたら、コメントをください。
あとコードにコメントが多く視認性が悪いかもしれません。そこは申し訳なく…

stripe実装

アカウント作成

stripeダッシュボードログイン
メールアドレスと名前、パスワードですぐにアカウントを作成できる。
スクリーンショット 2018-08-22 19.28.34.pngのコピー.png.png
スクリーンショット 2018-08-22 19.28.54.pngのコピー.png.png
スクリーンショット 2018-08-22 19.53.52.pngのコピー.png.png

公開可能キー、シークレットキーの取得

開発者、APIキー、から公開可能キー、シークレットキーを取得しておく。
スクリーンショット 2018-08-23 0.23.11.pngのコピー.png.png

stripe実装のアプリはダッシュボードを主軸として考えて管理していくと理解しやすくわかりやすいと思った。
つまりRailsからの通信の記録を、このダッシュボードに記録として刻んでいく作業。
だからこそダッシュボードのどこに何の記録を刻み残してあるのかを掴んでおくと、理解がスムーズに進むと思う。

前提知識としてRailsからの通信でStripe側(ダッシュボード)に刻むべき記録とは何か?と、大凡の画面一覧を見ようと思う。

Railsからの通信でStripe側(ダッシュボード)に刻むべき記録

  • Plan(商品を売るための計画情報)
    • Product(商品情報)
  • email(メールアドレス) + stripe token(カード情報) = Customer(顧客情報)
  • Plan + Customer = Subscription(定期支払い情報)

Product(商品情報)

Productの中にはname、typeを入れる

product = Stripe::Product.create({
    name: '商品の名前',# 商品の名前
    type: 'service',# タイプserviceとgoodがあるそうな
})

Stripe::Product.createとすることで、gemのStripeライブラリーが内部的にStripe側(ダッシュボード)に通信してProduct情報を記録してくれる

Plan(商品を売るための計画情報)

Planの中にはproduct、nickname、amount、currency、intervalを入れる。
または
Planの中にproduct(の中にname)、amount、currency、intervalを入れる

# Productを作成する場合(商品を複数持っている場合)

plan = Stripe::Plan.create({
  product: 'prod_CbvTFuXWh7BPJH',# Productを作る場合はIDを指定する
  nickname: 'プランの名前',# 名前
  amount: 1000,# 値段
  currency: 'jpy',# 通貨
  interval: 'month',# 月額課金
})


# Productを作成しない場合(商品を複数持っていない場合)

plan = Stripe::Plan.create(
  # productを作らずにnameだけでPlanを作る場合のやり方
  :product => {
    :name => ‘プランの名前’# 名前
  },
  :amount => 1000,# 値段
  :currency => 'jpy',# 通貨
  :interval => 'month',# 月額課金
  # :usage_type='licensed', #Subscription開始時に請求を開始するタイプ(即時決済)(前払い性)
  # これがなくてもデフォルトで'licensed'になるっぽいので記述していない。
  # 'Metered'は各期間の終わりにその期間分を請求することができる。利用料に応じて請求できるタイプ
)

Stripe::Plan.createとすることで、gemのStripeライブラリーが内部的にStripe側(ダッシュボード)に通信してPlan情報を記録してくれる

stripe token(カード情報)

カード番号を送信してstripe tokenを取得する
そのためのフォームをstripe公式ホームページからコピペしてきてviewに貼り付ける
カードエレメントクイックスタート

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

<form action="/charge" method="post" id="payment-form">
  <div class="form-row">
    <label for="card-element">
      Credit or debit card
    </label>
    <div id="card-element">
      <!-- A Stripe Element will be inserted here. -->
    </div>

    <!-- Used to display form errors. -->
    <div id="card-errors" role="alert"></div>
  </div>

  <button>Submit Payment</button>
</form>


<%# ******************************************************* %>
<%# ボタン %>
<script type="text/javascript">
  // Create a Stripe client.
  var stripe = Stripe('<%= publishable_key %>');

  // Create an instance of Elements.
  var elements = stripe.elements();

  // Custom styling can be passed to options when creating an Element.
  // (Note that this demo uses a wider set of styles than the guide below.)
  var style = {
    base: {
      color: '#32325d',
      lineHeight: '18px',
      fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
      fontSmoothing: 'antialiased',
      fontSize: '16px',
      '::placeholder': {
        color: '#aab7c4'
      }
    },
    invalid: {
      color: '#fa755a',
      iconColor: '#fa755a'
    }
  };

  // Create an instance of the card Element.
  var card = elements.create('card', {style: style});

  // Add an instance of the card Element into the `card-element` <div>.
  card.mount('#card-element');

  // Handle real-time validation errors from the card Element.
  card.addEventListener('change', function(event) {
    // フォームのエラー表示部分を取り出す
    var displayError = document.getElementById('card-errors');
    if (event.error) {
      // エラー文を直接表示
      displayError.textContent = event.error.message;
    } else {
      // エラー文を好きな文字で表示
      displayError.textContent = '';
    }
  });

  // Handle form submission.
  // 上のフォームを取り出す
  var form = document.getElementById('payment-form');

  // submitイベントが働いた時の動き
  form.addEventListener('submit', function(event) {
    event.preventDefault();

    // ストライプトークンを作成
    stripe.createToken(card).then(function(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);
        
      }
    });
  });
</script>

<%# ******************************************************* %>


<%# ******************************************************* %>
<%# stripeTokenHandler(token)関数 %>
<script type="text/javascript">
  function stripeTokenHandler(token) {
    // Insert the token ID into the form so it gets submitted to the server
    // 上のフォームを取り出す
    var form = document.getElementById('payment-form');
    var hiddenInput = document.createElement('input');
    hiddenInput.setAttribute('type', 'hidden');
    hiddenInput.setAttribute('name', 'stripeToken');
    hiddenInput.setAttribute('value', token.id);
    // フォームにトークンを挿入
    form.appendChild(hiddenInput);

    // Submit the form
    // フォームを送信
    form.submit();
    
  }
</script>
<%# ******************************************************* %>

email + stripe token = Customer(顧客情報)

Customerにはemail、source(stripe token)を入れる

customer = Stripe::Customer.create({
    email: 'stripestripe@gmail.com',# メールアドレス
    source: 'tok_18eYalAHEMiOZZp1l9ZTjSU0',# stripe token
})

Stripe::Customer.createとすることで、gemのStripeライブラリーが内部的にStripe側(ダッシュボード)に通信してCustomer情報を記録してくれる

Plan + Customer = Subscription(定期支払い情報)

Subscriptionにはcustomer、items(の中にplan)を入れる

subscription = Stripe::Subscription.create({
    customer: 'cus_4fdAW5ftNQow1a',# CustomerID
    items: [{plan: 'plan_CBXbz9i7AIOTzr'}],# PlanID
})

Stripe::Subscription.createとすることで、gemのStripeライブラリーが内部的にStripe側(ダッシュボード)に通信してSubscription情報を記録してくれる

これらのパラメータを当てはめる事によって、Stripeに記録を刻むことができる

##ダッシュボード画面一覧

ホーム画面

開発中は特に注意して見ることもないと思う
スクリーンショット 2018-08-23 1.16.30.pngのコピー.png.png

本番環境利用の申請画面

アカウント登録した時点でのテスト環境では、この画面が現れている状態
スクリーンショット 2018-08-23 1.16.58.pngのコピー.png.png

残高画面

顧客から支払ってもらった対価を銀行へ自動入金するスケジュールを設定したりする
スクリーンショット 2018-08-23 1.19.40.pngのコピー.png.png

顧客(Custmer)の管理をする画面

スクリーンショット 2018-08-23 1.19.53.pngのコピー.png.png

Billing内、請求書(Invoice)を管理する画面

スクリーンショット 2018-08-23 1.21.07.png.png

Billing内、月額課金(定期支払いSubscription)を管理する画面

スクリーンショット 2018-08-23 1.21.07.pngのコピー.png.png

登録されたPlanの詳細を管理する画面

(Planに割り当てられたproduct商品も見られる)
スクリーンショット 2018-08-23 1.21.20.pngのコピー.png.png

Railsなど、APIを使って外部からの通信での記録やアクションを管理する画面
スクリーンショット 2018-08-23 1.22.35.pngのコピー.png.png

webhook画面

何かのイベントが起こった時に自動的に、指定するアドレスに情報を送信する機能のイベントの種類やアドレスが設定できる画面
スクリーンショット 2018-08-23 1.22.50.pngのコピー.png.png

各種イベントのログが見られる画面

スクリーンショット 2018-08-23 1.23.05.pngのコピー.png.png

Railsなどの、アプリに追加したstripe決済機能で使っているAPIからの通信のログを見られる画面

スクリーンショット 2018-08-23 1.23.13.pngのコピー.png.png

#Rails側の実装

gem編集

stripeのgemをインストールする

Gemfile
  gem 'stripe'
$ bundle install

stripe初期設定

こちらの記事を参考に公開可能キーとシークレットキーを設定し、stripeを使用できる状態にする
Rails Stripe サブスクリプション決済実装 初期設定をしてAPIを使える状態にする

secrets.ymlの作成

configにsecrets.ymlはRailsのバージョンによっては最初から無い場合があるのでその時は新規作成する

config/secrets.yml
development:
  secret_key_base: **********************

  # stripeのキーの実装
  stripe_publishable_key: pk_test_**********************# 公開可能キー
  stripe_secret_key: sk_test_**********************シークレットキー

test:
  secret_key_base: **********************

stripe.rbの作成

config/initializersにstripe.rbは最初から無いので新規作成する

config/initializers/stripe.rb
Rails.configuration.stripe = {
  publishable_key: Rails.application.secrets.stripe_publishable_key,
  secret_key: Rails.application.secrets.stripe_secret_key
}

Stripe.api_key = Rails.application.secrets.stripe_secret_key

各種モデルとテーブルの作成

Planモデルを作成

$ rails g model Plan

Teamモデルを作成

$ rails g model Team

app/models/plan.rbを編集

app/models/plan.rb
class Plan < ApplicationRecord
  has_many   :teams
end

app/models/team.rbを編集

app/models/team.rb
class Team < ApplicationRecord
  # Userをownerという名前で使う
  # optional: trueで関連先を検査しないようにする
  belongs_to :owner, class_name: 'User', optional: true
  belongs_to :plan, optional: true

  # Team削除時にStripe::Subscriptionも削除
  around_destroy :delete_stripe_subscription_before_destroy
  
  private

  # Team削除時にStripe::Subscriptionも削除
  def delete_stripe_subscription_before_destroy
    team = self.where(user_id: current_user.id)
    ActiveRecord::Base.transaction do
      # サブスクリプションIDでサブスクリプションを呼び出し
      deleting_stripe_subscription = Stripe::Subscription.retrieve(team.stripe_subscription_id)
      yield
      # データベース上のTeamが削除されたと同時に、stripe側に保存されているサブスクリプションを削除
      deleting_stripe_subscription.delete
    end
  end
end

Planテーブルの作成

ユーザーが月額課金を始める際に、毎回Planを指定しなくてはならないので、ここから呼び出して使えるようにする

class CreatePlans < ActiveRecord::Migration[5.2]
  def change
    create_table :plans do |t|

      t.string :stripe_plan_id,       null: false# nameと同じ名前でOK
      t.string :name,                 null: false# プランの名前
      t.integer :amount,              null: false# 値段
      t.string :currency,             null: false# 通貨名
      t.string :interval,             null: false# 課金周期(月額month)

      t.timestamps
    end
  end
end

Teamテーブルを作成(ユーザーのStripeの現状を保存する)

class CreateTeams < ActiveRecord::Migration[5.2]
  def change
    create_table :teams do |t|

      t.string  :plan_id# プランID
      t.integer :user_id# 月額課金をキャンセル、再開時に必要なのでcurrent_user.idを保存する
      t.string   :stripe_card_id# カードトークン
      t.string   :stripe_customer_id# カスタマーID
      t.string   :stripe_subscription_id# サブスクリプションID
      t.datetime :active_until, null: false# カスタマーを作った時の時間

      t.timestamps
    end
  end
end

テーブル作成実行

$ rake db:migrate

Planの作成

月額課金を始める場合は、あらかじめstripe側のPlanを作成しておく必要がある。
stripeのダッシュボードから作成しそのままダッシュボードにPlanを記録する方法、またはRails側からseeds.rbを使ってコードで作成することでstripeのダッシュボードにPlanを記録する方法などがある。

前者

Billing、商品、+新規で商品を登録
スクリーンショット 2018-08-27 15.05.57.pngのコピー.png.png

の後、プランを登録
スクリーンショット 2018-08-27 15.06.52.pngのコピー.png.png

これでプランのIDを取得することができるので、月額課金(subscription)スタート時にこのIDを割り当てれば月額課金がスタートする事になる

subscription = Stripe::Subscription.create({
    customer: 'cus_4fdAW5ftNQow1a',# CustomerID
    items: [{plan: 'plan_CBXbz9i7AIOTzr'}],# PlanID
})

後者

db/seeds.rb
# Planを作ったあと、Plan名はレスポンスのjsonに無い(Stripe::Plan.createの中に無い)ので保存できないためPlan名はここで固定
planname = 'P Plan'

# Plan作成
plan = Stripe::Plan.create(
  # productを作らずにnameだけでPlanを作る場合のやり方
  :product => {
    :name => planname
  },
  :amount => 1000,
  :currency => 'jpy',
  :interval => 'month',
  # :usage_type='licensed', #Subscription開始時に請求を開始するタイプ(即時決済)(前払い性)
  # これがなくてもデフォルトで'licensed'になる
  # 'Metered'は各期間の終わりにその期間分を請求することができる。利用料に応じて請求できるタイプ
  # 今の場合だと、記述がないので'licensed'の設定
)



# Plan(Stripe::Plan.create)を作ることができていたら
if plan != nil

  # 保存するパラメーターを出力して有るかを見る
  puts plan
  puts plan.id
  puts planname# Plan名はjsonに無い
  puts plan.amount
  puts plan.currency
  puts plan.interval
  
  # データベースに保存
  Plan.create(
    id: 1,# Planが重複しないように1に固定
    stripe_plan_id: plan.id,
    name: planname,# Plan名はjsonに無いので直接保存
    amount: plan.amount,
    currency: plan.currency,
    interval: plan.interval
  )
end

seedファイルを実行

$ rake db:seed

各種viewの作成

  • カード情報を入力して月額課金をスタートするviewの作成
    • app/views/card/edit.html.erb
  • スタートした月額課金を停止するviewの作成
    • app/views/card/destroy.html.erb
  • 停止した月額課金を再開するviewの作成
    • app/views/card/restart.html.erb
  • これらが実行された後に移動するviewの作成
    • app/views/card/fin_subscription.html.erb

これら新しく作成したviewへのルートを作成する

config/routes.rb

Rails.application.routes.draw do

  devise_for :users
  root to: 'home#index'

  get 'card/edit'# app/views/card/edit.html.erb画面にアクセス
  
  get 'card/destroy'# app/views/card/destroy.html.erb画面にアクセス

  get 'card/restart'# app/views/card/restart.html.erb画面にアクセス

  get 'card/fin_subscription'# app/views/card/fin_subscription.html.erb画面にアクセス

  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

header部分を書き換える

自分のTeamを持っていなかった場合は、まだカードで月額課金を1度もスタートしたことがないので、カード情報を入力するフォームを表示するviewへのリンクを表示する
次にTeamを持っていたとしても、Team内のstripe_subscription_idを消したり追加したりして、それが有るか無いかで月額課金を停止したり再開したりを切り替えるようにするので、再開画面に行くか停止画面に行くかを切り替えられるようにする。

app/views/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Stripe1</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

  </head>

  <body>
    <header>
      <nav>
        <ul>
          <%# ログインしている場合 %>
          <% if user_signed_in? %>
            <li>
              <%= link_to '認証情報の変更', edit_user_registration_path %>
            </li>
            <li>
              <%= link_to 'ログアウト', destroy_user_session_path, method: :delete %>
            </li>


            <%# 自分のteamを呼び出す %>
            <% team = Team.find_by(user_id: current_user.id) %>

            <%# teamを持っていなかった場合 %>
            <% if team == nil %>

              <li>
                <%# カードを登録して月額課金を初めて開始するリンクを表示する(edit.html.erb) %>
                <%= link_to 'カード定期決済', card_edit_path %>
              </li>

            <% end %>

            <%# teamを持ってた場合 %>
            <% if team != nil %>

              <%# サブスクリプションIDがなかった場合 %>
              <% if team.stripe_subscription_id == nil %>
                <li>
                  <%# 月額課金を再開する画面へのリンクを表示(前回作ったteamを使ってサブスクリプションを作成させる)(restart.html.erb) %>
                  <%= link_to 'カード定期再開', card_restart_path %>
                </li>
              <% end %>

              <%# サブスクリプションIDがあった場合 %>
              <% if team.stripe_subscription_id != nil %>
                <li>
                  <%# 月額課金を終了する画面へのリンクを表示(destroy.html.erb) %>
                  <%= link_to 'カード定期キャンセル', card_destroy_path %>
                </li>
              <% end %>

            <% end %>

            <%# 最後のリストにログインしているかしていないかを確認できるように表示を作る %>
            <li>
                ログイン中
            </li>
  


          <%# ログアウトしている場合 %>
          <% else %>
            <li>
              <%= link_to 'サインアップ', new_user_registration_path %>
            </li>
            <li>
              <%= link_to 'ログイン', new_user_session_path %>
            </li>
            <%# 最後のリストにログインしているかしていないかを確認できるように表示を作る %>
            <li>
                ログアウト中
            </li>


          <% end %>

          
        </ul>
      </nav>
    </header>

    <p class="notice"><%= notice %></p>
    <p class="alert"><%= alert %></p>
    <%= yield %>

  </body>
</html>

各viewを編集

ナビゲーションバーの月額課金キャンセルのリンク先のview

app/views/card/destroy.html.erb
<h1>card#destroy</h1>

<ul>
  <li>
    <%# ボタンを作って、押したら"/home/cancel_subscription"にアクセスする %>
    <%# class: 'btn'でボタン化 %>
    <%= button_to '月額課金キャンセル', home_cancel_subscription_path, method: :put %>
  </li>
</ul>

ナビゲーションバーの月額課金再開のリンク先のview

app/views/card/restart.html.erb
<h1>card#restart</h1>

<ul>
  <li>
    <%# ボタンを作って、押したら"/home/restart_subscription"にアクセスする %>
    <%# class: 'btn'でボタン化 %>
    <%= button_to '月額課金再開', home_restart_subscription_path, method: :put %>
  </li>
</ul>

カード定期決済、月額課金キャンセル、月額課金再開を実行、成功後の画面

app/views/card/fin_subscription.html.erb
<h1>card#fin_subscription</h1>

<ul>
  <li>
    <%# ボタンを作って、押したら"/"にアクセスする %>
    <%# class: 'btn'でボタン化 %>
    <%= link_to "メインに戻る", root_path, class: 'btn btn-default' %>
  </li>
</ul>

ナビゲーションバーのカード定期決済のリンク先のview

app/views/card/edit.html.erbはカードエレメントクイックスタート
をコピペしてきて貼り付け、formのactionにstripe tokenを送りたいアクションのルートを指定する

app/views/card/edit.html.erb
<h1>card#edit</h1>

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

<%# 公開可能キーを呼び出す %>
<% publishable_key = Rails.application.secrets.stripe_publishable_key %>
<%# ******************************************************* %>
<%# フォーム %>
<%# 送信した結果をhome/createに送られる %>
<%# "/home/create" %>

<form action="<%= home_create_path %>" method="post" id="payment-form">
  <div class="form-row">

    <label for="card-element">Credit or debit card</label>
    
    <!-- A Stripe Element will be inserted here. -->
    <div id="card-element"></div>
    <!-- Used to display form errors. -->
    <div id="card-errors" role="alert"></div>
  </div>

  <button>Submit Payment</button>
</form>
<%# ******************************************************* %>


<%# ******************************************************* %>
<%# ボタン %>
<script type="text/javascript">
  // Create a Stripe client.
  <%# 呼び出した公開可能キーをセット %>
  var stripe = Stripe('<%= publishable_key %>');

  // Create an instance of Elements.
  var elements = stripe.elements();

  // Custom styling can be passed to options when creating an Element.
  // (Note that this demo uses a wider set of styles than the guide below.)
  var style = {
    base: {
      color: '#32325d',
      lineHeight: '18px',
      fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
      fontSmoothing: 'antialiased',
      fontSize: '16px',
      '::placeholder': {
        color: '#aab7c4'
      }
    },
    invalid: {
      color: '#fa755a',
      iconColor: '#fa755a'
    }
  };

  // Create an instance of the card Element.
  var card = elements.create('card', {style: style});

  // Add an instance of the card Element into the `card-element` <div>.
  card.mount('#card-element');

  // Handle real-time validation errors from the card Element.
  card.addEventListener('change', function(event) {
    // フォームのエラー表示部分を取り出す
    var displayError = document.getElementById('card-errors');
    if (event.error) {
      // エラー文を直接表示
      displayError.textContent = event.error.message;
    } else {
      // エラー文を好きな文字で表示
      displayError.textContent = '';
    }
  });

  // Handle form submission.
  // 上のフォームを取り出す
  var form = document.getElementById('payment-form');

  // submitイベントが働いた時の動き
  form.addEventListener('submit', function(event) {
    event.preventDefault();

    // ストライプトークンを作成
    stripe.createToken(card).then(function(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);
        
      }
    });
  });
</script>

<%# ******************************************************* %>


<%# ******************************************************* %>
<%# stripeTokenHandler(token)関数 %>
<script type="text/javascript">
  function stripeTokenHandler(token) {
    // Insert the token ID into the form so it gets submitted to the server
    // 上のフォームを取り出す
    var form = document.getElementById('payment-form');
    var hiddenInput = document.createElement('input');
    hiddenInput.setAttribute('type', 'hidden');
    hiddenInput.setAttribute('name', 'stripeToken');
    hiddenInput.setAttribute('value', token.id);
    // フォームにトークンを挿入
    form.appendChild(hiddenInput);

    // Submit the form
    // フォームを送信
    form.submit();
    
  }
</script>
<%# ******************************************************* %>

ナビゲーションバーのカード定期決済のリンク先のviewのcss

cardform.cssと、新しくcssファイルを作成してカードエレメントクイックスタート
からcssもコピペしてきて貼り付ける。width: 400px;のみ付け足す

app/assets/stylesheets/cardform.css
.StripeElement {
  background-color: white;
  height: 40px;
  /* 付け足す */
  width: 400px;
  padding: 10px 12px;
  border-radius: 4px;
  border: 1px solid transparent;
  box-shadow: 0 1px 3px 0 #e6ebf1;
  -webkit-transition: box-shadow 150ms ease;
  transition: box-shadow 150ms ease;
}

.StripeElement--focus {
  box-shadow: 0 1px 3px 0 #cfd7df;
}

.StripeElement--invalid {
  border-color: #fa755a;
}

.StripeElement--webkit-autofill {
  background-color: #fefde5 !important;
}

.center{
  border: 1px solid #aaa;
  width: 400px;
  text-align: center;
}

各種viewのボタンを押した時のコントローラーアクションの編集

stripe tokenをカードエレメントのフォームで指定した、アクションが受信するので、そのアクションを記述する

app/controllers/home_controller.rb
class HomeController < ApplicationController
  # クレジットカード情報を送ってストライプトークンがcreateアクションに送られてくるのだが
  # https通信ではないやり取りで安全性が確認できないためプロテクトから除外させる
  protect_from_forgery except: :create
  
  def index
  end

  # httpメソッドはpost
  # edit.html.erbのボタンで発動
  def create

    # Customer作成に必要なデータが揃ったか出力して確認
    logger.debug('createアクション実行中')
    logger.debug(params[:stripeToken])# 送られてきたストライプトークンを出力
    logger.debug('current_user.email' + current_user.email)# ログインしているユーザーのメールアドレス
    logger.debug('@current_user.email' + @current_user.email)# ログインしているユーザーのメールアドレス

    # params[:stripeToken]が無くてもCustomerは作れてしまう
    # Customerが作れてしまうからSubscriptionも作れてしまう
    # そのための判定

    # stripetokenが送られてきた場合
    if params[:stripeToken] != nil

      # カスタマーを作る
      customer = Stripe::Customer.create({
        email: current_user.email,# ログインしているユーザーのメールアドレス
        source: params[:stripeToken],# ストライプトークン
      })

      # Customerが作れたかどーかの判定
      if customer.id != nil 
        # customerが作成できたか出力して確認
        logger.debug('customer')
        logger.debug(customer)# customerそのもの
        logger.debug('customer.id')
        logger.debug(customer.id)# customer.id

        # 保存が完了したら、Planを呼び出す(プラン選択はないのでidで良い)
        # Plan名、もしくはidでデータベースからPlanを取り出す
        plan = Plan.find_by(id: 1)# idが1のPlanしか無いので1で良い(フォームでPlanを選ばせるようにするなどの工夫があると良い)

        # Subscriptionの作成に必要なデータが揃ったか出力して確認
        logger.debug('Plan.id')
        logger.debug(plan.stripe_plan_id)# stripe_plan_id

        # 作れていたら、今までに作ってきたデータをTeamに保存
        begin
          Team.transaction() do
            team = Team.new({
              user_id: current_user.id,
              plan_id: plan.stripe_plan_id,# ①プランID
              stripe_card_id: params[:stripeToken],# ②カードトークン
              stripe_customer_id: customer.id,# ③カスタマーID
              active_until: Time.at(Time.now.to_i)})

            # save!でモデルに定義したバリデーションを検証してもらえる
            team.save!

            # trial_endとbilling_cycle_anchorの時間を取得
            trial_end_time = Time.at(Time.local(2018, 12, 31, 12, 0, 0, 0).to_i)# 日時を指定したものをUNIXタイムにしたもの
            billing_cycle_anchor_time = Time.at(Time.local(2018, 12, 31, 12, 28, 0, 0).to_i)# 最初の請求日

            logger.debug('trial_end_time')
            logger.debug(trial_end_time.to_i)# 試用期間終了日
            logger.debug('billing_cycle_anchor_time')
            logger.debug(billing_cycle_anchor_time.to_i)# 最初の請求日

            # アップデートができるか試す
            # team.update( stripe_subscription_id: 'sub_49ty4767H20z6a', active_until: Time.at(Time.now.to_i))

            # エラーが確認できなかったらSubscriptionを作成
            subscription = Stripe::Subscription.create({
              customer: customer.id,
              items: [{plan: plan.stripe_plan_id}],
              tax_percent: 8.00,# 税金(サービスの税金なので税理士に相談して税率を決定)(消費税率にしてるだけ)
              trial_end: trial_end_time.to_i,# 無料の試用期間
              # (試用期間が終わるまでを表したUNIXのタイムスタンプ整数)
              # (早期に終了したい場合はtrial_end: 'now’にする)
              # (trial_period_daysで日数での指定もできるがtrial_endの方が使いやすい)
              # (試用期間終了3日前にWebhookからcustomer.subscription.trial_will_endイベントが送信される)
              # (試用期間終了後invoice.createdイベントが送信される)

              billing_cycle_anchor: billing_cycle_anchor_time.to_i,# 試用期間が終わった初めての請求日の設定
              # (請求日までの時間をUNIXのタイムスタンプ整数にしたもの)
              # (サブスクリプションの通常の定期更新の時の請求日は即時になるが、これがあると請求日サイクル日を設定できる)
              # (この設定がなければ月の最終日に請求がある?)
              # 試用期間終了後は、ここで設定した日付までを日割り計算した金額を即時決済され、設定した日付が来たら請求サイクルで決済される?
              # 例えば2018/8/9に登録したとして、2018/8/9に2018/08/28(月末、月頭でも良い)までの日割り計算された金額を即時決済(licensタイプ)され、請求サイクルの日付は2018/08/28の設定になる
              # キャンセルしたら即座に使えなくなる(Planが'licensed'で前払い性なのでいつ辞めてもそれ以降の金額は関係ない)
              # 前払い性なので、ここで設定した日付から1ヶ月サイクルまで使える状態を作れる


              # billing: 'send_invoice',# 請求書のデフォルト
              # days_until_due: 30,# 未払いを決定する日にち、整数値
              # 数分のPlanの設定やPlanの数量設定もできる
              # items=[
              #   {
              #     "plan": "{{GRADUATED_PLAN_ID}}",
              #     "quantity": "11",# 数量
              #   },
              # ]


            })

            # Subscriptionの実行が完了したら、Subscription_IDをTeamに保存
            # Subscriptionが作れたかどーかの判定
            if subscription.id != nil
              # subscriptionが作成できたか出力して確認
              logger.debug('subscription')
              logger.debug(subscription.id)# customerそのもの
              # ④サブスクリプションID
              # ⑤カスタマーを作った時(サブスクリプションを作った時)の時間
              team.update( stripe_subscription_id: subscription.id, active_until: Time.at(Time.now.to_i))

              # 月額課金がスタートしたので別画面に飛ばす
              render "card/fin_subscription"
              
            end
            
          end

          # エラー(バリデーション)が出た場合の処理
        rescue ActiveRecord::RecordInvalid => e
          # e.record.errors
          # 別画面に飛び、エラーメッセージを表示
          render plain: e.message

        end

      end
      
    end

  end



  # httpメソッドはdelete
  # destroy.html.erbのキャンセルボタンで発動
  def cancel_subscription

    # キャンセルでsubscription_IDが必要になる
    # ログインしているUser(current_user)でTeamを見つけてその中のstripe_subscription_idを取ってくる
    team = Team.find_by(user_id: current_user.id)

    # サブスクリプションが開始されていた場合
    if team.stripe_subscription_id != nil
      # サブスクリプションの停止(キャンセル)
      subscription = Stripe::Subscription.retrieve(team.stripe_subscription_id)
      subscription.delete(at_period_end: true)# 期間終了時にキャンセルのオプション付き

      # 月額課金がキャンセルされたのでレコードから:plan_idと:active_untilとstripe_subscription_idを削除
      # カードトークンとカスタマーIDは残す
      begin
        Team.transaction() do
          team.plan_id = nil
          team.stripe_subscription_id = nil
          team.active_until = Time.at(Time.now.to_i)
          team.save!

          # team.update( plan_id: "", stripe_subscription_id: "", active_until: Time.at(Time.now.to_i))
        end

        # 月額課金をキャンセルしたので別画面に飛ばす
        render "card/fin_subscription"

      # エラー(バリデーション)が出た場合の処理
      rescue ActiveRecord::RecordInvalid => e
        # e.record.errors
        # 別画面に飛び、エラーメッセージを表示
        render plain: e.message

      end

    end

  end



  # httpメソッドはput
  # restart.html.erbの再開ボタンで発動
  def restart_subscription

    # ログインしているUser(current_user)で自分のTeamを見つけてくる
    team = Team.find_by(user_id: current_user.id)

    # Plan名、もしくはidでデータベースからPlanを取り出す
    plan = Plan.find_by(id: 1)# idが1のPlanしか無いので1で良い(フォームでPlanを選ばせるようにするなどの工夫があると良い)
    # Subscriptionの作成に必要なデータが揃ったか出力して確認
    logger.debug('Plan.id')
    logger.debug(plan.stripe_plan_id)# stripe_plan_id

    # trial_endとbilling_cycle_anchorの時間を取得
    trial_end_time = Time.at(Time.local(2018, 12, 31, 12, 0, 0, 0).to_i)# 日時を指定したものをUNIXタイムにしたもの
    billing_cycle_anchor_time = Time.at(Time.local(2018, 12, 31, 12, 28, 0, 0).to_i)# 最初の請求日

    logger.debug('trial_end_time')
    logger.debug(trial_end_time.to_i)# 試用期間終了日
    logger.debug('billing_cycle_anchor_time')
    logger.debug(billing_cycle_anchor_time.to_i)# 最初の請求日

    # アップデートができるか試す
    # team.update( plan_id: 'plan_', stripe_subscription_id: 'sub_', active_until: Time.at(Time.now.to_i))

    # エラーが確認できなかったらSubscriptionを作成
    subscription = Stripe::Subscription.create({
      customer: team.stripe_customer_id,
      items: [{plan: plan.stripe_plan_id}],
      tax_percent: 8.00,# 税金(サービスの税金なので税理士に相談して税率を決定)(消費税率にしてるだけ)
      trial_end: trial_end_time.to_i,# 無料の試用期間
      # (試用期間が終わるまでを表したUNIXのタイムスタンプ整数)
      # (早期に終了したい場合はtrial_end: 'now'にする)
      # (trial_period_daysで日数での指定もできるがtrial_endの方が使いやすい)
      # (試用期間終了3日前にWebhookからcustomer.subscription.trial_will_endイベントが送信される)
      # (試用期間終了後invoice.createdイベントが送信される)
      billing_cycle_anchor: billing_cycle_anchor_time.to_i,# 試用期間が終わった初めての請求日の設定
      # (請求日までの時間をUNIXのタイムスタンプ整数にしたもの)
      # (サブスクリプションの通常の定期更新の時の請求日とは違う。だから定期更新の時の請求日と合わせるとやりやすい)
      # (この設定がなければ月の最終日に請求がある)
      # billing: 'send_invoice',# 請求書のデフォルト
      # days_until_due: 30,# 未払いを決定する日にち、整数値
    })

    # Subscriptionの実行が完了したら、Subscription_IDをTeamに保存
    if subscription.id != nil
      # subscriptionが作成できたか出力して確認
      logger.debug('subscription')
      logger.debug(subscription.id)# customerそのもの
      # ④サブスクリプションID
      # ⑤カスタマーを作った時(サブスクリプションを作った時)の時間

      begin
        Team.transaction() do
          team.plan_id = plan.stripe_plan_id
          team.stripe_subscription_id = subscription.id
          team.active_until = Time.at(Time.now.to_i)
          team.save!

          # team.update( plan_id: plan.stripe_plan_id, stripe_subscription_id: subscription.id, active_until: Time.at(Time.now.to_i))
        end

        # 月額課金がスタートしたので別画面に飛ばす
        render "card/fin_subscription"

      # エラー(バリデーション)が出た場合の処理
      rescue ActiveRecord::RecordInvalid => e
        # e.record.errors
        # 別画面に飛び、エラーメッセージを表示
        render plain: e.message

      end

    end

  end


end

各種viewのボタンを押した時のアクションを実行するルートを追加する

config/routes.rb
Rails.application.routes.draw do

  devise_for :users
  root to: 'home#index'

  get 'card/edit'# app/views/card/edit.html.erb画面にアクセス
  post 'home/create'# homeコントローラーのcreateアクション実行
  
  get 'card/destroy'# app/views/card/destroy.html.erb画面にアクセス
  put 'home/cancel_subscription'# homeコントローラーのcancel_subscriptionアクション実行

  get 'card/restart'# app/views/card/restart.html.erb画面にアクセス
  put 'home/restart_subscription'# homeコントローラーのrestart_subscriptionアクション実行

  get 'card/fin_subscription'# app/views/card/fin_subscription.html.erb画面にアクセス

  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

Invoice内容は後日更新する予定

これで一通りstripeの月額課金(定期更新購読)ができたが、内容に何かありましたら是非コメント下さい。
次はBootstrapで見た目を改善する。

Bootstrap実装

RailsのdeviseとStripe(とBootstrap)の組み合わせで、ただただユーザー登録させたユーザーに定期定額課金させるだけのappの実装方法③

26
26
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
26
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?