status; if (! in_array($status, [ AiEditRequest::STATUS_SUCCEEDED, AiEditRequest::STATUS_FAILED, AiEditRequest::STATUS_BLOCKED, ], true)) { return; } if (! $this->claimLock((int) $request->id, $status)) { return; } $event = Event::query() ->with('tenant.user') ->find((int) $request->event_id); if (! $event || ! $event->tenant) { return; } $request->loadMissing('style'); $this->notifyTenant($event->tenant, $event, $request, $status); if ((int) ($request->requested_by_user_id ?? 0) === 0) { $this->notifyGuest($event, $request, $status); } } private function claimLock(int $requestId, string $status): bool { $key = sprintf('ai-editing:terminal-notification:request:%d:status:%s', $requestId, $status); return Cache::add($key, 1, now()->addDays(7)); } private function notifyGuest(Event $event, AiEditRequest $request, string $status): void { [$title, $body] = match ($status) { AiEditRequest::STATUS_SUCCEEDED => [ 'Dein AI-Magic-Edit ist fertig ✨', 'Dein bearbeitetes Foto ist jetzt verfügbar.', ], AiEditRequest::STATUS_BLOCKED => [ 'AI-Magic-Edit wurde blockiert', 'Die Bearbeitung wurde durch die Sicherheitsregeln gestoppt.', ], default => [ 'AI-Magic-Edit fehlgeschlagen', 'Die Bearbeitung konnte nicht abgeschlossen werden. Bitte erneut versuchen.', ], }; $options = [ 'payload' => [ 'photo_id' => (int) $request->photo_id, 'count' => 1, ], 'priority' => $status === AiEditRequest::STATUS_SUCCEEDED ? 2 : 3, 'expires_at' => now()->addHours(6), 'audience_scope' => GuestNotificationAudience::ALL, ]; $deviceId = trim((string) ($request->requested_by_device_id ?? '')); if ($deviceId !== '') { $options['audience_scope'] = GuestNotificationAudience::GUEST; $options['target_identifier'] = $deviceId; } $this->guestNotifications->createNotification( $event, GuestNotificationType::UPLOAD_ALERT, $title, $body, $options ); } private function notifyTenant(Tenant $tenant, Event $event, AiEditRequest $request, string $status): void { $type = match ($status) { AiEditRequest::STATUS_SUCCEEDED => 'ai_edit_succeeded', AiEditRequest::STATUS_BLOCKED => 'ai_edit_blocked', default => 'ai_edit_failed', }; $log = $this->tenantNotificationLogger->log($tenant, [ 'type' => $type, 'channel' => 'system', 'status' => 'sent', 'sent_at' => now(), 'context' => [ 'scope' => 'ai', 'status' => $status, 'event_id' => (int) $event->id, 'event_slug' => (string) $event->slug, 'event_name' => $this->resolveEventName($event->name), 'request_id' => (int) $request->id, 'photo_id' => (int) $request->photo_id, 'style_key' => $request->style?->key, 'style_name' => $request->style?->name, 'failure_code' => $request->failure_code, ], ]); $this->createReceipt($tenant, (int) $log->id); } private function createReceipt(Tenant $tenant, int $logId): void { $userId = (int) ($tenant->user_id ?? 0); if ($userId <= 0) { return; } TenantNotificationReceipt::query()->create([ 'tenant_id' => (int) $tenant->id, 'notification_log_id' => $logId, 'user_id' => $userId, 'status' => 'delivered', ]); } private function resolveEventName(mixed $name): ?string { if (is_string($name) && trim($name) !== '') { return trim($name); } if (is_array($name)) { foreach ($name as $candidate) { if (is_string($candidate) && trim($candidate) !== '') { return trim($candidate); } } } return null; } }