diff options
Diffstat (limited to 'api/src/modules/interactions/interactions.controller.ts')
| -rw-r--r-- | api/src/modules/interactions/interactions.controller.ts | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/api/src/modules/interactions/interactions.controller.ts b/api/src/modules/interactions/interactions.controller.ts new file mode 100644 index 0000000..5008272 --- /dev/null +++ b/api/src/modules/interactions/interactions.controller.ts @@ -0,0 +1,190 @@ +/** + * 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); +} |