REFACTOR: Modulo de contatos
Some checks are pending
Deploy Dev / deploy (push) Waiting to run

- 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.
This commit is contained in:
Rafael Alves Lopes 2026-06-01 15:42:47 -03:00
parent 6097f730b7
commit 9298f1a162
10 changed files with 222 additions and 170 deletions

View File

@ -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,
],

View File

@ -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],
})

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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,
],
);
}
}

View File

@ -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],

View File

@ -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;
}