> */ public function providers(): array { return [ $this->buildProvider('paddle', 'Paddle', [ 'is_configured' => filled(config('paddle.webhook_secret')), 'label' => 'Webhook secret', ]), $this->buildProvider('revenuecat', 'RevenueCat', [ 'is_configured' => filled(config('services.revenuecat.webhook')), 'label' => 'Webhook secret', 'queue' => config('services.revenuecat.queue', 'webhooks'), 'job_class' => ProcessRevenueCatWebhook::class, ]), ]; } /** * @param array $config * @return array */ private function buildProvider(string $provider, string $label, array $config): array { $query = IntegrationWebhookEvent::query()->where('provider', $provider); $lastEvent = (clone $query)->orderByDesc('received_at')->first(); $lastProcessed = (clone $query) ->where('status', IntegrationWebhookEvent::STATUS_PROCESSED) ->orderByDesc('processed_at') ->first(); $lastFailed = (clone $query) ->where('status', IntegrationWebhookEvent::STATUS_FAILED) ->orderByDesc('failed_at') ->first(); $pendingCount = (clone $query) ->where('status', IntegrationWebhookEvent::STATUS_RECEIVED) ->count(); $recentFailures = (clone $query) ->where('status', IntegrationWebhookEvent::STATUS_FAILED) ->where('failed_at', '>=', now()->subDay()) ->count(); $queueName = $config['queue'] ?? null; $jobClass = $config['job_class'] ?? null; $queueBacklog = $this->countJobs($queueName, $jobClass); $failedJobs = $this->countFailedJobs($queueName, $jobClass); $status = $this->resolveStatus( (bool) ($config['is_configured'] ?? false), $pendingCount, $recentFailures, $failedJobs, $lastEvent ); return [ 'provider' => $provider, 'label' => $label, 'config_label' => (string) ($config['label'] ?? 'Config'), 'is_configured' => (bool) ($config['is_configured'] ?? false), 'status' => $status, 'status_label' => $this->statusLabel($status), 'pending_count' => $pendingCount, 'recent_failures' => $recentFailures, 'queue_backlog' => $queueBacklog, 'failed_jobs' => $failedJobs, 'last_event' => $this->formatEvent($lastEvent), 'last_processed' => $this->formatEvent($lastProcessed), 'last_failed' => $this->formatEvent($lastFailed), 'processing_lag' => $this->formatLag($lastEvent), ]; } private function resolveStatus( bool $isConfigured, int $pendingCount, int $recentFailures, int $failedJobs, ?IntegrationWebhookEvent $lastEvent, ): string { if (! $isConfigured) { return 'unconfigured'; } if ($pendingCount > 0) { return 'pending'; } if ($recentFailures > 0 || $failedJobs > 0) { return 'degraded'; } if (! $lastEvent) { return 'unknown'; } return 'healthy'; } private function statusLabel(string $status): string { return match ($status) { 'healthy' => __('admin.integrations_health.status.healthy'), 'pending' => __('admin.integrations_health.status.pending'), 'degraded' => __('admin.integrations_health.status.degraded'), 'unconfigured' => __('admin.integrations_health.status.unconfigured'), default => __('admin.integrations_health.status.unknown'), }; } private function formatEvent(?IntegrationWebhookEvent $event): ?array { if (! $event) { return null; } return [ 'status' => $event->status, 'event_type' => $event->event_type, 'received_at' => $event->received_at, 'processed_at' => $event->processed_at, 'failed_at' => $event->failed_at, 'error_message' => $event->error_message, ]; } private function formatLag(?IntegrationWebhookEvent $event): ?array { if (! $event || ! $event->received_at) { return null; } $end = $event->processed_at ?? now(); $seconds = $end->diffInSeconds($event->received_at); return [ 'seconds' => $seconds, 'label' => Carbon::now()->subSeconds($seconds)->diffForHumans(null, true), ]; } private function countJobs(?string $queueName, ?string $jobClass): int { if (! $queueName || ! $jobClass) { return 0; } return (int) DB::table('jobs') ->where('queue', $queueName) ->where('payload', 'like', '%'.$jobClass.'%') ->count(); } private function countFailedJobs(?string $queueName, ?string $jobClass): int { if (! $queueName || ! $jobClass) { return 0; } return (int) DB::table('failed_jobs') ->where('queue', $queueName) ->where('payload', 'like', '%'.$jobClass.'%') ->count(); } }