Help us understand the problem. What is going on with this article?

WebPayの定期課金機能を使わずに定期課金(定期購読)処理を行う

More than 5 years have passed since last update.

はじめに

本記事はWebPayの定期課金機能が提供されていない頃に書かれたものです
現在は定期課金機能が提供されており、以下の内容は提供されている定期課金機能を利用せずに定期課金を実現する方法について述べています。

定期課金が出来ないわけではない

WebPayはStripeと何が互換しているのか でも触れましたが、現在WebPayにはSubscriptionなるオブジェクトまわりの一切が存在しない状態のため、定期課金を目的とする場合に一手間必要になりますが、定期課金の引き落としのタイミングの処理が必要になる点だけで、単純に課金を行う場合に対してそんなにコストの差は無いかもしれません。

CheckoutHelperでカード情報をトークン化して、課金を行った場合、トークンを利用したタイミングでそのトークンは有効性を失います。
しかし、Customer(顧客)オブジェクトを作成して、そのidを利用すれば(最長でもカードの有効期限までですが)永続的に登録されたカード情報を利用して課金を行えます。

この情報(Customerのid)を控えておき、任意のタイミングで課金を発生させることで、定期課金のような処理が可能になります。

少し話しが逸れますが、このCustomerのidをサーバサイドトークンと私たちは呼称していますが、CheckoutHelperなどでつくられるTokenオブジェクトをクライアントサイドトークンと呼んでいますが
「クライアントサイドトークンを使ってサーバサイドトークンをつくる」というような説明になってしまい、なかなか混乱を招く側面が多く、代替となる良い命名を探しています。

少しのコードでWebPayを導入して定期課金を行う

では、上記の内容を少しのコードでWebPayを導入するのサンプルを拡張して定期購読処理を、元のコードの少なさに負けないよう少ないコードで実装していきます。

定期的な処理のトリガーまわり(タイミングや課金対象判定といった部分)は今回の範疇には入れていませんのでご注意ください。

購読者を登録する

購読者(Subscriber)を登録出来るようにデータベースを取り扱えるようにして、ActiveRecordを通して触るようにしました。Sinatra-ActiveRecordを使っています。
SubscriberにはWebPayで作成したCustomerオブジェクトのidを保存するためだけのカラム(customer_id)を準備しています。

class Subscriber < ActiveRecord::Base
  validates_presence_of :customer_id
end

用意したエンドポイントは

  • 購読者リストと新規登録用のインタフェースの表示
  • 新規購読処理(WebPayにCustomerをつくりidを控えて保存する)

の2点だけで以下のようになりました。

get '/subscribers' do
  @subscribers = Subscriber.all
  haml :subscribers
end

post '/subscribe' do
  begin
    customer = WebPay::Customer.create(card: params['webpay-token'])
    Subscriber.create(customer_id: customer.id)
    redirect to('/subscribers')
  rescue => e
    redirect to('/')
  end
end
%h4 購読者
%ul
  - @subscribers.each do |subscriber|
    %li= subscriber.customer_id

%h4 新規登録
%form{ action: '/subscribe', method: 'post' }
  %script{ src: 'https://checkout.webpay.jp/v1/', class: 'webpay-button', :'data-text' => 'WEB+DB PRESS を定期購読者を追加する', :'data-submit-text' => 'このカードで翌号から購読する', :'data-key' => WEBPAY_PUBLIC_KEY }

CheckoutHelperにべったりですが、割と短いコードでCustomerの作成して控えるところまで出来てしまいました。

Screenshot 2013-12-13 07.19.35.png

任意のタイミングで課金を行う

さて、課金の処理は控えたCustomerのidを渡してChargeを作成するだけです。例えばCustomerのidがcus_blg5tr9KV2ARbXPだとすると

WebPay::Charge.create(currency: 'jpy', amount: PRICE, customer: 'cus_blg5tr9KV2ARbXP')

を行うだけで課金を行えます。

このエンドポイントベースで更新処理をすることは無いかもしれませんが、デモまでに。
以下のエンドポイントを叩いたら全購読者に課金が行われるようにしました。
ついでに課金数をカウント出来るようにカラムを増やしています。

post '/renew' do
  begin
    Subscriber.all.each do |subscriber|
      charge = WebPay::Charge.create(currency: 'jpy', amount: WEBDB_PRESS_PRICE, customer: subscriber.customer_id)
      if charge.paid
        subscriber.charge_count += 1
        subscriber.save
      end
      sleep 1
    end
    redirect to('/subscribers')
  rescue => e
    redirect to('/')
  end
end

Screenshot 2013-12-13 08.36.14.png

多重課金を防ぐ

バッチとかで走らせて、途中でこけたら再開した時に多重課金とか怖いし、障害怖いよね(というユーザのご意見を頂きました)というのを解消するために、最近さりげなくuuidというパラメータが各オブジェクトの作成時に利用出来るようになっています。

APIドキュメントには

uuid:任意 デフォルトはnull

RFC4122に準拠したUUID(例:"f81d4fae-7dec-11d0-a765-00a0c91e6bf6")を設定すると、同じUUIDを持つリクエストが複数回送信されたとき、24時間の間に高々一度だけ処理がおこなわれることを保証します。以前の同じUUIDを持つリクエストで作成済みの課金がある場合は、それを通常の作成時と同じように返却します。

と書いている通り
「24時間以内にuuidが既に作成しているオブジェクトと被ってると新規作成せずに作成してあるものが返って来る」というようになっています。

今回触っているプロジェクトでuuidを使えるようにしてみます。
uuidをどのように利用するかはいくらか形が分かれそうですが、以下は「永続的にCustomerとuuidを手元で結びつけておき、Chargeの作成時に利用する」場合です。

まずCustomerを作成する際にUUIDを結びつけて保存しておきます。

+gem 'uuid'
 post '/subscribe' do
   begin
     customer = WebPay::Customer.create(card: params['webpay-token'])
-    Subscriber.create(customer_id: customer.id)
+    Subscriber.create(customer_id: customer.id, uuid: UUID.new.generate)
     redirect to('/subscribers')
  rescue => e
    redirect to('/')
  end
end

そして課金の際にuuidを渡します。これで24時間ほどの間同じuuidに対してchargeは1つしか作成出来なくなるので、それを利用して最後に作成したChargeのidも控えておきながら、カウントアップに分岐を追加しています。

 post '/renew' do
   begin
     Subscriber.all.each do |subscriber|
-      charge = WebPay::Charge.create(currency: 'jpy', amount: WEBDB_PRESS_PRICE, customer: subscriber.customer_id)
-      if charge.paid
+      charge = WebPay::Charge.create(currency: 'jpy', amount: WEBDB_PRESS_PRICE, customer: subscriber.customer_id, uuid: subscriber.uuid)
+      if charge.id != subscriber.last_charge_id
         subscriber.charge_count += 1
+        subscriber.last_charge_id = charge.id
         subscriber.save
       end
      sleep 1
    end
    redirect to('/subscribers')
  rescue => e
    redirect to('/')
  end
end

これで24時間の間はWEB+DB PRESSが1冊ずつしか売られなくなりました。間違って2回バッチを実行したり、途中で変に止まっておもむろに再開しても事故を避けられそうです。

最終的なデモはこんな風になっています
- https://webpay-sinatra-sample.herokuapp.com/subscribers

というわけで今回は定期課金を行う場合を想定して、進めて参りましたがいかがだったでしょうか。Subscriptionのオブジェクトが無いからカード番号を控えておかないと定期購読が出来なくなったわけではないことをお分かり頂けたなら幸いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away