はじめに
以前、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テーブル
ユーザテーブルには、次のように追加されます。
1 2 3 4 5 6 | 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に課金し、申し込まれる度に作成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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テーブル
サブスクリプションに紐づく課金アイテムが入ります。
1 2 3 4 5 6 7 8 9 10 | 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インストールすると、次のルーティングが追加されます。
1 2 3 4 5 | +--------+-----------+------------------------+------------------+------------------------------------------------------------------------+---------------------------------------------------+ | 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
クラスで、対象外に設定します。
1 2 3 4 5 6 7 8 9 10 11 | 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もセットで設定しましょう。