locateStripeSession($intent); if (! $session) { return false; } $lock = Cache::lock("checkout:webhook:stripe:{$intentId}", 30); if (! $lock->get()) { Log::info('[CheckoutWebhook] Stripe intent lock busy', [ 'intent_id' => $intentId, 'session_id' => $session->id, ]); return true; } try { $session->forceFill([ 'stripe_payment_intent_id' => $session->stripe_payment_intent_id ?: $intentId, 'provider' => CheckoutSession::PROVIDER_STRIPE, ])->save(); $metadata = [ 'stripe_last_event' => $eventType, 'stripe_last_event_id' => $event['id'] ?? null, 'stripe_intent_status' => $intent['status'] ?? null, 'stripe_last_update_at' => now()->toIso8601String(), ]; $this->mergeProviderMetadata($session, $metadata); return $this->applyStripeIntent($session, $eventType, $intent); } finally { $lock->release(); } } public function handlePayPalEvent(array $event): bool { $eventType = $event['event_type'] ?? null; $resource = $event['resource'] ?? []; if (! $eventType || ! is_array($resource)) { return false; } $orderId = $resource['order_id'] ?? $resource['id'] ?? null; $session = $this->locatePayPalSession($resource, $orderId); if (! $session) { return false; } $lockKey = "checkout:webhook:paypal:".($orderId ?: $session->id); $lock = Cache::lock($lockKey, 30); if (! $lock->get()) { Log::info('[CheckoutWebhook] PayPal lock busy', [ 'order_id' => $orderId, 'session_id' => $session->id, ]); return true; } try { $session->forceFill([ 'paypal_order_id' => $orderId ?: $session->paypal_order_id, 'provider' => CheckoutSession::PROVIDER_PAYPAL, ])->save(); $metadata = [ 'paypal_last_event' => $eventType, 'paypal_last_event_id' => $event['id'] ?? null, 'paypal_last_update_at' => now()->toIso8601String(), 'paypal_order_id' => $orderId, 'paypal_capture_id' => $resource['id'] ?? null, ]; $this->mergeProviderMetadata($session, $metadata); return $this->applyPayPalEvent($session, $eventType, $resource); } finally { $lock->release(); } } protected function applyStripeIntent(CheckoutSession $session, string $eventType, array $intent): bool { switch ($eventType) { case 'payment_intent.processing': case 'payment_intent.amount_capturable_updated': $this->sessions->markProcessing($session, [ 'stripe_intent_status' => $intent['status'] ?? null, ]); return true; case 'payment_intent.requires_action': $reason = $intent['next_action']['type'] ?? 'requires_action'; $this->sessions->markRequiresCustomerAction($session, $reason); return true; case 'payment_intent.payment_failed': $failure = $intent['last_payment_error']['message'] ?? 'payment_failed'; $this->sessions->markFailed($session, $failure); return true; case 'payment_intent.succeeded': if ($session->status !== CheckoutSession::STATUS_COMPLETED) { $this->sessions->markProcessing($session, [ 'stripe_intent_status' => $intent['status'] ?? null, ]); $this->assignment->finalise($session, [ 'source' => 'stripe_webhook', 'stripe_payment_intent_id' => $intent['id'] ?? null, 'stripe_charge_id' => $this->extractStripeChargeId($intent), ]); $this->sessions->markCompleted($session, now()); } return true; default: return false; } } protected function applyPayPalEvent(CheckoutSession $session, string $eventType, array $resource): bool { switch ($eventType) { case 'CHECKOUT.ORDER.APPROVED': $this->sessions->markProcessing($session, [ 'paypal_order_status' => $resource['status'] ?? null, ]); return true; case 'PAYMENT.CAPTURE.COMPLETED': if ($session->status !== CheckoutSession::STATUS_COMPLETED) { $this->sessions->markProcessing($session, [ 'paypal_order_status' => $resource['status'] ?? null, ]); $this->assignment->finalise($session, [ 'source' => 'paypal_webhook', 'paypal_order_id' => $resource['order_id'] ?? null, 'paypal_capture_id' => $resource['id'] ?? null, ]); $this->sessions->markCompleted($session, now()); } return true; case 'PAYMENT.CAPTURE.DENIED': $this->sessions->markFailed($session, 'paypal_capture_denied'); return true; default: return false; } } protected function mergeProviderMetadata(CheckoutSession $session, array $data): void { $session->provider_metadata = array_merge($session->provider_metadata ?? [], $data); $session->save(); } protected function locateStripeSession(array $intent): ?CheckoutSession { $intentId = $intent['id'] ?? null; if ($intentId) { $session = CheckoutSession::query() ->where('stripe_payment_intent_id', $intentId) ->first(); if ($session) { return $session; } } $metadata = $intent['metadata'] ?? []; $sessionId = $metadata['checkout_session_id'] ?? null; if ($sessionId) { return CheckoutSession::find($sessionId); } return null; } protected function locatePayPalSession(array $resource, ?string $orderId): ?CheckoutSession { if ($orderId) { $session = CheckoutSession::query() ->where('paypal_order_id', $orderId) ->first(); if ($session) { return $session; } } $metadata = $this->extractPayPalMetadata($resource); $sessionId = $metadata['checkout_session_id'] ?? null; if ($sessionId) { return CheckoutSession::find($sessionId); } return null; } protected function extractPayPalMetadata(array $resource): array { $customId = $resource['custom_id'] ?? ($resource['purchase_units'][0]['custom_id'] ?? null); if ($customId) { $decoded = json_decode($customId, true); if (is_array($decoded)) { return $decoded; } } $meta = Arr::get($resource, 'supplementary_data.related_ids', []); return is_array($meta) ? $meta : []; } protected function extractStripeChargeId(array $intent): ?string { $charges = $intent['charges']['data'] ?? null; if (is_array($charges) && count($charges) > 0) { return $charges[0]['id'] ?? null; } return null; } }