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
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/mongoose';
import { ChatService } from './chat.service';
import { TicketSchemaClass } from '../../ticket/infra/schemas/ticket.mongo.schema';
import { UserRole } from '../../user/user.schema';
import { NotFoundException } from '@nestjs/common';

Expand All @@ -12,6 +14,7 @@ const mockChatRepository: jest.Mocked<IChatRepository> = {
findByParticipant: jest.fn(),
findByTicketId: jest.fn(),
updateStatus: jest.fn(),
updateAgent: jest.fn(),
};

const mockMessageRepository: jest.Mocked<IMessageRepository> = {
Expand Down Expand Up @@ -41,6 +44,8 @@ describe('ChatService — envio de mensagem com arquivo', () => {
ChatService,
{ provide: 'IChatRepository', useValue: mockChatRepository },
{ provide: 'IMessageRepository', useValue: mockMessageRepository },
{ provide: getModelToken(TicketSchemaClass.name), useValue: {} },
{ provide: getModelToken(UserRole.ADMIN ? 'User' : 'User'), useValue: {} },
],
}).compile();

Expand Down
5 changes: 5 additions & 0 deletions backend/src/modules/chat/application/chat.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/mongoose';
import { ChatService } from './chat.service';
import { TicketSchemaClass } from '../../ticket/infra/schemas/ticket.mongo.schema';
import { ForbiddenException, NotFoundException } from '@nestjs/common';
import { ChatDetails, ChatStatus } from '../domain/chat.entity';
import { IChatRepository } from '../domain/chat.repository';
Expand All @@ -12,6 +14,7 @@ const mockChatRepository: jest.Mocked<IChatRepository> = {
findByParticipant: jest.fn(),
findByTicketId: jest.fn(),
updateStatus: jest.fn(),
updateAgent: jest.fn(),
};

const mockMessageRepository: jest.Mocked<IMessageRepository> = {
Expand Down Expand Up @@ -45,6 +48,8 @@ describe('ChatService', () => {
ChatService,
{ provide: 'IChatRepository', useValue: mockChatRepository },
{ provide: 'IMessageRepository', useValue: mockMessageRepository },
{ provide: getModelToken(TicketSchemaClass.name), useValue: {} },
{ provide: getModelToken(UserRole.ADMIN ? 'User' : 'User'), useValue: {} }, // Gambiarra provisória pra obter a string 'User'
],
}).compile();

Expand Down
40 changes: 38 additions & 2 deletions backend/src/modules/chat/application/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import type { IChatRepository } from '../domain/chat.repository';
import type { IMessageRepository } from '../../Messages/domain/message.repository';
import type { ChatDetails } from '../domain/chat.entity';
import { ChatStatus } from '../domain/chat.entity';
import { UserRole } from '../../user/user.schema';
import { User, UserDocument, UserRole } from '../../user/user.schema';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { TicketSchemaClass, TicketDocument } from '../../ticket/infra/schemas/ticket.mongo.schema';

@Injectable()
export class ChatService {
Expand All @@ -17,6 +20,8 @@ export class ChatService {
private readonly chatRepository: IChatRepository,
@Inject('IMessageRepository')
private readonly messageRepository: IMessageRepository,
@InjectModel(TicketSchemaClass.name) private ticketModel: Model<TicketDocument>,
@InjectModel(User.name) private userModel: Model<UserDocument>,
) {}

async createChat(
Expand Down Expand Up @@ -105,11 +110,42 @@ export class ChatService {
return result;
}

async updateAgentByTicketId(ticketId: string, agentId: string | null): Promise<ChatDetails | null> {
return this.chatRepository.updateAgent(ticketId, agentId);
}

async isParticipant(chatId: string, userId: string): Promise<boolean> {
const chat = await this.chatRepository.findById(chatId);
if (!chat) {
return false;
}
return chat.clientId === userId || chat.agentId === userId;

if (chat.clientId === userId || chat.agentId === userId) {
return true;
}

// Se o chat está na "Fila Aberta" (Sem dono)
if (!chat.agentId) {
const user = await this.userModel.findById(userId).exec();
const ticket = await this.ticketModel.findById(chat.ticketId).exec();

if (user && ticket) {
if (user.role === UserRole.ADMIN) {
return true;
}

if (user.role === UserRole.SUPPORT) {
const hasCategory = user.categories.some(
(cat) => cat.toString() === ticket.category.toString()
);

if (hasCategory && (user.level || 1) >= ticket.escalationLevel) {
return true;
}
}
}
}

return false;
}
}
4 changes: 4 additions & 0 deletions backend/src/modules/chat/chat.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import { ChatGateway } from './presentation/chat.gateway';
import { Message, MessageSchema } from '../Messages/infra/message.schema';
import { MessageRepositoryMongodb } from '../Messages/infra/message.repository.mongodb';
import { ChatController } from './presentation/chat.controller';
import { TicketSchemaClass, TicketSchema } from '../ticket/infra/schemas/ticket.mongo.schema';
import { User, UserSchema } from '../user/user.schema';

@Module({
imports: [
MongooseModule.forFeature([
{ name: Chat.name, schema: ChatSchema },
{ name: Message.name, schema: MessageSchema },
{ name: TicketSchemaClass.name, schema: TicketSchema },
{ name: User.name, schema: UserSchema },
]),
JwtModule.register({ secret: 'secret' }),
],
Expand Down
2 changes: 2 additions & 0 deletions backend/src/modules/chat/domain/chat.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ export interface IChatRepository {
findByTicketId(ticketId: string): Promise<ChatDetails | null>;

updateStatus(id: string, status: ChatStatus): Promise<ChatDetails | null>;

updateAgent(ticketId: string, agentId: string | null): Promise<ChatDetails | null>;
}
44 changes: 25 additions & 19 deletions backend/src/modules/chat/infra/chat.repository.mongodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,44 +39,42 @@ export class ChatRepositoryMongodb implements IChatRepository {

async findById(id: string): Promise<ChatDetails | null> {
const chatDoc = await this.chatModel.findById(id).exec();

if (!chatDoc) return null;

return {
id: chatDoc._id as string,
ticketId: chatDoc.ticketId.toString(),
clientId: chatDoc.clientId.toString(),
agentId: chatDoc.agentId.toString(),
groupId: chatDoc.groupId.toString(),
ticketId: chatDoc.ticketId?.toString() || '',
clientId: chatDoc.clientId?.toString() || '',
agentId: chatDoc.agentId?.toString() || '',
groupId: chatDoc.groupId?.toString() || '',
status: chatDoc.status as any,
};
}

async findByParticipant(userId: string): Promise<ChatDetails[]> {
const docs = await this.chatModel
.find({
$or: [{ clientId: userId }, { agentId: userId }],
})
.sort({ createdAt: -1 })
.exec();
return docs.map((doc) => this.toDetails(doc));
}

async findByTicketId(ticketId: string): Promise<ChatDetails | null> {
const chatDoc = await this.chatModel.findOne({ ticketId }).exec();

if (!chatDoc) return null;

return {
id: chatDoc._id as string,
ticketId: chatDoc.ticketId?.toString() || '',
clientId: chatDoc.clientId?.toString() || '',
agentId: chatDoc.agentId?.toString() || '',
agentId: chatDoc.agentId?.toString() || '',
groupId: chatDoc.groupId?.toString() || '',
status: chatDoc.status as any,
};
}

async findByParticipant(userId: string): Promise<ChatDetails[]> {
const docs = await this.chatModel
.find({
$or: [{ clientId: userId }, { agentId: userId }],
})
.sort({ createdAt: -1 })
.exec();
return docs.map((doc) => this.toDetails(doc));
}

async updateStatus(
id: string,
status: ChatStatus,
Expand All @@ -87,4 +85,12 @@ export class ChatRepositoryMongodb implements IChatRepository {
if (!doc) return null;
return this.toDetails(doc);
}

async updateAgent(ticketId: string, agentId: string | null): Promise<ChatDetails | null> {
const doc = await this.chatModel
.findOneAndUpdate({ ticketId }, { agentId }, { new: true })
.exec();
if (!doc) return null;
return this.toDetails(doc);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ describe('CreateTicketUseCase', () => {

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

useCase = new CreateTicketUseCase(repository, triageService);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ describe('DeleteTicketUseCase', () => {
beforeEach(() => {
repository = {
delete: jest.fn().mockResolvedValue(true),
};
create: jest.fn(),
save: jest.fn(),
readAll: jest.fn(),
readById: jest.fn(),
findByStatus: jest.fn(),
findByStatusCategory: jest.fn(),
} as unknown as jest.Mocked<ITicketRepository>;

useCase = new DeleteTicketUseCase(repository);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
Ticket,
TicketStatus,
} from '../../../domain/entities/ticket.entity';
import { ChatService } from '../../../../chat/application/chat.service';

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

Expand All @@ -25,14 +27,19 @@ describe('EscalateTicketUseCase', () => {
save: jest.fn(),
} as unknown as jest.Mocked<ITicketRepository>;

useCase = new EscalateTicketUseCase(repository);
chatService = {
updateAgentByTicketId: jest.fn(),
} as unknown as jest.Mocked<ChatService>;

useCase = new EscalateTicketUseCase(repository, chatService);
});

it('should escalate ticket successfully', async () => {
const input: EscalateTicketInput = {
id: ticket.id,
groupId: randomUUID(),
category: 'iot',
whatWasDone: 'Feito algo',
};

ticket.assignToAgent(randomUUID());
Expand All @@ -47,7 +54,7 @@ describe('EscalateTicketUseCase', () => {
expect(output).toBeDefined();
expect(output.id).toBe(ticket.id);
expect(output.status).toBe(TicketStatus.ESCALATED);
expect(output.escalationLevel).toBe(2);
expect(output.escalationLevel).toBe(1);
expect(output.updatedAt).toBeInstanceOf(Date);

expect(repository.readById).toHaveBeenCalledWith(input.id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
TicketStatus,
} from '../../../domain/entities/ticket.entity';
import { ITicketRepository } from '../../../domain/repository/ticket.repository.interface';
import { ChatService } from '../../../../chat/application/chat.service';

export interface EscalateTicketInput {
id: string;
Expand All @@ -28,7 +29,10 @@ export interface EscalateTicketOutput {

@Injectable()
export class EscalateTicketUseCase {
constructor(private readonly repository: ITicketRepository) {}
constructor(
private readonly repository: ITicketRepository,
private readonly chatService: ChatService,
) {}

async execute(input: EscalateTicketInput): Promise<EscalateTicketOutput> {
const foundedTicket = await this.repository.readById(input.id);
Expand All @@ -45,6 +49,12 @@ export class EscalateTicketUseCase {
throw new Error('Ticket not escalated');
}

try {
await this.chatService.updateAgentByTicketId(escalatedTicket.id, escalatedTicket.agentId);
} catch (e) {
console.warn('Chat não pôde ser atualizado ou não existe:', e);
}

const primitive = escalatedTicket.toPrimitives();

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
TicketStatus,
} from '../../../domain/entities/ticket.entity';
import { NewAgentTicketUseCase } from './newAgent.usecase';
import { ChatService } from '../../../../chat/application/chat.service';

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

Expand All @@ -25,7 +27,11 @@ describe('NewAgentTicketUseCase', () => {
save: jest.fn(),
} as unknown as jest.Mocked<ITicketRepository>;

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

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

it('should assing a new agent to a ticket successfully', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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';

export interface NewAgentTicketInput {
id: string;
Expand All @@ -15,7 +16,10 @@ export interface NewAgentTicketOutput {

@Injectable()
export class NewAgentTicketUseCase {
constructor(private readonly repository: ITicketRepository) {}
constructor(
private readonly repository: ITicketRepository,
private readonly chatService: ChatService,
) {}

async execute(input: NewAgentTicketInput): Promise<NewAgentTicketOutput> {
const foundedTicket = await this.repository.readById(input.id);
Expand All @@ -32,6 +36,13 @@ export class NewAgentTicketUseCase {
throw new Error('Fail to update ticket.');
}

try {
await this.chatService.updateAgentByTicketId(updatedTicket.id, updatedTicket.agentId);
} catch (e) {
// Ignora se o chat não existir, pois os módulos estão fracamente acoplados
console.warn('Chat não pôde ser atualizado ou não existe:', e);
}

return {
id: updatedTicket.id,
agentId: updatedTicket.agentId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ describe('Ticket entity', () => {

expect(primitiveTicket.groupId).toBe(newGroupId);
expect(primitiveTicket.category).toBe('ia');
expect(primitiveTicket.escalationLevel).toBe(2);
expect(primitiveTicket.escalationLevel).toBe(1);
expect(primitiveTicket.agentId).toBeNull();
expect(primitiveTicket.updatedAt).toBeInstanceOf(Date);
expect(primitiveTicket.updatedAt).not.toBeNull();
});
Expand Down
Loading
Loading