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

本記事はWebPayの定期課金機能を使わずに定期課金(定期購読)処理を行う - Qiitaの再録です。 Qiitaの方はWebPay Advent Calendar 2013のものであるため当時の状態を維持し こちらの記事では最新の情報に合わせて加筆、編集を加えております。

はじめに

WebPayは2014年5月より定期課金機能を提供していますが、この機能はシンプルなケースに対応することを目的としています。 サービスの構成によっては定期課金機能(Recursionオブジェクト)だけではまかないきれない場合があります。 そんなときに、提供されている定期課金機能を利用せずに定期課金を実現する方法について述べています。

定期課金を自作する方針

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

より柔軟な定期課金処理をしたい場合も、このCustomerのIDを記録しておき、Customerに対して任意のタイミングで課金を発生させることで実現できます。

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

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

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

定期的な処理のトリガーまわり(タイミングや課金対象判定といった部分)は今回の範疇には入れていませんのでご注意ください。 実際には課金対象かどうか、いくら課金するかをを保存してあるデータを参照して判断する処理を追加し、処理を定期的に実行するcronのような仕組みに乗せることになるでしょう。

購読者を登録する

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

1
2
3
class Subscriber < ActiveRecord::Base
  validates_presence_of :customer_id
end

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
1
2
3
4
5
6
7
8
%h4 購読者
%ul
  - @subscribers.each do |subscriber|
    %li= subscriber.customer_id

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

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

images

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

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

1
webpay.charge.create(currency: 'jpy', amount: PRICE, customer: 'cus_blg5tr9KV2ARbXP')

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
post '/renew' do
  begin
    Subscriber.all.each do |subscriber|
      charge = webpay.charge.create(currency: 'jpy', amount: WEBDB_PRESS_PRICE, customer: subscriber.customer_id, uuid: subscriber.uuid)
      if charge.paid
        subscriber.charge_count += 1
        subscriber.save
      end
      sleep 1
    end
    redirect to('/subscribers')
  rescue => e
    redirect to('/')
  end
end

images

多重課金を防ぐ

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

APIドキュメントには

uuid:任意 デフォルトはnull

RFC4122に準拠したUUID(例:“f81d4fae-7dec-11d0-a765-00a0c91e6bf6”)を設定すると、同じUUIDを持つリクエストが複数回送信されたとき、24時間の間に高々一度だけ処理がおこなわれることを保証します。以前の同じUUIDを持つリクエストで作成済みの課金がある場合は、それを取得したときと同様に返却します。たとえ以前の課金が失敗していても、再送時のレスポンスはエラーレスポンスにはならず、課金オブジェクトがそのまま返されます。課金の可否を判断するために、"paid"プロパティがtrueになっていることを必ず確認してください。

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

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

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

1
+gem 'uuid'
1
2
3
4
5
6
7
8
9
10
 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も控えておきながら、カウントアップに分岐を追加しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 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回バッチを実行したり、途中で変に止まっておもむろに再開しても事故を避けられそうです。

最終的なデモはこんな風になっています

というわけで今回は定期課金を行う場合を想定して、進めて参りましたがいかがだったでしょうか。 実際には、今回紹介した処理は単純なのでRecursionオブジェクトを利用しても実現できます。 毎月の課金額が異なる場合や、課金期間が毎週だったり、2月ごとだったりとRecursionオブジェクトだけでは対応できない場合、本記事を参考に実装してみてください。