FEAT: Atualização contempla agora um novo atendimento
All checks were successful
Deploy Dev / deploy (push) Successful in 53s
All checks were successful
Deploy Dev / deploy (push) Successful in 53s
This commit is contained in:
parent
5135d0b2ed
commit
da17cbda2d
@ -5,6 +5,11 @@ import { CustomerContactsService } from './customer-contacts.service';
|
|||||||
export class CustomerContactsController {
|
export class CustomerContactsController {
|
||||||
constructor(private readonly customerContactsService: CustomerContactsService) {}
|
constructor(private readonly customerContactsService: CustomerContactsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
listContacts() {
|
||||||
|
return this.customerContactsService.listContacts();
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':chatId')
|
@Get(':chatId')
|
||||||
getContact(@Param('chatId') chatId: string) {
|
getContact(@Param('chatId') chatId: string) {
|
||||||
return this.customerContactsService.getContact(decodeURIComponent(chatId));
|
return this.customerContactsService.getContact(decodeURIComponent(chatId));
|
||||||
|
|||||||
@ -43,6 +43,19 @@ export class CustomerContactsService implements OnModuleInit {
|
|||||||
return result.rows[0] || this.buildDefaultContact(chatId);
|
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) {
|
async saveContact(input: SaveContactInput) {
|
||||||
const result = await this.database.query(
|
const result = await this.database.query(
|
||||||
`
|
`
|
||||||
|
|||||||
@ -19,6 +19,8 @@ const SUPPORT_KEYWORDS = [
|
|||||||
'internet',
|
'internet',
|
||||||
'instabilidade',
|
'instabilidade',
|
||||||
'sistema',
|
'sistema',
|
||||||
|
'link',
|
||||||
|
'caiu',
|
||||||
];
|
];
|
||||||
|
|
||||||
const FINANCE_KEYWORDS = [
|
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 routing_attempts INTEGER NOT NULL DEFAULT 0,
|
||||||
ADD COLUMN IF NOT EXISTS last_routed_message_id VARCHAR(255),
|
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 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;
|
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,
|
user_id = EXCLUDED.user_id,
|
||||||
area_id = COALESCE(EXCLUDED.area_id, whatsapp_chat_atribuicoes.area_id),
|
area_id = COALESCE(EXCLUDED.area_id, whatsapp_chat_atribuicoes.area_id),
|
||||||
status = 'assigned',
|
status = 'assigned',
|
||||||
|
awaiting_customer_reply = FALSE,
|
||||||
assigned_at = CURRENT_TIMESTAMP,
|
assigned_at = CURRENT_TIMESTAMP,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
@ -199,6 +203,39 @@ export class WhatsappAssignmentService implements OnModuleInit {
|
|||||||
return result.rows[0] ? this.enrichAssignment(result.rows[0]) : null;
|
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) {
|
async routeIncomingMessage(chatId: string, message: string, messageId?: string) {
|
||||||
const cleanMessage = (message || '').trim();
|
const cleanMessage = (message || '').trim();
|
||||||
if (!cleanMessage) {
|
if (!cleanMessage) {
|
||||||
|
|||||||
@ -34,6 +34,11 @@ export class WhatsappController {
|
|||||||
return this.whatsappService.sendMessage(body.to, body.message, body.media, body.senderName);
|
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<string, string | null | undefined> }) {
|
||||||
|
return this.whatsappService.startAttendance(body.to, body.templateId, body.userId, body.areaId, body.variables);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('assign')
|
@Post('assign')
|
||||||
async assignChat(@Body() body: { chatId: string; userId: string; areaId?: string }) {
|
async assignChat(@Body() body: { chatId: string; userId: string; areaId?: string }) {
|
||||||
return this.assignmentService.assignChat(body.chatId, body.userId, body.areaId);
|
return this.assignmentService.assignChat(body.chatId, body.userId, body.areaId);
|
||||||
|
|||||||
@ -141,6 +141,10 @@ export class WhatsappService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!msg.fromMe) {
|
if (!msg.fromMe) {
|
||||||
|
if (messageBody || msg.hasMedia) {
|
||||||
|
await this.assignmentService.markCustomerReplied(remoteJid);
|
||||||
|
}
|
||||||
|
|
||||||
if (!messageBody) {
|
if (!messageBody) {
|
||||||
this.logger.log(`Triagem ignorada para ${remoteJid}: mensagem sem texto.`);
|
this.logger.log(`Triagem ignorada para ${remoteJid}: mensagem sem texto.`);
|
||||||
return;
|
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) {
|
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');
|
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);
|
const outboundMessage = this.formatOutboundMessage(message, senderName);
|
||||||
|
|
||||||
@ -519,6 +527,39 @@ export class WhatsappService implements OnModuleInit {
|
|||||||
return sentMsg;
|
return sentMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startAttendance(
|
||||||
|
to: string,
|
||||||
|
templateId: number,
|
||||||
|
userId: number,
|
||||||
|
areaId?: number | null,
|
||||||
|
variables?: Record<string, string | null | undefined>,
|
||||||
|
) {
|
||||||
|
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<string, string | null | undefined>) {
|
||||||
|
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) {
|
private formatOutboundMessage(message: string, senderName?: string) {
|
||||||
const cleanMessage = (message || '').trim();
|
const cleanMessage = (message || '').trim();
|
||||||
const cleanSenderName = (senderName || '').trim();
|
const cleanSenderName = (senderName || '').trim();
|
||||||
@ -539,6 +580,11 @@ export class WhatsappService implements OnModuleInit {
|
|||||||
return res.rows;
|
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) {
|
async saveTemplate(name: string, content: string) {
|
||||||
const res = await this.db.query(
|
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 *',
|
'INSERT INTO whatsapp_templates (name, content) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET content = EXCLUDED.content, updated_at = CURRENT_TIMESTAMP RETURNING *',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user