前回の記事でPayjpを用いた単発決済の実装方法を解説した。
そこで今回は、Payjpで定期課金機能を実装してみたいと思う。

想定

有料会員登録としてカード登録をしないと、主要なサービスが使えないWebアプリを想定する。
以下のような仕様であるとする。

  • 定期課金のプランは1つだけ
  • トライアル期間などはなし
  • カード登録をした瞬間に初回決済が走る
  • 有料プランの期間中に退会しても日割りの返金はなし
  • 決済日は前回決済日から1ヶ月後

モデル

Userモデルとそれに紐付くPaymentHistoryモデルを作成する。またプランは1つしかないが、PlanMasterモデルを用意しておく。

# app/models/user

# == Schema Information
#
# Table name: users
#
#  id                             :bigint   not null, primary key
#  payjp_customer_id(Payjp顧客ID)  :string   default("")
#  payjp_subscription_id          :string   default("")
#  created_at                     :datetime not null
#  updated_at                     :datetime not null
#  その他認証に必要な情報

class User < ApplicationRecord
  has_many :payment_histories
end
# app/models/payment_history

# == Schema Information
#
# Table name: payment_historyies
#
#  id                                  :uuid             not null, primary key
#  amount                              :integer          default(0), not null
#  error_detail                        :string
#  error_message                       :string
#  status(決済ステータス)                :integer          default("before_payment"), not null
#  created_at                          :datetime         not null
#  updated_at                          :datetime         not null
#  charge_id(Payjp決済ID)               :string           default(""), not null
#  user_id                             :uuid             not null
#
# Indexes
#
#  index_orders_on_user_id  (user_id)

class PaymentHistory < ApplicationRecord
  belongs_to :user

  enum status: {
    before_payment: 0, # 未決済
    completed: 1, # 決済完了
    failed: 2, # 決済失敗
  }
end
# app/models/plan_masters

# == Schema Information
#
# Table name: plan_masters
#
#  id                           :bigint           not null, primary key
#  name(プラン名)               :string           default(""), not null
#  created_at                   :datetime         not null
#  updated_at                   :datetime         not null
#  payjp_plan_id(PayjpプランID) :string           default(""), not null
#
# Indexes
#
#  index_plan_masters_on_payjp_plan_id  (payjp_plan_id)
#

class PlanMaster < ApplicationRecord
end

プランの作成

まず、Payjp側でプランを作成する。Payjpの管理画面にログインし、サイドバーの「プラン」をクリックすると下記の様な画面が出て来る。

payjp plan modal

「プラン作成」をクリックする作成モーダルが開くので、金額、課金間隔、ID、プラン名を入力する。IDはPayjp側のプランIDのことで、プランを作成したらこれをコピーしてPlanMasterモデルのpayjp_plan_idに保存しておく。

定期課金登録

ユーザーが定期課金を開始するためのロジックを作っていく。

チェックアウト

こちらは前回の記事で解説したので省略。

顧客情報の登録/定期課金の登録

まず、顧客情報をPayjp側で登録する必要がある。

# app/controllers/payjp/customers_controller.rb

class Payjp::CustomersController < ApplicationController
  def create
    Payjp.api_key = ENV["PAYJP_SECRET_KEY"]
    # responseにはPayjp::Customerオブジェクトが返ってくる
    response = Payjp::Customer.create(
      card: @payjp_token,
      metadata: { user: current_user.id } # payjp側のmetadataとしてRails側のuser_idを渡す
    )
    current_user.update(payjp_customer_id: response.id) 

    redirect_to 'リダイレクト先のpath'
  rescue Payjp::PayjpError => e
    render 'render先のアクション'
  rescue StandardError => e
    render 'render先のアクション'
  end
end

顧客が登録できたら、定期課金を登録するロジックを追加する。

# app/controllers/payjp/customers_controller.rb

class Payjp::CustomersController < ApplicationController
  def create
    Payjp.api_key = ENV["PAYJP_SECRET_KEY"]
    # 顧客の登録
    response = Payjp::Customer.create(
      card: @payjp_token,
      metadata: { user: current_user.id }
    )
    current_user.update(payjp_customer_id: response.id)

    # 定期課金の登録
    subscription_response = Payjp::Subscription.create(
      plan: Plan.first.payjp_plan_id, # 先ほど登録したプラン
      customer: current_user.payjp_customer_id
    )

    redirect_to 'リダイレクト先のpath'
  rescue Payjp::PayjpError => e
    render 'render先のアクション'
  rescue StandardError => e
    render 'render先のアクション'
  end
end

ここでは1つのアクションの中で顧客の登録と定期課金の登録を行なっているが、実際に使う際は個別の用途に合わせて欲しい。

決済情報の登録

ここまで出来たらあとは決済情報を保存する仕組みを作るだけ。
Payjpではwebhookが用意されており、イベントが発生すると登録されているURLにリクエストを投げてくれる。
Payjp側に定期課金登録を行なったら自動で決済してくれるので、webhookリクエストを受け取るためのapiを用意してあげればいい。
詳細はこちら

まずはPayjpの管理画面からWebhookトークンを取得する。管理画面サイドバー「API」をクリックして下の方にスクロールすると、Webhookの欄があり、発行元トークンという項目がある。

payjp-token

これを環境変数としてPAYJP_WEBHOOK_SIGNATUREで定義する。

# app/controllers/payjp/charges_controller.rb

class Payjp::ChargesController < ApplicationController
  before_action :payjp_webhook_auth, only: %i(create)

  # webhookを発火させるイベントを選択することは出来ないので、決済が成功/失敗したイベントに限定する
  PAYJP_REQUEST_TYPE = ['charge.succeeded', 'charge.failed']

  def create
    # payjp_params[:type]がPAYJP_REQUEST_TYPEに含まれていなければreturn
    return unless PAYJP_REQUEST_TYPE.include?(payjp_params[:type])

    user = User.find_by(payjp_customer_id: payjp_params[:data][:customer])
    payment_history = user.payment_histories.create(
      payed_on: Date.today,
      payment_amount: payjp_params[:data][:amount],
      payjp_charge_id: payjp_params[:data][:id]
    )
    unless is_charge_succeed
      payment_history.update(
        status: :failed,
        failure_code: payjp_params[:data][:failure_code],
        failure_message: payjp_params[:data][:failure_message]
      )
    end
    render status: 201
  end

  private

  def payjp_webhook_auth
    # トークンとHTTP_X_PAYJP_WEBHOOK_TOKENが違えば401を返却する。
    if request.headers["HTTP_X_PAYJP_WEBHOOK_TOKEN"] != ENV['PAYJP_WEBHOOK_SIGNATURE']
      render status: 401, json: { message: 'Unauthorized' } and return
    end
  end

  # 主要なデータはparams[:data]の中にある
  def payjp_params
    params.permit(
      :type,
      data: [
        :id,
        :amount,
        :customer,
        :subscription,
        :failure_code,
        :failure_message
      ]
    )
  end

  def is_charge_succeed
    payjp_params[:type] == PAYJP_REQUEST_TYPE[0]
  end
end

これでPayjpで決済が行われたときに、その情報をRails側でも拾えるようになった。
ちなみに、定期課金登録時の初回決済もWebhookで拾うことが出来る。

Webhookが本当に飛んでくるか確認

最後に、Webhookが本当に飛んでくるか確認をしたいと思う。ローカル環境のままだとWebhookを利用できないので、public URLを発行してくれるngrokを使う。

まずはインストール

$ brew install ngrok
$ ngrok --version
ngrok version 2.3.35

ローカルで起動しているページのポート番号を入れ起動

ngrok http 3000

こんな画面が出れば成功

Session Status                online
Session Expires               7 hours, 59 minutes
Version                       2.3.35
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://6gt2gc45.ngrok.io -> http://localhost:3000
Forwarding                    https://6gt2gc45.ngrok.io -> http://localhost:3000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

ここで得られたhttp://6gt2gc45.ngrok.io/payjp/chargesをPayjp管理画面でWebhookに登録する。

payjp-webhook

テストイベントを送信をクリックして、URLを先ほどのURL、イベント種類をcharge.succeededを選択し送信する。
おそらくエラーになるはずだが、ローカルで何らかのログが出たら成功している。
あとは開発環境で決済してみて、問題なく動作すれば完了。

まとめ

以上で定期課金機能を実装することが出来た。
Payjpはとても使いやすいので、決済サービスで悩んだ際は一度使ってみて欲しい。
次回は今回作成したコントローラーのテストを、Payjpのモックを作りながら解説したいと思う。