*/ public function getAllForUser(int $userId): Collection { if ($userId <= 0) { throw new InvalidArgumentException('Invalid user ID'); } $tickets = Ticket::with(['user', 'connectedApp', 'status', 'notifiedRoles', 'notifiedUsers', 'attachments', 'assignedUser', 'replies.user.roles']) ->where('user_id', $userId) ->latest() ->get(); return TicketData::collect($tickets); } /** * Get all tickets in the system (for admins/support). * * @return Collection */ public function getAll(): Collection { $tickets = Ticket::with(['user', 'connectedApp', 'status', 'notifiedRoles', 'notifiedUsers', 'attachments', 'assignedUser', 'replies.user.roles']) ->latest() ->get(); return TicketData::collect($tickets); } /** * Get lightweight ticket list for a specific user. * * @return Collection */ public function getListForUser(int $userId): Collection { if ($userId <= 0) { throw new InvalidArgumentException('Invalid user ID'); } $user = User::findOrFail($userId); $roleIds = $user->roles->pluck('id')->toArray(); $tickets = Ticket::with(['user', 'connectedApp', 'status']) ->where(function ($query) use ($userId, $roleIds): void { $query->where('user_id', $userId) ->orWhere('assigned_user_id', $userId) ->orWhereHas('notifiedUsers', function ($q) use ($userId): void { $q->where('users.id', $userId); }) ->orWhereHas('notifiedRoles', function ($q) use ($roleIds): void { $q->whereIn('roles.id', $roleIds); }); }) ->latest() ->get(); return TicketListData::collect($tickets); } /** * Get lightweight ticket list of all tickets. * * @return Collection */ public function getList(): Collection { $tickets = Ticket::with(['user', 'connectedApp', 'status']) ->latest() ->get(); return TicketListData::collect($tickets); } /** * Get a specific ticket. */ public function getTicket(int $id): TicketData { if ($id <= 0) { throw new InvalidArgumentException('Invalid ticket ID'); } $ticket = Ticket::with(['user', 'connectedApp', 'status', 'notifiedRoles', 'notifiedUsers', 'attachments', 'assignedUser', 'replies.user.roles']) ->findOrFail($id); return TicketData::fromModel($ticket); } /** * Update the status of a ticket. * * @throws Throwable */ public function updateStatus(int $ticketId, string $statusSlug): void { if ($ticketId <= 0) { throw new InvalidArgumentException('Invalid ticket ID'); } DB::transaction(function () use ($ticketId, $statusSlug): void { $ticket = Ticket::findOrFail($ticketId); $status = TicketStatus::where('name', $statusSlug)->firstOrFail(); $ticket->update(['status_id' => $status->id]); }); } /** * Assign/remove assigned user to ticket. * * @throws Throwable */ public function assignUser(int $ticketId, ?int $assignedUserId): void { if ($ticketId <= 0) { throw new InvalidArgumentException('Invalid ticket ID'); } DB::transaction(function () use ($ticketId, $assignedUserId): void { $ticket = Ticket::findOrFail($ticketId); $oldAssignedUserId = $ticket->assigned_user_id; $ticket->update(['assigned_user_id' => $assignedUserId]); if ($assignedUserId && $assignedUserId !== $oldAssignedUserId) { $assignedUser = User::findOrFail($assignedUserId); $assignedUser->notify(new TicketAssignedNotification($ticket)); } }); } /** * Post a comment/reply to a ticket. * * @throws Throwable */ public function postReply(PostReplyRequest $request): ReplyData { return DB::transaction(function () use ($request): ReplyData { $user = User::findOrFail($request->userId); $ticket = Ticket::findOrFail($request->ticketId); // Authorization check: Admin, Creator, Assigned, or Notified directly/via role $isAuthorized = $user->hasRole('Admin') || $user->hasRole('admin') || $ticket->user_id === $user->id || $ticket->assigned_user_id === $user->id || $ticket->notifiedUsers()->where('users.id', $user->id)->exists() || $ticket->notifiedRoles()->whereIn('roles.id', $user->roles->pluck('id')->toArray())->exists(); if (! $isAuthorized) { throw new AuthorizationException('You are not authorized to comment on this ticket.'); } /** @var TicketReply $reply */ $reply = TicketReply::query()->create([ 'ticket_id' => $request->ticketId, 'user_id' => $request->userId, 'message' => mb_trim($request->message), ]); // Transition ticket status from open to in-progress if it's currently open $ticket->load(['status', 'user', 'assignedUser']); if ($ticket->status->name === TicketStatusEnum::Open->value) { $inProgressStatus = TicketStatus::where('name', TicketStatusEnum::InProgress->value)->first(); if ($inProgressStatus) { $ticket->update(['status_id' => $inProgressStatus->id]); } } // Notify ticket raiser if they did not make this comment if ($ticket->user_id !== $request->userId) { $ticket->user->notify(new TicketCommentedNotification($ticket, $reply)); } // Notify assigned support user if they are set and did not make this comment if ($ticket->assigned_user_id && $ticket->assigned_user_id !== $request->userId) { $ticket->assignedUser->notify(new TicketCommentedNotification($ticket, $reply)); } return ReplyData::fromModel($reply->load('user.roles')); }); } /** * Create a new support ticket. * * @throws Throwable */ public function create(CreateTicketRequest $request): TicketData { return DB::transaction(function () use ($request): TicketData { $defaultStatus = TicketStatus::query() ->where('name', TicketStatusEnum::Open->value) ->firstOrFail(); /** @var Ticket $ticket */ $ticket = Ticket::query()->create([ 'user_id' => $request->userId, 'connected_app_id' => $request->connectedAppId, 'issue_category' => $request->issueCategory, 'description' => $request->description, 'status_id' => $defaultStatus->id, ]); if (! empty($request->notifiedRoleIds)) { $ticket->notifiedRoles()->sync($request->notifiedRoleIds); } if (! empty($request->notifiedUserIds)) { $ticket->notifiedUsers()->sync($request->notifiedUserIds); } if ($request->attachment instanceof UploadedFile) { $this->attachmentService->store($ticket->id, $request->attachment); } // Load relations to build complete DTO and trigger notifications $ticket->load(['user', 'connectedApp', 'status', 'notifiedRoles', 'notifiedUsers', 'attachments', 'assignedUser', 'replies.user.roles']); $this->sendNotifications($ticket); return TicketData::fromModel($ticket); }); } /** * Send notifications to all routed recipients. */ private function sendNotifications(Ticket $ticket): void { $notification = new TicketRaisedNotification($ticket); $notifiedUserIds = []; // Notify specific users foreach ($ticket->notifiedUsers as $user) { $user->notify($notification); $notifiedUserIds[] = $user->id; } // Notify role users foreach ($ticket->notifiedRoles as $role) { $users = User::role($role->name)->get(); foreach ($users as $user) { // Avoid double notifications if (! in_array($user->id, $notifiedUserIds, true)) { $user->notify($notification); $notifiedUserIds[] = $user->id; } } } } }