Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { ITicketRepository } from '../../../domain/repository/ticket.repository.
import { Ticket, TicketStatus } from '../../../domain/entities/ticket.entity';
import { NewAgentTicketUseCase } from './newAgent.usecase';
import { ChatService } from '../../../../chat/application/chat.service';
import { UserService } from '../../../../user/user.service';
import { UserRole } from '../../../../shared/enums/user.enum';
import { ForbiddenException, NotFoundException } from '@nestjs/common';

describe('NewAgentTicketUseCase', () => {
let repository: jest.Mocked<ITicketRepository>;
let chatService: jest.Mocked<ChatService>;
let userService: jest.Mocked<UserService>;
let useCase: NewAgentTicketUseCase;
let ticket: Ticket;

Expand All @@ -28,16 +32,25 @@ describe('NewAgentTicketUseCase', () => {
updateAgentByTicketId: jest.fn(),
} as unknown as jest.Mocked<ChatService>;

useCase = new NewAgentTicketUseCase(repository, chatService);
userService = {
findById: jest.fn(),
} as unknown as jest.Mocked<UserService>;

useCase = new NewAgentTicketUseCase(repository, chatService, userService);
});

it('should assign a new agent to a ticket successfully', async () => {
it('should assign a new agent to a ticket successfully as ADMIN', async () => {
const input = {
id: ticket.id,
agentId: randomUUID(),
};

repository.readById.mockResolvedValue(ticket);
userService.findById.mockResolvedValue({
id: input.agentId,
role: UserRole.ADMIN,
} as any);

ticket.assignToAgent(input.agentId);
repository.save.mockResolvedValue(ticket);

Expand All @@ -55,8 +68,86 @@ describe('NewAgentTicketUseCase', () => {
expect(chatService.updateAgentByTicketId).toHaveBeenCalledTimes(1);
expect(chatService.updateAgentByTicketId).toHaveBeenCalledWith(ticket.id, input.agentId);

expect(output).not.toHaveProperty('toPrimitives');
expect(output).not.toHaveProperty('escalate');
expect(output).not.toHaveProperty('assignToAgent');
});

it('should assign a new agent to a ticket successfully as SUPPORT with correct level and sector', async () => {
const input = {
id: ticket.id,
agentId: randomUUID(),
};

repository.readById.mockResolvedValue(ticket);
userService.findById.mockResolvedValue({
id: input.agentId,
role: UserRole.SUPPORT,
level: 1,
categories: [{ id: 'bi', name: 'bi' }],
} as any);

ticket.assignToAgent(input.agentId);
repository.save.mockResolvedValue(ticket);

const output = await useCase.execute(input);

expect(output).toBeDefined();
expect(output.agentId).toBe(input.agentId);
});

it('should fail if SUPPORT agent level is different from ticket escalationLevel (e.g. lower)', async () => {
const input = {
id: ticket.id,
agentId: randomUUID(),
};

const escalatedTicket = Object.assign(Object.create(Object.getPrototypeOf(ticket)), ticket);
escalatedTicket.escalationLevel = 3;

repository.readById.mockResolvedValue(escalatedTicket);
userService.findById.mockResolvedValue({
id: input.agentId,
role: UserRole.SUPPORT,
level: 1,
categories: [{ id: 'bi', name: 'bi' }],
} as any);

await expect(useCase.execute(input)).rejects.toThrow(ForbiddenException);
});

it('should fail if SUPPORT agent level is different from ticket escalationLevel (e.g. higher)', async () => {
const input = {
id: ticket.id,
agentId: randomUUID(),
};

const escalatedTicket = Object.assign(Object.create(Object.getPrototypeOf(ticket)), ticket);
escalatedTicket.escalationLevel = 1;

repository.readById.mockResolvedValue(escalatedTicket);
userService.findById.mockResolvedValue({
id: input.agentId,
role: UserRole.SUPPORT,
level: 2,
categories: [{ id: 'bi', name: 'bi' }],
} as any);

await expect(useCase.execute(input)).rejects.toThrow(ForbiddenException);
});

it('should fail if SUPPORT agent does not belong to the ticket category', async () => {
const input = {
id: ticket.id,
agentId: randomUUID(),
};

repository.readById.mockResolvedValue(ticket); // category is 'bi'
userService.findById.mockResolvedValue({
id: input.agentId,
role: UserRole.SUPPORT,
level: 1,
categories: [{ id: 'infra', name: 'infra' }], // different category
} as any);


await expect(useCase.execute(input)).rejects.toThrow(ForbiddenException);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Injectable } from '@nestjs/common';
import { TicketStatus } from '../../../domain/entities/ticket.entity';
import { ITicketRepository } from '../../../domain/repository/ticket.repository.interface';
import { ChatService } from '../../../../chat/application/chat.service';
import { UserService } from '../../../../user/user.service';
import { UserRole } from '../../../../shared/enums/user.enum';
import { ForbiddenException, NotFoundException } from '@nestjs/common';

export interface NewAgentTicketInput {
id: string;
Expand All @@ -19,13 +22,35 @@ export class NewAgentTicketUseCase {
constructor(
private readonly repository: ITicketRepository,
private readonly chatService: ChatService,
private readonly userService: UserService,
) {}

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

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

const user = await this.userService.findById(input.agentId);

if (!user) {
throw new NotFoundException('User not found.');
}

if (user.role === UserRole.SUPPORT) {
const userLevel = user.level || 1;
const ticketLevel = foundedTicket.level || 1;

if (userLevel !== ticketLevel) {
throw new ForbiddenException(`You do not have the exact required level (${ticketLevel}) to assign this ticket.`);
}

const ticketCategory = foundedTicket.ticketCategory.toString();
const hasCategory = user.categories?.some((cat) => cat.name === ticketCategory || cat.id?.toString() === ticketCategory || cat.id === ticketCategory);
if (!hasCategory) {
throw new ForbiddenException('You do not belong to the sector of this ticket.');
}
}

foundedTicket.assignToAgent(input.agentId);
Expand Down
8 changes: 8 additions & 0 deletions backend/src/modules/ticket/domain/entities/ticket.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ export class Ticket {
return [...this._history];
}

get ticketCategory() {
return this.category;
}

get level() {
return this.escalationLevel;
}

// Function to create a new ticket instance
static create(props: {
title: string;
Expand Down
2 changes: 1 addition & 1 deletion backend/src/modules/ticket/infra/mappers/ticket.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class TicketMapper {
return Ticket.restore({
_id: doc._id.toString(),
title: doc.title,
category: doc.category,
category: doc.category.toString(),
priority: doc.priority as TicketPriority,
status: doc.status as TicketStatus,
description: doc.description,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export class TicketController {
@ApiOperation({ summary: 'Atribui um agente ao ticket' })
@ApiParam({ name: 'id', example: 'uuid-do-ticket' })
@UseGuards(JwtGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.SUPPORT)
@Roles(UserRole.SUPPORT)
@ApiResponse({ status: 200, description: 'Agente atribuído com sucesso.' })
async assignAgent(@Request() req: any, @Param('id') id: string) {
const data = TicketMapper.toNewAgentInput(id, req.user.id);
Expand Down
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 @@ -18,6 +18,7 @@ import { TriageModule } from '../triage/triage.module';
import { GetHistoryFilteredUseCase } from './application/useCases/getHistoryFiltered/getHistoryFiltered.usecase';
import { CloseTicketUseCase } from './application/useCases/close/close.usecase';
import { ChatModule } from '../chat/chat.module';
import { UserModule } from '../user/user.module';

@Module({
imports: [
Expand All @@ -26,6 +27,7 @@ import { ChatModule } from '../chat/chat.module';
]),
TriageModule,
ChatModule,
UserModule,
],
controllers: [TicketController],
providers: [
Expand Down
Loading