1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
/**
* tickets.controller.ts
*
* Lógica de negócio do módulo de tickets.
* Orquestra o repository, aplica regras e formata respostas.
* NÃO acessa o banco diretamente — delega ao repository.
*/
import type { FastifyReply, FastifyRequest } from 'fastify';
import { createTicket, countTickets, findManyTickets } from './tickets.repository.js';
import type { CreateTicketBody, ListTicketsQuery, TicketParams } from './tickets.schema.js';
import { tickets } from '@/db/schema.js';
import { eq } from 'drizzle-orm';
// ─── POST /tickets ────────────────────────────────────────────────────────────
export async function createTicketHandler(
request: FastifyRequest<{ Body: CreateTicketBody }>,
reply: FastifyReply,
) {
const { title, description, priority, categoryId, creatorId } = request.body;
const ticket = await createTicket(request.server.db, {
title,
description,
priority,
categoryId: categoryId ?? null,
assigneeId: null, // Chamados nascem sem agente atribuído
creatorId,
status: 'open',
dueAt: null,
resolvedAt: null,
});
request.server.events.emit('ticket.created', {
id: ticket.id,
title: ticket.title,
creatorId: ticket.creatorId,
});
return reply.code(201).send(ticket);
}
// ─── GET /tickets ─────────────────────────────────────────────────────────────
export async function listTicketsHandler(
request: FastifyRequest<{ Querystring: ListTicketsQuery }>,
reply: FastifyReply,
) {
const filters = request.query;
const db = request.server.db;
// Executa contagem e listagem em paralelo para não dobrar o tempo de resposta
const [total, data] = await Promise.all([
countTickets(db, filters),
findManyTickets(db, filters),
]);
const totalPages = Math.ceil(total / filters.limit);
return reply.send({
data,
meta: {
page: filters.page,
limit: filters.limit,
total,
totalPages,
},
});
}
// ─── GET /tickets/:id ─────────────────────────────────────────────────────────
export async function getTicketByIdHandler(
request: FastifyRequest<{ Params: TicketParams }>,
reply: FastifyReply,
) {
const { findTicketById } = await import('./tickets.repository.js');
const ticket = await findTicketById(request.server.db, request.params.id);
if (!ticket) {
return reply.notFound(`Ticket #${request.params.id} não encontrado`);
}
return reply.send(ticket);
}
// ─── DELETE /tickets/:id ──────────────────────────────────────────────────────
export async function deleteTicketHandler(
request: FastifyRequest<{ Params: TicketParams }>,
reply: FastifyReply,
) {
const db = request.server.db;
const { id } = request.params;
const callerUser = request.user!;
const { findTicketById } = await import('./tickets.repository.js');
const existingTicket = await findTicketById(db, id);
if (!existingTicket) {
return reply.notFound('Chamado não encontrado.');
}
if (callerUser.role !== 'admin' && existingTicket.creatorId !== callerUser.id) {
return reply.forbidden('Você não tem permissão para excluir este chamado.');
}
await db.delete(tickets).where(eq(tickets.id, id));
return reply.code(204).send();
}
|