/** * interactions.controller.ts * * Lógica de negócio para todas as interações sobre tickets. * Orquestra o repository, aplica guards e formata respostas. */ import type { FastifyReply, FastifyRequest } from 'fastify'; import { findTicketById } from '@/modules/tickets/tickets.repository.js'; import { updateTicketStatus, assignTicket, updateTicketPriority, addComment, findCommentsByTicketId, } from './interactions.repository.js'; import { getSettings } from '@/modules/settings/settings.repository.js'; import type { TicketIdParams, UpdateTicketStatusBody, AssignTicketBody, UpdateTicketPriorityBody, AddCommentBody, } from './interactions.schema.js'; // ─── Guard reutilizável ─────────────────────────────────────────────────────── // Verifica se o ticket existe antes de qualquer operação async function assertTicketExists( request: FastifyRequest, reply: FastifyReply, ticketId: number, ) { const ticket = await findTicketById(request.server.db, ticketId); if (!ticket) { await reply.notFound(`Ticket #${ticketId} não encontrado`); return null; } return ticket; } // ─── PATCH /tickets/:ticketId/status ───────────────────────────────────────── export async function updateTicketStatusHandler( request: FastifyRequest<{ Params: TicketIdParams; Body: UpdateTicketStatusBody }>, reply: FastifyReply, ) { const { ticketId } = request.params; const { status, reason } = request.body; const db = request.server.db; const ticket = await assertTicketExists(request, reply, ticketId); if (!ticket) return; // Impede mover um ticket já fechado if (ticket.status === 'closed') { return reply.badRequest('Tickets fechados não podem ser reabertos por esta rota.'); } const updated = await updateTicketStatus(db, ticketId, status); // Se um motivo foi fornecido, registra automaticamente como nota interna if (reason && request.body.authorId) { await addComment(db, { ticketId, authorId: (request.body as { authorId?: number }).authorId ?? 0, content: `Status alterado para **${status}**. Motivo: ${reason}`, isInternal: true, }); } if (updated) { request.server.events.emit('ticket.updated', { id: updated.id, status: updated.status, assigneeId: updated.assigneeId, creatorId: ticket.creatorId, authorId: request.user!.id, }); } return reply.send(updated); } // ─── PATCH /tickets/:ticketId/assign ───────────────────────────────────────── export async function assignTicketHandler( request: FastifyRequest<{ Params: TicketIdParams; Body: AssignTicketBody }>, reply: FastifyReply, ) { const { ticketId } = request.params; const db = request.server.db; const ticket = await assertTicketExists(request, reply, ticketId); if (!ticket) return; const updated = await assignTicket(db, ticketId, request.body.assigneeId); if (updated) { request.server.events.emit('ticket.updated', { id: updated.id, status: updated.status, assigneeId: updated.assigneeId, creatorId: ticket.creatorId, authorId: request.user!.id, }); } return reply.send(updated); } // ─── PATCH /tickets/:ticketId/priority ─────────────────────────────────────── export async function updateTicketPriorityHandler( request: FastifyRequest<{ Params: TicketIdParams; Body: UpdateTicketPriorityBody }>, reply: FastifyReply, ) { const { ticketId } = request.params; const ticket = await assertTicketExists(request, reply, ticketId); if (!ticket) return; const updated = await updateTicketPriority( request.server.db, ticketId, request.body.priority, ); return reply.send(updated); } // ─── POST /tickets/:ticketId/comments ──────────────────────────────────────── export async function addCommentHandler( request: FastifyRequest<{ Params: TicketIdParams; Body: AddCommentBody }>, reply: FastifyReply, ) { const { ticketId } = request.params; const { content, isInternal, authorId } = request.body; const ticket = await assertTicketExists(request, reply, ticketId); if (!ticket) return; // Não permite adicionar comentários em tickets fechados ou resolvidos if (ticket.status === 'closed' || ticket.status === 'resolved') { return reply.badRequest('Não é possível comentar em um ticket fechado ou resolvido.'); } // Verifica a configuração do sistema para bloqueio de chat de requesters if (request.user?.role === 'requester') { const settings = await getSettings(request.server.db); if (!settings.allowRequesterChat) { return reply.forbidden('O chat de chamados está desativado para usuários padrão no momento.'); } } const comment = await addComment(request.server.db, { ticketId, authorId, content, isInternal, }); request.server.events.emit('comment.added', { ticketId: comment.ticketId, authorId: comment.authorId, isInternal: comment.isInternal, creatorId: ticket.creatorId, assigneeId: ticket.assigneeId, }); return reply.code(201).send(comment); } // ─── GET /tickets/:ticketId/comments ───────────────────────────────────────── export async function listCommentsHandler( request: FastifyRequest<{ Params: TicketIdParams }>, reply: FastifyReply, ) { const { ticketId } = request.params; const ticket = await assertTicketExists(request, reply, ticketId); if (!ticket) return; const includeInternal = request.user!.role !== 'requester'; const result = await findCommentsByTicketId(request.server.db, ticketId, includeInternal); return reply.send(result); }