From 9298f1a1629f11343424e6a6978f78e92acfeee6 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Mon, 1 Jun 2026 15:42:47 -0300 Subject: [PATCH] REFACTOR: Modulo de contatos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Criado módulo de contatos para centralizar a gestão dos contatos dos clientes. - Movidos arquivos relacionados a contatos do módulo admin para o novo módulo de contatos. - Atualizadas as importações e dependências para refletir a nova estrutura do módulo. - Criado DTO para contato e repositório para gerenciar as operações de banco de dados relacionadas a contatos. --- src/app.module.ts | 2 + src/modules/admin/admin.module.ts | 4 - .../admin/customer-contacts.service.ts | 135 ------------------ .../contacts.controller.ts} | 26 ++-- src/modules/contacts/contacts.module.ts | 11 ++ src/modules/contacts/contacts.service.ts | 81 +++++++++++ src/modules/contacts/dto/contact.dto.ts | 43 ++++++ .../repositories/contacts.repository.ts | 71 +++++++++ src/modules/whatsapp/whatsapp.module.ts | 3 +- src/modules/whatsapp/whatsapp.service.ts | 16 +-- 10 files changed, 222 insertions(+), 170 deletions(-) delete mode 100644 src/modules/admin/customer-contacts.service.ts rename src/modules/{admin/customer-contacts.controller.ts => contacts/contacts.controller.ts} (76%) create mode 100644 src/modules/contacts/contacts.module.ts create mode 100644 src/modules/contacts/contacts.service.ts create mode 100644 src/modules/contacts/dto/contact.dto.ts create mode 100644 src/modules/contacts/repositories/contacts.repository.ts diff --git a/src/app.module.ts b/src/app.module.ts index 4022a3a..1977124 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { DatabaseModule } from './infra/database/database.module'; import { AdminModule } from './modules/admin/admin.module'; import { AttendanceModule } from './modules/attendance/attendance.module'; import { AuthModule } from './modules/auth/auth.module'; +import { ContactsModule } from './modules/contacts/contacts.module'; import { KnowledgeBaseModule } from './modules/knowledge-base/knowledge-base.module'; import { WhatsappModule } from './modules/whatsapp/whatsapp.module'; @@ -21,6 +22,7 @@ import { WhatsappModule } from './modules/whatsapp/whatsapp.module'; AuthModule, AdminModule, AttendanceModule, + ContactsModule, KnowledgeBaseModule, WhatsappModule, ], diff --git a/src/modules/admin/admin.module.ts b/src/modules/admin/admin.module.ts index 90114d4..680617a 100644 --- a/src/modules/admin/admin.module.ts +++ b/src/modules/admin/admin.module.ts @@ -5,8 +5,6 @@ import { AgentNotesController } from './agent-notes.controller'; import { AgentNotesService } from './agent-notes.service'; import { AgentPresenceController } from './agent-presence.controller'; import { AgentPresenceService } from './agent-presence.service'; -import { CustomerContactsController } from './customer-contacts.controller'; -import { CustomerContactsService } from './customer-contacts.service'; import { AdminAccessRepository } from './repositories/admin-access.repository'; @Module({ @@ -14,14 +12,12 @@ import { AdminAccessRepository } from './repositories/admin-access.repository'; AdminAccessController, AgentNotesController, AgentPresenceController, - CustomerContactsController, ], providers: [ AdminAccessService, AdminAccessRepository, AgentNotesService, AgentPresenceService, - CustomerContactsService, ], exports: [AgentPresenceService], }) diff --git a/src/modules/admin/customer-contacts.service.ts b/src/modules/admin/customer-contacts.service.ts deleted file mode 100644 index ef252e1..0000000 --- a/src/modules/admin/customer-contacts.service.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { DatabaseService } from '../../infra/database/database.service'; - -interface SaveContactInput { - chatId: string; - phone?: string | null; - callSmsPhone?: string | null; - email?: string | null; - name?: string | null; - company?: string | null; - note?: string | null; - userId?: number | null; -} - -@Injectable() -export class CustomerContactsService implements OnModuleInit { - constructor(private readonly database: DatabaseService) {} - - async onModuleInit() { - await this.database.query(` - CREATE TABLE IF NOT EXISTS agenda_contatos ( - chat_id VARCHAR(255) PRIMARY KEY, - phone VARCHAR(80) NOT NULL, - call_sms_phone VARCHAR(80), - email VARCHAR(255), - name VARCHAR(255), - company VARCHAR(255), - note TEXT, - updated_by_user_id INTEGER REFERENCES usuarios(id) ON DELETE SET NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - `); - await this.database.query(` - ALTER TABLE agenda_contatos - ADD COLUMN IF NOT EXISTS call_sms_phone VARCHAR(80), - ADD COLUMN IF NOT EXISTS email VARCHAR(255); - `); - } - - async getContact(chatId: string) { - const result = await this.database.query( - ` - SELECT chat_id, phone, call_sms_phone, email, name, company, note, updated_by_user_id, created_at, updated_at - FROM agenda_contatos - WHERE chat_id = $1 - LIMIT 1 - `, - [chatId], - ); - - return result.rows[0] || this.buildDefaultContact(chatId); - } - - async listContacts() { - const result = await this.database.query( - ` - SELECT chat_id, phone, call_sms_phone, email, name, company, note, updated_by_user_id, created_at, updated_at - FROM agenda_contatos - ORDER BY updated_at DESC NULLS LAST, created_at DESC NULLS LAST - `, - ); - - return result.rows; - } - - async saveContact(input: SaveContactInput) { - const result = await this.database.query( - ` - INSERT INTO agenda_contatos ( - chat_id, phone, call_sms_phone, email, name, company, note, updated_by_user_id, created_at, updated_at - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - ON CONFLICT (chat_id) DO UPDATE SET - phone = EXCLUDED.phone, - call_sms_phone = EXCLUDED.call_sms_phone, - email = EXCLUDED.email, - name = EXCLUDED.name, - company = EXCLUDED.company, - note = EXCLUDED.note, - updated_by_user_id = EXCLUDED.updated_by_user_id, - updated_at = CURRENT_TIMESTAMP - RETURNING chat_id, phone, call_sms_phone, email, name, company, note, updated_by_user_id, created_at, updated_at - `, - [ - input.chatId, - this.normalizePhone(input.phone, input.chatId), - this.normalizeOptionalPhone(input.callSmsPhone), - this.normalizeEmail(input.email), - this.normalizeText(input.name), - this.normalizeText(input.company), - this.normalizeText(input.note), - input.userId || null, - ], - ); - - return result.rows[0]; - } - - private buildDefaultContact(chatId: string) { - return { - chat_id: chatId, - phone: this.normalizePhone(null, chatId), - call_sms_phone: null, - email: null, - name: null, - company: null, - note: null, - updated_by_user_id: null, - created_at: null, - updated_at: null, - }; - } - - private normalizePhone(phone: string | null | undefined, chatId: string) { - const cleanPhone = String(phone || '').trim(); - if (cleanPhone) return cleanPhone; - return chatId.endsWith('@lid') ? '' : String(chatId || '').split('@')[0]; - } - - private normalizeOptionalPhone(phone: string | null | undefined) { - const cleanPhone = String(phone || '').replace(/\D/g, ''); - return cleanPhone || null; - } - - private normalizeEmail(value?: string | null) { - const email = String(value || '').trim().toLowerCase(); - return email || null; - } - - private normalizeText(value?: string | null) { - const text = String(value || '').trim(); - return text || null; - } -} diff --git a/src/modules/admin/customer-contacts.controller.ts b/src/modules/contacts/contacts.controller.ts similarity index 76% rename from src/modules/admin/customer-contacts.controller.ts rename to src/modules/contacts/contacts.controller.ts index f9ec7ca..4cdccca 100644 --- a/src/modules/admin/customer-contacts.controller.ts +++ b/src/modules/contacts/contacts.controller.ts @@ -1,22 +1,12 @@ import { Body, Controller, Get, Param, Put } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { CustomerContactsService } from './customer-contacts.service'; - -interface SaveContactBody { - phone?: string | null; - whatsappPhone?: string | null; - callSmsPhone?: string | null; - email?: string | null; - name?: string | null; - company?: string | null; - note?: string | null; - userId?: number | null; -} +import { SaveContactDto } from './dto/contact.dto'; +import { ContactsService } from './contacts.service'; @ApiTags('Contatos') @Controller('contacts') -export class CustomerContactsController { - constructor(private readonly customerContactsService: CustomerContactsService) {} +export class ContactsController { + constructor(private readonly contactsService: ContactsService) {} @ApiOperation({ summary: 'Lista contatos', @@ -25,7 +15,7 @@ export class CustomerContactsController { @ApiResponse({ status: 200, description: 'Lista de contatos retornada.' }) @Get() listContacts() { - return this.customerContactsService.listContacts(); + return this.contactsService.listContacts(); } @ApiOperation({ @@ -35,7 +25,7 @@ export class CustomerContactsController { @ApiParam({ name: 'chatId', description: 'Identificador codificado do chat.' }) @Get(':chatId') getContact(@Param('chatId') chatId: string) { - return this.customerContactsService.getContact(decodeURIComponent(chatId)); + return this.contactsService.getContact(decodeURIComponent(chatId)); } @ApiOperation({ @@ -61,9 +51,9 @@ export class CustomerContactsController { @Put(':chatId') saveContact( @Param('chatId') chatId: string, - @Body() body: SaveContactBody, + @Body() body: SaveContactDto, ) { - return this.customerContactsService.saveContact({ + return this.contactsService.saveContact({ chatId: decodeURIComponent(chatId), phone: body.whatsappPhone || body.phone, callSmsPhone: body.callSmsPhone, diff --git a/src/modules/contacts/contacts.module.ts b/src/modules/contacts/contacts.module.ts new file mode 100644 index 0000000..6af5d25 --- /dev/null +++ b/src/modules/contacts/contacts.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ContactsController } from './contacts.controller'; +import { ContactsService } from './contacts.service'; +import { ContactsRepository } from './repositories/contacts.repository'; + +@Module({ + controllers: [ContactsController], + providers: [ContactsService, ContactsRepository], + exports: [ContactsService], +}) +export class ContactsModule {} diff --git a/src/modules/contacts/contacts.service.ts b/src/modules/contacts/contacts.service.ts new file mode 100644 index 0000000..95b6812 --- /dev/null +++ b/src/modules/contacts/contacts.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@nestjs/common'; +import { ContactsRepository } from './repositories/contacts.repository'; + +interface SaveContactInput { + chatId: string; + phone?: string | null; + callSmsPhone?: string | null; + email?: string | null; + name?: string | null; + company?: string | null; + note?: string | null; + userId?: number | null; +} + +@Injectable() +export class ContactsService { + constructor(private readonly contactsRepository: ContactsRepository) {} + + async getContact(chatId: string) { + const result = await this.contactsRepository.getContact(chatId); + + return result.rows[0] || this.buildDefaultContact(chatId); + } + + async listContacts() { + const result = await this.contactsRepository.listContacts(); + + return result.rows; + } + + async saveContact(input: SaveContactInput) { + const result = await this.contactsRepository.saveContact({ + chatId: input.chatId, + phone: this.normalizePhone(input.phone, input.chatId), + callSmsPhone: this.normalizeOptionalPhone(input.callSmsPhone), + email: this.normalizeEmail(input.email), + name: this.normalizeText(input.name), + company: this.normalizeText(input.company), + note: this.normalizeText(input.note), + userId: input.userId || null, + }); + + return result.rows[0]; + } + + private buildDefaultContact(chatId: string) { + return { + chat_id: chatId, + phone: this.normalizePhone(null, chatId), + call_sms_phone: null, + email: null, + name: null, + company: null, + note: null, + updated_by_user_id: null, + created_at: null, + updated_at: null, + }; + } + + private normalizePhone(phone: string | null | undefined, chatId: string) { + const cleanPhone = String(phone || '').trim(); + if (cleanPhone) return cleanPhone; + return chatId.endsWith('@lid') ? '' : String(chatId || '').split('@')[0]; + } + + private normalizeOptionalPhone(phone: string | null | undefined) { + const cleanPhone = String(phone || '').replace(/\D/g, ''); + return cleanPhone || null; + } + + private normalizeEmail(value?: string | null) { + const email = String(value || '').trim().toLowerCase(); + return email || null; + } + + private normalizeText(value?: string | null) { + const text = String(value || '').trim(); + return text || null; + } +} diff --git a/src/modules/contacts/dto/contact.dto.ts b/src/modules/contacts/dto/contact.dto.ts new file mode 100644 index 0000000..6baf329 --- /dev/null +++ b/src/modules/contacts/dto/contact.dto.ts @@ -0,0 +1,43 @@ +import { Type } from 'class-transformer'; +import { IsEmail, IsInt, IsOptional, IsString, MaxLength } from 'class-validator'; + +export class SaveContactDto { + @IsOptional() + @IsString() + @MaxLength(80) + phone?: string | null; + + @IsOptional() + @IsString() + @MaxLength(80) + whatsappPhone?: string | null; + + @IsOptional() + @IsString() + @MaxLength(80) + callSmsPhone?: string | null; + + @IsOptional() + @IsEmail() + @MaxLength(255) + email?: string | null; + + @IsOptional() + @IsString() + @MaxLength(255) + name?: string | null; + + @IsOptional() + @IsString() + @MaxLength(255) + company?: string | null; + + @IsOptional() + @IsString() + note?: string | null; + + @IsOptional() + @Type(() => Number) + @IsInt() + userId?: number | null; +} diff --git a/src/modules/contacts/repositories/contacts.repository.ts b/src/modules/contacts/repositories/contacts.repository.ts new file mode 100644 index 0000000..7109b1a --- /dev/null +++ b/src/modules/contacts/repositories/contacts.repository.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common'; +import { DatabaseService } from '../../../infra/database/database.service'; + +interface SaveContactPersistenceInput { + chatId: string; + phone: string; + callSmsPhone?: string | null; + email?: string | null; + name?: string | null; + company?: string | null; + note?: string | null; + userId?: number | null; +} + +@Injectable() +export class ContactsRepository { + constructor(private readonly database: DatabaseService) {} + + getContact(chatId: string) { + return this.database.query( + ` + SELECT chat_id, phone, call_sms_phone, email, name, company, note, updated_by_user_id, created_at, updated_at + FROM agenda_contatos + WHERE chat_id = $1 + LIMIT 1 + `, + [chatId], + ); + } + + listContacts() { + return this.database.query( + ` + SELECT chat_id, phone, call_sms_phone, email, name, company, note, updated_by_user_id, created_at, updated_at + FROM agenda_contatos + ORDER BY updated_at DESC NULLS LAST, created_at DESC NULLS LAST + `, + ); + } + + saveContact(input: SaveContactPersistenceInput) { + return this.database.query( + ` + INSERT INTO agenda_contatos ( + chat_id, phone, call_sms_phone, email, name, company, note, updated_by_user_id, created_at, updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT (chat_id) DO UPDATE SET + phone = EXCLUDED.phone, + call_sms_phone = EXCLUDED.call_sms_phone, + email = EXCLUDED.email, + name = EXCLUDED.name, + company = EXCLUDED.company, + note = EXCLUDED.note, + updated_by_user_id = EXCLUDED.updated_by_user_id, + updated_at = CURRENT_TIMESTAMP + RETURNING chat_id, phone, call_sms_phone, email, name, company, note, updated_by_user_id, created_at, updated_at + `, + [ + input.chatId, + input.phone, + input.callSmsPhone || null, + input.email || null, + input.name || null, + input.company || null, + input.note || null, + input.userId || null, + ], + ); + } +} diff --git a/src/modules/whatsapp/whatsapp.module.ts b/src/modules/whatsapp/whatsapp.module.ts index 1d4c365..bef8fa9 100644 --- a/src/modules/whatsapp/whatsapp.module.ts +++ b/src/modules/whatsapp/whatsapp.module.ts @@ -4,11 +4,12 @@ import { WhatsappGateway } from './whatsapp.gateway'; import { WhatsappController } from './whatsapp.controller'; import { AttendanceModule } from '../attendance/attendance.module'; import { AuthModule } from '../auth/auth.module'; +import { ContactsModule } from '../contacts/contacts.module'; import { WhatsappTemplateRepository } from './repositories/whatsapp-template.repository'; import { WhatsappTemplateService } from './whatsapp-template.service'; @Module({ - imports: [AttendanceModule, AuthModule], + imports: [AttendanceModule, AuthModule, ContactsModule], providers: [WhatsappService, WhatsappGateway, WhatsappTemplateService, WhatsappTemplateRepository], controllers: [WhatsappController], exports: [WhatsappService], diff --git a/src/modules/whatsapp/whatsapp.service.ts b/src/modules/whatsapp/whatsapp.service.ts index 7ab6950..f5277ad 100644 --- a/src/modules/whatsapp/whatsapp.service.ts +++ b/src/modules/whatsapp/whatsapp.service.ts @@ -2,7 +2,7 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Client, LocalAuth, MessageMedia } from 'whatsapp-web.js'; import { WhatsappGateway } from './whatsapp.gateway'; import { AttendanceAssignmentService } from '../attendance/attendance-assignment.service'; -import { DatabaseService } from '../../infra/database/database.service'; +import { ContactsService } from '../contacts/contacts.service'; import { WhatsappTemplateService } from './whatsapp-template.service'; import * as fs from 'fs'; import * as path from 'path'; @@ -20,7 +20,7 @@ export class WhatsappService implements OnModuleInit { constructor( private readonly gateway: WhatsappGateway, private readonly assignmentService: AttendanceAssignmentService, - private readonly db: DatabaseService, + private readonly contactsService: ContactsService, private readonly whatsappTemplateService: WhatsappTemplateService, ) {} @@ -387,16 +387,8 @@ export class WhatsappService implements OnModuleInit { private async getCustomerContact(chatId: string) { try { - const result = await this.db.query( - ` - SELECT chat_id, phone, name, company, note, updated_by_user_id, created_at, updated_at - FROM agenda_contatos - WHERE chat_id = $1 - LIMIT 1 - `, - [chatId], - ); - return result.rows[0] || null; + const contact = await this.contactsService.getContact(chatId); + return contact?.updated_at ? contact : null; } catch { return null; }