$data * @param array $files */ public function create(User $user, array $data, array $files = []): Ticket { for ($attempt = 1; $attempt <= 5; $attempt++) { try { return DB::transaction(fn (): Ticket => $this->createInsideTransaction($user, $data, $files)); } catch (QueryException $exception) { if (! $this->isReferenceCollision($exception) || $attempt === 5) { throw $exception; } } } throw new \RuntimeException('Unable to create a unique Helpdesk ticket reference.'); } /** * Create the ticket while the caller owns the database transaction. * * @param array $data * @param array $files */ private function createInsideTransaction(User $user, array $data, array $files = []): Ticket { $defaultStatusSlug = (string) config('helpdesk.default_status_slug', 'new'); $status = Schema::hasTable('helpdesk_statuses') && Schema::hasColumn('helpdesk_statuses', 'is_default') ? Status::query()->where('is_default', true)->orderBy('sort_order')->first() : null; if (! $status) { $status = Status::query()->where('slug', $defaultStatusSlug)->first(); } if (! $status) { $status = Status::query()->create([ 'name' => 'New', 'slug' => $defaultStatusSlug, 'color' => 'sky', 'sort_order' => 1, 'is_closed' => false, ]); } $boardId = $data['board_id'] ?? null; if (blank($boardId) && Schema::hasTable('helpdesk_kanban_boards')) { $boardId = KanbanBoard::query() ->where('is_default', true) ->orderBy('sort_order') ->value('id') ?: KanbanBoard::query()->orderBy('sort_order')->value('id'); } /** @var Ticket $ticket */ $ticket = Ticket::query()->create([ 'reference' => $this->references->next(), 'company_id' => $data['company_id'] ?? ($user->company_id ?? null), 'user_id' => $user->id, 'agent_id' => $this->assignment->selectAgentId($data['category_id'] ?? null), 'status_id' => $status->id, 'board_id' => $boardId, 'priority_id' => $data['priority_id'], 'category_id' => $data['category_id'] ?? null, 'project_id' => $data['project_id'] ?? null, 'product_id' => $data['product_id'] ?? null, 'team_id' => $data['team_id'] ?? null, 'subject' => $data['subject'], 'body' => $data['body'], 'due_at' => $data['due_at'] ?? null, 'last_activity_at' => now(), ]); if (! empty($data['tag_ids']) && is_array($data['tag_ids'])) { $ticket->tags()->sync(array_values(array_unique(array_map('intval', $data['tag_ids'])))); } $ticket = $this->automation->apply($ticket, $data); $ticket = $this->sla->applyPolicy($ticket); $this->checklists->applyTemplates($ticket); $this->attachments->storeMany($ticket, $files, (int) $user->id); $ticket = $ticket->fresh(['status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'attachments', 'checklistItems']) ?? $ticket; $this->workflow->dispatch($ticket, 'ticket.created', $user); return $ticket; } /** * Determine whether a database error was caused by the ticket reference unique index. */ private function isReferenceCollision(QueryException $exception): bool { $message = $exception->getMessage(); return str_contains($message, 'helpdesk_tickets_reference_unique') || str_contains($message, 'Duplicate entry') && str_contains($message, 'reference') || str_contains($message, 'UNIQUE constraint failed') && str_contains($message, 'helpdesk_tickets.reference'); } /** * Add a public or internal comment and optional attachments. * * @param array $files */ public function comment(Ticket $ticket, User $user, string $body, bool $internal = false, array $files = [], ?int $parentId = null, ?string $subject = null): Comment { return DB::transaction(function () use ($ticket, $user, $body, $internal, $files, $parentId, $subject): Comment { $parent = $parentId === null ? null : Comment::query() ->where('ticket_id', $ticket->id) ->findOrFail($parentId); $guid = (string) Str::uuid(); $unid = strtoupper(hash('sha256', $guid . '|' . $ticket->id . '|' . microtime(true))); $parentUnid = $parent?->unid; $rootUnid = $parent?->root_unid ?: $parentUnid ?: $unid; $plainTextBody = trim(preg_replace('/\s+/', ' ', strip_tags($body)) ?: ''); $payload = [ 'parent_id' => $parentId, 'user_id' => $user->id, 'body' => $body, 'body_text' => $plainTextBody, 'is_internal' => $internal, 'guid' => $guid, 'unid' => $unid, 'parent_unid' => $parentUnid, 'root_unid' => $rootUnid, 'originator_id' => $unid . ':1', 'sync_sequence' => 1, 'sync_hash' => hash('sha256', $body), 'source_system' => 'wms-helpdesk', ]; if (Schema::hasColumn('helpdesk_comments', 'subject')) { $payload['subject'] = trim((string) ($subject ?: ($parentId === null ? 'Response' : 'RE: Response'))); } /** @var Comment $comment */ $comment = $ticket->comments()->create($payload); $ticket->forceFill(['last_activity_at' => now()])->save(); if (! $internal) { $this->sla->recordPublicResponse($ticket, $user); } $this->attachments->storeMany($ticket, $files, (int) $user->id, $comment); $this->workflow->dispatch($ticket->fresh(['status', 'priority', 'category', 'project', 'team']) ?? $ticket, $parentId === null ? 'ticket.commented' : 'ticket.reply_created', $user); return $comment->fresh(['user', 'attachments']) ?? $comment; }); } /** * Close a ticket. */ public function close(Ticket $ticket, User $user): Ticket { $status = Status::query()->where('slug', config('helpdesk.closed_status_slug'))->firstOrFail(); $ticket->forceFill([ 'status_id' => $status->id, 'completed_at' => now(), 'last_activity_at' => now(), ])->save(); $ticket = $ticket->fresh(['status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket; $this->sla->recordClosure($ticket); $this->workflow->dispatch($ticket, 'ticket.closed', $user); return $ticket; } }