Compare commits

..

No commits in common. "2fd19e9bae63dbb44d777a00de31d7dfeb4dced0" and "5a212571917a1a06a6b406d8f81b1219afb277a1" have entirely different histories.

4 changed files with 25 additions and 162 deletions

View File

@ -1,17 +1,6 @@
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
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;
}
@Controller('contacts')
export class CustomerContactsController {
constructor(private readonly customerContactsService: CustomerContactsService) {}
@ -29,13 +18,11 @@ export class CustomerContactsController {
@Put(':chatId')
saveContact(
@Param('chatId') chatId: string,
@Body() body: SaveContactBody,
@Body() body: { phone?: string | null; name?: string | null; company?: string | null; note?: string | null; userId?: number | null },
) {
return this.customerContactsService.saveContact({
chatId: decodeURIComponent(chatId),
phone: body.whatsappPhone || body.phone,
callSmsPhone: body.callSmsPhone,
email: body.email,
phone: body.phone,
name: body.name,
company: body.company,
note: body.note,

View File

@ -4,8 +4,6 @@ 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;
@ -21,8 +19,6 @@ export class CustomerContactsService implements OnModuleInit {
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,
@ -31,17 +27,12 @@ export class CustomerContactsService implements OnModuleInit {
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
SELECT chat_id, phone, name, company, note, updated_by_user_id, created_at, updated_at
FROM agenda_contatos
WHERE chat_id = $1
LIMIT 1
@ -55,9 +46,10 @@ export class CustomerContactsService implements OnModuleInit {
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
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
`,
);
@ -68,25 +60,20 @@ export class CustomerContactsService implements OnModuleInit {
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
chat_id, phone, name, company, note, updated_by_user_id, created_at, updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
VALUES ($1, $2, $3, $4, $5, $6, 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
RETURNING chat_id, phone, 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),
@ -101,8 +88,6 @@ export class CustomerContactsService implements OnModuleInit {
return {
chat_id: chatId,
phone: this.normalizePhone(null, chatId),
call_sms_phone: null,
email: null,
name: null,
company: null,
note: null,
@ -118,16 +103,6 @@ export class CustomerContactsService implements OnModuleInit {
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

@ -31,7 +31,7 @@ export class KnowledgeBaseService implements OnModuleInit {
ALTER TABLE bot_triage_intents
ADD COLUMN IF NOT EXISTS response_message TEXT,
ADD COLUMN IF NOT EXISTS resolution_question TEXT,
ADD COLUMN IF NOT EXISTS escalation_message TEXT NOT NULL DEFAULT 'Certo, vou encaminhar seu atendimento para o time responsável.';
ADD COLUMN IF NOT EXISTS escalation_message TEXT NOT NULL DEFAULT 'Certo, vou encaminhar seu atendimento para um especialista no assunto.';
`).catch(() => undefined);
await this.database.query(`
@ -529,7 +529,7 @@ export class KnowledgeBaseService implements OnModuleInit {
audience_id, label, area_id, keywords, response_message, resolution_question,
escalation_message, sort_order, active, updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 'Certo, vou encaminhar seu atendimento para o time responsável.'), $8, TRUE, CURRENT_TIMESTAMP)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 'Certo, vou encaminhar seu atendimento para um especialista no assunto.'), $8, TRUE, CURRENT_TIMESTAMP)
RETURNING *
`,
[

View File

@ -421,7 +421,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
return builderFlowResult;
}
const configuredFlowResult = await this.routeConfiguredFlow(chatId, cleanMessage, current, messageId, variables);
const configuredFlowResult = await this.routeConfiguredFlow(chatId, cleanMessage, current, messageId);
if (configuredFlowResult) {
return configuredFlowResult;
}
@ -429,14 +429,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
const detectedArea = await this.detectKnownArea(cleanMessage);
if (detectedArea) {
const assignment = await this.queueChat(
chatId,
detectedArea.id,
this.buildVirtualAgentTransferNote({
name: variables.nome,
intentLabel: detectedArea.name,
}),
);
const assignment = await this.queueChat(chatId, detectedArea.id, `Roteado automaticamente pelo ${VIRTUAL_AGENT_NAME}`);
await this.markBotRoute(chatId, messageId);
const hasPreviousTriage = current?.status === 'bot_triage';
const botMessage = hasPreviousTriage
@ -460,14 +453,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
if (attempts >= 2) {
const supportArea = await this.getAreaByName('Suporte');
const assignment = await this.queueChat(
chatId,
supportArea.id,
this.buildVirtualAgentTransferNote({
name: variables.nome,
fallbackReason: 'não teve a solicitação classificada pelo Agente Virtual',
}),
);
const assignment = await this.queueChat(chatId, supportArea.id, 'Roteado para suporte por falta de classificacao');
await this.markBotRoute(chatId, messageId);
return {
@ -604,11 +590,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
const assignment = await this.queueChat(
chatId,
areaId,
this.buildVirtualAgentTransferNote({
name: variables.nome,
audienceLabel: currentNode.title,
intentLabel: matchedChild.title,
}),
`Roteado pelo fluxo do ${VIRTUAL_AGENT_NAME}: ${matchedChild.title}`,
);
await this.markBotRoute(chatId, messageId);
@ -619,7 +601,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
'Atendente virtual',
VIRTUAL_AGENT_NAME,
this.applyBotVariables(
matchedChild.message_text || 'Certo, vou encaminhar seu atendimento para o time responsável.',
matchedChild.message_text || `Certo, vou encaminhar seu atendimento para ${matchedChild.area_nome || 'a especialidade correta'}.`,
variables,
),
),
@ -646,11 +628,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
const assignment = await this.queueChat(
chatId,
fallbackAreaId,
this.buildVirtualAgentTransferNote({
name: variables.nome,
audienceLabel: currentNode.title,
fallbackReason: 'não teve a solicitação classificada pelo Agente Virtual',
}),
`Fallback do fluxo do ${VIRTUAL_AGENT_NAME}: ${currentNode.title}`,
);
await this.markBotRoute(chatId, messageId);
@ -681,13 +659,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
};
}
private async routeConfiguredFlow(
chatId: string,
message: string,
current: any,
messageId?: string,
variables: BotMessageVariables = {},
) {
private async routeConfiguredFlow(chatId: string, message: string, current: any, messageId?: string) {
const flow = await this.getActiveTriageFlow();
if (!flow || !flow.audiences.length) return null;
@ -722,7 +694,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
}
if (current.triage_step === 'resolution' && current.triage_intent_id) {
return this.routeConfiguredResolution(chatId, message, flow, current, messageId, variables);
return this.routeConfiguredResolution(chatId, message, flow, current, messageId);
}
return this.routeConfiguredAudience(chatId, message, flow, current, messageId);
@ -806,29 +778,14 @@ export class WhatsappAssignmentService implements OnModuleInit {
};
}
private async routeConfiguredResolution(
chatId: string,
message: string,
flow: TriageFlow,
current: any,
messageId?: string,
variables: BotMessageVariables = {},
) {
const audience = flow.audiences.find((item) =>
item.intents.some((intentItem) => Number(intentItem.id) === Number(current.triage_intent_id)),
);
const intent = audience?.intents.find((item) => Number(item.id) === Number(current.triage_intent_id));
private async routeConfiguredResolution(chatId: string, message: string, flow: TriageFlow, current: any, messageId?: string) {
const intent = flow.audiences
.flatMap((audience) => audience.intents)
.find((item) => Number(item.id) === Number(current.triage_intent_id));
if (!intent) {
const fallbackAreaId = flow.fallback_area_id || (await this.getAreaByName('Suporte')).id;
const assignment = await this.queueChat(
chatId,
fallbackAreaId,
this.buildVirtualAgentTransferNote({
name: variables.nome,
fallbackReason: 'não teve a solicitação classificada pelo Agente Virtual',
}),
);
const assignment = await this.queueChat(chatId, fallbackAreaId, `Fallback configurado pelo ${VIRTUAL_AGENT_NAME}`);
await this.markBotRoute(chatId, messageId);
return {
assignment,
@ -889,11 +846,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
const assignment = await this.queueChat(
chatId,
intent.area_id,
this.buildVirtualAgentTransferNote({
name: variables.nome,
audienceLabel: audience?.label,
intentLabel: intent.label,
}),
`Cliente solicitou especialista apos orientacao do ${VIRTUAL_AGENT_NAME}: ${intent.label}`,
);
await this.markBotRoute(chatId, messageId);
@ -903,7 +856,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
botMessage: this.formatSenderMessage(
'Atendente virtual',
VIRTUAL_AGENT_NAME,
intent.escalation_message || 'Certo, vou encaminhar seu atendimento para o time responsável.',
intent.escalation_message || `Certo, vou encaminhar seu atendimento para ${intent.area_nome}.`,
),
};
}
@ -1296,58 +1249,6 @@ export class WhatsappAssignmentService implements OnModuleInit {
return ['ferias'].includes(normalized) ? 'as' : 'o';
}
private buildVirtualAgentTransferNote(input: {
name?: string | null;
audienceLabel?: string | null;
intentLabel?: string | null;
fallbackReason?: string;
}) {
const subject = this.getFirstName(input.name) || 'Cliente';
const audienceDescription = this.getAudienceDescription(input.audienceLabel);
const topic = this.getTopicLabel(input.intentLabel);
if (audienceDescription && topic) {
return `${subject} ${audienceDescription} e quer saber sobre ${topic}`;
}
if (topic) {
return `${subject} quer saber sobre ${topic}`;
}
if (audienceDescription && input.fallbackReason) {
return `${subject} ${audienceDescription} e ${input.fallbackReason}`;
}
if (input.fallbackReason) {
return `${subject} ${input.fallbackReason}`;
}
return `Roteado pelo ${VIRTUAL_AGENT_NAME}`;
}
private getAudienceDescription(value?: string | null) {
const normalized = this.normalize(value || '');
if (!normalized) return '';
if (normalized.includes('ex-colaborador') || normalized.includes('ex colaborador')) return 'é um ex-colaborador';
if (normalized.includes('candidato') || normalized.includes('vaga')) return 'é um candidato';
if (normalized.includes('colaborador') || normalized.includes('funcionario')) return 'é um colaborador';
return '';
}
private getTopicLabel(value?: string | null) {
const text = this.cleanVariable(value);
const normalized = this.normalize(text);
const knownTopics: Record<string, string> = {
beneficios: 'Benefícios',
ferias: 'Férias',
rescisao: 'Rescisão',
};
return knownTopics[normalized] || text;
}
private applyBotVariables(message: string, variables: BotMessageVariables) {
const firstName = this.getFirstName(variables.nome);
const fullName = this.cleanVariable(variables.nome);