Stripe Checkout Webhookを使って、確保した在庫を戻す方法

# アプリケーションの流れ
1. ユーザーが購入ボタンをクリック
1. 在庫を確保
1. stripe決済画面へ遷移
1. ユーザーがアプリケーションから離脱
1. 30分経っても決済がない場合、確保した在庫を戻す
# チェックアウトセッションを作る

https://stripe.com/docs/payments/checkout/managing-limited-inventory
こちらのドキュメントを参考にしてセッションを作ります。

また**line_item**に**metadata**を設定して、後で商品idとwebhookを使って在庫のDB処理をします。

```php:CartController.blade.php
public function checkout()
    {
        $user = User::findOrFail(Auth::id());
        $products = $user->products;

        $lineItems = [];
        foreach ($products as $product) {
            $quantity = Stock::where('product_id', $product->id)->sum('quantity');

            if ($product->pivot->quantity > $quantity) {
                return redirect()->route('user.cart.index')->with(['message' => '在庫がなくなりました。', 'status' => 'alert']);
            } else {
                $lineItem = [
                    'quantity' => $product->pivot->quantity,
                    'price_data' => [
                        'currency' => 'jpy',
                        'unit_amount' => $product->price,
                        'product_data' => [
                            'name' => $product->name,
                            'description' => $product->information,
                            'metadata' => [
                                'product' => $product->id
                            ]
                        ],
                    ],
                ];
                array_push($lineItems, $lineItem);
            }
        }

        //Stripe決済画面に遷移する前に在庫を確保
        foreach($products as $product) {
            Stock::create([
                'product_id' => $product->id,
                'type' => \Constant::PRODUCT_LIST['reduce'],
                'quantity' => $product->pivot->quantity * -1,
            ]);
        }

        \Stripe\Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
        header('Content-Type: application/json');

        $session = \Stripe\Checkout\Session::create([
            'line_items' => [$lineItems],
            'mode' => 'payment',
            'expires_at' => time() + (1800),
            'success_url' => route('user.cart.success'),
            'cancel_url' => route('user.cart.cancel'),
        ]);

        $publicKey = env('STRIPE_PUBLIC_KEY');

        return view('user.checkout', compact('session', 'publicKey'));
    }
```

# ルーティング設定

```web.php
use App\Http\Controllers\WebhookController;

Route::post('/stripe/webhook', [WebhookController::class, 'webhook']);
```
# webhookの設定
stripeダッシュボード→開発者→webhook→エンドポイントの追加まで進み、エンドポイントURLとリッスンするイベントを設定します。

今回はStripeのWebhookを利用するにあたって、ローカルでHTTPSテスト環境をNGROKで構築しています。
NGROKで生成したURLを使用

エンドポイントには 「https://~~~~~~~~/stripe/webhook」と設定し、リッスンするイベントには「chekout.session.expired」を追加。

![スクリーンショット 2023-04-15 11.09.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3331147/a9d5c3ba-44a9-c088-07dd-89bfe0a5d9ea.png)


# webhookメソッドでDB処理

変数endpoint_secretにはエンドポイントを追加した際に署名シークレットというところに文字列があるのでそれを代入してください。
![スクリーンショット 2023-04-15 12.09.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3331147/c5bf56a3-049d-a0f5-4016-2a1c15d59aeb.png)

line_itemsを取得するにはallLineItemsメソッドの引数にセッションIDを入れるとデータが取れます。
```php
$stripe->checkout->sessions->allLineItems($session->id);
```
上記のままだとline_itemに設定したmetadataが取得できないので、allLineItemsメソッドの引数にもう一つ以下を加える。

```php
$stripe->checkout->sessions->allLineItems($session->id, ['expand' => ['data.price.product']]);
```


在庫処理する全体像
```WebhookController.php
<?php

namespace App\Http\Controllers;

use App\Models\Stock;
use Illuminate\Http\Request;

class WebhookController extends Controller
{
    public function webhook(Request $request)
    {
        \Stripe\Stripe::setApiKey(env('STRIPE_SECRET_KEY'));

        $endpoint_secret = env('STRIPE_WEBHOOK');

        $payload = $request->getContent();
        $sig_header = $request->header('stripe-signature');

        $event = null;
        try {
            $event = \Stripe\Webhook::constructEvent(
                $payload,
                $sig_header,
                $endpoint_secret
            );
        } catch (\UnexpectedValueException $e) {
            // Invalid payload.
            return response()->json('Invalid payload', 400);
        } catch (\Stripe\Exception\SignatureVerificationException $e) {
            // Invalid Signature.
            return response()->json('Invalid signature', 400);
        }

        //Stripe決済画面に遷移し、30分経っても決済がない場合、確保しておいた在庫を戻す
        if ($event->type == 'checkout.session.expired') {
            $session = $event->data->object;

            $stripe = new \Stripe\StripeClient(env('STRIPE_SECRET_KEY'));
            $line_items = $stripe->checkout->sessions->allLineItems($session->id, ['expand' => ['data.price.product']]);

            foreach ($line_items as $line_item) {
                $metadata = $line_item->price->product->metadata;
                $product_id = $metadata->product;
                $quantity = $line_item->quantity;

                Stock::create([
                    'product_id' => $product_id,
                    'type' => \Constant::PRODUCT_LIST['add'],
                    'quantity' => $quantity
                ]);
            }
        }

        return response()->json('ok', 200);
    }
}
```

# CSRF除外設定
最後にwebhookの処理時にpostメソッドを使いますが、CSRF対策できないので、Laravelで特定のルート（stripe/webhook）だけを除外します。

app/Http/Middleware/VerifyCsrfToken.phpを開いて以下を設定

```VerifyCsrfToken.php
protected $except = [
        'stripe/webhook'
    ];
```