isConfigured()) { $this->markSyncError($feedback, 'Zammad integration is not configured.'); return null; } $customerEmail = $this->resolveCustomerEmail($feedback, $actor); if ($customerEmail === null) { $this->markSyncError($feedback, 'No valid customer email for Zammad ticket.'); return null; } $payload = [ 'title' => $this->buildTitle($feedback), 'group' => config('zammad.default_group'), 'customer_id' => 'guess:'.$customerEmail, 'article' => [ 'subject' => $this->buildTitle($feedback), 'body' => $this->buildArticleBody($feedback), 'type' => 'web', 'internal' => false, ], ]; try { $response = Http::timeout((int) config('zammad.timeout', 30)) ->acceptJson() ->withHeaders([ 'Authorization' => 'Token token='.config('zammad.api_token'), ]) ->post(config('zammad.base_url').'/api/v1/tickets', $payload); } catch (\Throwable $e) { $this->markSyncError($feedback, 'Zammad request failed: '.$e->getMessage()); Log::warning('Zammad ticket create failed', [ 'feedback_id' => $feedback->id, 'error' => $e->getMessage(), ]); return null; } if (! $response->successful()) { $message = data_get($response->json(), 'error_human') ?? data_get($response->json(), 'error') ?? 'HTTP '.$response->status(); $this->markSyncError($feedback, (string) $message); Log::warning('Zammad ticket create rejected', [ 'feedback_id' => $feedback->id, 'status' => $response->status(), 'body' => $response->json(), ]); return null; } $ticketId = data_get($response->json(), 'id'); $ticketNumber = data_get($response->json(), 'number'); if (! is_numeric($ticketId)) { $this->markSyncError($feedback, 'Zammad response missing ticket id.'); return null; } $syncedAt = now(); $feedback->forceFill([ 'zammad_ticket_id' => (int) $ticketId, 'zammad_ticket_number' => is_scalar($ticketNumber) ? (string) $ticketNumber : null, 'zammad_synced_at' => $syncedAt, 'zammad_sync_error' => null, ])->save(); return [ 'zammad_ticket_id' => (int) $ticketId, 'zammad_ticket_number' => is_scalar($ticketNumber) ? (string) $ticketNumber : (string) $ticketId, 'zammad_synced_at' => $syncedAt, ]; } public function ticketUrl(Feedback $feedback): ?string { if ($feedback->zammad_ticket_id === null) { return null; } return config('zammad.base_url').'/#ticket/zoom/'.$feedback->zammad_ticket_id; } private function resolveCustomerEmail(Feedback $feedback, ?ApiActor $actor): ?string { foreach ([$feedback->reporter, $actor?->identifier] as $candidate) { if (is_string($candidate) && filter_var($candidate, FILTER_VALIDATE_EMAIL)) { return Str::lower($candidate); } } $fallback = (string) config('zammad.fallback_customer'); if ($fallback !== '' && filter_var($fallback, FILTER_VALIDATE_EMAIL)) { return Str::lower($fallback); } return null; } private function buildTitle(Feedback $feedback): string { $typeLabel = match ($feedback->type) { 'bug' => 'Bug', 'suggestion' => 'Saran', default => 'Feedback', }; return sprintf('[%s] %s', $typeLabel, $feedback->title); } private function buildArticleBody(Feedback $feedback): string { $lines = [ $feedback->message, '', '---', 'Feedback #'.$feedback->id, 'Tipe: '.$feedback->type, ]; if ($feedback->reporter) { $lines[] = 'Pelapor: '.$feedback->reporter; } if ($feedback->reporter_name) { $lines[] = 'Nama: '.$feedback->reporter_name; } if ($feedback->page) { $lines[] = 'Halaman: '.$feedback->page; } if ($feedback->url) { $lines[] = 'URL: '.$feedback->url; } if ($feedback->user_agent) { $lines[] = 'User-Agent: '.$feedback->user_agent; } if ($feedback->reported_at) { $lines[] = 'Dilaporkan: '.$feedback->reported_at->toIso8601String(); } return implode("\n", $lines); } private function markSyncError(Feedback $feedback, string $message): void { $feedback->forceFill([ 'zammad_sync_error' => Str::limit($message, 1000), ])->save(); } }