Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/model.nlp

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { CloseTicketUseCase } from './close.usecase';
import { ITicketRepository } from '../../../domain/repository/ticket.repository.interface';
import { Ticket } from '../../../domain/entities/ticket.entity';

describe('CloseTicketUseCase', () => {
let useCase: CloseTicketUseCase;
let repository: jest.Mocked<ITicketRepository>;

beforeEach(() => {
repository = {
create: jest.fn(),
readById: jest.fn(),
readAll: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
} as unknown as jest.Mocked<ITicketRepository>;

useCase = new CloseTicketUseCase(repository);
});

it('should close a ticket successfully', async () => {
// Arrange
const ticket = Ticket.create({
title: 'Erro no sistema',
category: 'TI',
description: 'Sistema não responde',
clientId: 'client-id',
});

// precisa estar IN_PROGRESS para fechar
ticket.assignToAgent('agent-id');

repository.readById.mockResolvedValue(ticket);
repository.save.mockResolvedValue(ticket);

// Act
const result = await useCase.execute({
id: ticket.id,
solution: 'Servidor reiniciado',
});

// Assert
expect(repository.readById).toHaveBeenCalledWith(ticket.id);
expect(repository.save).toHaveBeenCalled();

expect(result.status).toBe('CLOSED');
});

it('should throw error if ticket not found', async () => {
repository.readById.mockResolvedValue(null);

await expect(
useCase.execute({
id: 'invalid-id',
solution: 'Teste',
}),
).rejects.toThrow('Ticket not found');
});

it('should throw error if ticket is not IN_PROGRESS', async () => {
const ticket = Ticket.create({
title: 'Erro no sistema',
category: 'TI',
description: 'Sistema não responde',
clientId: 'client-id',
});

repository.readById.mockResolvedValue(ticket);

await expect(
useCase.execute({
id: ticket.id,
solution: 'Teste',
}),
).rejects.toThrow();
});

it('should throw error if solution is empty', async () => {
const ticket = Ticket.create({
title: 'Erro no sistema',
category: 'TI',
description: 'Sistema não responde',
clientId: 'client-id',
});

ticket.assignToAgent('agent-id');

repository.readById.mockResolvedValue(ticket);

await expect(
useCase.execute({
id: ticket.id,
solution: '',
}),
).rejects.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Injectable } from '@nestjs/common';
import {
TicketPriority,
TicketStatus,
} from '../../../domain/entities/ticket.entity';
import { ITicketRepository } from '../../../domain/repository/ticket.repository.interface';

export interface CloseTicketInput {
id: string;
solution: string;
}

export interface CloseTicketOutput {
id: string;
title: string;
category: string;
priority: TicketPriority;
description: string;
clientId: string;
status: TicketStatus;
agentId: string | null;
escalationLevel: number;
createdAt: Date;
updatedAt: Date | null;
}

@Injectable()
export class CloseTicketUseCase {
constructor(private readonly repository: ITicketRepository) {}

async execute(input: CloseTicketInput): Promise<CloseTicketOutput> {
const foundedTicket = await this.repository.readById(input.id);

if (!foundedTicket) {
throw new Error('Ticket not found');
}

// regra de negócio
foundedTicket.close(input.solution);

const closedTicket = await this.repository.save(foundedTicket);

if (!closedTicket) {
throw new Error('Ticket not closed');
}

const primitive = closedTicket.toPrimitives();

return {
id: primitive._id,
title: primitive.title,
category: primitive.category,
priority: primitive.priority,
description: primitive.description,
clientId: primitive.clientId,
status: primitive.status,
agentId: primitive.agentId,
escalationLevel: primitive.escalationLevel,
createdAt: primitive.createdAt,
updatedAt: primitive.updatedAt,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '../../domain/entities/ticket.entity';
import { randomUUID } from 'crypto';
import request from 'supertest';
import { CloseTicketUseCase } from '../../application/useCases/close/close.usecase';

describe('TicketController', () => {
let app: INestApplication;
Expand All @@ -27,6 +28,7 @@ describe('TicketController', () => {
let getHistoryUseCase: GetHistoryTicketUseCase;
let newAgentUseCase: NewAgentTicketUseCase;
let escalateTicketUseCase: EscalateTicketUseCase;
let closeTicketUseCase: CloseTicketUseCase;
let deleteUseCase: DeleteTicketUseCase;

const ticketData = {
Expand Down Expand Up @@ -65,6 +67,10 @@ describe('TicketController', () => {
provide: NewAgentTicketUseCase,
useValue: { execute: jest.fn() },
},
{
provide: CloseTicketUseCase,
useValue: { execute: jest.fn() },
},
{
provide: DeleteTicketUseCase,
useValue: { execute: jest.fn() },
Expand All @@ -90,6 +96,7 @@ describe('TicketController', () => {
getHistoryUseCase = modulesFixture.get(GetHistoryTicketUseCase);
newAgentUseCase = modulesFixture.get(NewAgentTicketUseCase);
escalateTicketUseCase = modulesFixture.get(EscalateTicketUseCase);
closeTicketUseCase = modulesFixture.get(CloseTicketUseCase);
deleteUseCase = modulesFixture.get(DeleteTicketUseCase);
});

Expand Down Expand Up @@ -334,6 +341,53 @@ describe('TicketController', () => {
expect(escalateTicketUseCase.execute).toHaveBeenCalledTimes(1);
});

it('PUT /tickets/:id/close should close a ticket and return updated', async () => {
const agentId = randomUUID();

// precisa estar IN_PROGRESS antes de fechar
ticket.assignToAgent(agentId);

const primitives = ticket.toPrimitives();

// simula fechamento
ticket.close('Servidor reiniciado');

jest.spyOn(closeTicketUseCase, 'execute').mockResolvedValue({
id: primitives._id,
title: primitives.title,
category: primitives.category,
priority: primitives.priority,
description: primitives.description,
clientId: primitives.clientId,
status: TicketStatus.CLOSED,
agentId: agentId,
escalationLevel: primitives.escalationLevel,
createdAt: primitives.createdAt,
updatedAt: primitives.updatedAt,
});

const payload = {
solution: 'Servidor reiniciado',
};

const response = await request(httpServer)
.put(`/tickets/${ticket.id}/close`)
.send(payload)
.expect(200);

expect(response.body).toBeInstanceOf(Object);

expect(response.body).toEqual(
expect.objectContaining({
id: primitives._id,
status: TicketStatus.CLOSED,
agentId: agentId,
}),
);

expect(closeTicketUseCase.execute).toHaveBeenCalledTimes(1);
});

it('DELETE /tickets/:id/ should delete a ticket and return a boolean', async () => {
jest.spyOn(deleteUseCase, 'execute').mockResolvedValue(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import { GetHistoryTicketUseCase } from '../../application/useCases/getHistory/g
import { NewAgentTicketUseCase } from '../../application/useCases/newAgent/newAgent.usecase';
import { ReadAllTicketUseCase } from '../../application/useCases/readAll/readAll.usecase';
import { ReadByIdTicketUseCase } from '../../application/useCases/readById/readById.usecase';
import { CloseTicketUseCase } from '../../application/useCases/close/close.usecase';
import { CreateTicketRequest } from '../dtos/create.dto';
import { EscalateTicketRequest } from '../dtos/escalateTicket.dto';
import { TicketMapper } from '../mappers/ticket.mapper';
import { AssignAgentRequest } from '../dtos/assignAgent.dto';
import { CloseTicketRequest } from '../dtos/closeTicket.dto';
import {
ApiBody,
ApiOperation,
Expand All @@ -39,7 +41,8 @@ export class TicketController {
private readonly escalateUseCase: EscalateTicketUseCase,
private readonly newAgentUseCase: NewAgentTicketUseCase,
private readonly deleteUseCase: DeleteTicketUseCase,
) {}
private readonly closeUseCase: CloseTicketUseCase,
) { }

@Post()
@ApiOperation({ summary: 'Cria um ticket' })
Expand Down Expand Up @@ -123,4 +126,20 @@ export class TicketController {

return { deleted: response };
}

@Put(':id/close')
@ApiOperation({ summary: 'Fecha um ticket' })
@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,
) {
const data = TicketMapper.toCloseTicketInput(id, body);

const response = await this.closeUseCase.execute(data);

return response;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { IsUUID, IsString } from 'class-validator';
import { IsString, IsMongoId } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { randomUUID } from 'crypto';

export class AssignAgentRequest {
@ApiProperty({ example: randomUUID(), description: 'ID do agente' })
@IsUUID()
@IsMongoId()
@IsString()
agentId!: string;
}
11 changes: 11 additions & 0 deletions backend/src/modules/ticket/presentation/dtos/closeTicket.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty } from 'class-validator';

export class CloseTicketRequest {
@ApiProperty({
example: 'O problema foi resolvido reiniciando o servidor.',
})
@IsString()
@IsNotEmpty()
solution: string;
}
11 changes: 11 additions & 0 deletions backend/src/modules/ticket/presentation/mappers/ticket.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CreateTicketInput } from '../../application/useCases/create/create.usec
import { EscalateTicketInput } from '../../application/useCases/escalate/escalate.usecase';
import { NewAgentTicketInput } from '../../application/useCases/newAgent/newAgent.usecase';
import { AssignAgentRequest } from '../dtos/assignAgent.dto';
import { CloseTicketRequest } from '../dtos/closeTicket.dto';
import { CreateTicketRequest } from '../dtos/create.dto';
import { EscalateTicketRequest } from '../dtos/escalateTicket.dto';

Expand Down Expand Up @@ -34,4 +35,14 @@ export class TicketMapper {
category: req.category,
};
}

static toCloseTicketInput(
id: string,
body: CloseTicketRequest,
) {
return {
id,
solution: body.solution,
};
}
}
2 changes: 2 additions & 0 deletions backend/src/modules/ticket/ticket.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EscalateTicketUseCase } from './application/useCases/escalate/escalate.
import { DeleteTicketUseCase } from './application/useCases/delete/delete.usecase';
import { NewAgentTicketUseCase } from './application/useCases/newAgent/newAgent.usecase';
import { TriageModule } from '../triage/triage.module';
import { CloseTicketUseCase } from './application/useCases/close/close.usecase';

@Module({
imports: [
Expand All @@ -33,6 +34,7 @@ import { TriageModule } from '../triage/triage.module';
EscalateTicketUseCase,
DeleteTicketUseCase,
NewAgentTicketUseCase,
CloseTicketUseCase,
],
})
export class TicketModule {}
Loading