Compare commits
2 Commits
5a21257191
...
2fd19e9bae
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fd19e9bae | |||
| d495698c02 |
@ -1,6 +1,17 @@
|
||||
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) {}
|
||||
@ -18,11 +29,13 @@ export class CustomerContactsController {
|
||||
@Put(':chatId')
|
||||
saveContact(
|
||||
@Param('chatId') chatId: string,
|
||||
@Body() body: { phone?: string | null; name?: string | null; company?: string | null; note?: string | null; userId?: number | null },
|
||||
@Body() body: SaveContactBody,
|
||||
) {
|
||||
return this.customerContactsService.saveContact({
|
||||
chatId: decodeURIComponent(chatId),
|
||||
phone: body.phone,
|
||||
phone: body.whatsappPhone || body.phone,
|
||||
callSmsPhone: body.callSmsPhone,
|
||||
email: body.email,
|
||||
name: body.name,
|
||||
company: body.company,
|
||||
note: body.note,
|
||||
|
||||
@ -4,6 +4,8 @@ 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;
|
||||
@ -19,6 +21,8 @@ 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,
|
||||
@ -27,12 +31,17 @@ 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, name, company, note, updated_by_user_id, created_at, updated_at
|
||||
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
|
||||
@ -46,10 +55,9 @@ export class CustomerContactsService implements OnModuleInit {
|
||||
async listContacts() {
|
||||
const result = await this.database.query(
|
||||
`
|
||||
SELECT chat_id, phone, name, company, note, updated_by_user_id, created_at, updated_at
|
||||
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
|
||||
LIMIT 80
|
||||
`,
|
||||
);
|
||||
|
||||
@ -60,20 +68,25 @@ export class CustomerContactsService implements OnModuleInit {
|
||||
const result = await this.database.query(
|
||||
`
|
||||
INSERT INTO agenda_contatos (
|
||||
chat_id, phone, name, company, note, updated_by_user_id, created_at, updated_at
|
||||
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, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
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, name, company, note, updated_by_user_id, created_at, updated_at
|
||||
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),
|
||||
@ -88,6 +101,8 @@ 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,
|
||||
@ -103,6 +118,16 @@ 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;
|
||||
|
||||
@ -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 um especialista no assunto.';
|
||||
ADD COLUMN IF NOT EXISTS escalation_message TEXT NOT NULL DEFAULT 'Certo, vou encaminhar seu atendimento para o time responsável.';
|
||||
`).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 um especialista no assunto.'), $8, TRUE, CURRENT_TIMESTAMP)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 'Certo, vou encaminhar seu atendimento para o time responsável.'), $8, TRUE, CURRENT_TIMESTAMP)
|
||||
RETURNING *
|
||||
`,
|
||||
[
|
||||
|
||||
@ -421,7 +421,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
||||
return builderFlowResult;
|
||||
}
|
||||
|
||||
const configuredFlowResult = await this.routeConfiguredFlow(chatId, cleanMessage, current, messageId);
|
||||
const configuredFlowResult = await this.routeConfiguredFlow(chatId, cleanMessage, current, messageId, variables);
|
||||
if (configuredFlowResult) {
|
||||
return configuredFlowResult;
|
||||
}
|
||||
@ -429,7 +429,14 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
||||
const detectedArea = await this.detectKnownArea(cleanMessage);
|
||||
|
||||
if (detectedArea) {
|
||||
const assignment = await this.queueChat(chatId, detectedArea.id, `Roteado automaticamente pelo ${VIRTUAL_AGENT_NAME}`);
|
||||
const assignment = await this.queueChat(
|
||||
chatId,
|
||||
detectedArea.id,
|
||||
this.buildVirtualAgentTransferNote({
|
||||
name: variables.nome,
|
||||
intentLabel: detectedArea.name,
|
||||
}),
|
||||
);
|
||||
await this.markBotRoute(chatId, messageId);
|
||||
const hasPreviousTriage = current?.status === 'bot_triage';
|
||||
const botMessage = hasPreviousTriage
|
||||
@ -453,7 +460,14 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
||||
|
||||
if (attempts >= 2) {
|
||||
const supportArea = await this.getAreaByName('Suporte');
|
||||
const assignment = await this.queueChat(chatId, supportArea.id, 'Roteado para suporte por falta de classificacao');
|
||||
const assignment = await this.queueChat(
|
||||
chatId,
|
||||
supportArea.id,
|
||||
this.buildVirtualAgentTransferNote({
|
||||
name: variables.nome,
|
||||
fallbackReason: 'não teve a solicitação classificada pelo Agente Virtual',
|
||||
}),
|
||||
);
|
||||
await this.markBotRoute(chatId, messageId);
|
||||
|
||||
return {
|
||||
@ -590,7 +604,11 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
||||
const assignment = await this.queueChat(
|
||||
chatId,
|
||||
areaId,
|
||||
`Roteado pelo fluxo do ${VIRTUAL_AGENT_NAME}: ${matchedChild.title}`,
|
||||
this.buildVirtualAgentTransferNote({
|
||||
name: variables.nome,
|
||||
audienceLabel: currentNode.title,
|
||||
intentLabel: matchedChild.title,
|
||||
}),
|
||||
);
|
||||
await this.markBotRoute(chatId, messageId);
|
||||
|
||||
@ -601,7 +619,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
||||
'Atendente virtual',
|
||||
VIRTUAL_AGENT_NAME,
|
||||
this.applyBotVariables(
|
||||
matchedChild.message_text || `Certo, vou encaminhar seu atendimento para ${matchedChild.area_nome || 'a especialidade correta'}.`,
|
||||
matchedChild.message_text || 'Certo, vou encaminhar seu atendimento para o time responsável.',
|
||||
variables,
|
||||
),
|
||||
),
|
||||
@ -628,7 +646,11 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
||||
const assignment = await this.queueChat(
|
||||
chatId,
|
||||
fallbackAreaId,
|
||||
`Fallback do fluxo do ${VIRTUAL_AGENT_NAME}: ${currentNode.title}`,
|
||||
this.buildVirtualAgentTransferNote({
|
||||
name: variables.nome,
|
||||
audienceLabel: currentNode.title,
|
||||
fallbackReason: 'não teve a solicitação classificada pelo Agente Virtual',
|
||||
}),
|
||||
);
|
||||
await this.markBotRoute(chatId, messageId);
|
||||
|
||||
@ -659,7 +681,13 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
private async routeConfiguredFlow(chatId: string, message: string, current: any, messageId?: string) {
|
||||
private async routeConfiguredFlow(
|
||||
chatId: string,
|
||||
message: string,
|
||||
current: any,
|
||||
messageId?: string,
|
||||
variables: BotMessageVariables = {},
|
||||
) {
|
||||
const flow = await this.getActiveTriageFlow();
|
||||
if (!flow || !flow.audiences.length) return null;
|
||||
|
||||
@ -694,7 +722,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
||||
}
|
||||
|
||||
if (current.triage_step === 'resolution' && current.triage_intent_id) {
|
||||
return this.routeConfiguredResolution(chatId, message, flow, current, messageId);
|
||||
return this.routeConfiguredResolution(chatId, message, flow, current, messageId, variables);
|
||||
}
|
||||
|
||||
return this.routeConfiguredAudience(chatId, message, flow, current, messageId);
|
||||
@ -778,14 +806,29 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
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));
|
||||
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));
|
||||
|
||||
if (!intent) {
|
||||
const fallbackAreaId = flow.fallback_area_id || (await this.getAreaByName('Suporte')).id;
|
||||
const assignment = await this.queueChat(chatId, fallbackAreaId, `Fallback configurado pelo ${VIRTUAL_AGENT_NAME}`);
|
||||
const assignment = await this.queueChat(
|
||||
chatId,
|
||||
fallbackAreaId,
|
||||
this.buildVirtualAgentTransferNote({
|
||||
name: variables.nome,
|
||||
fallbackReason: 'não teve a solicitação classificada pelo Agente Virtual',
|
||||
}),
|
||||
);
|
||||
await this.markBotRoute(chatId, messageId);
|
||||
return {
|
||||
assignment,
|
||||
@ -846,7 +889,11 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
||||
const assignment = await this.queueChat(
|
||||
chatId,
|
||||
intent.area_id,
|
||||
`Cliente solicitou especialista apos orientacao do ${VIRTUAL_AGENT_NAME}: ${intent.label}`,
|
||||
this.buildVirtualAgentTransferNote({
|
||||
name: variables.nome,
|
||||
audienceLabel: audience?.label,
|
||||
intentLabel: intent.label,
|
||||
}),
|
||||
);
|
||||
await this.markBotRoute(chatId, messageId);
|
||||
|
||||
@ -856,7 +903,7 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
||||
botMessage: this.formatSenderMessage(
|
||||
'Atendente virtual',
|
||||
VIRTUAL_AGENT_NAME,
|
||||
intent.escalation_message || `Certo, vou encaminhar seu atendimento para ${intent.area_nome}.`,
|
||||
intent.escalation_message || 'Certo, vou encaminhar seu atendimento para o time responsável.',
|
||||
),
|
||||
};
|
||||
}
|
||||
@ -1249,6 +1296,58 @@ 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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user