extractMetadata($payload); $intentId = $metadata['addon_intent'] ?? null; $addonKey = $metadata['addon_key'] ?? null; if (! $intentId || ! $addonKey) { return false; } $transactionId = $data['id'] ?? null; $checkoutId = data_get($data, 'attributes.checkout_id') ?? null; $addon = EventPackageAddon::query() ->where('addon_key', $addonKey) ->where(function ($query) use ($intentId, $checkoutId, $transactionId) { $query->where('metadata->addon_intent', $intentId) ->orWhere('checkout_id', $checkoutId) ->orWhere('transaction_id', $transactionId); }) ->latest('id') ->first(); if (! $addon) { Log::info('[AddonWebhook] Add-on intent not found', [ 'intent' => $intentId, 'addon_key' => $addonKey, 'transaction_id' => $transactionId, ]); return false; } if ($addon->status === 'completed') { return true; // idempotent } $increments = $this->resolveAddonIncrements($addon, $addonKey); DB::transaction(function () use ($addon, $transactionId, $checkoutId, $data, $increments) { $addon->forceFill([ 'transaction_id' => $transactionId, 'checkout_id' => $addon->checkout_id ?: $checkoutId, 'status' => 'completed', 'amount' => $this->resolveAmount($data), 'currency' => Arr::get($data, 'attributes.currency') ?? Arr::get($data, 'currency'), 'metadata' => array_merge($addon->metadata ?? [], ['webhook_payload' => $data]), 'receipt_payload' => Arr::get($data, 'attributes.urls.receipt') ? ['receipt_url' => Arr::get($data, 'attributes.urls.receipt')] : null, 'purchased_at' => now(), ])->save(); /** @var EventPackage $eventPackage */ $eventPackage = EventPackage::query() ->lockForUpdate() ->find($addon->event_package_id); if (! $eventPackage) { return; } $eventPackage->forceFill([ 'extra_photos' => ($eventPackage->extra_photos ?? 0) + (int) ($increments['extra_photos'] ?? 0) * $addon->quantity, 'extra_guests' => ($eventPackage->extra_guests ?? 0) + (int) ($increments['extra_guests'] ?? 0) * $addon->quantity, 'extra_gallery_days' => ($eventPackage->extra_gallery_days ?? 0) + (int) ($increments['extra_gallery_days'] ?? 0) * $addon->quantity, ]); if (($increments['extra_gallery_days'] ?? 0) > 0) { $base = $eventPackage->gallery_expires_at ?? now(); $eventPackage->gallery_expires_at = $base->copy()->addDays((int) ($increments['extra_gallery_days'] ?? 0) * $addon->quantity); } $eventPackage->save(); $tenant = $addon->tenant; if ($tenant) { Notification::route('mail', [$tenant->contact_email ?? $tenant->user?->email]) ->notify(new AddonPurchaseReceipt($addon)); $opsEmail = config('mail.ops_address'); if ($opsEmail) { Notification::route('mail', $opsEmail)->notify(new AddonPurchased($addon)); } } }); return true; } /** * @param array $data * @return array */ private function extractMetadata(array $data): array { $metadata = []; if (isset($data['meta']['custom_data']) && is_array($data['meta']['custom_data'])) { $metadata = $data['meta']['custom_data']; } if (isset($data['metadata']) && is_array($data['metadata'])) { $metadata = array_merge($metadata, $data['metadata']); } if (isset($data['attributes']['custom_data']) && is_array($data['attributes']['custom_data'])) { $metadata = array_merge($metadata, $data['attributes']['custom_data']); } return $metadata; } private function resolveAmount(array $data): ?float { $total = Arr::get($data, 'attributes.total'); if ($total === null || $total === '') { return null; } if (! is_numeric($total)) { return null; } return round(((float) $total) / 100, 2); } /** * @return array */ private function resolveAddonIncrements(EventPackageAddon $addon, string $addonKey): array { $stored = Arr::get($addon->metadata ?? [], 'increments', []); if (is_array($stored) && $stored !== []) { $filtered = collect($stored) ->map(fn ($value) => is_numeric($value) ? (int) $value : 0) ->filter(fn ($value) => $value > 0) ->all(); if ($filtered !== []) { return $filtered; } } return $this->catalog->resolveIncrements($addonKey); } }