3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

この記事は、「オンライン決済サービスPAY.JPを使ってみた情報をシェアしよう! by PAY Advent Calendar 2024」https://qiita.com/advent-calendar/2024/payjp-01 を見つけ、たまたま最近自社サービスで定期課金の実装を行う機会があったために投稿しようと記述しています。
また、自社サービスはrailsで実装しているため、サンプルでコードを記載する場合には以下になります。

  • ruby v2.7.8
  • rails v6.0

Pay.jpとは

クレジットカード決済(単発・定期課金)を導入するために用いることのできるオンライン決済サービスです。
Pay.jp様はとてもわかり易いドキュメントとシンプルなAPIをご用意してくださっているため、基本的なクレカ決済や定期課金を実装したいシーンでは有用な選択肢となります

今回のゴール

今回は、Pay.jpを用いて定期課金機能の実装の例を提示しようと思います。
そのため、簡単に今回の記事用に以下の仕様を制定します。

仕様

前提条件

  • 店舗を持つユーザーが利用する
  • すでに無料機能として、ユーザー(User)が自分の店舗(Shop)の基本情報の登録などを行っている

機能

  • 店舗はプレミアムプランに登録できる
    • プランは「ライト」「スタンダード」「リッチ」から選択できる
    • 登録時にクレジットカードを情報を入力する
    • 登録後、即時に課金が発生し、以降毎月の同日時が決済予定日時になり、課金が発生する
  • 解約をすることができる
    • 解約後、次回の決済予定日時までは機能を利用できるが、決済予定日時に課金が発生しない
    • 解約後に決済予定日時を過ぎた場合は、以降は機能を利用できなくなる
    • 解約後、次回の決済予定日時までは再開ができる。再開した場合、次回の決済予定日時に課金が発生し、引き続き機能が利用できる
  • 毎月自動で更新される
    • Pay.jpのWebhookを用いる
    • 詳細は後述するが、Pay.jp側で定期課金の更新や課金のイベントが発生した際にデータを送信してくれる
  • 自動更新失敗時
    • 失敗時には猶予期間を5日間設ける
    • 猶予期間内は登録中の機能を利用可能
    • 猶予期間内に新しいカードを登録した場合は引き続き課金され引き続き利用可能になる

gemを使うよ

実装の例では、Pay.jpの提供しているRubyライブラリを用います。
https://github.com/payjp/payjp-ruby

Gemfile
gem 'payjp'
bundle install

APIのこのときどうかくの?という具体例がまとまっているドキュメントがみあたらなかったため、githubの中のコード読みながら実装しています。
基本的に、Pay.jpが提供しているAPIは利用できるように対応しているはずなので、問題なさそうです。

必要な概念の整理と必要なAPI

Pay.jp側で定期課金を登録するには、いくつかの概念(オブジェクトの種類)を理解する必要があります。ただ、どれもそんなに複雑なわけではないので簡単にまとめます。

プラン Plan

Plan オブジェクトは、定期課金の基本的な課金サイクル・料金・トライアル日数などを定義するためのものです。
たとえば、先述の「ライト」「スタンダード」「リッチ」プランを用意しておけば、ユーザーがサブスクリプション登録を行う際に、これらのプランIDを指定するだけで、自動的に決済サイクルと料金が決定されます。

GUIから簡単に作成できますし、Pay.jpのRubyライブラリを用いて以下のようなコードでも作成可能です。

require 'payjp'
Payjp.api_key = ENV['PAYJP_SECRET_KEY']

plan = Payjp::Plan.create(
  id: "plan_light",     # 独自ID。省略時は自動で割り振られる
  amount: 1200,
  currency: "jpy",
  interval: "month",
  trial_days: 7,
  name: "ライトプラン"
)

顧客 Customer

Customer オブジェクトは、「どのユーザーがどの支払い方法(カード)で、どのプランに紐づいているのか」をPay.jp側で管理するためのデータモデルです。
ユーザーが定期課金を行う際には、まずRails側でユーザーのにPay.jp側のCustomerオブジェクトのIDを紐づけることで、一意の決済顧客として扱うことが可能になります。

Customerの作成は、Pay.jpのRubyライブラリを用いると以下のようなコードになります。

require 'payjp'

Payjp.api_key = ENV['PAYJP_SECRET_KEY'] # 秘密鍵を設定

customer = Payjp::Customer.create(
  email: user.email
  # metadata: "任意のメモ。ユーザーID: #{user.id} など"
)

これによりPay.jp側で顧客(Customer)が作成され、customer.id を取得できます。このIDをuser.payjp_customer_idに格納しておけば、後々カード登録や定期課金を紐づける際に使用できます。

カード Card

Card オブジェクトは、Customer に紐づくクレジットカード情報をPay.jp側で管理するためのものです。 セキュリティ上、クレジットカード情報(カード番号、セキュリティコード、期限など)はRails側に直接保存せずにカードIDだけをRails側に保存しておくことができます。
カードを登録するには、まずフロントエンド側でPay.jpのチェックアウトのスクリプトやJavaScriptを用いてトークンを作成し、そのトークンをサーバーサイドに送信、そこでPayjp::Customerにカードを関連付けるという流れを取ることが多いです。

以下はカードトークンを用いて顧客にカードを追加する例です。

# すでに顧客が作成済みで user.payjp_customer_id にIDが保持されている前提
customer = Payjp::Customer.retrieve(user.payjp_customer_id)

# フロントエンドで生成したトークン "tok_xxx" を受け取った上でカードを作成
card = customer.cards.create(
  card: params[:payjp_card_token]
)

これでユーザーはPay.jp上で有効なカードを紐づけられます。
複数カードを紐づけることも可能ですが、基本的にはひとつのメインカードで運用するケースが多いでしょう。

定期課金 Subscription

Subscription オブジェクトは、顧客(Customer)が特定のプラン(Plan)に基づいて定期的に課金されることを示すものです。
Subscriptionを作成することで、Pay.jp側で指定されたプランに基づいて自動的に請求が行われ、更新処理が走る仕組みが提供されます。

Subscription作成の流れ

  1. PlanオブジェクトをPay.jp側であらかじめ用意しておく。
  2. Customerオブジェクトを用いて顧客を作成する。
  3. CustomerCardを追加する。
  4. Subscriptionを作成し、CustomerPlanを紐づける。

コード例:

require 'payjp'
Payjp.api_key = ENV['PAYJP_SECRET_KEY']

subscription = Payjp::Subscription.create(
  plan: 'plan_light', # Pay.jp側で作成済みのプランID
  customer: payjp_customer_id, # 作成済みの顧客ID
  prorate: false # 必要に応じて日割り計算などをしたい場合はオプションを参照
)

これでsubscription.idが取得でき、Pay.jp側でこのサブスクリプションが管理されます。
支払い間隔が月次なら、毎月の支払い日になるとPay.jpが自動で課金し、成功時にはcharge.succeededなどのイベントが発生します。

課金 Charge

Charge オブジェクトは、実際に発生した支払い(請求)の履歴や状態を表します。
Subscriptionを起点に自動的に課金が発生した場合も、都度Chargeが作成されます。
また単発課金の場合にはPayjp::Charge.createによって明示的に課金することが可能です。

今回は定期課金の話なので、通常は手動でChargeを作成する必要はありませんが、Webhookを受け取る際や課金履歴を参照する際には、このChargeオブジェクトが重要になります。
例:

# IDから課金情報を取得
charge = Payjp::Charge.retrieve('ch_xxx')
puts charge.amount # 金額
puts charge.paid   # trueの場合、支払い成功
puts charge.refunded # 返金されたか否か

Webhook

Webhookとは、Pay.jp側で課金に関するイベント(サブスクリプションの更新、課金の成功・失敗など)が発生した際に、Rails側へHTTPリクエストを送って通知してくれる仕組みです。
リアルタイムでPay.jp側での自動更新や課金の発生などを検知できるため便利です。

Webhook実装の流れ

  1. Rails側でWebhookを受信するエンドポイントを用意する(例: POST /webhooks/payjp)。
  2. Pay.jpの管理画面からこのエンドポイントURLを設定する。
  3. Pay.jp側でイベント発生時に、上記URLへPOSTリクエストが飛ぶ。
  4. Rails側で受信したJSONを解析し、object, type(イベント種類)などを見て処理を行う。

実装例

ここまでで、Pay.jpを用いた定期課金機能を実装する際に必要となる概念(Plan, Customer, Card, Subscription, Charge)やWebhooksの基本的な考え方を整理しました。
ここから具体的に、それらを組み合わせてカードの登録から定期課金の登録、解約と再開、Webhookでの検知について記載していきます。

カード登録

まずはユーザーがプレミアムプランを申し込むフローを想定しましょう。今回は、Pay.jpのscriptを用いるとカード情報を取得するフォームを簡単に作成できるのでこれを利用していきます。
このようにフォームの中でPlanを選択させたあと、カード情報の入力後に確定ボタンを押すと、Pay.jp側でカード情報をトークン化し、data-payjp-token-name="subscription[payjp_card_token]"で指定しているように、パラメータのsubscription[payjp_card_token]に値を挿入したうえでフォームのsubmitを実行してくれます。

参考:https://pay.jp/docs/cardtoken
EMV3Dセキュア導入義務化のお知らせに伴い、checkoutのURLをhttps://checkout.pay.jp/prereleaseに変更していますが、2025年 2月4日以降はhttps://checkout.pay.jp/でOK

= form_with(model: [:shop, @subscription]) do |f|
    - @plans.each do |plan|
      = f.radio_button :plan_id, plan.id, required: true
      = f.label :plan_id, plan.display_name, value: plan.id
    
    / srcの https://checkout.pay.jp/prerelease は 2025年 2/4 ~ 4/30の期間に`https://checkout.pay.jp/`に変更する必要がある
    script.payjp-button(
      data-payjp-key="XXXXX"
      data-payjp-on-created="callback_success"
      data-payjp-on-failed="callback_fail"
      data-payjp-partial="false"
      data-payjp-submit-text="プレミアムプランへ登録を確定する"
      data-payjp-text="クレジットカード情報を入力する"
      data-payjp-token-name="subscription[payjp_card_token]"
      src="https://checkout.pay.jp/prerelease"
      data-payjp-three-d-secure="true"
      data-payjp-three-d-secure-workflow="subwindow"
      data-payjp-extra-attribute-email
      data-payjp-extra-attribute-phone
    )

サブスクリプション開始

フロントでPay.jpのチェックアウトでカード情報がトークン化されて送信されたあとのコントローラ側の処理の例です。
また、今回はユーザーが初めての定期課金登録である前提となっていますので、必要であれば適宜既存の顧客やsubscriptionの存在チェックなどをしてください。

class Shop::SubscriptionsController < ApplicationController
  before_action :authenticate_user!

  def new
    @plans = [
      { id: "plan_light", name: "ライトプラン", amount: 1000 },
      { id: "plan_standard", name: "スタンダードプラン", amount: 3000 },
      { id: "plan_rich", name: "リッチプラン", amount: 10000 }
    ]
  end

  def create
    # ユーザーが選択したプランID (例: "plan_light")
    plan_id = subscription_params[:plan_id]
    unless plan_id.present?
      return redirect_to new_premium_subscription_path, alert: "プランを選択してください"
    end
    # カードトークンが渡されているはず
    if subscription_params[:payjp_card_token].blank?
      return redirect_to new_premium_subscription_path, alert: "カード情報が不正です"
    end

    # Pay.jp初期化
    Payjp.api_key = ENV['PAYJP_SECRET_KEY']

    # Pay.jpへ顧客を作成する。このとき、トークン化されたカード情報を渡すことでカードも同時に登録してくれる。
    customer = Payjp::Customer.create(card: subscription_params[:payjp_card_token])
    current_user.update!(payjp_customer_id: customer['id'])

    # サブスクリプション作成
    subscription = Payjp::Subscription.create(
      plan: plan_id,
      customer: customer['id'],
      prorate: false ## 日割り計算なし
    )

    # Rails側DBにサブスク情報保持(例: Subscriptionモデル)
    # Subscriptionテーブルには user_id, payjp_subscription_id, plan_id, status(有効/猶予中/解約済み), next_charge_at などを保持
    current_user.create_subscription!(
      payjp_subscription_id: subscription.id,
      plan_id: plan_id,
      status: "active", # 課金成功後、有効化
      next_charge_at: subscription.current_period_end ? Time.at(subscription.current_period_end) : nil
    )

    redirect_to dashboard_path, notice: "プレミアムプランに登録しました!"
  end
end

def subscription_params
  params.require(:subscription).permit(:payjp_card_token, :plan_id)
end

上記の流れで、ユーザーはプレミアムプラン契約が開始され、Pay.jp上でサブスクリプションが管理されるようになります。
また、Rails側ではとくに何もしていませんが、Pay.jp側ではこのタイミングで課金が発生しています。そのため、定期課金の詳細などを確認すると、実際に課金が発生していることがわかります。
また、webhookを登録している場合にはこのタイミングでイベントが送信されているはずです。

プランの解約と再開

解約処理例として、ユーザーがマイページから解約ボタンを押したときのフローは以下のように実装できます。
また、解約後に有効期限内であればsubscriptonを再開できるようにします。この場合は、カード情報の再登録などが不要です。

参考:定期課金のAPI

class Shop::SubscriptionsController < ApplicationController
  before_action :authenticate_user!

  # 解約
  def cancel
    unless current_user.subscription&.active?
      return redirect_to dashboard_path, alert: "プレミアムプランに未加入です"
    end

    Payjp.api_key = ENV['PAYJP_SECRET_KEY']
    subscription = Payjp::Subscription.retrieve(current_user.subscription.payjp_subscription_id)
    subscription.cancel

    # Rails側のDB更新: 状態をcanceledに変更し、期限日までは利用可とする
    current_user.subscription.update!(status: "canceled")

    redirect_to dashboard_path, notice: "プレミアムプランを解約しました。次回更新日まではご利用いただけます。"
  end

  # 有効期限内の再開処理
  def resume
    unless current_user.subscription&.canceled?
      return redirect_to dashboard_path, alert: "再開可能なサブスクリプションがありません"
    end

    Payjp.api_key = ENV['PAYJP_SECRET_KEY']
    subscription = Payjp::Subscription.retrieve(current_user.subscription.payjp_subscription_id)
    subscription.resume # 再開

    current_user.subscription.update!(status: "active")

    redirect_to dashboard_path, notice: "プレミアムプランを再開しました。"
  end
end

自動更新とその検知

月次の自動更新時にWebhookで送信されてきたデータを処理するコード例です。
event["data"]には、イベントの種類に応じてCharge(課金)オブジェクトやSubscription(定期課金)オブジェクトなどが存在します。

Railsにおける実装例:

# routes.rb
post '/webhooks/payjp', to: 'webhooks#payjp'

# webhooks_controller.rb
class WebhooksController < ApplicationController
  protect_from_forgery except: :payjp
  
  def payjp
    event = JSON.parse(request.body.read)
    case event["type"]
    when "charge.succeeded"
      # 課金成功時の処理
      # event["data"]にはchargeオブジェクトが存在する
      # 対応するユーザーを特定して課金成功の状態反映
      Charge.create(
        payjp_charge_id: event["data"]['id'],
        amount: event["data"]['amount'],
        payment_at: Time.zone.at(event["data"]['created']),
        paid: event["data"]['paid']
      ) 
    when "charge.failed"
      # 課金失敗時の処理
      # event["data"]にはchargeオブジェクトが存在する。失敗時の処理が必要であれば記載
    when "subscription.renewed"
      # 定期課金の自動更新の成功時のイベント。rails側でも定期課金の期間などを更新しておく
      # event["data"]にはsubscriptionオブジェクトが存在する。
      subscription = Subscription.find_by(payjp_subscription_id: event["data"]['id'])
      subscription.update!(next_charge_at: event["data"]['current_period_end'])
    when "subscription.paused"
      # 定期課金の自動更新の失敗時のイベント。Pay.jp側では一時停止となる
      # event["data"]にはsubscriptionオブジェクトが存在する。
      # 別途、猶予期間がきれたときには定期課金が終了する処理や、メール通知を行う処理を実装する
      subscription = Subscription.find_by(payjp_subscription_id: event["data"]['id'])
      subscription.update!(status: 'paused', grace_period_end_at: Time.current.since(5.days))
      SubscriptionMailer.notify_fail_renew(subscription).deliver_now
    else
      # 他にも様々なイベントがあるので必要に応じて対応
    end

    render json: {status: 'ok'}, status: 200
  end
end

まとめ

ここまでの流れで、Pay.jpを利用した定期課金システムの全体像から、

  • 必要なオブジェクトと概念整理
  • 実際のRails上でのコード例
  • Webhook活用による自動更新の検知

までをまとめてきました。
実際にサービスに組み込む際には、課金失敗時の処理をどうするかなどもう少し細かい設計について踏み込まないといけないとは思いますが、とりあえずPay.jpで定期課金を導入したいという点では一通り参考になるのではないかと思います!

また、今回の記事は一部ChatGPTにお手伝いしてもらいましたが、参考資料はPay.jpの公式ドキュメントとAPIドキュメント、payjp-rubyのライブラリのみとなります。

rubyライブラリについては、少々中身を直接読んで確認する必要があったものの、そのほかの公式ドキュメントは非常にわかりやすくてスムーズに調査・実装できましたので、定期課金に限らず、オンライン決済の導入に迷っている方は直接読んでみるとよいかと思います。
この記事が皆さんの助けになれば幸いです。
お読みいただきありがとうございました。

参考資料

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?