【Laravel × Stripe】サブスクリプション処理を実装する手順３~Webhookで決済処理~

【Laravel × Stripe】サブスクリプション処理を実装する手順２~CheckOutで決済ページの表示~
の続きです。

https://qiita.com/takahiro-2020/items/056c927c5b85aca27f9c

https://qiita.com/takahiro-2020/items/b3473a2fb4f78c425ab9

https://qiita.com/takahiro-2020/items/3df331abc855537c246d

https://qiita.com/takahiro-2020/items/cf3be9352e1a1525a83c


<br>


webhookは、Stripeがイベント（支払いの成功、サブスクリプションの更新など）を検出した際に、事前に設定したURL（エンドポイント）にHTTPリクエストを送信する仕組みです。これにより、リアルタイムでStripeのイベントに応答し、データベースの更新やメール通知などを自動的に行うことができます。
<br>

つまり、Striepe側で決済を実行し、成功の有無をプロジェクト側に受け取ることができます。
<br>

ここでは決済が成功したときに、webhookからレスポンスを受け取り、userテーブルにサブスク契約中であることを登録する処理を記述していきます。
<br>



## Stripeダッシュボードからwebhookのエンドポイントを登録する

> エンドポイントとは、webhookを受け取るURLのことです。
> ここでは、[ドメインURL]/stripe/subscription/webhook
<br>


`ルーティング(シリーズ１のおさらい)`
```php
# 決済完了ウェブホック 
/*  */
Route::post('stripe/subscription/webhook',
[StripSubscriptionController::class, 'webhook']);
```
<br>




Stripeのダッシュボードを開き、[開発者]->[エンドポイント]を開き、[エンドポイントを追加]をクリックします。

![スクリーンショット 2024-08-12 10.17.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1248327/6ab0d92f-ca8a-da40-940a-72f8490ddddf.png)

<br>



エンドポイントURLを入力・イベントを選択し、[イベントを追加]ボタンをクリックします。



##### エンドポイントURL：
[ドメインURL]/stripe/subscription/webhook

##### イベント：
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
<br>

![スクリーンショット 2024-08-12 10.20.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1248327/8a8eb3fd-553b-0be5-e6b5-4bb897307fb6.png)

<br>


署名シークレットをコピーしてプロジェクトに保存します。


`.env`
```
STRIPE_SUBSCRIPTION_ENDPOINT_SECRET=[サブスクエンドポイント証明シークレット]
```

`config/stripe.php`
```php
<?php
return [
    'public_key' => env('STRIPE_KEY'),
    'secret_key' => env('STRIPE_SECRET'),
    'price_id'   => env('STRIPE_PRICE_ID'),

    //追加
    'subscription_endpoint_secret'=> 
    env('STRIPE_SUBSCRIPTION_ENDPOINT_SECRET'),
];
```


## csrf除外処理
` App\Http\Middleware\VerifyCsrfToken.php`
```php
<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
     protected $except = [

     
        # サブスク決済完了ウェブホック 
        'stripe/subscription/webhook',
        
        
    ];
}

```


## webhookを受け取る処理


 ### webhookを受け取り、イベントごとに処理を分岐
`コントローラー：StripSubscriptionController.php`
```php
/**
* サブスク決済完了ウェブホック
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
/
public function webhook(Request $request)
{
    payload = json_decode($request->getContent(), true);

    # Stripeの署名を検証する
    $sigHeader = $request->header('Stripe-Signature');
    $endpointSecret = config('stripe.subscription_endpoint_secret'); //エンドポイントシークレット


    $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
    $event = null;

    try {
        $event = \Stripe\Webhook::constructEvent(
        $payload, $sig_header, $endpoint_secret
      );
    } catch(\UnexpectedValueException $e) {
          // Invalid payload
          http_response_code(400);
          exit();
    } catch(\Stripe\Exception\SignatureVerificationException $e) {
          // Invalid signature
          http_response_code(400);
          exit();
    }



    # イベント別の分岐処理
    switch ($event->type) 
    {
        
        ## サブスクが更新されました
        case 'customer.subscription.updated':

            /* サブスクの[契約更新]フルフィルメントを実行 */
            $session = $event->data->object;
            return $this->handleCoustomerSubscriptionUpdate($request, $session);
            break;


         ## サブスクが削除されました
        case 'customer.subscription.deleted':

            /* サブスクの[契約キャンセル]フルフィルメントを実行 */
            $session = $event->data->object;
            return $this->handleCoustomerSubscriptionDeleted($request, $session);
            break;


        default:
            // 未知のイベントに対する処理
            return response([], 200);
            break;

    }
               
}
```



### イベントに応じたフィルフィメント処理
`コントローラー：StripSubscriptionController.php`
```php
    /**
     * サブスクの[契約更新]フルフィルメントを実行するためのコード
     *
     * @param  Object $session //Stripe Checkout Session オブジェクト
     * @return Json
     */
    private function handleCoustomerSubscriptionUpdate($request, $session)
    {
        # CheckoutSessionが処理済みの時は、スキップ
        $session_id = $session['id'];
        $column     = 'stripe_checkout_session_id';
        $check_day  = now()->copy()->subHour(23);



        # 客の情報
        $user = User::where('stripe_id', $session['customer'])->first();
        if( !$user ){
            return response(['message' => '一致するユーザー情報がありません。'], 403);
        }


        # サブスク登録の確認
        if( $user->subscription_id ){
            return response(['message' => 'ユーザーのサブスク契約すみです。'], 403);
        }



        # 新規契約のとき=>UserにサブスクIDを保存
        $subscription_id = $session->items->data[0]->plan['id'];
        $is_update = isset( $user->subscription_id );
        if( !$is_update || $user->subscription_id != $subscription_id ){
            $user->update(['subscription_id'=>$subscription_id]);
        }


        # 200レスポンスを返す
        return response( compact('user','subscription','point_history','ticket_history'), 200);
    }



    /**
     * サブスクの[契約キャンセル]フルフィルメントを実行するためのコード
     *
     * @param  Object $session //Stripe Checkout Session オブジェクト
     * @return Json
     */
    private function handleCoustomerSubscriptionDeleted($request, $session)
    {
        # サブスクプランの情報
        $subscription_id = $session->items->data[0]->plan['id'];
        $subscription    = self::Subscriptions()[$subscription_id];
        if( !$subscription ){
            return response(['message' => 'サブスク情報がアプリケーション側で登録されていません。'], 403);
        }

        # 客の情報
        $user = User::where('stripe_id', $session['customer'])->first();
        if( !$user ){
            return response(['message' => '一致するユーザー情報がありません。'], 403);
        }

        # UserkaraサブスクIDを削除
        if( !$user->subscription_id ){
            return response(['message' => 'ユーザーのサブスク契約情報が登録されていません。'], 403);
        }
        $user->update(['subscription_id'=>null]);//削除


        # 200レスポンスを返す
        return response( compact('user','subscription'), 200);
    }

```



長くなったので、次回はカスタマーポータルを表示する処理について記述していきます。

https://qiita.com/takahiro-2020/items/cf3be9352e1a1525a83c