カテゴリー: BackEnd

Laravel Cashier サブスクリプションに使用するテーブルを理解する

はじめに

以前、Laravel Cashierを使用してサブスクリプションを実装する方法について説明しました。しかし、Laravel Cashierは単純なStripeのラッパークラスではありません。そのため、例えばクレジットカードの決済に失敗したとき、どのようにハンドリングされるのか、しっかりと把握しておく必要があります。
そこで、今回はLaravel Cashierが実際にどのようなテーブルを持ち、またStripeでサブスクリプションの自動更新が失敗した場合の仕組みについて説明したいと思います。

Laravel Cashierのテーブル

Laravel CashierはStripeのラッパーではありますが、自分でテーブルも持っています。Laravel Cashierのインストール後に、 php artisan migrateを実行すると、vendor/larave/cashier/database/migrations配下にあるマイグレーションが実行されます。
Subscriptionの作成時はStripeのAPIをコールしますが、課金状況のチェックなどは、基本的にテーブルの内容を見にいきます。
そのため、Laravel Cashierで使用されるテーブルが、どのような動きをするのか、把握しておくことが大切です。

usersテーブル

ユーザテーブルには、次のように追加されます。

        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();
        });

これらのカラムは、ユーザがSubscriptionに課金し、申し込んだときに作成されます。

  • stripe_id: string
    Stripeの顧客ID ( cus_**** )が入ります。
  • card_brand: string
    支払い方法に使用するカードブランドが入ります。
  • card_last_four: string
    支払い方法に使用するカードの下4桁がはいります。
  • trial_ends_at: timestamp
    支払い情報を登録せずに、試用させる場合に、試用終了時の日付が入ります。
    例えば、機能制限無しの期間限定試用版を実装する場合に使うことができます。

subscriptionsテーブル

subscriptionsテーブルは、ユーザがSubscriptionに課金し、申し込まれる度に作成されます。

        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']);
        });
  • user_id: unsigned big integer
    ユーザIDです。 ( Userクラスが、 Billableトレイトを使用すると、 $user->subscriptionsで取得できます。)
  • name: サブスクリプション名
    課金後に、サブスクリプションの状況などを取得するときに使用する名前です。サブスクリプション作成時に指定します。
    $user->subscribed('サブスクリプション名')といった形で使用します。(この場合は、サブスクリプション中であるかどうかをbooleanで返します。)
  • stripe_id: string
    StripeのサブスクリフションIDが入ります。( sub_***の形式です。)
  • stripe_status: string
    Stripeの課金状況が入ります。 $subscription->active()など、課金状況を確認するときに使用されるカラムです。
  • stripe_plan: string
    StripeのプランIDもしくは料金IDがあります。 ( plan_***もしくは price_***の形式です。)
  • quantity: integer
    サブスクリプションの数量です。
  • trial_ends_at: timestamp
    試用期間の終了日時です。
  • ends_at: timestamp
    課金の終了日時です。キャンセルすると、ここにサブスクリプションの最終日時が入ります。 cancelNow()メソッドでキャンセルした場合は、現在日時が入ります。

supscription_itemsテーブル

サブスクリプションに紐づく課金アイテムが入ります。

        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']);
        });

注意点としては、複数のアイテムがある場合は、 subscriptionsテーブルの、 stripe_planカラムと quantityプランはnullになります。

  • subscription_id: unsigned big integer
    subscriptionテーブルのidです。
  • stripe_id: string
    Stripeの定期支払いアイテムIDです。 ( si_***形式です。)
  • stripe_plan: string
    StripeのプランIDもしくは料金IDがあります。 ( plan_***もしくは price_***の形式です。)
  • quantity: integer
    数量が入ります。

課金情報の更新方法

実際には、利用者のクレジットカードのエラーにより、うまく自動更新できないことがあります。その場合は、Webhookを使用して、Stripe側からコールしてもらいます。
Laravel Cashierインストールすると、次のルーティングが追加されます。

+--------+-----------+------------------------+------------------+------------------------------------------------------------------------+---------------------------------------------------+
| Domain | Method    | URI                    | Name             | Action                                                                 | Middleware                                        |
+--------+-----------+------------------------+------------------+------------------------------------------------------------------------+---------------------------------------------------+
|        | POST      | stripe/webhook         | cashier.webhook  | Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook       |                                                   |
+--------+-----------+------------------------+------------------+------------------------------------------------------------------------+---------------------------------------------------+

このパスで、以下のWebhookを受け取ることができます。

  • customer.subscription.updated
  • customer.subscription.deleted
  • customer.updated
  • customer.deleted
  • invoice.payment_action_required

Stripeの管理画面で、これらのイベントを送信されるように設定します。
[Stripe管理画面]>[開発者]>[Webhook]>[アカウントからイベントを受信するイベント]>[+エンドポイントを追加] を開き、実際のアプリケーションのURLに、イベントが送信されるように設定します。

また、このWebhookはwebのルーティングに追加されるため、CSRF保護の対象外に設定する必要があります。 VerifyCsrfTokenクラスで、対象外に設定します。

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */     protected $except = [
         'stripe/*',
     ];
}

これで完了です。Stripeから決済失敗のWebhookが3回連続で届くと、先ほどの subscriptionsテーブルが更新され、課金が無効になります。

さいごに

いかがでしたか。課金情報の問い合わせのために、何度もStripeにAPIリクエストすることなく、Laravel Cashierは動作しますが、Webhookをしっかり実装しないと、自動更新の失敗を検知することができません。
Laravel Cashierを使用するときは、必ずWebhookもセットで設定しましょう。

おすすめ書籍

カイザー

シェア
執筆者:
カイザー

最近の投稿

Goの抽象構文木でコードを解析する

はじめに Goでアプリケーショ…

14時間 前

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

4週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前