php-tenant/Jobs/DispatchEntitlementWebhook.php
Snider d0ad2737cb refactor: rename namespace from Core\Mod\Tenant to Core\Tenant
Simplifies the namespace hierarchy by removing the intermediate Mod
segment. Updates all 118 files including models, services, controllers,
middleware, tests, and composer.json autoload configuration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:30:46 +00:00

188 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Tenant\Jobs;
use Core\Tenant\Enums\WebhookDeliveryStatus;
use Core\Tenant\Models\EntitlementWebhook;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
/**
* Job to dispatch entitlement webhook deliveries asynchronously.
*
* Handles retry logic with exponential backoff.
*/
class DispatchEntitlementWebhook implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*/
public int $tries = 3;
/**
* The number of seconds to wait before retrying.
*
* @var array<int>
*/
public array $backoff = [60, 300, 900]; // 1min, 5min, 15min
/**
* Create a new job instance.
*/
public function __construct(
public int $webhookId,
public string $eventName,
public array $eventPayload
) {
$this->onQueue('webhooks');
}
/**
* Execute the job.
*/
public function handle(): void
{
$webhook = EntitlementWebhook::find($this->webhookId);
if (! $webhook) {
Log::warning('Entitlement webhook not found', ['webhook_id' => $this->webhookId]);
return;
}
// Skip if webhook is inactive (circuit breaker may have triggered)
if (! $webhook->isActive()) {
Log::info('Entitlement webhook is inactive, skipping', [
'webhook_id' => $this->webhookId,
'event' => $this->eventName,
]);
return;
}
$data = [
'event' => $this->eventName,
'data' => $this->eventPayload,
'timestamp' => now()->toIso8601String(),
];
try {
$headers = [
'Content-Type' => 'application/json',
'X-Request-Source' => config('app.name'),
'User-Agent' => config('app.name').' Entitlement Webhook',
];
if ($webhook->secret) {
$headers['X-Signature'] = hash_hmac('sha256', json_encode($data), $webhook->secret);
}
$response = Http::withHeaders($headers)
->timeout(10)
->post($webhook->url, $data);
$status = match ($response->status()) {
200, 201, 202, 204 => WebhookDeliveryStatus::SUCCESS,
default => WebhookDeliveryStatus::FAILED,
};
// Create delivery record
$webhook->deliveries()->create([
'uuid' => Str::uuid(),
'event' => $this->eventName,
'attempts' => $this->attempts(),
'status' => $status,
'http_status' => $response->status(),
'payload' => $data,
'response' => $response->json() ?: ['body' => substr($response->body(), 0, 1000)],
'created_at' => now(),
]);
if ($status === WebhookDeliveryStatus::SUCCESS) {
$webhook->resetFailureCount();
Log::info('Entitlement webhook delivered successfully', [
'webhook_id' => $webhook->id,
'event' => $this->eventName,
'http_status' => $response->status(),
]);
} else {
$webhook->incrementFailureCount();
$webhook->updateLastDeliveryStatus($status);
Log::warning('Entitlement webhook delivery failed', [
'webhook_id' => $webhook->id,
'event' => $this->eventName,
'http_status' => $response->status(),
'response' => substr($response->body(), 0, 500),
]);
// Throw exception to trigger retry
throw new \RuntimeException("Webhook returned {$response->status()}");
}
$webhook->updateLastDeliveryStatus($status);
} catch (\Exception $e) {
$webhook->incrementFailureCount();
$webhook->updateLastDeliveryStatus(WebhookDeliveryStatus::FAILED);
// Create failure delivery record
$webhook->deliveries()->create([
'uuid' => Str::uuid(),
'event' => $this->eventName,
'attempts' => $this->attempts(),
'status' => WebhookDeliveryStatus::FAILED,
'payload' => $data,
'response' => ['error' => $e->getMessage()],
'created_at' => now(),
]);
Log::error('Entitlement webhook dispatch exception', [
'webhook_id' => $webhook->id,
'event' => $this->eventName,
'error' => $e->getMessage(),
'attempt' => $this->attempts(),
]);
throw $e;
}
}
/**
* Handle job failure after all retries exhausted.
*/
public function failed(\Throwable $exception): void
{
$webhook = EntitlementWebhook::find($this->webhookId);
Log::error('Entitlement webhook job failed permanently', [
'webhook_id' => $this->webhookId,
'event' => $this->eventName,
'error' => $exception->getMessage(),
'circuit_broken' => $webhook?->isCircuitBroken() ?? false,
]);
}
/**
* Get the tags that should be assigned to the job.
*
* @return array<string>
*/
public function tags(): array
{
return [
'entitlement-webhook',
"webhook:{$this->webhookId}",
"event:{$this->eventName}",
];
}
}