FEAT: Adicionado sistema de gerenciamento de filas e assimilações de conversas a atendentes
All checks were successful
Deploy Dev / deploy (push) Successful in 3s
All checks were successful
Deploy Dev / deploy (push) Successful in 3s
This commit is contained in:
parent
8790ce70d0
commit
8f3cb9f75f
@ -1,50 +1,417 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { DatabaseService } from '../../infra/database/database.service';
|
||||
|
||||
interface TransferInput {
|
||||
chatId: string;
|
||||
areaId: number;
|
||||
userId?: number | null;
|
||||
note?: string | null;
|
||||
}
|
||||
|
||||
const SUPPORT_KEYWORDS = [
|
||||
'suporte',
|
||||
'bug',
|
||||
'erro',
|
||||
'falha',
|
||||
'problema',
|
||||
'tecnico',
|
||||
'tecnica',
|
||||
'internet',
|
||||
'instabilidade',
|
||||
'sistema',
|
||||
];
|
||||
|
||||
const FINANCE_KEYWORDS = [
|
||||
'financeiro',
|
||||
'fatura',
|
||||
'boleto',
|
||||
'dinheiro',
|
||||
'cartao',
|
||||
'cartao',
|
||||
'atraso',
|
||||
'pagamento',
|
||||
'cobranca',
|
||||
'nota',
|
||||
];
|
||||
|
||||
const SALES_KEYWORDS = [
|
||||
'comercial',
|
||||
'produto',
|
||||
'novo',
|
||||
'contratar',
|
||||
'compra',
|
||||
'plano',
|
||||
'proposta',
|
||||
'preco',
|
||||
'valor',
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class WhatsappAssignmentService {
|
||||
export class WhatsappAssignmentService implements OnModuleInit {
|
||||
private readonly logger = new Logger(WhatsappAssignmentService.name);
|
||||
|
||||
constructor(private readonly db: DatabaseService) {}
|
||||
|
||||
async assignChat(chatId: string, userId: string, areaId?: string) {
|
||||
this.logger.log(`Atribuindo chat ${chatId} ao usuário ${userId}`);
|
||||
|
||||
async onModuleInit() {
|
||||
await this.ensureSchema();
|
||||
}
|
||||
|
||||
async ensureSchema() {
|
||||
await this.db.query(`
|
||||
ALTER TABLE whatsapp_chat_atribuicoes
|
||||
ALTER COLUMN user_id DROP NOT NULL,
|
||||
ALTER COLUMN area_id DROP NOT NULL;
|
||||
`).catch(() => undefined);
|
||||
|
||||
await this.db.query(`
|
||||
ALTER TABLE whatsapp_chat_atribuicoes
|
||||
ADD COLUMN IF NOT EXISTS status VARCHAR(40) NOT NULL DEFAULT 'assigned',
|
||||
ADD COLUMN IF NOT EXISTS conversation_started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN IF NOT EXISTS expires_at TIMESTAMP WITH TIME ZONE DEFAULT (CURRENT_TIMESTAMP + INTERVAL '24 hours'),
|
||||
ADD COLUMN IF NOT EXISTS transfer_note TEXT,
|
||||
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 updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP;
|
||||
`);
|
||||
}
|
||||
|
||||
async assignChat(chatId: string, userId: string | number, areaId?: string | number | null) {
|
||||
this.logger.log(`Atribuindo chat ${chatId} ao usuario ${userId}`);
|
||||
|
||||
const query = `
|
||||
INSERT INTO whatsapp_chat_atribuicoes (chat_id, user_id, area_id)
|
||||
VALUES ($1, $2, $3)
|
||||
INSERT INTO whatsapp_chat_atribuicoes (
|
||||
chat_id, user_id, area_id, status, conversation_started_at, expires_at, assigned_at, updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, 'assigned', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + INTERVAL '24 hours', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (chat_id) DO UPDATE SET
|
||||
user_id = EXCLUDED.user_id,
|
||||
area_id = COALESCE(EXCLUDED.area_id, whatsapp_chat_atribuicoes.area_id),
|
||||
status = 'assigned',
|
||||
assigned_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *;
|
||||
`;
|
||||
|
||||
const result = await this.db.query(query, [chatId, Number(userId), areaId ? Number(areaId) : null]);
|
||||
return this.enrichAssignment(result.rows[0]);
|
||||
}
|
||||
|
||||
async queueChat(chatId: string, areaId: number, note?: string | null) {
|
||||
const query = `
|
||||
INSERT INTO whatsapp_chat_atribuicoes (
|
||||
chat_id, user_id, area_id, status, conversation_started_at, expires_at, transfer_note, assigned_at, updated_at
|
||||
)
|
||||
VALUES ($1, NULL, $2, 'queued', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + INTERVAL '24 hours', $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (chat_id) DO UPDATE SET
|
||||
user_id = NULL,
|
||||
area_id = EXCLUDED.area_id,
|
||||
status = 'queued',
|
||||
conversation_started_at = CASE
|
||||
WHEN whatsapp_chat_atribuicoes.expires_at <= CURRENT_TIMESTAMP THEN CURRENT_TIMESTAMP
|
||||
ELSE whatsapp_chat_atribuicoes.conversation_started_at
|
||||
END,
|
||||
expires_at = CASE
|
||||
WHEN whatsapp_chat_atribuicoes.expires_at <= CURRENT_TIMESTAMP THEN CURRENT_TIMESTAMP + INTERVAL '24 hours'
|
||||
ELSE whatsapp_chat_atribuicoes.expires_at
|
||||
END,
|
||||
transfer_note = EXCLUDED.transfer_note,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *;
|
||||
`;
|
||||
|
||||
const result = await this.db.query(query, [chatId, areaId, note || null]);
|
||||
return this.enrichAssignment(result.rows[0]);
|
||||
}
|
||||
|
||||
async transferChat(input: TransferInput) {
|
||||
const status = input.userId ? 'assigned' : 'queued';
|
||||
const query = `
|
||||
INSERT INTO whatsapp_chat_atribuicoes (
|
||||
chat_id, user_id, area_id, status, conversation_started_at, expires_at, transfer_note, assigned_at, updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + INTERVAL '24 hours', $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (chat_id) DO UPDATE SET
|
||||
user_id = EXCLUDED.user_id,
|
||||
area_id = EXCLUDED.area_id,
|
||||
assigned_at = CURRENT_TIMESTAMP
|
||||
status = EXCLUDED.status,
|
||||
transfer_note = EXCLUDED.transfer_note,
|
||||
assigned_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *;
|
||||
`;
|
||||
|
||||
const result = await this.db.query(query, [chatId, userId, areaId]);
|
||||
return result.rows[0];
|
||||
|
||||
const result = await this.db.query(query, [
|
||||
input.chatId,
|
||||
input.userId || null,
|
||||
input.areaId,
|
||||
status,
|
||||
input.note || null,
|
||||
]);
|
||||
return this.enrichAssignment(result.rows[0]);
|
||||
}
|
||||
|
||||
async getAssignment(chatId: string) {
|
||||
const query = `SELECT * FROM whatsapp_chat_atribuicoes WHERE chat_id = $1`;
|
||||
const query = `
|
||||
SELECT * FROM whatsapp_chat_atribuicoes
|
||||
WHERE chat_id = $1
|
||||
LIMIT 1
|
||||
`;
|
||||
const result = await this.db.query(query, [chatId]);
|
||||
return result.rows[0];
|
||||
const assignment = result.rows[0];
|
||||
|
||||
if (!assignment) return null;
|
||||
|
||||
if (assignment.expires_at && new Date(assignment.expires_at).getTime() <= Date.now()) {
|
||||
await this.db.query(
|
||||
`UPDATE whatsapp_chat_atribuicoes SET status = 'expired', user_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE chat_id = $1`,
|
||||
[chatId],
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.enrichAssignment(assignment);
|
||||
}
|
||||
|
||||
async releaseChat(chatId: string) {
|
||||
this.logger.log(`Liberando chat ${chatId}`);
|
||||
const query = `DELETE FROM whatsapp_chat_atribuicoes WHERE chat_id = $1`;
|
||||
await this.db.query(query, [chatId]);
|
||||
const query = `
|
||||
UPDATE whatsapp_chat_atribuicoes
|
||||
SET user_id = NULL, status = 'queued', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE chat_id = $1
|
||||
RETURNING *;
|
||||
`;
|
||||
const result = await this.db.query(query, [chatId]);
|
||||
return result.rows[0] ? this.enrichAssignment(result.rows[0]) : null;
|
||||
}
|
||||
|
||||
async routeIncomingMessage(chatId: string, message: string, messageId?: string) {
|
||||
const cleanMessage = (message || '').trim();
|
||||
if (!cleanMessage) {
|
||||
const current = await this.getAssignment(chatId);
|
||||
return { assignment: current, shouldSendBotMessage: false, botMessage: null };
|
||||
}
|
||||
|
||||
const current = await this.getAssignment(chatId);
|
||||
|
||||
if (current?.last_routed_message_id && messageId && current.last_routed_message_id === messageId) {
|
||||
return { assignment: current, shouldSendBotMessage: false, botMessage: null };
|
||||
}
|
||||
|
||||
if (current && current.status !== 'bot_triage') {
|
||||
return { assignment: current, shouldSendBotMessage: false, botMessage: null };
|
||||
}
|
||||
|
||||
const detectedArea = await this.detectKnownArea(cleanMessage);
|
||||
|
||||
if (detectedArea) {
|
||||
const assignment = await this.queueChat(chatId, detectedArea.id, 'Roteado automaticamente pelo Omnino');
|
||||
await this.markBotRoute(chatId, messageId);
|
||||
const hasPreviousTriage = current?.status === 'bot_triage';
|
||||
const botMessage = hasPreviousTriage
|
||||
? `Vi aqui que voce quer falar com ${detectedArea.article} ${detectedArea.name}. Irei te encaminhar.`
|
||||
: `Oi, vou te ajudar. Vi aqui que voce quer falar com ${detectedArea.article} ${detectedArea.name}. Irei te encaminhar.`;
|
||||
|
||||
return {
|
||||
assignment,
|
||||
shouldSendBotMessage: true,
|
||||
botMessage: this.formatSenderMessage('Atendente virtual', 'Omnino', botMessage),
|
||||
};
|
||||
}
|
||||
|
||||
if (current?.status === 'bot_triage') {
|
||||
const attempts = Number(current.routing_attempts || 0);
|
||||
|
||||
if (this.shouldSuppressRepeatedGreeting(cleanMessage, current.last_bot_sent_at)) {
|
||||
await this.markMessageRouted(chatId, messageId);
|
||||
return { assignment: current, shouldSendBotMessage: false, botMessage: null };
|
||||
}
|
||||
|
||||
if (attempts >= 2) {
|
||||
const supportArea = await this.getAreaByName('Suporte');
|
||||
const assignment = await this.queueChat(chatId, supportArea.id, 'Roteado para suporte por falta de classificacao');
|
||||
await this.markBotRoute(chatId, messageId);
|
||||
|
||||
return {
|
||||
assignment,
|
||||
shouldSendBotMessage: true,
|
||||
botMessage: this.formatSenderMessage(
|
||||
'Atendente virtual',
|
||||
'Omnino',
|
||||
'Nao consegui identificar a area ideal com seguranca. Vou te encaminhar para o suporte para agilizar seu atendimento.',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const assignment = await this.upsertTriage(chatId, attempts + 1, messageId);
|
||||
|
||||
return {
|
||||
assignment,
|
||||
shouldSendBotMessage: true,
|
||||
botMessage: this.formatSenderMessage(
|
||||
'Atendente virtual',
|
||||
'Omnino',
|
||||
'Para eu te encaminhar corretamente, responda por favor com uma destas opcoes: suporte, financeiro ou comercial.',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const assignment = await this.upsertTriage(chatId, 1, messageId);
|
||||
const intentMessage =
|
||||
'Ola, tudo bem? Me chamo Omnino e irei te transferir para alguem especializado. Gostaria de falar com suporte para resolver algum bug ou problema, financeiro para faturas, ou comercial para contratar um novo produto?';
|
||||
|
||||
return {
|
||||
assignment,
|
||||
shouldSendBotMessage: true,
|
||||
botMessage: this.formatSenderMessage('Atendente virtual', 'Omnino', intentMessage),
|
||||
};
|
||||
}
|
||||
|
||||
async getChatsByArea(areaId: string) {
|
||||
const query = `SELECT * FROM whatsapp_chat_atribuicoes WHERE area_id = $1`;
|
||||
const query = `SELECT * FROM whatsapp_chat_atribuicoes WHERE area_id = $1 AND status <> 'expired'`;
|
||||
const result = await this.db.query(query, [areaId]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async getChatsByUser(userId: string) {
|
||||
const query = `SELECT * FROM whatsapp_chat_atribuicoes WHERE user_id = $1`;
|
||||
const query = `SELECT * FROM whatsapp_chat_atribuicoes WHERE user_id = $1 AND status = 'assigned'`;
|
||||
const result = await this.db.query(query, [userId]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
private async detectKnownArea(message: string) {
|
||||
const normalized = this.normalize(message);
|
||||
const targetName = this.matchAny(normalized, FINANCE_KEYWORDS)
|
||||
? 'Financeiro'
|
||||
: this.matchAny(normalized, SALES_KEYWORDS)
|
||||
? 'Comercial'
|
||||
: this.matchAny(normalized, SUPPORT_KEYWORDS)
|
||||
? 'Suporte'
|
||||
: null;
|
||||
|
||||
if (!targetName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getAreaByName(targetName);
|
||||
}
|
||||
|
||||
private async getAreaByName(targetName: string) {
|
||||
const result = await this.db.query<{ id: number; nome: string }>(
|
||||
`SELECT id, nome FROM areas WHERE nome = $1 LIMIT 1`,
|
||||
[targetName],
|
||||
);
|
||||
|
||||
if (result.rows[0]) {
|
||||
return {
|
||||
id: result.rows[0].id,
|
||||
name: result.rows[0].nome.toLowerCase(),
|
||||
article: result.rows[0].nome === 'Comercial' ? 'o' : 'o',
|
||||
};
|
||||
}
|
||||
|
||||
return { id: 1, name: targetName.toLowerCase(), article: 'o' };
|
||||
}
|
||||
|
||||
private async upsertTriage(chatId: string, attempts: number, messageId?: string) {
|
||||
const query = `
|
||||
INSERT INTO whatsapp_chat_atribuicoes (
|
||||
chat_id, user_id, area_id, status, routing_attempts, last_routed_message_id,
|
||||
last_bot_sent_at, conversation_started_at, expires_at, assigned_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1, NULL, NULL, 'bot_triage', $2, $3,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + INTERVAL '24 hours', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT (chat_id) DO UPDATE SET
|
||||
user_id = NULL,
|
||||
area_id = NULL,
|
||||
status = 'bot_triage',
|
||||
routing_attempts = EXCLUDED.routing_attempts,
|
||||
last_routed_message_id = EXCLUDED.last_routed_message_id,
|
||||
last_bot_sent_at = CURRENT_TIMESTAMP,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *;
|
||||
`;
|
||||
|
||||
const result = await this.db.query(query, [chatId, attempts, messageId || null]);
|
||||
return this.enrichAssignment(result.rows[0]);
|
||||
}
|
||||
|
||||
private async markBotRoute(chatId: string, messageId?: string, updateBotTimestamp = true) {
|
||||
await this.db.query(
|
||||
`
|
||||
UPDATE whatsapp_chat_atribuicoes
|
||||
SET
|
||||
routing_attempts = 0,
|
||||
last_routed_message_id = $2,
|
||||
last_bot_sent_at = CASE WHEN $3 THEN CURRENT_TIMESTAMP ELSE last_bot_sent_at END,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE chat_id = $1
|
||||
`,
|
||||
[chatId, messageId || null, updateBotTimestamp],
|
||||
);
|
||||
}
|
||||
|
||||
private async markMessageRouted(chatId: string, messageId?: string) {
|
||||
await this.db.query(
|
||||
`
|
||||
UPDATE whatsapp_chat_atribuicoes
|
||||
SET
|
||||
last_routed_message_id = $2,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE chat_id = $1
|
||||
`,
|
||||
[chatId, messageId || null],
|
||||
);
|
||||
}
|
||||
|
||||
private matchAny(text: string, keywords: string[]) {
|
||||
return keywords.some((keyword) => text.includes(this.normalize(keyword)));
|
||||
}
|
||||
|
||||
private normalize(value: string) {
|
||||
return String(value || '')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
private shouldSuppressRepeatedGreeting(message: string, lastBotSentAt?: string | Date | null) {
|
||||
if (!lastBotSentAt) return false;
|
||||
|
||||
const normalized = this.normalize(message).replace(/[!?.,\s]+/g, ' ').trim();
|
||||
const greetingOnlyMessages = ['oi', 'ola', 'olá', 'bom dia', 'boa tarde', 'boa noite'];
|
||||
if (!greetingOnlyMessages.includes(normalized)) return false;
|
||||
|
||||
const lastBotTime = new Date(lastBotSentAt).getTime();
|
||||
if (Number.isNaN(lastBotTime)) return false;
|
||||
|
||||
return Date.now() - lastBotTime < 10000;
|
||||
}
|
||||
|
||||
private formatSenderMessage(senderRole: string, senderName: string, message: string) {
|
||||
return `*${senderRole}: ${senderName}*\n\n${message}`;
|
||||
}
|
||||
|
||||
private async enrichAssignment(assignment: any) {
|
||||
if (!assignment) return null;
|
||||
|
||||
const result = await this.db.query(
|
||||
`
|
||||
SELECT
|
||||
wca.*,
|
||||
a.nome AS area_nome,
|
||||
u.nome AS user_nome,
|
||||
u.email AS user_email
|
||||
FROM whatsapp_chat_atribuicoes wca
|
||||
LEFT JOIN areas a ON a.id = wca.area_id
|
||||
LEFT JOIN usuarios u ON u.id = wca.user_id
|
||||
WHERE wca.id = $1
|
||||
`,
|
||||
[assignment.id],
|
||||
);
|
||||
|
||||
return result.rows[0] || assignment;
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,8 +30,8 @@ export class WhatsappController {
|
||||
}
|
||||
|
||||
@Post('send')
|
||||
async sendMessage(@Body() body: { to: string; message: string; media?: { data: string; mimetype: string; filename?: string } }) {
|
||||
return this.whatsappService.sendMessage(body.to, body.message, body.media);
|
||||
async sendMessage(@Body() body: { to: string; message: string; senderName?: string; media?: { data: string; mimetype: string; filename?: string } }) {
|
||||
return this.whatsappService.sendMessage(body.to, body.message, body.media, body.senderName);
|
||||
}
|
||||
|
||||
@Post('assign')
|
||||
@ -39,6 +39,11 @@ export class WhatsappController {
|
||||
return this.assignmentService.assignChat(body.chatId, body.userId, body.areaId);
|
||||
}
|
||||
|
||||
@Post('transfer')
|
||||
async transferChat(@Body() body: { chatId: string; areaId: number; userId?: number | null; note?: string | null }) {
|
||||
return this.assignmentService.transferChat(body);
|
||||
}
|
||||
|
||||
@Delete('release/:chatId')
|
||||
async releaseChat(@Param('chatId') chatId: string) {
|
||||
return this.assignmentService.releaseChat(chatId);
|
||||
|
||||
@ -12,6 +12,8 @@ export class WhatsappService implements OnModuleInit {
|
||||
private readonly logger = new Logger(WhatsappService.name);
|
||||
private status: 'DISCONNECTED' | 'AWAITING_QR' | 'CONNECTED' = 'DISCONNECTED';
|
||||
private currentQr: string | null = null;
|
||||
private readonly processedIncomingMessages = new Set<string>();
|
||||
private readonly incomingRoutingQueues = new Map<string, Promise<void>>();
|
||||
|
||||
constructor(
|
||||
private readonly gateway: WhatsappGateway,
|
||||
@ -94,7 +96,9 @@ export class WhatsappService implements OnModuleInit {
|
||||
if (msg.from === 'status@broadcast') return;
|
||||
|
||||
const remoteJid = msg.id.remote || (msg.fromMe ? msg.to : msg.from);
|
||||
this.logger.log(`Mensagem registrada (fromMe: ${msg.fromMe}) remote: ${remoteJid} - ${msg.body}`);
|
||||
const messageId = msg.id._serialized;
|
||||
const messageBody = (msg.body || '').trim();
|
||||
this.logger.log(`Mensagem registrada (fromMe: ${msg.fromMe}) remote: ${remoteJid} - ${messageBody}`);
|
||||
|
||||
let mediaData: any = null;
|
||||
if (msg.hasMedia) {
|
||||
@ -119,7 +123,7 @@ export class WhatsappService implements OnModuleInit {
|
||||
body: msg.body,
|
||||
timestamp: msg.timestamp,
|
||||
isGroupMsg: remoteJid.endsWith('@g.us'),
|
||||
id: msg.id._serialized,
|
||||
id: messageId,
|
||||
fromMe: msg.fromMe,
|
||||
notifyName: msg['_data']?.notifyName || '',
|
||||
hasMedia: msg.hasMedia,
|
||||
@ -127,9 +131,6 @@ export class WhatsappService implements OnModuleInit {
|
||||
});
|
||||
|
||||
// Salva ou atualiza a conversa na persistência híbrida
|
||||
const persistentChats = await this.loadPersistentChats();
|
||||
const isNewNumber = !persistentChats[remoteJid];
|
||||
|
||||
await this.addOrUpdatePersistentChat(remoteJid, {
|
||||
name: msg['_data']?.notifyName || remoteJid.split('@')[0],
|
||||
preview: msg.hasMedia ? `[Mídia: ${mediaData?.filename || 'Arquivo'}]` : (msg.body || '[Mídia]'),
|
||||
@ -137,19 +138,55 @@ export class WhatsappService implements OnModuleInit {
|
||||
unreadCount: msg.fromMe ? 0 : 1
|
||||
});
|
||||
|
||||
if (!msg.fromMe && isNewNumber) {
|
||||
try {
|
||||
this.logger.log(`Auto-resposta de boas vindas enviada para novo contato: ${remoteJid}`);
|
||||
await this.client.sendMessage(remoteJid, "Olá! Seja bem-vindo a Sothis Telecom Como podemos te ajudar?");
|
||||
} catch (err) {
|
||||
this.logger.error(`Erro ao enviar auto-resposta de boas vindas para ${remoteJid}:`, err);
|
||||
if (!msg.fromMe) {
|
||||
if (!messageBody) {
|
||||
this.logger.log(`Triagem ignorada para ${remoteJid}: mensagem sem texto.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.processedIncomingMessages.has(messageId)) {
|
||||
return;
|
||||
}
|
||||
this.processedIncomingMessages.add(messageId);
|
||||
if (this.processedIncomingMessages.size > 1000) {
|
||||
const [oldest] = this.processedIncomingMessages;
|
||||
this.processedIncomingMessages.delete(oldest);
|
||||
}
|
||||
|
||||
await this.enqueueIncomingRoute(remoteJid, msg, messageBody, messageId);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.initialize();
|
||||
}
|
||||
|
||||
private async enqueueIncomingRoute(remoteJid: string, msg: any, messageBody: string, messageId: string) {
|
||||
const previousRoute = this.incomingRoutingQueues.get(remoteJid) || Promise.resolve();
|
||||
|
||||
let nextRoute: Promise<void>;
|
||||
nextRoute = previousRoute
|
||||
.catch(() => undefined)
|
||||
.then(async () => {
|
||||
try {
|
||||
const routeResult = await this.assignmentService.routeIncomingMessage(remoteJid, messageBody, messageId);
|
||||
if (routeResult.shouldSendBotMessage && routeResult.botMessage) {
|
||||
this.logger.log(`Omnino roteou ${remoteJid} para area ${routeResult.assignment?.area_nome || routeResult.assignment?.area_id}`);
|
||||
await msg.reply(routeResult.botMessage);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`Erro ao rotear conversa inicial de ${remoteJid}:`, err);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.incomingRoutingQueues.get(remoteJid) === nextRoute) {
|
||||
this.incomingRoutingQueues.delete(remoteJid);
|
||||
}
|
||||
});
|
||||
|
||||
this.incomingRoutingQueues.set(remoteJid, nextRoute);
|
||||
await nextRoute;
|
||||
}
|
||||
|
||||
private getPersistFilePath() {
|
||||
return path.join(process.cwd(), 'whatsapp-chats-persist.json');
|
||||
}
|
||||
@ -323,16 +360,18 @@ export class WhatsappService implements OnModuleInit {
|
||||
throw new Error('Mídia não encontrada para esta mensagem');
|
||||
}
|
||||
|
||||
async sendMessage(to: string, message: string, media?: { data: string; mimetype: string; filename?: string }) {
|
||||
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 outboundMessage = this.formatOutboundMessage(message, senderName);
|
||||
|
||||
let sentMsg;
|
||||
if (media) {
|
||||
this.logger.log(`Enviando mídia para ${to}: ${media.filename} (${media.mimetype})`);
|
||||
const messageMedia = new MessageMedia(media.mimetype, media.data, media.filename);
|
||||
sentMsg = await this.client.sendMessage(to, messageMedia, { caption: message });
|
||||
sentMsg = await this.client.sendMessage(to, messageMedia, { caption: outboundMessage });
|
||||
} else {
|
||||
sentMsg = await this.client.sendMessage(to, message);
|
||||
sentMsg = await this.client.sendMessage(to, outboundMessage);
|
||||
}
|
||||
|
||||
// Sincronizar na persistência também!
|
||||
@ -346,6 +385,21 @@ export class WhatsappService implements OnModuleInit {
|
||||
return sentMsg;
|
||||
}
|
||||
|
||||
private formatOutboundMessage(message: string, senderName?: string) {
|
||||
const cleanMessage = (message || '').trim();
|
||||
const cleanSenderName = (senderName || '').trim();
|
||||
|
||||
if (!cleanSenderName) {
|
||||
return cleanMessage;
|
||||
}
|
||||
|
||||
if (!cleanMessage) {
|
||||
return `*Atendente: ${cleanSenderName}*`;
|
||||
}
|
||||
|
||||
return `*Atendente: ${cleanSenderName}*\n\n${cleanMessage}`;
|
||||
}
|
||||
|
||||
async getTemplates() {
|
||||
const res = await this.db.query('SELECT * FROM whatsapp_templates ORDER BY id ASC');
|
||||
return res.rows;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user