getContent(); $secret = (string) config('services.stripe.webhook_secret'); if (filled($secret) && ! $this->signatureValid($payload, (string) $request->header('Stripe-Signature'), $secret)) { abort(400, 'invalid signature'); } $event = json_decode($payload, true) ?: []; $object = $event['data']['object'] ?? []; // Handle exactly ONE canonical event (a card checkout also emits // payment_intent.succeeded with a different id) and only when actually // paid — async methods complete the session while still unpaid. if (($event['type'] ?? '') !== 'checkout.session.completed' || ($object['payment_status'] ?? null) !== 'paid') { return response()->json(['ignored' => true]); } $meta = $object['metadata'] ?? []; $action->fromStripeEvent([ 'id' => (string) ($event['id'] ?? ''), 'email' => $object['customer_details']['email'] ?? $object['customer_email'] ?? ($meta['email'] ?? 'unknown@example.com'), 'name' => $object['customer_details']['name'] ?? ($meta['name'] ?? null), 'stripe_customer_id' => $object['customer'] ?? null, 'plan' => $meta['plan'] ?? 'start', 'datacenter' => $meta['datacenter'] ?? 'fsn', 'amount_cents' => (int) ($object['amount_total'] ?? $object['amount'] ?? 0), 'currency' => strtoupper($object['currency'] ?? 'eur'), ]); return response()->json(['ok' => true]); } /** Verify Stripe's `t=…,v1=…` signature: HMAC-SHA256 of "{t}.{payload}". */ private function signatureValid(string $payload, string $header, string $secret): bool { if (blank($header)) { return false; } $parts = []; foreach (explode(',', $header) as $segment) { [$key, $value] = array_pad(explode('=', $segment, 2), 2, ''); $parts[$key] = $value; } $timestamp = $parts['t'] ?? null; $signature = $parts['v1'] ?? null; if ($timestamp === null || $signature === null) { return false; } $expected = hash_hmac('sha256', $timestamp.'.'.$payload, $secret); return hash_equals($expected, $signature); } }