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


## はじめに

**本記事はWebPayの定期課金機能が提供されていない頃に書かれたものです**
現在は[定期課金機能が提供されており](https://webpay.jp/docs/recursions)、以下の内容は提供されている定期課金機能を利用せずに定期課金を実現する方法について述べています。


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

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

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

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

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

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

では、上記の内容を[少しのコードでWebPayを導入する](http://qiita.com/hmsk/items/744f9a7e37638340a992)のサンプルを拡張して定期購読処理を、元のコードの少なさに負けないよう少ないコードで実装していきます。

- [hmsk/webpay-sinatra-sample](https://github.com/hmsk/webpay-sinatra-sample)
 - [前の記事からの差分](https://github.com/hmsk/webpay-sinatra-sample/compare/e10898e57febcc88ccc520b759d30a2c5880ba94...b0e55dce07e3fa49f1abe8e20024ab2eb191379b)

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

### 購読者を登録する

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

```ruby
class Subscriber < ActiveRecord::Base
  validates_presence_of :customer_id
end
```

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

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

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

```ruby
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
```

```haml
%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](https://qiita-image-store.s3.amazonaws.com/0/48/07af6b22-e2d0-c673-f971-8333756c4308.png "Screenshot 2013-12-13 07.19.35.png")


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

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

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

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

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

```ruby
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](https://qiita-image-store.s3.amazonaws.com/0/48/10120a03-20eb-85a7-169e-1d86d89228c0.png "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を結びつけて保存しておきます。

```diff
+gem 'uuid'
```

```diff
 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も控えておきながら、カウントアップに分岐を追加しています。

```diff
 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のオブジェクトが無いからカード番号を控えておかないと定期購読が出来なくなったわけではないことをお分かり頂けたなら幸いです。