From 5b003bdd78f03f1bba89258e5d29cab608c34028 Mon Sep 17 00:00:00 2001 From: YgorPereira Date: Sat, 2 May 2026 11:35:14 -0300 Subject: [PATCH 1/5] refactor: switch agentId field in entity to agent of AgentField type --- ...t.entity.spec.ts => ticket.entity.spec.ts} | 5 ++-- .../ticket/domain/entities/ticket.entity.ts | 28 +++++++++++++------ .../controllers/ticket.controller.ts | 12 ++++---- 3 files changed, 28 insertions(+), 17 deletions(-) rename backend/src/modules/ticket/domain/entities/{tickect.entity.spec.ts => ticket.entity.spec.ts} (97%) diff --git a/backend/src/modules/ticket/domain/entities/tickect.entity.spec.ts b/backend/src/modules/ticket/domain/entities/ticket.entity.spec.ts similarity index 97% rename from backend/src/modules/ticket/domain/entities/tickect.entity.spec.ts rename to backend/src/modules/ticket/domain/entities/ticket.entity.spec.ts index 224ec5f..f2ed2b3 100644 --- a/backend/src/modules/ticket/domain/entities/tickect.entity.spec.ts +++ b/backend/src/modules/ticket/domain/entities/ticket.entity.spec.ts @@ -7,6 +7,7 @@ import { TicketEvents, TicketEventMessage, TicketValidationErrors, + AgentField } from './ticket.entity'; describe('Ticket entity', () => { @@ -35,7 +36,7 @@ describe('Ticket entity', () => { const primitiveTicket = ticket.toPrimitives(); expect(primitiveTicket.status).toBe(TicketStatus.OPEN); - expect(primitiveTicket.agentId).toBeNull(); + expect(primitiveTicket.agent).toBeNull(); expect(primitiveTicket.groupId).toBeNull(); expect(primitiveTicket.escalationLevel).toBe(1); expect(primitiveTicket.createdAt).toBeInstanceOf(Date); @@ -53,7 +54,7 @@ describe('Ticket entity', () => { expect(ticket.history.length).toBe(2); expect(ticket.history[1]).toMatchObject({ event: TicketEvents.NEW_AGENT, - responsibleAgent: primitiveTicket.agentId, + responsibleAgent: primitiveTicket.agent, status: TicketStatus.IN_PROGRESS, message: TicketEventMessage.NEW_AGENT_MSG, }); diff --git a/backend/src/modules/ticket/domain/entities/ticket.entity.ts b/backend/src/modules/ticket/domain/entities/ticket.entity.ts index bdc7e78..7476dd5 100644 --- a/backend/src/modules/ticket/domain/entities/ticket.entity.ts +++ b/backend/src/modules/ticket/domain/entities/ticket.entity.ts @@ -39,19 +39,25 @@ export enum TicketValidationErrors { export type TicketHistoryEntry = { event: TicketEvents; - responsibleAgent: string | null; + responsibleAgent: AgentField | null; status: TicketStatus; message: string; solution?: string | null; occurredAt: Date; }; +export type AgentField = { + id: string; + name: string; +}; + export class Ticket { // Strutucture definition private _id: string; private _status: TicketStatus = TicketStatus.OPEN; - private _agentId: string | null = null; + // private _agentId: string | null = null; + private _agent: AgentField | null = null; private _groupId: string | null = null; private escalationLevel: number = 1; private attachmentsUrls: string[] = []; @@ -76,7 +82,11 @@ export class Ticket { } get agentId() { - return this._agentId; + return this._agent ? this._agent.id : null; + } + + get agent() { + return this._agent; } get groupId() { @@ -135,7 +145,7 @@ export class Ticket { fileUrls?: string[]; status: TicketStatus; clientId: string; - agentId?: string; + agent?: AgentField | null; groupId?: string; escalationLevel: number; history: TicketHistoryEntry[]; @@ -152,7 +162,7 @@ export class Ticket { ticket._id = props._id; - ticket._agentId = props.agentId ?? null; + ticket._agent = props.agent ?? null; ticket._groupId = props.groupId ?? null; ticket.attachmentsUrls = props.fileUrls ?? []; @@ -179,7 +189,7 @@ export class Ticket { clientId: this._clientId, fileUrls: this.attachmentsUrls, status: this.status, - agentId: this._agentId, + agent: this._agent, groupId: this._groupId, escalationLevel: this.escalationLevel, history: this.history, @@ -217,7 +227,7 @@ export class Ticket { this.addHistory({ event: TicketEvents.NEW_AGENT, - responsibleAgent: this._agentId, + responsibleAgent: this._agent ?? null, status: TicketStatus.IN_PROGRESS, message: TicketEventMessage.NEW_AGENT_MSG, }); @@ -250,7 +260,7 @@ export class Ticket { this.addHistory({ event: TicketEvents.ESCALATE, - responsibleAgent: previousAgentId, + responsibleAgent: this._agent ?? null, status: TicketStatus.ESCALATED, message: TicketEventMessage.ESCALATE_MSG, solution: whatWasDone ?? null, @@ -274,7 +284,7 @@ export class Ticket { this.addHistory({ event: TicketEvents.CLOSE_TICKET, - responsibleAgent: this._agentId, + responsibleAgent: this._agent ?? null, status: TicketStatus.CLOSED, message: TicketEventMessage.CLOSE_TICKET_MSG, solution: solution, diff --git a/backend/src/modules/ticket/presentation/controllers/ticket.controller.ts b/backend/src/modules/ticket/presentation/controllers/ticket.controller.ts index 2811ac3..0769349 100644 --- a/backend/src/modules/ticket/presentation/controllers/ticket.controller.ts +++ b/backend/src/modules/ticket/presentation/controllers/ticket.controller.ts @@ -43,7 +43,10 @@ import { Roles } from '../../../auth/guards/roles.decorator'; import { UserRole } from '../../../shared/enums/user.enum'; import { GetHistoryFilteredUseCase } from '../../application/useCases/getHistoryFiltered/getHistoryFiltered.usecase'; import { GetHistoryFiltersRequest } from '../dtos/getHistory.dto'; -import { TicketEvents, TicketStatus } from '../../domain/entities/ticket.entity'; +import { + TicketEvents, + TicketStatus, +} from '../../domain/entities/ticket.entity'; @ApiTags('Ticket') @Controller('tickets') @@ -59,7 +62,7 @@ export class TicketController { private readonly newAgentUseCase: NewAgentTicketUseCase, private readonly deleteUseCase: DeleteTicketUseCase, private readonly closeUseCase: CloseTicketUseCase, - ) { } + ) {} @Post() @ApiOperation({ summary: 'Cria um ticket' }) @@ -195,10 +198,7 @@ export class TicketController { @ApiParam({ name: 'id', example: 'uuid-do-ticket' }) @ApiBody({ type: CloseTicketRequest }) @ApiResponse({ status: 200, description: 'Ticket fechado com sucesso.' }) - async closeTicket( - @Param('id') id: string, - @Body() body: CloseTicketRequest, - ) { + async closeTicket(@Param('id') id: string, @Body() body: CloseTicketRequest) { const data = TicketMapper.toCloseTicketInput(id, body); const response = await this.closeUseCase.execute(data); From 5ec5aa812fd827cf1a9d320205f1fbc09665bdb4 Mon Sep 17 00:00:00 2001 From: YgorPereira Date: Sat, 2 May 2026 13:14:55 -0300 Subject: [PATCH 2/5] refactor: rafactoring ticket repository to do aggregat and return agent name --- .../ticket.mongodb.repository.int.spec.ts | 7 ++- .../repositories/ticket.mongodb.repository.ts | 59 +++++++++++++++---- .../infra/schemas/ticket.mongo.schema.ts | 7 ++- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/backend/src/modules/ticket/domain/repository/ticket.mongodb.repository.int.spec.ts b/backend/src/modules/ticket/domain/repository/ticket.mongodb.repository.int.spec.ts index 5582b84..6252876 100644 --- a/backend/src/modules/ticket/domain/repository/ticket.mongodb.repository.int.spec.ts +++ b/backend/src/modules/ticket/domain/repository/ticket.mongodb.repository.int.spec.ts @@ -83,7 +83,7 @@ describe('ITicketRepository', () => { expect(primitiveResult.escalationLevel).toBe(1); expect(ticketToCreate.id).toBe(createResult.id); - expect(primitiveResult.agentId).toBeNull(); + expect(primitiveResult.agent).toBeNull(); expect(primitiveResult.groupId).toBeNull(); expect(primitiveResult.clientId).toBe(ticketToCreate.clientId); }); @@ -157,7 +157,7 @@ describe('ITicketRepository', () => { const savedTicket = await repository.save(ticket); expect(savedTicket).toBeDefined(); - expect(savedTicket?.agentId).toBe(newAgentId); + expect(savedTicket?.agent).toBe(newAgentId); expect(savedTicket?.status).toBe(TicketStatus.IN_PROGRESS); }); @@ -194,6 +194,7 @@ describe('ITicketRepository', () => { const foundedTicket = await repository.readById(createdTicket.id); + console.log(foundedTicket); expect(foundedTicket).toBeNull(); }); @@ -272,6 +273,8 @@ describe('ITicketRepository', () => { categories: [categoryId], }); + console.log(result); + expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); expect(result.length).toBe(2); diff --git a/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts b/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts index bdf6325..89ea67c 100644 --- a/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts +++ b/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts @@ -1,4 +1,4 @@ -import { Model, QueryFilter } from 'mongoose'; +import { Model } from 'mongoose'; import { Ticket } from '../../domain/entities/ticket.entity'; import { ITicketRepository } from '../../domain/repository/ticket.repository.interface'; import { TicketLean, TicketSchemaClass } from '../schemas/ticket.mongo.schema'; @@ -43,32 +43,71 @@ export class TicketMongoRepository extends ITicketRepository { agentId?: string; categories?: string[]; }): Promise { - let query: QueryFilter = {}; + let matchStage: Record = {}; if (filters?.agentId) { - query = { + matchStage = { $or: [ { agentId: filters.agentId }, { category: { $in: filters.categories ?? [] }, agentId: null }, ], }; } else if (filters?.clientId) { - query = { clientId: filters.clientId }; + matchStage = { clientId: filters.clientId }; } - const tickets = await this.ticketModel.find(query).exec(); + const tickets = await this.ticketModel + .aggregate([ + { $match: matchStage }, + { + $lookup: { + from: 'users', + localField: 'agentId', + foreignField: '_id', + as: 'agentData', + }, + }, + { + $addFields: { + agent: { + id: { $arrayElemAt: ['$agentData._id', 0] }, + name: { $arrayElemAt: ['$agentData.name', 0] }, + }, + }, + }, + { $project: { agentData: 0 } }, + ]) + .exec(); return tickets.map((t) => TicketMapper.toDomain(t)); } async readById(id: string): Promise { - const foundedTicket = await this.ticketModel - .findById(id) - .lean() + const [result]: TicketLean[] = await this.ticketModel + .aggregate([ + { $match: { _id: id } }, + { + $lookup: { + from: 'users', + localField: 'agentId', + foreignField: '_id', + as: 'agentData', + }, + }, + { + $addFields: { + agent: { + id: { $arrayElemAt: ['$agentData._id', 0] }, + name: { $arrayElemAt: ['$agentData.name', 0] }, + }, + }, + }, + { $project: { agentData: 0 } }, + ]) .exec(); - if (!foundedTicket) return null; + if (!result) return null; - return TicketMapper.toDomain(foundedTicket); + return TicketMapper.toDomain(result); } async delete(id: string): Promise { diff --git a/backend/src/modules/ticket/infra/schemas/ticket.mongo.schema.ts b/backend/src/modules/ticket/infra/schemas/ticket.mongo.schema.ts index 17ca095..796f49c 100644 --- a/backend/src/modules/ticket/infra/schemas/ticket.mongo.schema.ts +++ b/backend/src/modules/ticket/infra/schemas/ticket.mongo.schema.ts @@ -24,7 +24,7 @@ export class TicketHistoryEntrySchema { @Prop({ type: String, default: null }) solution: string | null; - + @Prop({ type: String, required: false }) attachmentUrl?: string; @@ -87,6 +87,9 @@ export class TicketSchemaClass { export type TicketDocument = HydratedDocument; -export type TicketLean = TicketSchemaClass & { _id: string }; +export type TicketLean = Omit & { + _id: string; + agent: { id: string; name: string } | null; +}; export const TicketSchema = SchemaFactory.createForClass(TicketSchemaClass); From 698a9c418029fa3389930e5bfb64853fd499aa05 Mon Sep 17 00:00:00 2001 From: YgorPereira Date: Sat, 2 May 2026 13:28:11 -0300 Subject: [PATCH 3/5] refactor: change the read usecases outputs to return agent obj --- .../ticket/application/useCases/readAll/readAll.usecase.ts | 5 +++-- .../ticket/application/useCases/readById/readById.usecase.ts | 5 +++-- backend/src/modules/ticket/infra/mappers/ticket.mapper.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/src/modules/ticket/application/useCases/readAll/readAll.usecase.ts b/backend/src/modules/ticket/application/useCases/readAll/readAll.usecase.ts index 8462df7..d6aa658 100644 --- a/backend/src/modules/ticket/application/useCases/readAll/readAll.usecase.ts +++ b/backend/src/modules/ticket/application/useCases/readAll/readAll.usecase.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { + AgentField, Ticket, TicketPriority, TicketStatus, @@ -15,7 +16,7 @@ export interface ReadAllTicketOutput { description: string; clientId: string; status: TicketStatus; - agentId: string | null; + agent: AgentField | null; escalationLevel: number; createdAt: Date; updatedAt: Date | null; @@ -51,7 +52,7 @@ export class ReadAllTicketUseCase { description: primitive.description, clientId: primitive.clientId, status: primitive.status, - agentId: primitive.agentId, + agent: primitive.agent, escalationLevel: primitive.escalationLevel, createdAt: primitive.createdAt, updatedAt: primitive.updatedAt, diff --git a/backend/src/modules/ticket/application/useCases/readById/readById.usecase.ts b/backend/src/modules/ticket/application/useCases/readById/readById.usecase.ts index 2891449..6acf1e5 100644 --- a/backend/src/modules/ticket/application/useCases/readById/readById.usecase.ts +++ b/backend/src/modules/ticket/application/useCases/readById/readById.usecase.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { + AgentField, TicketPriority, TicketStatus, } from '../../../domain/entities/ticket.entity'; @@ -13,7 +14,7 @@ export interface ReadByIdTicketOutput { description: string; clientId: string; status: TicketStatus; - agentId: string | null; + agent: AgentField | null; groupId: string | null; escalationLevel: number; createdAt: Date; @@ -42,7 +43,7 @@ export class ReadByIdTicketUseCase { description: primitive.description, clientId: primitive.clientId, status: primitive.status, - agentId: primitive.agentId, + agent: primitive.agent, groupId: primitive.groupId, escalationLevel: primitive.escalationLevel, createdAt: primitive.createdAt, diff --git a/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts b/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts index b2c60d5..5256356 100644 --- a/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts +++ b/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts @@ -18,7 +18,7 @@ export class TicketMapper { status: doc.status as TicketStatus, description: doc.description, clientId: doc.clientId, - agentId: doc.agentId ?? undefined, + agent: doc.agent ?? null, escalationLevel: doc.escalationLevel, fileUrls: doc.attachmentsUrls, history: doc.history.map( From c7137637d4894ea4a10a049611209fe17a787ace Mon Sep 17 00:00:00 2001 From: YgorPereira Date: Sat, 2 May 2026 14:22:59 -0300 Subject: [PATCH 4/5] refactor: refactori assingtoagent flow --- .../domain/entities/ticket.entity.spec.ts | 11 ++--- .../ticket/domain/entities/ticket.entity.ts | 15 +++--- .../ticket.mongodb.repository.int.spec.ts | 49 +++++++++++++------ .../ticket/infra/mappers/ticket.mapper.ts | 4 +- .../repositories/ticket.mongodb.repository.ts | 27 ++++++++-- 5 files changed, 71 insertions(+), 35 deletions(-) diff --git a/backend/src/modules/ticket/domain/entities/ticket.entity.spec.ts b/backend/src/modules/ticket/domain/entities/ticket.entity.spec.ts index f2ed2b3..1000e78 100644 --- a/backend/src/modules/ticket/domain/entities/ticket.entity.spec.ts +++ b/backend/src/modules/ticket/domain/entities/ticket.entity.spec.ts @@ -7,7 +7,6 @@ import { TicketEvents, TicketEventMessage, TicketValidationErrors, - AgentField } from './ticket.entity'; describe('Ticket entity', () => { @@ -36,7 +35,7 @@ describe('Ticket entity', () => { const primitiveTicket = ticket.toPrimitives(); expect(primitiveTicket.status).toBe(TicketStatus.OPEN); - expect(primitiveTicket.agent).toBeNull(); + expect(primitiveTicket.agentId).toBeNull(); expect(primitiveTicket.groupId).toBeNull(); expect(primitiveTicket.escalationLevel).toBe(1); expect(primitiveTicket.createdAt).toBeInstanceOf(Date); @@ -54,12 +53,12 @@ describe('Ticket entity', () => { expect(ticket.history.length).toBe(2); expect(ticket.history[1]).toMatchObject({ event: TicketEvents.NEW_AGENT, - responsibleAgent: primitiveTicket.agent, + responsibleAgent: null, status: TicketStatus.IN_PROGRESS, message: TicketEventMessage.NEW_AGENT_MSG, }); - expect(primitiveTicket.agentId).toBe(newAgentId); + expect(ticket.agentId).toBe(newAgentId); expect(primitiveTicket.status).toBe(TicketStatus.IN_PROGRESS); expect(primitiveTicket.updatedAt).toBeInstanceOf(Date); expect(primitiveTicket.updatedAt).not.toBeNull(); @@ -72,11 +71,12 @@ describe('Ticket entity', () => { const newGroupId = randomUUID(); ticket.escalate(newGroupId, 'ia'); + expect(ticket.agentId).toBe(null); expect(ticket.status).toBe(TicketStatus.ESCALATED); expect(ticket.history.length).toBe(3); expect(ticket.history[2]).toMatchObject({ event: TicketEvents.ESCALATE, - responsibleAgent: newAgentId, + responsibleAgent: null, status: TicketStatus.ESCALATED, message: TicketEventMessage.ESCALATE_MSG, }); @@ -86,7 +86,6 @@ describe('Ticket entity', () => { expect(primitiveTicket.groupId).toBe(newGroupId); expect(primitiveTicket.category).toBe('ia'); expect(primitiveTicket.escalationLevel).toBe(1); - expect(primitiveTicket.agentId).toBeNull(); expect(primitiveTicket.updatedAt).toBeInstanceOf(Date); expect(primitiveTicket.updatedAt).not.toBeNull(); }); diff --git a/backend/src/modules/ticket/domain/entities/ticket.entity.ts b/backend/src/modules/ticket/domain/entities/ticket.entity.ts index 7476dd5..4b570e9 100644 --- a/backend/src/modules/ticket/domain/entities/ticket.entity.ts +++ b/backend/src/modules/ticket/domain/entities/ticket.entity.ts @@ -56,7 +56,7 @@ export class Ticket { private _id: string; private _status: TicketStatus = TicketStatus.OPEN; - // private _agentId: string | null = null; + private _agentId: string | null = null; private _agent: AgentField | null = null; private _groupId: string | null = null; private escalationLevel: number = 1; @@ -82,7 +82,7 @@ export class Ticket { } get agentId() { - return this._agent ? this._agent.id : null; + return this._agentId ?? null; } get agent() { @@ -189,7 +189,7 @@ export class Ticket { clientId: this._clientId, fileUrls: this.attachmentsUrls, status: this.status, - agent: this._agent, + agentId: this.agentId, groupId: this._groupId, escalationLevel: this.escalationLevel, history: this.history, @@ -207,7 +207,7 @@ export class Ticket { // Appends a new event to the ticket history private addHistory(props: { event: TicketEvents; - responsibleAgent: string | null; + responsibleAgent: AgentField | null; status: TicketStatus; message: string; solution?: string | null; @@ -227,7 +227,7 @@ export class Ticket { this.addHistory({ event: TicketEvents.NEW_AGENT, - responsibleAgent: this._agent ?? null, + responsibleAgent: null, status: TicketStatus.IN_PROGRESS, message: TicketEventMessage.NEW_AGENT_MSG, }); @@ -253,14 +253,13 @@ export class Ticket { this.escalationLevel++; } - const previousAgentId = this._agentId; this._agentId = null; - + this._agent = null; this._status = TicketStatus.ESCALATED; this.addHistory({ event: TicketEvents.ESCALATE, - responsibleAgent: this._agent ?? null, + responsibleAgent: null, status: TicketStatus.ESCALATED, message: TicketEventMessage.ESCALATE_MSG, solution: whatWasDone ?? null, diff --git a/backend/src/modules/ticket/domain/repository/ticket.mongodb.repository.int.spec.ts b/backend/src/modules/ticket/domain/repository/ticket.mongodb.repository.int.spec.ts index 6252876..66ac671 100644 --- a/backend/src/modules/ticket/domain/repository/ticket.mongodb.repository.int.spec.ts +++ b/backend/src/modules/ticket/domain/repository/ticket.mongodb.repository.int.spec.ts @@ -22,11 +22,16 @@ import { TicketStatus, } from '../entities/ticket.entity'; import { randomUUID } from 'crypto'; +import { User, UserSchema } from '../../../user/user.schema'; +import { getModelToken } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { UserRole } from '../../../shared/enums/user.enum'; describe('ITicketRepository', () => { let moduleRef: TestingModule; let repository: ITicketRepository; let connection: Connection; + let userModel: Model; beforeAll(async () => { moduleRef = await Test.createTestingModule({ @@ -40,10 +45,8 @@ describe('ITicketRepository', () => { }), }), MongooseModule.forFeature([ - { - name: TicketSchemaClass.name, - schema: TicketSchema, - }, + { name: TicketSchemaClass.name, schema: TicketSchema }, + { name: User.name, schema: UserSchema }, ]), ], providers: [ @@ -53,11 +56,13 @@ describe('ITicketRepository', () => { repository = moduleRef.get(ITicketRepository); connection = moduleRef.get(getConnectionToken()); + userModel = moduleRef.get>(getModelToken(User.name)); }); afterEach(async () => { const collections = connection.collections; await collections['tickets'].deleteMany({}); + await collections['users'].deleteMany({}); }); afterAll(async () => { @@ -81,9 +86,7 @@ describe('ITicketRepository', () => { expect(primitiveResult.status).toBe(TicketStatus.OPEN); expect(primitiveResult.escalationLevel).toBe(1); - expect(ticketToCreate.id).toBe(createResult.id); - expect(primitiveResult.agent).toBeNull(); expect(primitiveResult.groupId).toBeNull(); expect(primitiveResult.clientId).toBe(ticketToCreate.clientId); }); @@ -103,7 +106,6 @@ describe('ITicketRepository', () => { expect(resultReadAll).toBeDefined(); expect(Array.isArray(resultReadAll)).toBe(true); expect(resultReadAll.length).toBe(1); - resultReadAll.map((t) => expect(t).toBeInstanceOf(Ticket)); }); @@ -138,6 +140,16 @@ describe('ITicketRepository', () => { }); it('Should Save a ticket successfully', async () => { + // cria o agente no banco com ObjectId gerado pelo mongo + const createdUser = await userModel.create({ + name: 'João Silva', + email: `agent_${randomUUID()}@test.com`, + password: 'hashed_password', + role: UserRole.AGENT, + }); + + const agentObjectId = createdUser._id.toString(); + const ticketToCreate = Ticket.create({ title: 'chamado 5', category: randomUUID(), @@ -147,17 +159,20 @@ describe('ITicketRepository', () => { const ticket = await repository.create(ticketToCreate); - expect(ticket).toBeDefined(); expect(ticket.agentId).toBe(null); expect(ticket.status).toBe(TicketStatus.OPEN); - const newAgentId = randomUUID(); - ticket.assignToAgent(newAgentId); + ticket.assignToAgent(agentObjectId); const savedTicket = await repository.save(ticket); expect(savedTicket).toBeDefined(); - expect(savedTicket?.agent).toBe(newAgentId); + expect(savedTicket?.agent?.id).toBe(agentObjectId); + expect(savedTicket?.agent?.name).toBe('João Silva'); + expect(savedTicket?.agent).toEqual({ + id: agentObjectId, + name: 'João Silva', + }); expect(savedTicket?.status).toBe(TicketStatus.IN_PROGRESS); }); @@ -194,7 +209,6 @@ describe('ITicketRepository', () => { const foundedTicket = await repository.readById(createdTicket.id); - console.log(foundedTicket); expect(foundedTicket).toBeNull(); }); @@ -238,7 +252,14 @@ describe('ITicketRepository', () => { }); it('should read all tickets by agentId or unassigned tickets in the same group', async () => { - const agentId = randomUUID(); + const createdUser = await userModel.create({ + name: 'Agente Teste', + email: `agent_${randomUUID()}@test.com`, + password: 'hashed_password', + role: UserRole.SUPPORT, + }); + + const agentId = createdUser._id.toString(); const categoryId = randomUUID(); const ticket1 = Ticket.create({ @@ -273,8 +294,6 @@ describe('ITicketRepository', () => { categories: [categoryId], }); - console.log(result); - expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); expect(result.length).toBe(2); diff --git a/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts b/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts index 5256356..4f53eda 100644 --- a/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts +++ b/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts @@ -18,7 +18,9 @@ export class TicketMapper { status: doc.status as TicketStatus, description: doc.description, clientId: doc.clientId, - agent: doc.agent ?? null, + agent: doc.agent?.id + ? { id: doc.agent.id.toString(), name: doc.agent.name } + : null, escalationLevel: doc.escalationLevel, fileUrls: doc.attachmentsUrls, history: doc.history.map( diff --git a/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts b/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts index 89ea67c..80dbc66 100644 --- a/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts +++ b/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts @@ -35,7 +35,7 @@ export class TicketMongoRepository extends ITicketRepository { return null; } - return TicketMapper.toDomain(updated); + return await this.readById(updated._id); } async readAll(filters?: { @@ -62,8 +62,16 @@ export class TicketMongoRepository extends ITicketRepository { { $lookup: { from: 'users', - localField: 'agentId', - foreignField: '_id', + let: { agentIdStr: '$agentId' }, + pipeline: [ + { + $match: { + $expr: { + $eq: ['$_id', { $toObjectId: '$$agentIdStr' }], + }, + }, + }, + ], as: 'agentData', }, }, @@ -88,8 +96,16 @@ export class TicketMongoRepository extends ITicketRepository { { $lookup: { from: 'users', - localField: 'agentId', - foreignField: '_id', + let: { agentIdStr: '$agentId' }, + pipeline: [ + { + $match: { + $expr: { + $eq: ['$_id', { $toObjectId: '$$agentIdStr' }], + }, + }, + }, + ], as: 'agentData', }, }, @@ -106,6 +122,7 @@ export class TicketMongoRepository extends ITicketRepository { .exec(); if (!result) return null; + console.log('Result from readById in repository:', result); return TicketMapper.toDomain(result); } From 97ad9bff66c6f509f1d3aebc91500e2f7910a4a9 Mon Sep 17 00:00:00 2001 From: YgorPereira Date: Sat, 2 May 2026 14:57:05 -0300 Subject: [PATCH 5/5] feat: finish all of aggregate --- .../useCases/getHistory/getHistory.usecase.ts | 3 +- .../getHistoryFiltered.usecase.spec.ts | 13 ++-- .../getHistoryFiltered.usecase.ts | 16 +++-- .../domain/entities/ticket.entity.spec.ts | 2 +- .../ticket/domain/entities/ticket.entity.ts | 3 +- .../ticket/infra/mappers/ticket.mapper.ts | 7 ++- .../repositories/ticket.mongodb.repository.ts | 63 +++++++++++-------- .../infra/schemas/ticket.mongo.schema.ts | 5 +- .../controllers/ticket.controller.spec.ts | 2 +- .../controllers/ticket.controller.ts | 2 +- 10 files changed, 67 insertions(+), 49 deletions(-) diff --git a/backend/src/modules/ticket/application/useCases/getHistory/getHistory.usecase.ts b/backend/src/modules/ticket/application/useCases/getHistory/getHistory.usecase.ts index 5bd8802..03edd56 100644 --- a/backend/src/modules/ticket/application/useCases/getHistory/getHistory.usecase.ts +++ b/backend/src/modules/ticket/application/useCases/getHistory/getHistory.usecase.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { + AgentField, TicketEvents, TicketStatus, } from '../../../domain/entities/ticket.entity'; @@ -7,7 +8,7 @@ import { ITicketRepository } from '../../../domain/repository/ticket.repository. export interface TicketHistoryEntryOutput { event: TicketEvents; - responsibleAgent: string | null; + responsibleAgent: AgentField | null; status: TicketStatus; message: string; solution?: string | null; diff --git a/backend/src/modules/ticket/application/useCases/getHistoryFiltered/getHistoryFiltered.usecase.spec.ts b/backend/src/modules/ticket/application/useCases/getHistoryFiltered/getHistoryFiltered.usecase.spec.ts index b23610b..17df3c2 100644 --- a/backend/src/modules/ticket/application/useCases/getHistoryFiltered/getHistoryFiltered.usecase.spec.ts +++ b/backend/src/modules/ticket/application/useCases/getHistoryFiltered/getHistoryFiltered.usecase.spec.ts @@ -66,12 +66,12 @@ describe('GetHistoryFilteredUseCase', () => { repository.readById.mockResolvedValue(ticket); const output = await useCase.execute(ticket.id, { - responsibleAgent: agentId, + responsibleAgentId: agentId, }); - expect(output.history.every((e) => e.responsibleAgent === agentId)).toBe( - true, - ); + expect( + output.history.every((e) => e.responsibleAgent?.id === agentId), + ).toBe(true); expect(output.history).toHaveLength(1); }); @@ -121,17 +121,16 @@ describe('GetHistoryFilteredUseCase', () => { const output = await useCase.execute(ticket.id, { status: TicketStatus.IN_PROGRESS, - responsibleAgent: agentId, + responsibleAgentId: agentId, event: TicketEvents.NEW_AGENT, fromDate: before, }); expect(output.history).toHaveLength(1); expect(output.history[0].status).toBe(TicketStatus.IN_PROGRESS); - expect(output.history[0].responsibleAgent).toBe(agentId); + expect(output.history[0].responsibleAgent?.id).toBe(agentId); expect(output.history[0].event).toBe(TicketEvents.NEW_AGENT); }); - it('should return empty history when no entries match filters', async () => { repository.readById.mockResolvedValue(ticket); diff --git a/backend/src/modules/ticket/application/useCases/getHistoryFiltered/getHistoryFiltered.usecase.ts b/backend/src/modules/ticket/application/useCases/getHistoryFiltered/getHistoryFiltered.usecase.ts index 1f6c189..47a7ab1 100644 --- a/backend/src/modules/ticket/application/useCases/getHistoryFiltered/getHistoryFiltered.usecase.ts +++ b/backend/src/modules/ticket/application/useCases/getHistoryFiltered/getHistoryFiltered.usecase.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { + AgentField, TicketEvents, TicketStatus, } from '../../../domain/entities/ticket.entity'; @@ -7,7 +8,7 @@ import { ITicketRepository } from '../../../domain/repository/ticket.repository. export interface TicketHistoryEntryOutput { event: TicketEvents; - responsibleAgent: string | null; + responsibleAgent: AgentField | null; status: TicketStatus; message: string; solution?: string | null; @@ -27,7 +28,7 @@ export class GetHistoryFilteredUseCase { id: string, filters: { status?: TicketStatus; - responsibleAgent?: string; + responsibleAgentId?: string; event?: TicketEvents; fromDate?: Date; }, @@ -44,9 +45,9 @@ export class GetHistoryFilteredUseCase { history = history.filter((entry) => entry.status === filters.status); } - if (filters.responsibleAgent) { + if (filters.responsibleAgentId) { history = history.filter( - (entry) => entry.responsibleAgent === filters.responsibleAgent, + (entry) => entry.responsibleAgent?.id === filters.responsibleAgentId, ); } @@ -61,7 +62,12 @@ export class GetHistoryFilteredUseCase { return { id: foundTicket.id, - history: [...history], + history: history.map((entry) => ({ + ...entry, + responsibleAgent: entry.responsibleAgent + ? { id: entry.responsibleAgent.id, name: entry.responsibleAgent.name } + : null, + })), }; } } diff --git a/backend/src/modules/ticket/domain/entities/ticket.entity.spec.ts b/backend/src/modules/ticket/domain/entities/ticket.entity.spec.ts index 1000e78..efecd2b 100644 --- a/backend/src/modules/ticket/domain/entities/ticket.entity.spec.ts +++ b/backend/src/modules/ticket/domain/entities/ticket.entity.spec.ts @@ -53,7 +53,7 @@ describe('Ticket entity', () => { expect(ticket.history.length).toBe(2); expect(ticket.history[1]).toMatchObject({ event: TicketEvents.NEW_AGENT, - responsibleAgent: null, + responsibleAgent: { id: newAgentId, name: '' }, status: TicketStatus.IN_PROGRESS, message: TicketEventMessage.NEW_AGENT_MSG, }); diff --git a/backend/src/modules/ticket/domain/entities/ticket.entity.ts b/backend/src/modules/ticket/domain/entities/ticket.entity.ts index 4b570e9..a8aead0 100644 --- a/backend/src/modules/ticket/domain/entities/ticket.entity.ts +++ b/backend/src/modules/ticket/domain/entities/ticket.entity.ts @@ -190,6 +190,7 @@ export class Ticket { fileUrls: this.attachmentsUrls, status: this.status, agentId: this.agentId, + agent: this.agent, groupId: this._groupId, escalationLevel: this.escalationLevel, history: this.history, @@ -227,7 +228,7 @@ export class Ticket { this.addHistory({ event: TicketEvents.NEW_AGENT, - responsibleAgent: null, + responsibleAgent: { id: agentId, name: '' }, status: TicketStatus.IN_PROGRESS, message: TicketEventMessage.NEW_AGENT_MSG, }); diff --git a/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts b/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts index 4f53eda..d2225fb 100644 --- a/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts +++ b/backend/src/modules/ticket/infra/mappers/ticket.mapper.ts @@ -5,11 +5,12 @@ import { TicketPriority, TicketStatus, TicketHistoryEntry, + AgentField, } from '../../domain/entities/ticket.entity'; -import { TicketDocument, TicketLean } from '../schemas/ticket.mongo.schema'; +import { TicketLean } from '../schemas/ticket.mongo.schema'; export class TicketMapper { - static toDomain(doc: TicketDocument | TicketLean): Ticket { + static toDomain(doc: TicketLean): Ticket { return Ticket.restore({ _id: doc._id.toString(), title: doc.title, @@ -26,7 +27,7 @@ export class TicketMapper { history: doc.history.map( (h): TicketHistoryEntry => ({ event: h.event as TicketEvents, - responsibleAgent: h.responsibleAgent, + responsibleAgent: h.responsibleAgent as AgentField | null, status: h.status as TicketStatus, message: h.message as TicketEventMessage, solution: h.solution, diff --git a/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts b/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts index 80dbc66..f9e420c 100644 --- a/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts +++ b/backend/src/modules/ticket/infra/repositories/ticket.mongodb.repository.ts @@ -15,8 +15,8 @@ export class TicketMongoRepository extends ITicketRepository { async create(ticket: Ticket): Promise { const raw = TicketMapper.toPersistence(ticket); - const created = await this.ticketModel.create(raw); - return TicketMapper.toDomain(created); + await this.ticketModel.create(raw); + return this.readById(raw._id) as Promise; } async save(ticket: Ticket): Promise { @@ -56,36 +56,45 @@ export class TicketMongoRepository extends ITicketRepository { matchStage = { clientId: filters.clientId }; } - const tickets = await this.ticketModel - .aggregate([ - { $match: matchStage }, - { - $lookup: { - from: 'users', - let: { agentIdStr: '$agentId' }, - pipeline: [ - { - $match: { - $expr: { - $eq: ['$_id', { $toObjectId: '$$agentIdStr' }], + let tickets; + + if (!filters) { + tickets = await this.ticketModel.find().lean().exec(); + } else { + tickets = await this.ticketModel + .aggregate([ + { $match: matchStage }, + { + $lookup: { + from: 'users', + let: { agentIdStr: '$agentId' }, + pipeline: [ + { + $match: { + $expr: { + $eq: ['$_id', { $toObjectId: '$$agentIdStr' }], + }, }, }, - }, - ], - as: 'agentData', + ], + as: 'agentData', + }, }, - }, - { - $addFields: { - agent: { - id: { $arrayElemAt: ['$agentData._id', 0] }, - name: { $arrayElemAt: ['$agentData.name', 0] }, + { + $addFields: { + agent: { + id: { $arrayElemAt: ['$agentData._id', 0] }, + name: { $arrayElemAt: ['$agentData.name', 0] }, + }, }, }, - }, - { $project: { agentData: 0 } }, - ]) - .exec(); + { $project: { agentData: 0 } }, + ]) + .exec(); + } + + if (!tickets) return []; + return tickets.map((t) => TicketMapper.toDomain(t)); } diff --git a/backend/src/modules/ticket/infra/schemas/ticket.mongo.schema.ts b/backend/src/modules/ticket/infra/schemas/ticket.mongo.schema.ts index 796f49c..0e53b2f 100644 --- a/backend/src/modules/ticket/infra/schemas/ticket.mongo.schema.ts +++ b/backend/src/modules/ticket/infra/schemas/ticket.mongo.schema.ts @@ -13,8 +13,8 @@ export class TicketHistoryEntrySchema { @Prop({ required: true, enum: Object.values(TicketEvents) }) event: string; - @Prop({ type: String, default: null }) - responsibleAgent: string | null; + @Prop({ type: { id: String, name: String }, default: null }) + responsibleAgent: { id: string; name: string } | null; @Prop({ required: true, enum: Object.values(TicketStatus) }) status: string; @@ -89,6 +89,7 @@ export type TicketDocument = HydratedDocument; export type TicketLean = Omit & { _id: string; + agentId: string | null; agent: { id: string; name: string } | null; }; diff --git a/backend/src/modules/ticket/presentation/controllers/ticket.controller.spec.ts b/backend/src/modules/ticket/presentation/controllers/ticket.controller.spec.ts index 880101a..5b1d3dc 100644 --- a/backend/src/modules/ticket/presentation/controllers/ticket.controller.spec.ts +++ b/backend/src/modules/ticket/presentation/controllers/ticket.controller.spec.ts @@ -546,7 +546,7 @@ describe('TicketController', () => { expect.objectContaining({ status: TicketStatus.IN_PROGRESS, event: TicketEvents.NEW_AGENT, - responsibleAgent: agentId, + responsibleAgentId: agentId, }), ); expect(getHistoryUseCase.execute).not.toHaveBeenCalled(); diff --git a/backend/src/modules/ticket/presentation/controllers/ticket.controller.ts b/backend/src/modules/ticket/presentation/controllers/ticket.controller.ts index 0769349..e16be61 100644 --- a/backend/src/modules/ticket/presentation/controllers/ticket.controller.ts +++ b/backend/src/modules/ticket/presentation/controllers/ticket.controller.ts @@ -138,7 +138,7 @@ export class TicketController { if (hasFilters) { response = await this.getHistoryFilteredUseCase.execute(id, { status: filters.status, - responsibleAgent: filters.responsibleAgent, + responsibleAgentId: filters.responsibleAgent, event: filters.event, fromDate: filters.fromDate ? new Date(filters.fromDate) : undefined, });