Add integrations health monitoring
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-02 18:35:12 +01:00
parent 9057a4cd15
commit fc3e6715db
21 changed files with 715 additions and 13 deletions

View File

@@ -3,7 +3,9 @@
namespace App\Jobs;
use App\Models\EventPurchase;
use App\Models\IntegrationWebhookEvent;
use App\Models\Tenant;
use App\Services\Integrations\IntegrationWebhookRecorder;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -25,24 +27,27 @@ class ProcessRevenueCatWebhook implements ShouldQueue
private ?string $eventId;
private ?int $webhookEventId;
public int $tries = 5;
public int $backoff = 60;
/**
* @param array<string, mixed> $payload
* @param array<string, mixed> $payload
*/
public function __construct(array $payload, ?string $eventId = null)
public function __construct(array $payload, ?string $eventId = null, ?int $webhookEventId = null)
{
$this->payload = $payload;
$this->eventId = $eventId !== '' ? $eventId : null;
$this->webhookEventId = $webhookEventId;
$this->queue = config('services.revenuecat.queue', 'webhooks');
$this->onQueue($this->queue);
}
public function handle(): void
{
$webhookEvent = $this->resolveWebhookEvent();
$appUserId = $this->value('event.app_user_id')
?? $this->value('subscriber.app_user_id');
@@ -50,6 +55,8 @@ class ProcessRevenueCatWebhook implements ShouldQueue
Log::warning('RevenueCat webhook missing app_user_id', [
'event_id' => $this->eventId,
]);
$this->markFailed($webhookEvent, 'Missing app_user_id');
return;
}
@@ -59,6 +66,8 @@ class ProcessRevenueCatWebhook implements ShouldQueue
'event_id' => $this->eventId,
'app_user_id' => $appUserId,
]);
$this->markFailed($webhookEvent, 'Tenant not found');
return;
}
@@ -74,13 +83,15 @@ class ProcessRevenueCatWebhook implements ShouldQueue
if (EventPurchase::where('provider', 'revenuecat')
->where('external_receipt_id', $transactionId)
->exists()) {
$this->markIgnored($webhookEvent, 'Duplicate transaction');
return;
}
$amount = (float) ($this->value('event.price') ?? 0);
$currency = strtoupper((string) ($this->value('event.currency') ?? 'EUR'));
DB::transaction(function () use ($tenant, $credits, $transactionId, $productId, $amount, $currency) {
DB::transaction(function () use ($tenant, $credits, $transactionId, $amount, $currency) {
$tenant->refresh();
$purchase = EventPurchase::create([
@@ -104,6 +115,14 @@ class ProcessRevenueCatWebhook implements ShouldQueue
'product_id' => $productId,
'credits' => $credits,
]);
$this->markProcessed($webhookEvent);
}
public function failed(\Throwable $exception): void
{
$webhookEvent = $this->resolveWebhookEvent();
$this->markFailed($webhookEvent, $exception->getMessage());
}
private function updateSubscriptionStatus(Tenant $tenant): void
@@ -149,7 +168,7 @@ class ProcessRevenueCatWebhook implements ShouldQueue
}
foreach ([':', '-', '_'] as $delimiter) {
$needle = strtolower($prefix) . $delimiter;
$needle = strtolower($prefix).$delimiter;
if (str_starts_with($lower, $needle)) {
$candidate = substr($appUserId, strlen($needle));
if (is_numeric($candidate)) {
@@ -226,7 +245,7 @@ class ProcessRevenueCatWebhook implements ShouldQueue
return $mappings;
}
private function value(string $path, $default = null)
private function value(string $path, $default = null): mixed
{
$segments = explode('.', $path);
$value = $this->payload;
@@ -241,4 +260,40 @@ class ProcessRevenueCatWebhook implements ShouldQueue
return $value;
}
private function resolveWebhookEvent(): ?IntegrationWebhookEvent
{
if (! $this->webhookEventId) {
return null;
}
return IntegrationWebhookEvent::find($this->webhookEventId);
}
private function markProcessed(?IntegrationWebhookEvent $event): void
{
if (! $event) {
return;
}
app(IntegrationWebhookRecorder::class)->markProcessed($event);
}
private function markIgnored(?IntegrationWebhookEvent $event, string $reason): void
{
if (! $event) {
return;
}
app(IntegrationWebhookRecorder::class)->markIgnored($event, ['reason' => $reason]);
}
private function markFailed(?IntegrationWebhookEvent $event, string $reason): void
{
if (! $event) {
return;
}
app(IntegrationWebhookRecorder::class)->markFailed($event, $reason);
}
}