From da17cbda2d17bc968cc9b90830664d222c8724cc Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Wed, 20 May 2026 13:56:23 -0300 Subject: [PATCH] =?UTF-8?q?FEAT:=20Atualiza=C3=A7=C3=A3o=20contempla=20ago?= =?UTF-8?q?ra=20um=20novo=20atendimento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/customer-contacts.controller.ts | 5 ++ .../admin/customer-contacts.service.ts | 13 ++++++ .../whatsapp/whatsapp-assignment.service.ts | 37 +++++++++++++++ src/modules/whatsapp/whatsapp.controller.ts | 5 ++ src/modules/whatsapp/whatsapp.service.ts | 46 +++++++++++++++++++ 5 files changed, 106 insertions(+) diff --git a/src/modules/admin/customer-contacts.controller.ts b/src/modules/admin/customer-contacts.controller.ts index 75195f9..555f255 100644 --- a/src/modules/admin/customer-contacts.controller.ts +++ b/src/modules/admin/customer-contacts.controller.ts @@ -5,6 +5,11 @@ import { CustomerContactsService } from './customer-contacts.service'; export class CustomerContactsController { constructor(private readonly customerContactsService: CustomerContactsService) {} + @Get() + listContacts() { + return this.customerContactsService.listContacts(); + } + @Get(':chatId') getContact(@Param('chatId') chatId: string) { return this.customerContactsService.getContact(decodeURIComponent(chatId)); diff --git a/src/modules/admin/customer-contacts.service.ts b/src/modules/admin/customer-contacts.service.ts index f2b2ec9..cc1a671 100644 --- a/src/modules/admin/customer-contacts.service.ts +++ b/src/modules/admin/customer-contacts.service.ts @@ -43,6 +43,19 @@ export class CustomerContactsService implements OnModuleInit { return result.rows[0] || this.buildDefaultContact(chatId); } + async listContacts() { + const result = await this.database.query( + ` + SELECT chat_id, phone, 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 + LIMIT 80 + `, + ); + + return result.rows; + } + async saveContact(input: SaveContactInput) { const result = await this.database.query( ` diff --git a/src/modules/whatsapp/whatsapp-assignment.service.ts b/src/modules/whatsapp/whatsapp-assignment.service.ts index 4880e5e..3165c1d 100644 --- a/src/modules/whatsapp/whatsapp-assignment.service.ts +++ b/src/modules/whatsapp/whatsapp-assignment.service.ts @@ -19,6 +19,8 @@ const SUPPORT_KEYWORDS = [ 'internet', 'instabilidade', 'sistema', + 'link', + 'caiu', ]; const FINANCE_KEYWORDS = [ @@ -72,6 +74,7 @@ export class WhatsappAssignmentService implements OnModuleInit { ADD COLUMN IF NOT EXISTS routing_attempts INTEGER NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS last_routed_message_id VARCHAR(255), ADD COLUMN IF NOT EXISTS last_bot_sent_at TIMESTAMP WITH TIME ZONE, + ADD COLUMN IF NOT EXISTS awaiting_customer_reply BOOLEAN NOT NULL DEFAULT FALSE, ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP; `); } @@ -88,6 +91,7 @@ export class WhatsappAssignmentService implements OnModuleInit { user_id = EXCLUDED.user_id, area_id = COALESCE(EXCLUDED.area_id, whatsapp_chat_atribuicoes.area_id), status = 'assigned', + awaiting_customer_reply = FALSE, assigned_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP RETURNING *; @@ -199,6 +203,39 @@ export class WhatsappAssignmentService implements OnModuleInit { return result.rows[0] ? this.enrichAssignment(result.rows[0]) : null; } + async markAwaitingCustomerReply(chatId: string) { + const result = await this.db.query( + ` + UPDATE whatsapp_chat_atribuicoes + SET awaiting_customer_reply = TRUE, updated_at = CURRENT_TIMESTAMP + WHERE chat_id = $1 + RETURNING * + `, + [chatId], + ); + + return result.rows[0] ? this.enrichAssignment(result.rows[0]) : null; + } + + async markCustomerReplied(chatId: string) { + const result = await this.db.query( + ` + UPDATE whatsapp_chat_atribuicoes + SET awaiting_customer_reply = FALSE, updated_at = CURRENT_TIMESTAMP + WHERE chat_id = $1 AND awaiting_customer_reply = TRUE + RETURNING * + `, + [chatId], + ); + + return result.rows[0] ? this.enrichAssignment(result.rows[0]) : null; + } + + async canSendAgentMessage(chatId: string) { + const assignment = await this.getAssignment(chatId); + return !assignment?.awaiting_customer_reply; + } + async routeIncomingMessage(chatId: string, message: string, messageId?: string) { const cleanMessage = (message || '').trim(); if (!cleanMessage) { diff --git a/src/modules/whatsapp/whatsapp.controller.ts b/src/modules/whatsapp/whatsapp.controller.ts index f52282a..988e2a8 100644 --- a/src/modules/whatsapp/whatsapp.controller.ts +++ b/src/modules/whatsapp/whatsapp.controller.ts @@ -34,6 +34,11 @@ export class WhatsappController { return this.whatsappService.sendMessage(body.to, body.message, body.media, body.senderName); } + @Post('start-attendance') + async startAttendance(@Body() body: { to: string; templateId: number; userId: number; areaId?: number | null; variables?: Record }) { + return this.whatsappService.startAttendance(body.to, body.templateId, body.userId, body.areaId, body.variables); + } + @Post('assign') async assignChat(@Body() body: { chatId: string; userId: string; areaId?: string }) { return this.assignmentService.assignChat(body.chatId, body.userId, body.areaId); diff --git a/src/modules/whatsapp/whatsapp.service.ts b/src/modules/whatsapp/whatsapp.service.ts index ac7cff4..26f5aae 100644 --- a/src/modules/whatsapp/whatsapp.service.ts +++ b/src/modules/whatsapp/whatsapp.service.ts @@ -141,6 +141,10 @@ export class WhatsappService implements OnModuleInit { }); if (!msg.fromMe) { + if (messageBody || msg.hasMedia) { + await this.assignmentService.markCustomerReplied(remoteJid); + } + if (!messageBody) { this.logger.log(`Triagem ignorada para ${remoteJid}: mensagem sem texto.`); return; @@ -494,6 +498,10 @@ export class WhatsappService implements OnModuleInit { async sendMessage(to: string, message: string, media?: { data: string; mimetype: string; filename?: string }, senderName?: string) { if (this.status !== 'CONNECTED') throw new Error('WhatsApp não está conectado'); + const canSendMessage = await this.assignmentService.canSendAgentMessage(to); + if (!canSendMessage) { + throw new Error('Aguarde o cliente responder antes de enviar novas mensagens.'); + } const outboundMessage = this.formatOutboundMessage(message, senderName); @@ -519,6 +527,39 @@ export class WhatsappService implements OnModuleInit { return sentMsg; } + async startAttendance( + to: string, + templateId: number, + userId: number, + areaId?: number | null, + variables?: Record, + ) { + const template = await this.getTemplateById(templateId); + if (!template) { + throw new Error('Template de WhatsApp nao encontrado'); + } + + const renderedContent = this.renderTemplateContent(template.content, variables); + const sentMessage = await this.sendMessage(to, renderedContent); + const assignment = await this.assignmentService.assignChat(to, userId, areaId || null); + const lockedAssignment = await this.assignmentService.markAwaitingCustomerReply(to); + + return { + chatId: to, + template: { ...template, content: renderedContent }, + messageId: sentMessage?.id?._serialized || null, + assignment: lockedAssignment || assignment, + }; + } + + private renderTemplateContent(content: string, variables?: Record) { + return String(content || '').replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => { + const value = variables?.[key] ?? variables?.[String(key).toLowerCase()]; + const normalized = String(value || '').trim(); + return normalized || match; + }); + } + private formatOutboundMessage(message: string, senderName?: string) { const cleanMessage = (message || '').trim(); const cleanSenderName = (senderName || '').trim(); @@ -539,6 +580,11 @@ export class WhatsappService implements OnModuleInit { return res.rows; } + private async getTemplateById(id: number) { + const res = await this.db.query('SELECT * FROM whatsapp_templates WHERE id = $1 LIMIT 1', [id]); + return res.rows[0] || null; + } + async saveTemplate(name: string, content: string) { const res = await this.db.query( 'INSERT INTO whatsapp_templates (name, content) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET content = EXCLUDED.content, updated_at = CURRENT_TIMESTAMP RETURNING *',