アプリケーションの流れ
- ユーザーが購入ボタンをクリック
- 在庫を確保
- stripe決済画面へ遷移
- ユーザーがアプリケーションから離脱
- 30分経っても決済がない場合、確保した在庫を戻す
チェックアウトセッションを作る
https://stripe.com/docs/payments/checkout/managing-limited-inventory
こちらのドキュメントを参考にしてセッションを作ります。
またline_itemにmetadataを設定して、後で商品idとwebhookを使って在庫のDB処理をします。
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'));
}
ルーティング設定
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」を追加。
webhookメソッドでDB処理
変数endpoint_secretにはエンドポイントを追加した際に署名シークレットというところに文字列があるのでそれを代入してください。
line_itemsを取得するにはallLineItemsメソッドの引数にセッションIDを入れるとデータが取れます。
$stripe->checkout->sessions->allLineItems($session->id);
上記のままだとline_itemに設定したmetadataが取得できないので、allLineItemsメソッドの引数にもう一つ以下を加える。
$stripe->checkout->sessions->allLineItems($session->id, ['expand' => ['data.price.product']]);
在庫処理する全体像
<?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を開いて以下を設定
protected $except = [
'stripe/webhook'
];