181 lines
5.6 KiB
PHP
181 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Integrations;
|
|
|
|
use App\Jobs\ProcessRevenueCatWebhook;
|
|
use App\Models\IntegrationWebhookEvent;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class IntegrationHealthService
|
|
{
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
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<string, mixed> $config
|
|
* @return array<string, mixed>
|
|
*/
|
|
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();
|
|
}
|
|
}
|