Laravel Cashierでサブスクリプション(定期購読)をやってみる

サブスクリプション(定期購読)の実装をサポートするライブラリであるLaravel Cashierを触ってみます!
内部的にはStripeを利用して決済処理を行っている様です。

Laravel Cashierをインストールする

公式ドキュメントに従って、必要な物をインストールする
DBにテーブルを追加する必要があるので、php migrateでマイグレーションを流す。
以下の様な3つのマイグレーションが流れる。

        Schema::table('users', function (Blueprint $table) {
            $table->string('stripe_id')->nullable()->index();
            $table->string('card_brand')->nullable();
            $table->string('card_last_four', 4)->nullable();
            $table->timestamp('trial_ends_at')->nullable();
        });
        Schema::create('subscriptions', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('user_id');
            $table->string('name');
            $table->string('stripe_id');
            $table->string('stripe_status');
            $table->string('stripe_plan')->nullable();
            $table->integer('quantity')->nullable();
            $table->timestamp('trial_ends_at')->nullable();
            $table->timestamp('ends_at')->nullable();
            $table->timestamps();

            $table->index(['user_id', 'stripe_status']);
        });
        Schema::create('subscription_items', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('subscription_id');
            $table->string('stripe_id')->index();
            $table->string('stripe_plan');
            $table->integer('quantity');
            $table->timestamps();

            $table->unique(['subscription_id', 'stripe_plan']);
        });

MySQL WorkbenchでER図を生成するとこんな感じ。
subscriptionsテーブルには定期購読の情報が保存される。
subscription_itemsテーブルには定期購読のプランの情報が保存される。

stripeからAPIキーを取得する

公開キーと秘密キーを取得して.envに追加する。

.envで通貨が設定できるらしい。
en以外のロケールを使用する場合は、ext-intl拡張をインストールする必要があるらしい。

CASHIER_CURRENCY=ja_JP

stripeの顧客データ(Customer)を生成する

BillableトレイトのcreateOrGetStripeCustomer()メソッドで顧客データを生成できる

$stripeCustomer = $user->createOrGetStripeCustomer();

テストアプリではユーザ詳細画面を開いたら顧客データが生成される様にした。

実際に顧客データを生成してテーブルを確認してみる。
userテーブルのstripe_idカラムにIDが記録されているのが分かる。

Stripe上にも顧客データが登録されている。

支払い方法を保存する

支払い方法(カード情報)を登録するフォームを作成する。
この辺は前にも調べたなぁ
テストカード番号が用意されているのでこれを使う

$user->updateDefaultPaymentMethod($paymentMethod);

動かしてみるとconsoleにエラーが発生していた。
APIキーが誤っているらしい。

POST https://api.stripe.com/v1/setup_intents/seti_xxxx/confirm 401
{
  "error": {
    "message": "Invalid API Key provided: aaa",
    "type": "invalid_request_error"
  }
}

フロントでStripeを初期化する時にパブリックキーを渡す必要があった

const stripe = Stripe('pk_test_xxxxxxxxxxx');

改めて保存してみる。
userテーブルにカード情報が追加されていた
といっても表示用の一部の情報しかないので、実際に決済する場合はstripeサーバからpayment_methodのIDを取得しないといけない。

定期購読(Subscription)する

購入フォームを作った。
ボタンが1つあるだけ。

バックエンドの処理はこんな感じ。
paymentMethods()メソッドで保存済みの支払い方法を取得して、newSubscriptionメソッドで定期購読(Subscription)を作成している

$paymentMethods = $user->paymentMethods();
$paymentMethod  = $paymentMethods[0];
$user->newSubscription('subscription-A', 'plan-A')->create($paymentMethod);

動かしてみるとエラーが発生した。

urlencode() expects parameter 1 to be string, object given

createメソッドに、$paymentMethodではなく$paymentMethod->idを渡す必要があった。

$user->newSubscription('subscription-A', 'plan-A')->create($paymentMethod->id);

更にエラーが発生した。
plan-Aというプランは存在しないとの事。

No such plan: plan-A

先にstripeのダッシュボードでプランを作成しておく必要がある模様。
月額500円のプランを作った。

newSubscriptionメソッドの第二引数を修正した。

$user->newSubscription('subscription-A', 'price_xxxx')->create($paymentMethod->id);

購入処理が完了したのでテーブルを確認してみる。
descriptionテーブルに以下の様なレコードが追加されていた。

description_itemテーブルにも同様にレコードが作成されている。

stripe側にも支払いデータが登録されている。

顧客ページを確認してみると定期支払い情報が追加されていた。
次回のインボイスを見ると来月同日の請求になっている。

特に何も指定しないと定期購入は即座に課金されて、来月の同日に次の支払いが行われる模様。
図にするとこんな感じ。

プランの変更

月額1000円のプランを追加して、プランの切り替えを試してみる
画面はこんな感じ。

バックエンドの処理はこれだけ。
swapメソッドに変更先のプランのIDを渡す。

$user->subscription('subscription-A')->swap($planId);

プラン変更後、Stripeのダッシュボードを確認すると、保留中のインボイスが増えた。
月額498円の免除と月額997円の新しい請求が保留されている。
デフォルトだと即時請求ではないらしい。(swapAndInvoiceメソッドを使えば即時請求になるとの事。)
保留されている請求は、次の「請求サイクル」で処理される。
Prorations(比例配分)という仕組みにより月額料金が日割り計算されている。

subscriptionsテーブルとsubscription_itemsテーブルのstripe_plan列が変わっている
(この画像だとモザイクで分からないが・・・)

請求サイクルについて

サブスクリプションを作成した日がアンカー日、つまり請求日となる。
トライアル期間(試用期間)を含む場合は、トライアル期間の最終日がアンカー日となる

ドキュメント

定期購読のキャンセル

定期購読を解約してみる。

cancelメソッドを使用して解約する。

$user->subscription('subscription-A')->cancel();

キャンセルするとdescriptionテーブルのends_atカラムに日時が登録された。

stripe側では、次の請求日に解約される状態になった。

解約してから次の請求日までの期間をGrace Period(猶予期間)と呼ぶらしい。

reduceメソッドを試用すれば猶予期間中の契約を復元できる

直ちに定期購入を終了する

cancelNowメソッドを使えば、猶予期間無しで解約できる
実際に試してDBを確認すると、subscriptionsテーブルのstripe_statusカラムがactiveからcanceledに変わった。

stripeでもキャンセルされている。

数量指定をしてみる。

画面は定期購入画面を流用。

画像に alt 属性が指定されていません。ファイル名: image-208.png

定期購読契約時にquantityメソッドで数量を指定できる。
利用人数×月額n円の様な課金形態の場合に使用する

$user->newSubscription('subscription-A', $planId)->quantity($quantity)->create($paymentMethod->id);

月額1000円のプランを4個契約するとStripe上では以下の様になった。

subscriptionテーブルとsubscription_itemsテーブルには、新しいレコードが追加され、quantityカラムに数量が記録されていた。

トライアル(試用期間)を設定してみる

こちらも定期購入画面を流用。

5日間の試用期間を追加してみた。
今日が5/27で、トライアル終了日(最初の請求日)が6/1になった。
今日を含めて5日間のトライアル期間になるみたい。

subscriptionテーブルを確認すると、レコードが追加され、stripe_statusカラムにはtrialingが、trial_ends_atカラムにはトライアル終了日時が登録された。

トライアル(試用期間)を付与すると請求サイクルが後ろにずれる。

クーポンを使ってみる

まずはStripeのダッシュボードでクーポンを作成する。

半額になるクーポンを作成した。
IDが発行されるので控える。

画面にクーポンID入力欄を追加する

月額500円のプランを半額クーポンを使用して契約してみる
ちゃんと半額の250円分の支払いが発生した。

内訳を見るとクーポンが適応されているのがわかる。

DBには特に変わった箇所は無く、クーポンに関する情報はStripe側でのみ管理されている模様。

税率を設定してみる

Stripeのダッシュボードから税率を作成する。
消費税という名目で8%で作成した。
あ、今はもう10%か・・・。

Billingトレイトを継承しているモデルにtaxRatesメソッドを追加して適応したい税率IDを返す

    public function taxRates()
    {
        return ['txr_xxxx'];
    }

実際に月額1000円のプランを定期購読してStripeで確認してみると1080円になっていた。

内訳にも消費税が記載されている。

複数プランの同時購読

月額500円のプランと月額1000円のプランを同時に購読してみる。
処理はこんな感じ。

        $user->newSubscription('subscription-A', [
            'price_aaaa',
            'price_bbbb'
        ])->create($paymentMethod->id);

Stripe上で確認すると複数のプランを同時に購読できている事がわかる。

subscriptionsテーブルは、これまで通り、1つのレコードが追加されている。

subscription_itemsテーブルにはレコードが2つ追加された。
プラン毎にレコードが追加される仕様らしい。

所感

Stripeすげぇ

コメントする