From 18eb3a1545c0f5f59ab099124b59dac712f45ca4 Mon Sep 17 00:00:00 2001 From: Gabriel Viell Castilho Date: Wed, 27 May 2026 11:23:08 -0300 Subject: [PATCH] feat: add notification when access request is created --- backend/shared/events/access-request.event.ts | 8 ++ .../dto/create-notification.dto.ts | 1 + .../listeners/access-request.listener.ts | 39 ++++++++++ .../notification/notification.module.ts | 3 + .../shared/enums/notification.enum.ts | 1 + .../modules/user/user.access-request.spec.ts | 38 ++++++++-- backend/src/modules/user/user.service.spec.ts | 76 +++++++++++++++---- backend/src/modules/user/user.service.ts | 15 ++++ 8 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 backend/shared/events/access-request.event.ts create mode 100644 backend/src/modules/notification/application/listeners/access-request.listener.ts diff --git a/backend/shared/events/access-request.event.ts b/backend/shared/events/access-request.event.ts new file mode 100644 index 0000000..f678f5d --- /dev/null +++ b/backend/shared/events/access-request.event.ts @@ -0,0 +1,8 @@ + +export class AccessRequestEvent { + constructor( + public readonly name: string, + public readonly email: string, + public readonly cnpj: string, + ) {} +} diff --git a/backend/src/modules/notification/application/dto/create-notification.dto.ts b/backend/src/modules/notification/application/dto/create-notification.dto.ts index a72f125..f3cfd3f 100644 --- a/backend/src/modules/notification/application/dto/create-notification.dto.ts +++ b/backend/src/modules/notification/application/dto/create-notification.dto.ts @@ -10,6 +10,7 @@ export interface CreateNotificationDTO { ticketId?: string; } + export interface CreateMessageNotificationDTO { receiverId: string; diff --git a/backend/src/modules/notification/application/listeners/access-request.listener.ts b/backend/src/modules/notification/application/listeners/access-request.listener.ts new file mode 100644 index 0000000..b13a052 --- /dev/null +++ b/backend/src/modules/notification/application/listeners/access-request.listener.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { NotificationType } from '../../shared/enums/notification.enum'; +import { UserService } from '../../../user/user.service'; +import { CreateNotificationUseCase } from '../use-cases/create-notification.use-case'; +import { NotificationStreamService } from '../services/notificatio.stream.service'; +import { UserRole } from '../../../shared/enums/user.enum'; +import { AccessRequestEvent } from '../../../../../shared/events/access-request.event'; + +@Injectable() +export class AccessRequestListener { + constructor( + private readonly userService: UserService, + private readonly createNotificationUseCase: CreateNotificationUseCase, + private readonly notificationStreamService: NotificationStreamService, + ) {} + + @OnEvent(NotificationType.ACCESS_REQUEST) + async handle(event: AccessRequestEvent) { + console.log('[AccessRequestListener] Evento recebido:', event); + + // Buscar todos os administradores + const admins = await this.userService.findAll(1, 1000, { role: UserRole.ADMIN }); + if (!admins.data.length) return; + + await Promise.all( + admins.data.map(async (admin) => { + const notification = await this.createNotificationUseCase.execute({ + title: 'Nova solicitação de acesso', + message: 'Um novo usuário solicitou acesso ao sistema', + clientId: '', + supportAgentId: admin.id, + type: NotificationType.ACCESS_REQUEST, + }); + this.notificationStreamService.send(admin.id, notification.toPrimitives()); + }) + ); + } +} diff --git a/backend/src/modules/notification/notification.module.ts b/backend/src/modules/notification/notification.module.ts index f77a170..d897bf9 100644 --- a/backend/src/modules/notification/notification.module.ts +++ b/backend/src/modules/notification/notification.module.ts @@ -22,10 +22,12 @@ import { NotificationSSEController } import { NotificationStreamService } from "./application/services/notificatio.stream.service"; + import { TicketClosedListener } from "./application/listeners/ticket-closed.listener"; import { UserModule } from "../user/user.module"; import { TicketOpenListener } from "./application/listeners/ticket-open.listener"; import { ReceivedMessageListener } from "./application/listeners/received-message.listener"; +import { AccessRequestListener } from "./application/listeners/access-request.listener"; import { CreateMessageNotificationUseCase } from "./application/use-cases/create-message-notification.use-case"; import { NotificationController } from "./presetation/controllers/notification.controller"; import { ListNotificationsUseCase } from "./application/use-cases/list-notifications.use-case"; @@ -57,6 +59,7 @@ import { ReadNotificationUseCase } from "./application/use-cases/read-notificati TicketClosedListener, TicketOpenListener, ReceivedMessageListener, + AccessRequestListener, { provide: INotificationRepository, diff --git a/backend/src/modules/notification/shared/enums/notification.enum.ts b/backend/src/modules/notification/shared/enums/notification.enum.ts index a6d5fe7..3ae5a2e 100644 --- a/backend/src/modules/notification/shared/enums/notification.enum.ts +++ b/backend/src/modules/notification/shared/enums/notification.enum.ts @@ -2,4 +2,5 @@ export enum NotificationType { TICKET_CLOSED = 'ticket_closed', TICKET_OPEN = 'ticket_open', NEW_MESSAGE = 'new_message', + ACCESS_REQUEST = 'access_request', } \ No newline at end of file diff --git a/backend/src/modules/user/user.access-request.spec.ts b/backend/src/modules/user/user.access-request.spec.ts index 3d83169..e583522 100644 --- a/backend/src/modules/user/user.access-request.spec.ts +++ b/backend/src/modules/user/user.access-request.spec.ts @@ -2,14 +2,17 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getConnectionToken, MongooseModule } from '@nestjs/mongoose'; import { Connection, Types } from 'mongoose'; import { MongoMemoryServer } from 'mongodb-memory-server'; +import { EventEmitter2 } from '@nestjs/event-emitter'; import { UserService } from './user.service'; import { UserSchema } from './user.schema'; import { AccessRequestSchema } from './accessRequest.schema'; + import { CompanyService } from '../company/company.service'; import { CategoryService } from '../category/category.service'; import { JwtService } from '@nestjs/jwt'; import { EmailService } from '../email/email.service'; + import { CompanySchema } from '../company/company.schema'; import { CategorySchema } from '../category/category.schema'; @@ -26,6 +29,7 @@ describe('AccessRequest (Integration)', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ MongooseModule.forRoot(mongod.getUri()), + MongooseModule.forFeature([ { name: 'User', schema: UserSchema }, { name: 'AccessRequest', schema: AccessRequestSchema }, @@ -33,8 +37,17 @@ describe('AccessRequest (Integration)', () => { { name: 'Category', schema: CategorySchema }, ]), ], + providers: [ UserService, + + { + provide: EventEmitter2, + useValue: { + emit: jest.fn(), + }, + }, + { provide: CompanyService, useValue: { @@ -44,6 +57,7 @@ describe('AccessRequest (Integration)', () => { name: 'Empresa Teste', cnpj: '123', }), + findById: jest.fn().mockResolvedValue({ _id: companyId, id: companyId.toString(), @@ -69,25 +83,37 @@ describe('AccessRequest (Integration)', () => { provide: EmailService, useValue: { sendCreatePasswordEmail: jest.fn(), + sendResetPasswordEmail: jest.fn(), }, }, ], }).compile(); service = module.get(UserService); - connection = module.get(getConnectionToken()); + + connection = module.get( + getConnectionToken(), + ); }); afterEach(async () => { - const collections = connection.collections; - for (const key in collections) { - await collections[key].deleteMany({}); + if (connection && connection.readyState === 1) { + const collections = connection.collections; + + for (const key in collections) { + await collections[key].deleteMany({}); + } } }); afterAll(async () => { - await connection.close(); - await mongod.stop(); + if (connection && connection.readyState === 1) { + await connection.close(); + } + + if (mongod) { + await mongod.stop(); + } }); it('should create access request', async () => { diff --git a/backend/src/modules/user/user.service.spec.ts b/backend/src/modules/user/user.service.spec.ts index 9dc3699..aa436db 100644 --- a/backend/src/modules/user/user.service.spec.ts +++ b/backend/src/modules/user/user.service.spec.ts @@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getConnectionToken, MongooseModule } from '@nestjs/mongoose'; import { Connection, Types } from 'mongoose'; import { MongoMemoryServer } from 'mongodb-memory-server'; +import { EventEmitter2 } from '@nestjs/event-emitter'; import { UserService } from './user.service'; import { UserSchema } from './user.schema'; @@ -25,6 +26,7 @@ describe('UserService (Integration)', () => { const module: TestingModule = await Test.createTestingModule({ imports: [ MongooseModule.forRoot(mongod.getUri()), + MongooseModule.forFeature([ { name: 'User', schema: UserSchema }, { name: 'AccessRequest', schema: AccessRequestSchema }, @@ -32,8 +34,17 @@ describe('UserService (Integration)', () => { { name: 'Category', schema: CategorySchema }, ]), ], + providers: [ UserService, + + { + provide: EventEmitter2, + useValue: { + emit: jest.fn(), + }, + }, + { provide: CompanyService, useValue: { @@ -42,6 +53,7 @@ describe('UserService (Integration)', () => { name: 'Test Company', cnpj: '123', }), + findById: jest.fn().mockResolvedValue({ id: 'company-id', name: 'Test Company', @@ -49,6 +61,7 @@ describe('UserService (Integration)', () => { }), }, }, + { provide: CategoryService, useValue: { @@ -59,12 +72,14 @@ describe('UserService (Integration)', () => { }), }, }, + { provide: JwtService, useValue: { signAsync: jest.fn().mockResolvedValue('fake-token'), }, }, + { provide: EmailService, useValue: { @@ -76,19 +91,30 @@ describe('UserService (Integration)', () => { }).compile(); service = module.get(UserService); - connection = module.get(getConnectionToken()); + + connection = module.get( + getConnectionToken(), + ); }); afterEach(async () => { - const collections = connection.collections; - for (const key in collections) { - await collections[key].deleteMany({}); + if (connection && connection.readyState === 1) { + const collections = connection.collections; + + for (const key in collections) { + await collections[key].deleteMany({}); + } } }); afterAll(async () => { - await connection.close(); - await mongod.stop(); + if (connection && connection.readyState === 1) { + await connection.close(); + } + + if (mongod) { + await mongod.stop(); + } }); it('should create user successfully', async () => { @@ -128,7 +154,9 @@ describe('UserService (Integration)', () => { UserRole.CLIENT, ); - const result = await service.findById(created._id.toString()); + const result = await service.findById( + created._id.toString(), + ); expect(result).toBeDefined(); expect(result.email).toBe('test@email.com'); @@ -141,6 +169,7 @@ describe('UserService (Integration)', () => { 'Password123!', UserRole.CLIENT, ); + await service.createUser( 'User2', 'u2@email.com', @@ -163,6 +192,7 @@ describe('UserService (Integration)', () => { 'Password123!', UserRole.CLIENT, ); + await service.createUser( 'Maria', 'm@email.com', @@ -170,7 +200,11 @@ describe('UserService (Integration)', () => { UserRole.CLIENT, ); - const result = await service.findAll(1, 10, { name: 'gab' }); + const result = await service.findAll( + 1, + 10, + { name: 'gab' }, + ); expect(result.data.length).toBe(1); expect(result.data[0].name).toBe('Gabriel'); @@ -184,9 +218,12 @@ describe('UserService (Integration)', () => { UserRole.CLIENT, ); - const updated = await service.updateUser(created._id.toString(), { - name: 'New Name', - }); + const updated = await service.updateUser( + created._id.toString(), + { + name: 'New Name', + }, + ); expect(updated.name).toBe('New Name'); }); @@ -199,9 +236,13 @@ describe('UserService (Integration)', () => { UserRole.CLIENT, ); - await service.deleteUser(created._id.toString()); + await service.deleteUser( + created._id.toString(), + ); - await expect(service.findById(created._id.toString())).rejects.toThrow(); + await expect( + service.findById(created._id.toString()), + ).rejects.toThrow(); }); it('should throw error when email already exists on update', async () => { @@ -220,9 +261,12 @@ describe('UserService (Integration)', () => { ); await expect( - service.updateUser(user2._id.toString(), { - email: 'same@email.com', - }), + service.updateUser( + user2._id.toString(), + { + email: 'same@email.com', + }, + ), ).rejects.toThrow(); }); }); \ No newline at end of file diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts index 554935b..250029b 100644 --- a/backend/src/modules/user/user.service.ts +++ b/backend/src/modules/user/user.service.ts @@ -14,6 +14,10 @@ import { JwtService } from '@nestjs/jwt'; import { EmailService } from '../email/email.service'; import { UserRole } from '../shared/enums/user.enum'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { NotificationType } from '../notification/shared/enums/notification.enum'; +import { AccessRequestEvent } from '../../../shared/events/access-request.event'; + @Injectable() export class UserService { @@ -26,6 +30,7 @@ export class UserService { private categoryService: CategoryService, private jwtService: JwtService, private emailService: EmailService, + private eventEmitter: EventEmitter2, ) {} async findAll( @@ -220,6 +225,7 @@ export class UserService { throw new BadRequestException('CNPJ não cadastrado'); } + await this.accessRequestModel.create({ name, email, @@ -227,6 +233,15 @@ export class UserService { status: 'PENDING', }); + // Emitir evento para notificação de administradores + this.eventEmitter.emit( + NotificationType.ACCESS_REQUEST, + new AccessRequestEvent(name, email, cnpj), + ); + + // Log + console.log('[UserService] Evento ACCESS_REQUEST emitido:', { name, email, cnpj }); + return { message: 'Solicitação enviada com sucesso' }; }