You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
182 lines
5.6 KiB
182 lines
5.6 KiB
<?php |
|
|
|
namespace App\Services; |
|
|
|
use App\Models\Feedback; |
|
use App\Support\ApiActor; |
|
use Illuminate\Support\Facades\Http; |
|
use Illuminate\Support\Facades\Log; |
|
use Illuminate\Support\Str; |
|
|
|
class ZammadTicketService |
|
{ |
|
public function isConfigured(): bool |
|
{ |
|
if (! config('zammad.enabled')) { |
|
return false; |
|
} |
|
|
|
return config('zammad.base_url') !== '' |
|
&& config('zammad.api_token') !== ''; |
|
} |
|
|
|
/** |
|
* @return array{zammad_ticket_id: int, zammad_ticket_number: string, zammad_synced_at: \Illuminate\Support\Carbon}|null |
|
*/ |
|
public function createTicketFromFeedback(Feedback $feedback, ?ApiActor $actor = null): ?array |
|
{ |
|
if (! $this->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(); |
|
} |
|
}
|
|
|