all(); // Optional, mirrors bnf_event's `{encryptOrderId?}` — a redundant, // signature-independent way to locate the booking directly from the // URL (used by ConfirmPaymentAction, T5.10) alongside whatever // order id the gateway's own signed payload carries. Never fatal if // it's missing or fails to decrypt; the signature check is what // actually authenticates this request. $bookingId = $this->decryptBookingId($encryptBookingId); try { $result = $this->gateways->make($method)->handleWebhook($payload); } catch (InvalidWebhookSignatureException $exception) { // Raw payload persisted regardless of outcome (domain.md §6). Log::warning('Payment webhook rejected: invalid signature', [ 'gateway' => $method->value, 'booking_id' => $bookingId, 'payload' => $payload, ]); throw $exception; } Log::info('Payment webhook received', [ 'gateway' => $method->value, 'booking_id' => $bookingId, 'status' => $result->status->value, 'gateway_transaction_id' => $result->gatewayTransactionId, 'payload' => $payload, ]); $payment = $result->gatewayTransactionId !== null ? $this->confirmPaymentAction->handle($method, $result->gatewayTransactionId) : null; if ($payment === null) { Log::warning('Payment webhook has no matching payment to confirm', [ 'gateway' => $method->value, 'gateway_transaction_id' => $result->gatewayTransactionId, ]); } // KBZ retries any delivery that doesn't get back this exact literal // body — we acknowledge regardless of whether a matching payment // was found, since retrying won't fix that mismatch. return response('success', 200); } private function decryptBookingId(?string $encryptBookingId): ?int { if ($encryptBookingId === null) { return null; } try { return (int) Crypt::decryptString($encryptBookingId); } catch (DecryptException) { return null; } } }