REFACTOR: Organizado fluxo de WhatsApp e atendimento
- Templates de WhatsApp agora são gerenciados por um repositório e serviçodedicado. - DTOS para WhatsApp foram criados para melhor estruturação dos dados. - AtteandanceAssigmentRepository foi criado para gerenciar as atribuições de atendimento. - Db Query focados apenas em repositorios, deixando os serviços mais limpos e focados na lógica de negócio. - Encodings corrigidos.
This commit is contained in:
parent
1f5d84e065
commit
6097f730b7
@ -1,6 +1,6 @@
|
|||||||
import { BadRequestException, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
import { DatabaseService } from '../../infra/database/database.service';
|
|
||||||
import { AgentPresenceService } from '../admin/agent-presence.service';
|
import { AgentPresenceService } from '../admin/agent-presence.service';
|
||||||
|
import { AttendanceAssignmentRepository } from './repositories/attendance-assignment.repository';
|
||||||
|
|
||||||
interface TransferInput {
|
interface TransferInput {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -116,113 +116,23 @@ const SALES_KEYWORDS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AttendanceAssignmentService implements OnModuleInit {
|
export class AttendanceAssignmentService {
|
||||||
private readonly logger = new Logger(AttendanceAssignmentService.name);
|
private readonly logger = new Logger(AttendanceAssignmentService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly db: DatabaseService,
|
private readonly attendanceAssignmentRepository: AttendanceAssignmentRepository,
|
||||||
private readonly agentPresenceService: AgentPresenceService,
|
private readonly agentPresenceService: AgentPresenceService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
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 awaiting_customer_reply BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
ADD COLUMN IF NOT EXISTS reserved_user_id INTEGER REFERENCES usuarios(id) ON DELETE SET NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS reserved_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
ADD COLUMN IF NOT EXISTS pause_released_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
ADD COLUMN IF NOT EXISTS triage_flow_id INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS triage_audience_id INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS triage_intent_id INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS triage_step VARCHAR(40),
|
|
||||||
ADD COLUMN IF NOT EXISTS triage_builder_version_id INTEGER,
|
|
||||||
ADD COLUMN IF NOT EXISTS triage_builder_node_id INTEGER,
|
|
||||||
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) {
|
async assignChat(chatId: string, userId: string | number, areaId?: string | number | null) {
|
||||||
this.logger.log(`Atribuindo chat ${chatId} ao usuario ${userId}`);
|
this.logger.log(`Atribuindo chat ${chatId} ao usuario ${userId}`);
|
||||||
await this.assertUserCanReceiveAssignment(Number(userId));
|
await this.assertUserCanReceiveAssignment(Number(userId));
|
||||||
|
const result = await this.attendanceAssignmentRepository.assignChat(chatId, Number(userId), areaId ? Number(areaId) : null);
|
||||||
const query = `
|
|
||||||
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',
|
|
||||||
awaiting_customer_reply = FALSE,
|
|
||||||
reserved_user_id = NULL,
|
|
||||||
reserved_at = NULL,
|
|
||||||
pause_released_at = NULL,
|
|
||||||
triage_flow_id = NULL,
|
|
||||||
triage_audience_id = NULL,
|
|
||||||
triage_intent_id = NULL,
|
|
||||||
triage_step = NULL,
|
|
||||||
triage_builder_version_id = NULL,
|
|
||||||
triage_builder_node_id = NULL,
|
|
||||||
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]);
|
return this.enrichAssignment(result.rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async queueChat(chatId: string, areaId: number, note?: string | null) {
|
async queueChat(chatId: string, areaId: number, note?: string | null) {
|
||||||
const query = `
|
const result = await this.attendanceAssignmentRepository.queueChat(chatId, areaId, note || null);
|
||||||
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',
|
|
||||||
reserved_user_id = NULL,
|
|
||||||
reserved_at = NULL,
|
|
||||||
pause_released_at = NULL,
|
|
||||||
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,
|
|
||||||
triage_flow_id = NULL,
|
|
||||||
triage_audience_id = NULL,
|
|
||||||
triage_intent_id = NULL,
|
|
||||||
triage_step = NULL,
|
|
||||||
triage_builder_version_id = NULL,
|
|
||||||
triage_builder_node_id = NULL,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
RETURNING *;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await this.db.query(query, [chatId, areaId, note || null]);
|
|
||||||
return this.enrichAssignment(result.rows[0]);
|
return this.enrichAssignment(result.rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,47 +142,18 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
await this.assertUserCanReceiveAssignment(input.userId);
|
await this.assertUserCanReceiveAssignment(input.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = `
|
const result = await this.attendanceAssignmentRepository.transferChat({
|
||||||
INSERT INTO whatsapp_chat_atribuicoes (
|
chatId: input.chatId,
|
||||||
chat_id, user_id, area_id, status, conversation_started_at, expires_at, transfer_note, assigned_at, updated_at
|
userId: input.userId || null,
|
||||||
)
|
areaId: input.areaId,
|
||||||
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,
|
|
||||||
status = EXCLUDED.status,
|
|
||||||
transfer_note = EXCLUDED.transfer_note,
|
|
||||||
reserved_user_id = NULL,
|
|
||||||
reserved_at = NULL,
|
|
||||||
pause_released_at = NULL,
|
|
||||||
triage_flow_id = NULL,
|
|
||||||
triage_audience_id = NULL,
|
|
||||||
triage_intent_id = NULL,
|
|
||||||
triage_step = NULL,
|
|
||||||
triage_builder_version_id = NULL,
|
|
||||||
triage_builder_node_id = NULL,
|
|
||||||
assigned_at = CURRENT_TIMESTAMP,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
RETURNING *;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await this.db.query(query, [
|
|
||||||
input.chatId,
|
|
||||||
input.userId || null,
|
|
||||||
input.areaId,
|
|
||||||
status,
|
status,
|
||||||
input.note || null,
|
note: input.note || null,
|
||||||
]);
|
});
|
||||||
return this.enrichAssignment(result.rows[0]);
|
return this.enrichAssignment(result.rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAssignment(chatId: string) {
|
async getAssignment(chatId: string) {
|
||||||
const query = `
|
const result = await this.attendanceAssignmentRepository.getAssignment(chatId);
|
||||||
SELECT * FROM whatsapp_chat_atribuicoes
|
|
||||||
WHERE chat_id = $1
|
|
||||||
LIMIT 1
|
|
||||||
`;
|
|
||||||
const result = await this.db.query(query, [chatId]);
|
|
||||||
const assignment = result.rows[0];
|
const assignment = result.rows[0];
|
||||||
|
|
||||||
if (!assignment) return null;
|
if (!assignment) return null;
|
||||||
@ -282,10 +163,7 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (assignment.expires_at && new Date(assignment.expires_at).getTime() <= Date.now()) {
|
if (assignment.expires_at && new Date(assignment.expires_at).getTime() <= Date.now()) {
|
||||||
await this.db.query(
|
await this.attendanceAssignmentRepository.expireAssignment(chatId);
|
||||||
`UPDATE whatsapp_chat_atribuicoes SET status = 'expired', user_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE chat_id = $1`,
|
|
||||||
[chatId],
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,102 +172,31 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
|
|
||||||
async releaseChat(chatId: string) {
|
async releaseChat(chatId: string) {
|
||||||
this.logger.log(`Liberando chat ${chatId}`);
|
this.logger.log(`Liberando chat ${chatId}`);
|
||||||
const query = `
|
const result = await this.attendanceAssignmentRepository.releaseChat(chatId);
|
||||||
UPDATE whatsapp_chat_atribuicoes
|
|
||||||
SET
|
|
||||||
user_id = NULL,
|
|
||||||
status = 'queued',
|
|
||||||
reserved_user_id = NULL,
|
|
||||||
reserved_at = NULL,
|
|
||||||
pause_released_at = NULL,
|
|
||||||
triage_flow_id = NULL,
|
|
||||||
triage_audience_id = NULL,
|
|
||||||
triage_intent_id = NULL,
|
|
||||||
triage_step = NULL,
|
|
||||||
triage_builder_version_id = NULL,
|
|
||||||
triage_builder_node_id = NULL,
|
|
||||||
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;
|
return result.rows[0] ? this.enrichAssignment(result.rows[0]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeChat(chatId: string, userId?: string | number | null) {
|
async closeChat(chatId: string, userId?: string | number | null) {
|
||||||
this.logger.log(`Encerrando chat ${chatId}`);
|
this.logger.log(`Encerrando chat ${chatId}`);
|
||||||
|
const result = await this.attendanceAssignmentRepository.closeChat(chatId, userId);
|
||||||
const params: Array<string | number | null> = [chatId];
|
|
||||||
const userGuard = userId ? 'AND (user_id = $2 OR user_id IS NULL)' : '';
|
|
||||||
if (userId) params.push(Number(userId));
|
|
||||||
|
|
||||||
const result = await this.db.query(
|
|
||||||
`
|
|
||||||
UPDATE whatsapp_chat_atribuicoes
|
|
||||||
SET
|
|
||||||
user_id = NULL,
|
|
||||||
area_id = NULL,
|
|
||||||
status = 'expired',
|
|
||||||
awaiting_customer_reply = FALSE,
|
|
||||||
reserved_user_id = NULL,
|
|
||||||
reserved_at = NULL,
|
|
||||||
pause_released_at = NULL,
|
|
||||||
triage_flow_id = NULL,
|
|
||||||
triage_audience_id = NULL,
|
|
||||||
triage_intent_id = NULL,
|
|
||||||
triage_step = NULL,
|
|
||||||
triage_builder_version_id = NULL,
|
|
||||||
triage_builder_node_id = NULL,
|
|
||||||
transfer_note = NULL,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE chat_id = $1
|
|
||||||
${userGuard}
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
params,
|
|
||||||
);
|
|
||||||
|
|
||||||
return result.rows[0] || null;
|
return result.rows[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearTransferNote(chatId: string) {
|
async clearTransferNote(chatId: string) {
|
||||||
const result = await this.db.query(
|
const result = await this.attendanceAssignmentRepository.clearTransferNote(chatId);
|
||||||
`
|
|
||||||
UPDATE whatsapp_chat_atribuicoes
|
|
||||||
SET transfer_note = NULL, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE chat_id = $1
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[chatId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return result.rows[0] ? this.enrichAssignment(result.rows[0]) : null;
|
return result.rows[0] ? this.enrichAssignment(result.rows[0]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async markAwaitingCustomerReply(chatId: string) {
|
async markAwaitingCustomerReply(chatId: string) {
|
||||||
const result = await this.db.query(
|
const result = await this.attendanceAssignmentRepository.markAwaitingCustomerReply(chatId);
|
||||||
`
|
|
||||||
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;
|
return result.rows[0] ? this.enrichAssignment(result.rows[0]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async markCustomerReplied(chatId: string) {
|
async markCustomerReplied(chatId: string) {
|
||||||
const result = await this.db.query(
|
const result = await this.attendanceAssignmentRepository.markCustomerReplied(chatId);
|
||||||
`
|
|
||||||
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;
|
return result.rows[0] ? this.enrichAssignment(result.rows[0]) : null;
|
||||||
}
|
}
|
||||||
@ -506,14 +313,12 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getChatsByArea(areaId: string) {
|
async getChatsByArea(areaId: string) {
|
||||||
const query = `SELECT * FROM whatsapp_chat_atribuicoes WHERE area_id = $1 AND status <> 'expired'`;
|
const result = await this.attendanceAssignmentRepository.getChatsByArea(areaId);
|
||||||
const result = await this.db.query(query, [areaId]);
|
|
||||||
return result.rows;
|
return result.rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChatsByUser(userId: string) {
|
async getChatsByUser(userId: string) {
|
||||||
const query = `SELECT * FROM whatsapp_chat_atribuicoes WHERE user_id = $1 AND status = 'assigned'`;
|
const result = await this.attendanceAssignmentRepository.getChatsByUser(userId);
|
||||||
const result = await this.db.query(query, [userId]);
|
|
||||||
return result.rows;
|
return result.rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -570,20 +375,7 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
const matchedChild = this.matchBuilderChild(message, currentNode.children || []);
|
const matchedChild = this.matchBuilderChild(message, currentNode.children || []);
|
||||||
if (matchedChild) {
|
if (matchedChild) {
|
||||||
if (matchedChild.node_type === 'close') {
|
if (matchedChild.node_type === 'close') {
|
||||||
await this.db.query(
|
await this.attendanceAssignmentRepository.closeBotConversation(chatId, messageId);
|
||||||
`
|
|
||||||
UPDATE whatsapp_chat_atribuicoes
|
|
||||||
SET status = 'expired',
|
|
||||||
user_id = NULL,
|
|
||||||
area_id = NULL,
|
|
||||||
triage_builder_version_id = NULL,
|
|
||||||
triage_builder_node_id = NULL,
|
|
||||||
last_routed_message_id = $2,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE chat_id = $1
|
|
||||||
`,
|
|
||||||
[chatId, messageId || null],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assignment: null,
|
assignment: null,
|
||||||
@ -856,24 +648,7 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
normalized.includes('obrigado');
|
normalized.includes('obrigado');
|
||||||
|
|
||||||
if (wantsClose && !wantsEscalation) {
|
if (wantsClose && !wantsEscalation) {
|
||||||
await this.db.query(
|
await this.attendanceAssignmentRepository.closeBotConversation(chatId, messageId);
|
||||||
`
|
|
||||||
UPDATE whatsapp_chat_atribuicoes
|
|
||||||
SET status = 'expired',
|
|
||||||
user_id = NULL,
|
|
||||||
area_id = NULL,
|
|
||||||
triage_flow_id = NULL,
|
|
||||||
triage_audience_id = NULL,
|
|
||||||
triage_intent_id = NULL,
|
|
||||||
triage_step = NULL,
|
|
||||||
triage_builder_version_id = NULL,
|
|
||||||
triage_builder_node_id = NULL,
|
|
||||||
last_routed_message_id = $2,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE chat_id = $1
|
|
||||||
`,
|
|
||||||
[chatId, messageId || null],
|
|
||||||
);
|
|
||||||
const assignment = await this.getAssignment(chatId);
|
const assignment = await this.getAssignment(chatId);
|
||||||
return {
|
return {
|
||||||
assignment,
|
assignment,
|
||||||
@ -903,7 +678,7 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
botMessage: this.formatSenderMessage(
|
botMessage: this.formatSenderMessage(
|
||||||
'Atendente virtual',
|
'Atendente virtual',
|
||||||
VIRTUAL_AGENT_NAME,
|
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 o time responsavel.',
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -968,43 +743,14 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getActiveTriageFlow(): Promise<TriageFlow | null> {
|
private async getActiveTriageFlow(): Promise<TriageFlow | null> {
|
||||||
const flows = await this.db.query<any>(
|
const flows = await this.attendanceAssignmentRepository.getActiveTriageFlow().catch(() => ({ rows: [] }));
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM bot_triage_flows
|
|
||||||
WHERE active = TRUE
|
|
||||||
ORDER BY id ASC
|
|
||||||
LIMIT 1
|
|
||||||
`,
|
|
||||||
).catch(() => ({ rows: [] }));
|
|
||||||
|
|
||||||
const flow = flows.rows[0];
|
const flow = flows.rows[0];
|
||||||
if (!flow) return null;
|
if (!flow) return null;
|
||||||
|
|
||||||
const audiences = await this.db.query<any>(
|
const audiences = await this.attendanceAssignmentRepository.listActiveTriageAudiences(flow.id);
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM bot_triage_audiences
|
|
||||||
WHERE flow_id = $1
|
|
||||||
AND active = TRUE
|
|
||||||
ORDER BY sort_order ASC, id ASC
|
|
||||||
`,
|
|
||||||
[flow.id],
|
|
||||||
);
|
|
||||||
|
|
||||||
const intents = await this.db.query<any>(
|
const intents = await this.attendanceAssignmentRepository.listActiveTriageIntents(audiences.rows.map((audience) => audience.id));
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
bti.*,
|
|
||||||
a.nome AS area_nome
|
|
||||||
FROM bot_triage_intents bti
|
|
||||||
INNER JOIN areas a ON a.id = bti.area_id
|
|
||||||
WHERE bti.audience_id = ANY($1::int[])
|
|
||||||
AND bti.active = TRUE
|
|
||||||
ORDER BY bti.sort_order ASC, bti.id ASC
|
|
||||||
`,
|
|
||||||
[audiences.rows.map((audience) => audience.id)],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...flow,
|
...flow,
|
||||||
@ -1016,34 +762,12 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getPublishedBotFlow(): Promise<BotFlowVersion | null> {
|
private async getPublishedBotFlow(): Promise<BotFlowVersion | null> {
|
||||||
const versionResult = await this.db.query<any>(
|
const versionResult = await this.attendanceAssignmentRepository.getPublishedBotFlowVersion().catch(() => ({ rows: [] }));
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM bot_flow_versions
|
|
||||||
WHERE status = 'published'
|
|
||||||
AND root_node_id IS NOT NULL
|
|
||||||
ORDER BY published_at DESC, id DESC
|
|
||||||
LIMIT 1
|
|
||||||
`,
|
|
||||||
).catch(() => ({ rows: [] }));
|
|
||||||
|
|
||||||
const version = versionResult.rows[0];
|
const version = versionResult.rows[0];
|
||||||
if (!version) return null;
|
if (!version) return null;
|
||||||
|
|
||||||
const nodeResult = await this.db.query<any>(
|
const nodeResult = await this.attendanceAssignmentRepository.listBotFlowNodes(version.id).catch(() => ({ rows: [] }));
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
node.*,
|
|
||||||
area.nome AS area_nome,
|
|
||||||
fallback_area.nome AS fallback_area_nome
|
|
||||||
FROM bot_flow_nodes node
|
|
||||||
LEFT JOIN areas area ON area.id = node.area_id
|
|
||||||
LEFT JOIN areas fallback_area ON fallback_area.id = node.fallback_area_id
|
|
||||||
WHERE node.version_id = $1
|
|
||||||
ORDER BY node.parent_id NULLS FIRST, node.sort_order ASC, node.id ASC
|
|
||||||
`,
|
|
||||||
[version.id],
|
|
||||||
).catch(() => ({ rows: [] }));
|
|
||||||
|
|
||||||
const byId = new Map<number, BotFlowNode>();
|
const byId = new Map<number, BotFlowNode>();
|
||||||
nodeResult.rows.forEach((node) => byId.set(Number(node.id), { ...node, children: [] }));
|
nodeResult.rows.forEach((node) => byId.set(Number(node.id), { ...node, children: [] }));
|
||||||
@ -1097,16 +821,7 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async detectConfiguredArea(normalizedMessage: string) {
|
private async detectConfiguredArea(normalizedMessage: string) {
|
||||||
const result = await this.db.query<{ id: number; nome: string; keyword: string }>(
|
const result = await this.attendanceAssignmentRepository.listRoutingKeywords().catch(() => ({ rows: [] }));
|
||||||
`
|
|
||||||
SELECT a.id, a.nome, ark.keyword
|
|
||||||
FROM area_routing_keywords ark
|
|
||||||
INNER JOIN areas a ON a.id = ark.area_id
|
|
||||||
WHERE ark.active = TRUE
|
|
||||||
AND a.ativo = TRUE
|
|
||||||
ORDER BY LENGTH(ark.keyword) DESC
|
|
||||||
`,
|
|
||||||
).catch(() => ({ rows: [] }));
|
|
||||||
|
|
||||||
const match = result.rows.find((row) => normalizedMessage.includes(this.normalize(row.keyword)));
|
const match = result.rows.find((row) => normalizedMessage.includes(this.normalize(row.keyword)));
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
@ -1126,10 +841,7 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getAreaByName(targetName: string) {
|
private async getAreaByName(targetName: string) {
|
||||||
const result = await this.db.query<{ id: number; nome: string }>(
|
const result = await this.attendanceAssignmentRepository.getAreaByName(targetName);
|
||||||
`SELECT id, nome FROM areas WHERE nome = $1 LIMIT 1`,
|
|
||||||
[targetName],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.rows[0]) {
|
if (result.rows[0]) {
|
||||||
return {
|
return {
|
||||||
@ -1143,27 +855,7 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async upsertTriage(chatId: string, attempts: number, messageId?: string) {
|
private async upsertTriage(chatId: string, attempts: number, messageId?: string) {
|
||||||
const query = `
|
const result = await this.attendanceAssignmentRepository.upsertTriage(chatId, attempts, messageId);
|
||||||
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]);
|
return this.enrichAssignment(result.rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1176,44 +868,17 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
messageId?: string,
|
messageId?: string,
|
||||||
intentId: number | null = null,
|
intentId: number | null = null,
|
||||||
) {
|
) {
|
||||||
const query = `
|
const result = await this.attendanceAssignmentRepository.upsertConfiguredTriage({
|
||||||
INSERT INTO whatsapp_chat_atribuicoes (
|
|
||||||
chat_id, user_id, area_id, status, routing_attempts, last_routed_message_id,
|
|
||||||
last_bot_sent_at, triage_flow_id, triage_audience_id, triage_intent_id, triage_step,
|
|
||||||
conversation_started_at, expires_at, assigned_at, updated_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
$1, NULL, NULL, 'bot_triage', $2, $3,
|
|
||||||
CURRENT_TIMESTAMP, $4, $5, $6, $7,
|
|
||||||
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,
|
|
||||||
triage_flow_id = EXCLUDED.triage_flow_id,
|
|
||||||
triage_audience_id = EXCLUDED.triage_audience_id,
|
|
||||||
triage_intent_id = EXCLUDED.triage_intent_id,
|
|
||||||
triage_step = EXCLUDED.triage_step,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
RETURNING *;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await this.db.query(query, [
|
|
||||||
chatId,
|
chatId,
|
||||||
attempts,
|
|
||||||
messageId || null,
|
|
||||||
flowId,
|
flowId,
|
||||||
audienceId,
|
audienceId,
|
||||||
intentId,
|
|
||||||
step,
|
step,
|
||||||
]);
|
attempts,
|
||||||
|
messageId,
|
||||||
|
intentId,
|
||||||
|
});
|
||||||
return this.enrichAssignment(result.rows[0]);
|
return this.enrichAssignment(result.rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async upsertBuilderTriage(
|
private async upsertBuilderTriage(
|
||||||
chatId: string,
|
chatId: string,
|
||||||
versionId: number,
|
versionId: number,
|
||||||
@ -1221,72 +886,16 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
attempts: number,
|
attempts: number,
|
||||||
messageId?: string,
|
messageId?: string,
|
||||||
) {
|
) {
|
||||||
const query = `
|
const result = await this.attendanceAssignmentRepository.upsertBuilderTriage(chatId, versionId, nodeId, attempts, messageId);
|
||||||
INSERT INTO whatsapp_chat_atribuicoes (
|
|
||||||
chat_id, user_id, area_id, status, routing_attempts, last_routed_message_id,
|
|
||||||
last_bot_sent_at, triage_builder_version_id, triage_builder_node_id,
|
|
||||||
conversation_started_at, expires_at, assigned_at, updated_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
$1, NULL, NULL, 'bot_triage', $2, $3,
|
|
||||||
CURRENT_TIMESTAMP, $4, $5,
|
|
||||||
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,
|
|
||||||
triage_flow_id = NULL,
|
|
||||||
triage_audience_id = NULL,
|
|
||||||
triage_intent_id = NULL,
|
|
||||||
triage_step = NULL,
|
|
||||||
triage_builder_version_id = EXCLUDED.triage_builder_version_id,
|
|
||||||
triage_builder_node_id = EXCLUDED.triage_builder_node_id,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
RETURNING *;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await this.db.query(query, [
|
|
||||||
chatId,
|
|
||||||
attempts,
|
|
||||||
messageId || null,
|
|
||||||
versionId,
|
|
||||||
nodeId,
|
|
||||||
]);
|
|
||||||
return this.enrichAssignment(result.rows[0]);
|
return this.enrichAssignment(result.rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async markBotRoute(chatId: string, messageId?: string, updateBotTimestamp = true) {
|
private async markBotRoute(chatId: string, messageId?: string, updateBotTimestamp = true) {
|
||||||
await this.db.query(
|
await this.attendanceAssignmentRepository.markBotRoute(chatId, messageId, updateBotTimestamp);
|
||||||
`
|
|
||||||
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) {
|
private async markMessageRouted(chatId: string, messageId?: string) {
|
||||||
await this.db.query(
|
await this.attendanceAssignmentRepository.markMessageRouted(chatId, messageId);
|
||||||
`
|
|
||||||
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[]) {
|
private matchAny(text: string, keywords: string[]) {
|
||||||
return keywords.some((keyword) => text.includes(this.normalize(keyword)));
|
return keywords.some((keyword) => text.includes(this.normalize(keyword)));
|
||||||
}
|
}
|
||||||
@ -1382,7 +991,7 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
if (!lastBotSentAt) return false;
|
if (!lastBotSentAt) return false;
|
||||||
|
|
||||||
const normalized = this.normalize(message).replace(/[!?.,\s]+/g, ' ').trim();
|
const normalized = this.normalize(message).replace(/[!?.,\s]+/g, ' ').trim();
|
||||||
const greetingOnlyMessages = ['oi', 'ola', 'olá', 'bom dia', 'boa tarde', 'boa noite'];
|
const greetingOnlyMessages = ['oi', 'ola', 'olá', 'bom dia', 'boa tarde', 'boa noite'];
|
||||||
if (!greetingOnlyMessages.includes(normalized)) return false;
|
if (!greetingOnlyMessages.includes(normalized)) return false;
|
||||||
|
|
||||||
const lastBotTime = new Date(lastBotSentAt).getTime();
|
const lastBotTime = new Date(lastBotSentAt).getTime();
|
||||||
@ -1398,21 +1007,7 @@ export class AttendanceAssignmentService implements OnModuleInit {
|
|||||||
private async enrichAssignment(assignment: any) {
|
private async enrichAssignment(assignment: any) {
|
||||||
if (!assignment) return null;
|
if (!assignment) return null;
|
||||||
|
|
||||||
const result = await this.db.query(
|
const result = await this.attendanceAssignmentRepository.enrichAssignment(assignment.id);
|
||||||
`
|
|
||||||
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;
|
return result.rows[0] || assignment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AdminModule } from '../admin/admin.module';
|
import { AdminModule } from '../admin/admin.module';
|
||||||
import { AttendanceAssignmentService } from './attendance-assignment.service';
|
import { AttendanceAssignmentService } from './attendance-assignment.service';
|
||||||
|
import { AttendanceAssignmentRepository } from './repositories/attendance-assignment.repository';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AdminModule],
|
imports: [AdminModule],
|
||||||
providers: [AttendanceAssignmentService],
|
providers: [AttendanceAssignmentService, AttendanceAssignmentRepository],
|
||||||
exports: [AttendanceAssignmentService],
|
exports: [AttendanceAssignmentService],
|
||||||
})
|
})
|
||||||
export class AttendanceModule {}
|
export class AttendanceModule {}
|
||||||
|
|||||||
@ -0,0 +1,473 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DatabaseService } from '../../../infra/database/database.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AttendanceAssignmentRepository {
|
||||||
|
constructor(private readonly database: DatabaseService) {}
|
||||||
|
|
||||||
|
assignChat(chatId: string, userId: number, areaId?: number | null) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
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',
|
||||||
|
awaiting_customer_reply = FALSE,
|
||||||
|
reserved_user_id = NULL,
|
||||||
|
reserved_at = NULL,
|
||||||
|
pause_released_at = NULL,
|
||||||
|
triage_flow_id = NULL,
|
||||||
|
triage_audience_id = NULL,
|
||||||
|
triage_intent_id = NULL,
|
||||||
|
triage_step = NULL,
|
||||||
|
triage_builder_version_id = NULL,
|
||||||
|
triage_builder_node_id = NULL,
|
||||||
|
assigned_at = CURRENT_TIMESTAMP,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING *;
|
||||||
|
`,
|
||||||
|
[chatId, userId, areaId || null],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
queueChat(chatId: string, areaId: number, note?: string | null) {
|
||||||
|
return this.database.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',
|
||||||
|
reserved_user_id = NULL,
|
||||||
|
reserved_at = NULL,
|
||||||
|
pause_released_at = NULL,
|
||||||
|
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,
|
||||||
|
triage_flow_id = NULL,
|
||||||
|
triage_audience_id = NULL,
|
||||||
|
triage_intent_id = NULL,
|
||||||
|
triage_step = NULL,
|
||||||
|
triage_builder_version_id = NULL,
|
||||||
|
triage_builder_node_id = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING *;
|
||||||
|
`,
|
||||||
|
[chatId, areaId, note || null],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
transferChat(input: { chatId: string; userId?: number | null; areaId: number; status: string; note?: string | null }) {
|
||||||
|
return this.database.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,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
transfer_note = EXCLUDED.transfer_note,
|
||||||
|
reserved_user_id = NULL,
|
||||||
|
reserved_at = NULL,
|
||||||
|
pause_released_at = NULL,
|
||||||
|
triage_flow_id = NULL,
|
||||||
|
triage_audience_id = NULL,
|
||||||
|
triage_intent_id = NULL,
|
||||||
|
triage_step = NULL,
|
||||||
|
triage_builder_version_id = NULL,
|
||||||
|
triage_builder_node_id = NULL,
|
||||||
|
assigned_at = CURRENT_TIMESTAMP,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING *;
|
||||||
|
`,
|
||||||
|
[input.chatId, input.userId || null, input.areaId, input.status, input.note || null],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssignment(chatId: string) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
SELECT * FROM whatsapp_chat_atribuicoes
|
||||||
|
WHERE chat_id = $1
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
[chatId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
expireAssignment(chatId: string) {
|
||||||
|
return this.database.query(
|
||||||
|
`UPDATE whatsapp_chat_atribuicoes SET status = 'expired', user_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE chat_id = $1`,
|
||||||
|
[chatId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseChat(chatId: string) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_chat_atribuicoes
|
||||||
|
SET
|
||||||
|
user_id = NULL,
|
||||||
|
status = 'queued',
|
||||||
|
reserved_user_id = NULL,
|
||||||
|
reserved_at = NULL,
|
||||||
|
pause_released_at = NULL,
|
||||||
|
triage_flow_id = NULL,
|
||||||
|
triage_audience_id = NULL,
|
||||||
|
triage_intent_id = NULL,
|
||||||
|
triage_step = NULL,
|
||||||
|
triage_builder_version_id = NULL,
|
||||||
|
triage_builder_node_id = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE chat_id = $1
|
||||||
|
RETURNING *;
|
||||||
|
`,
|
||||||
|
[chatId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeChat(chatId: string, userId?: string | number | null) {
|
||||||
|
const params: Array<string | number | null> = [chatId];
|
||||||
|
const userGuard = userId ? 'AND (user_id = $2 OR user_id IS NULL)' : '';
|
||||||
|
if (userId) params.push(Number(userId));
|
||||||
|
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_chat_atribuicoes
|
||||||
|
SET
|
||||||
|
user_id = NULL,
|
||||||
|
area_id = NULL,
|
||||||
|
status = 'expired',
|
||||||
|
awaiting_customer_reply = FALSE,
|
||||||
|
reserved_user_id = NULL,
|
||||||
|
reserved_at = NULL,
|
||||||
|
pause_released_at = NULL,
|
||||||
|
triage_flow_id = NULL,
|
||||||
|
triage_audience_id = NULL,
|
||||||
|
triage_intent_id = NULL,
|
||||||
|
triage_step = NULL,
|
||||||
|
triage_builder_version_id = NULL,
|
||||||
|
triage_builder_node_id = NULL,
|
||||||
|
transfer_note = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE chat_id = $1
|
||||||
|
${userGuard}
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTransferNote(chatId: string) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_chat_atribuicoes
|
||||||
|
SET transfer_note = NULL, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE chat_id = $1
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[chatId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
markAwaitingCustomerReply(chatId: string) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_chat_atribuicoes
|
||||||
|
SET awaiting_customer_reply = TRUE, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE chat_id = $1
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[chatId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
markCustomerReplied(chatId: string) {
|
||||||
|
return this.database.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],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChatsByArea(areaId: string) {
|
||||||
|
return this.database.query(`SELECT * FROM whatsapp_chat_atribuicoes WHERE area_id = $1 AND status <> 'expired'`, [areaId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChatsByUser(userId: string) {
|
||||||
|
return this.database.query(`SELECT * FROM whatsapp_chat_atribuicoes WHERE user_id = $1 AND status = 'assigned'`, [userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBotConversation(chatId: string, messageId?: string) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_chat_atribuicoes
|
||||||
|
SET status = 'expired',
|
||||||
|
user_id = NULL,
|
||||||
|
area_id = NULL,
|
||||||
|
triage_flow_id = NULL,
|
||||||
|
triage_audience_id = NULL,
|
||||||
|
triage_intent_id = NULL,
|
||||||
|
triage_step = NULL,
|
||||||
|
triage_builder_version_id = NULL,
|
||||||
|
triage_builder_node_id = NULL,
|
||||||
|
last_routed_message_id = $2,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE chat_id = $1
|
||||||
|
`,
|
||||||
|
[chatId, messageId || null],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveTriageFlow() {
|
||||||
|
return this.database.query<any>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM bot_triage_flows
|
||||||
|
WHERE active = TRUE
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
listActiveTriageAudiences(flowId: number) {
|
||||||
|
return this.database.query<any>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM bot_triage_audiences
|
||||||
|
WHERE flow_id = $1
|
||||||
|
AND active = TRUE
|
||||||
|
ORDER BY sort_order ASC, id ASC
|
||||||
|
`,
|
||||||
|
[flowId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
listActiveTriageIntents(audienceIds: number[]) {
|
||||||
|
return this.database.query<any>(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
bti.*,
|
||||||
|
a.nome AS area_nome
|
||||||
|
FROM bot_triage_intents bti
|
||||||
|
INNER JOIN areas a ON a.id = bti.area_id
|
||||||
|
WHERE bti.audience_id = ANY($1::int[])
|
||||||
|
AND bti.active = TRUE
|
||||||
|
ORDER BY bti.sort_order ASC, bti.id ASC
|
||||||
|
`,
|
||||||
|
[audienceIds],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPublishedBotFlowVersion() {
|
||||||
|
return this.database.query<any>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM bot_flow_versions
|
||||||
|
WHERE status = 'published'
|
||||||
|
AND root_node_id IS NOT NULL
|
||||||
|
ORDER BY published_at DESC, id DESC
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
listBotFlowNodes(versionId: number) {
|
||||||
|
return this.database.query<any>(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
node.*,
|
||||||
|
area.nome AS area_nome,
|
||||||
|
fallback_area.nome AS fallback_area_nome
|
||||||
|
FROM bot_flow_nodes node
|
||||||
|
LEFT JOIN areas area ON area.id = node.area_id
|
||||||
|
LEFT JOIN areas fallback_area ON fallback_area.id = node.fallback_area_id
|
||||||
|
WHERE node.version_id = $1
|
||||||
|
ORDER BY node.parent_id NULLS FIRST, node.sort_order ASC, node.id ASC
|
||||||
|
`,
|
||||||
|
[versionId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
listRoutingKeywords() {
|
||||||
|
return this.database.query<{ id: number; nome: string; keyword: string }>(
|
||||||
|
`
|
||||||
|
SELECT a.id, a.nome, ark.keyword
|
||||||
|
FROM area_routing_keywords ark
|
||||||
|
INNER JOIN areas a ON a.id = ark.area_id
|
||||||
|
WHERE ark.active = TRUE
|
||||||
|
AND a.ativo = TRUE
|
||||||
|
ORDER BY LENGTH(ark.keyword) DESC
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAreaByName(targetName: string) {
|
||||||
|
return this.database.query<{ id: number; nome: string }>(
|
||||||
|
`SELECT id, nome FROM areas WHERE nome = $1 LIMIT 1`,
|
||||||
|
[targetName],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
upsertTriage(chatId: string, attempts: number, messageId?: string) {
|
||||||
|
return this.database.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 *;
|
||||||
|
`,
|
||||||
|
[chatId, attempts, messageId || null],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
upsertConfiguredTriage(input: {
|
||||||
|
chatId: string;
|
||||||
|
flowId: number;
|
||||||
|
audienceId: number | null;
|
||||||
|
step: 'audience' | 'intent' | 'resolution';
|
||||||
|
attempts: number;
|
||||||
|
messageId?: string;
|
||||||
|
intentId: number | null;
|
||||||
|
}) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
INSERT INTO whatsapp_chat_atribuicoes (
|
||||||
|
chat_id, user_id, area_id, status, routing_attempts, last_routed_message_id,
|
||||||
|
last_bot_sent_at, triage_flow_id, triage_audience_id, triage_intent_id, triage_step,
|
||||||
|
conversation_started_at, expires_at, assigned_at, updated_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1, NULL, NULL, 'bot_triage', $2, $3,
|
||||||
|
CURRENT_TIMESTAMP, $4, $5, $6, $7,
|
||||||
|
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,
|
||||||
|
triage_flow_id = EXCLUDED.triage_flow_id,
|
||||||
|
triage_audience_id = EXCLUDED.triage_audience_id,
|
||||||
|
triage_intent_id = EXCLUDED.triage_intent_id,
|
||||||
|
triage_step = EXCLUDED.triage_step,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING *;
|
||||||
|
`,
|
||||||
|
[input.chatId, input.attempts, input.messageId || null, input.flowId, input.audienceId, input.intentId, input.step],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
upsertBuilderTriage(chatId: string, versionId: number, nodeId: number, attempts: number, messageId?: string) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
INSERT INTO whatsapp_chat_atribuicoes (
|
||||||
|
chat_id, user_id, area_id, status, routing_attempts, last_routed_message_id,
|
||||||
|
last_bot_sent_at, triage_builder_version_id, triage_builder_node_id,
|
||||||
|
conversation_started_at, expires_at, assigned_at, updated_at
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1, NULL, NULL, 'bot_triage', $2, $3,
|
||||||
|
CURRENT_TIMESTAMP, $4, $5,
|
||||||
|
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,
|
||||||
|
triage_flow_id = NULL,
|
||||||
|
triage_audience_id = NULL,
|
||||||
|
triage_intent_id = NULL,
|
||||||
|
triage_step = NULL,
|
||||||
|
triage_builder_version_id = EXCLUDED.triage_builder_version_id,
|
||||||
|
triage_builder_node_id = EXCLUDED.triage_builder_node_id,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING *;
|
||||||
|
`,
|
||||||
|
[chatId, attempts, messageId || null, versionId, nodeId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
markBotRoute(chatId: string, messageId?: string, updateBotTimestamp = true) {
|
||||||
|
return this.database.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],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
markMessageRouted(chatId: string, messageId?: string) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_chat_atribuicoes
|
||||||
|
SET
|
||||||
|
last_routed_message_id = $2,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE chat_id = $1
|
||||||
|
`,
|
||||||
|
[chatId, messageId || null],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enrichAssignment(assignmentId: number) {
|
||||||
|
return this.database.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
|
||||||
|
`,
|
||||||
|
[assignmentId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
157
src/modules/whatsapp/dto/whatsapp.dto.ts
Normal file
157
src/modules/whatsapp/dto/whatsapp.dto.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsObject,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
MaxLength,
|
||||||
|
ValidateNested,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class WhatsappMediaDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
data: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(160)
|
||||||
|
mimetype: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(260)
|
||||||
|
filename?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendWhatsappMessageDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
to: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(120)
|
||||||
|
senderName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => WhatsappMediaDto)
|
||||||
|
media?: WhatsappMediaDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StartAttendanceDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
to: string;
|
||||||
|
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
templateId: number;
|
||||||
|
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
userId: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
areaId?: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
variables?: Record<string, string | null | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AssignChatDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
chatId: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
areaId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TransferChatDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
chatId: string;
|
||||||
|
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
areaId: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
userId?: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
note?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CloseChatDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
chatId: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
userId?: string | number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateWhatsappTemplateDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(255)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
areaId?: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(40)
|
||||||
|
requestedByRole?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(40)
|
||||||
|
category?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateWhatsappTemplateDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(255)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
areaId?: number | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(40)
|
||||||
|
category?: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,132 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DatabaseService } from '../../../infra/database/database.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WhatsappTemplateRepository {
|
||||||
|
constructor(private readonly database: DatabaseService) {}
|
||||||
|
|
||||||
|
listTemplates() {
|
||||||
|
return this.database.query(`
|
||||||
|
SELECT
|
||||||
|
wt.*,
|
||||||
|
a.nome AS area_nome
|
||||||
|
FROM whatsapp_templates wt
|
||||||
|
LEFT JOIN areas a ON a.id = wt.area_id
|
||||||
|
ORDER BY wt.id ASC
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTemplateById(id: number) {
|
||||||
|
return this.database.query('SELECT * FROM whatsapp_templates WHERE id = $1 LIMIT 1', [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTemplate(input: {
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
category: string;
|
||||||
|
areaId?: number | null;
|
||||||
|
status: string;
|
||||||
|
requestedByRole: string;
|
||||||
|
adminApprovedAtSql: 'NULL' | 'CURRENT_TIMESTAMP';
|
||||||
|
metaSubmittedAtSql: 'NULL' | 'CURRENT_TIMESTAMP';
|
||||||
|
}) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
INSERT INTO whatsapp_templates (
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
category,
|
||||||
|
area_id,
|
||||||
|
status,
|
||||||
|
requested_by_role,
|
||||||
|
admin_approved_at,
|
||||||
|
meta_submitted_at,
|
||||||
|
meta_approved_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, ${input.adminApprovedAtSql}, ${input.metaSubmittedAtSql}, NULL, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (name) DO UPDATE SET
|
||||||
|
content = EXCLUDED.content,
|
||||||
|
category = EXCLUDED.category,
|
||||||
|
area_id = EXCLUDED.area_id,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
requested_by_role = EXCLUDED.requested_by_role,
|
||||||
|
admin_approved_at = EXCLUDED.admin_approved_at,
|
||||||
|
meta_submitted_at = EXCLUDED.meta_submitted_at,
|
||||||
|
meta_approved_at = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[input.name, input.content, input.category, input.areaId || null, input.status, input.requestedByRole],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTemplate(id: number, input: { name: string; content: string; category: string; areaId?: number | null }) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_templates
|
||||||
|
SET
|
||||||
|
name = $1,
|
||||||
|
content = $2,
|
||||||
|
category = $3,
|
||||||
|
area_id = $4,
|
||||||
|
status = 'meta_review',
|
||||||
|
admin_approved_at = CURRENT_TIMESTAMP,
|
||||||
|
meta_submitted_at = CURRENT_TIMESTAMP,
|
||||||
|
meta_approved_at = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $5
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[input.name, input.content, input.category, input.areaId || null, id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
approveTemplateByAdmin(id: number) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_templates
|
||||||
|
SET
|
||||||
|
status = 'meta_review',
|
||||||
|
admin_approved_at = CURRENT_TIMESTAMP,
|
||||||
|
meta_submitted_at = CURRENT_TIMESTAMP,
|
||||||
|
meta_approved_at = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
rejectTemplateByAdmin(id: number) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_templates
|
||||||
|
SET
|
||||||
|
status = 'rejected',
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTemplate(id: number) {
|
||||||
|
return this.database.query('DELETE FROM whatsapp_templates WHERE id = $1', [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshMetaApprovals() {
|
||||||
|
return this.database.query(`
|
||||||
|
UPDATE whatsapp_templates
|
||||||
|
SET
|
||||||
|
status = 'approved',
|
||||||
|
meta_approved_at = COALESCE(meta_approved_at, meta_submitted_at + INTERVAL '15 minutes'),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE status = 'meta_review'
|
||||||
|
AND meta_submitted_at IS NOT NULL
|
||||||
|
AND meta_submitted_at <= CURRENT_TIMESTAMP - INTERVAL '15 minutes'
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/modules/whatsapp/whatsapp-template.service.ts
Normal file
66
src/modules/whatsapp/whatsapp-template.service.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { WhatsappTemplateRepository } from './repositories/whatsapp-template.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WhatsappTemplateService {
|
||||||
|
constructor(private readonly whatsappTemplateRepository: WhatsappTemplateRepository) {}
|
||||||
|
|
||||||
|
async getTemplates() {
|
||||||
|
await this.whatsappTemplateRepository.refreshMetaApprovals();
|
||||||
|
const result = await this.whatsappTemplateRepository.listTemplates();
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTemplateById(id: number) {
|
||||||
|
await this.whatsappTemplateRepository.refreshMetaApprovals();
|
||||||
|
const result = await this.whatsappTemplateRepository.getTemplateById(id);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveTemplate(name: string, content: string, areaId?: number | null, requestedByRole = 'admin', category = 'UTILITY') {
|
||||||
|
const isSupervisor = requestedByRole === 'supervisor';
|
||||||
|
const result = await this.whatsappTemplateRepository.saveTemplate({
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
category: this.normalizeTemplateCategory(category),
|
||||||
|
areaId,
|
||||||
|
status: isSupervisor ? 'admin_review' : 'meta_review',
|
||||||
|
requestedByRole,
|
||||||
|
adminApprovedAtSql: isSupervisor ? 'NULL' : 'CURRENT_TIMESTAMP',
|
||||||
|
metaSubmittedAtSql: isSupervisor ? 'NULL' : 'CURRENT_TIMESTAMP',
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTemplate(id: number, name: string, content: string, areaId?: number | null, category = 'UTILITY') {
|
||||||
|
const result = await this.whatsappTemplateRepository.updateTemplate(id, {
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
areaId,
|
||||||
|
category: this.normalizeTemplateCategory(category),
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async approveTemplateByAdmin(id: number) {
|
||||||
|
const result = await this.whatsappTemplateRepository.approveTemplateByAdmin(id);
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async rejectTemplateByAdmin(id: number) {
|
||||||
|
const result = await this.whatsappTemplateRepository.rejectTemplateByAdmin(id);
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTemplate(id: number) {
|
||||||
|
await this.whatsappTemplateRepository.deleteTemplate(id);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeTemplateCategory(category?: string) {
|
||||||
|
const normalized = String(category || 'UTILITY').trim().toUpperCase();
|
||||||
|
return ['UTILITY', 'MARKETING', 'AUTHENTICATION'].includes(normalized) ? normalized : 'UTILITY';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,13 +2,24 @@ import { Controller, Get, Post, Body, Delete, Param } from '@nestjs/common';
|
|||||||
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { WhatsappService } from './whatsapp.service';
|
import { WhatsappService } from './whatsapp.service';
|
||||||
import { AttendanceAssignmentService } from '../attendance/attendance-assignment.service';
|
import { AttendanceAssignmentService } from '../attendance/attendance-assignment.service';
|
||||||
|
import {
|
||||||
|
AssignChatDto,
|
||||||
|
CloseChatDto,
|
||||||
|
CreateWhatsappTemplateDto,
|
||||||
|
SendWhatsappMessageDto,
|
||||||
|
StartAttendanceDto,
|
||||||
|
TransferChatDto,
|
||||||
|
UpdateWhatsappTemplateDto,
|
||||||
|
} from './dto/whatsapp.dto';
|
||||||
|
import { WhatsappTemplateService } from './whatsapp-template.service';
|
||||||
|
|
||||||
@ApiTags('WhatsApp')
|
@ApiTags('WhatsApp')
|
||||||
@Controller('whatsapp')
|
@Controller('whatsapp')
|
||||||
export class WhatsappController {
|
export class WhatsappController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly whatsappService: WhatsappService,
|
private readonly whatsappService: WhatsappService,
|
||||||
private readonly assignmentService: AttendanceAssignmentService
|
private readonly assignmentService: AttendanceAssignmentService,
|
||||||
|
private readonly whatsappTemplateService: WhatsappTemplateService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@ -80,7 +91,7 @@ export class WhatsappController {
|
|||||||
})
|
})
|
||||||
@ApiResponse({ status: 201, description: 'Mensagem enviada.' })
|
@ApiResponse({ status: 201, description: 'Mensagem enviada.' })
|
||||||
@Post('send')
|
@Post('send')
|
||||||
async sendMessage(@Body() body: { to: string; message: string; senderName?: string; media?: { data: string; mimetype: string; filename?: string } }) {
|
async sendMessage(@Body() body: SendWhatsappMessageDto) {
|
||||||
return this.whatsappService.sendMessage(body.to, body.message, body.media, body.senderName);
|
return this.whatsappService.sendMessage(body.to, body.message, body.media, body.senderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +114,7 @@ export class WhatsappController {
|
|||||||
})
|
})
|
||||||
@ApiResponse({ status: 201, description: 'Atendimento ativo iniciado.' })
|
@ApiResponse({ status: 201, description: 'Atendimento ativo iniciado.' })
|
||||||
@Post('start-attendance')
|
@Post('start-attendance')
|
||||||
async startAttendance(@Body() body: { to: string; templateId: number; userId: number; areaId?: number | null; variables?: Record<string, string | null | undefined> }) {
|
async startAttendance(@Body() body: StartAttendanceDto) {
|
||||||
return this.whatsappService.startAttendance(body.to, body.templateId, body.userId, body.areaId, body.variables);
|
return this.whatsappService.startAttendance(body.to, body.templateId, body.userId, body.areaId, body.variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +134,7 @@ export class WhatsappController {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
@Post('assign')
|
@Post('assign')
|
||||||
async assignChat(@Body() body: { chatId: string; userId: string; areaId?: string }) {
|
async assignChat(@Body() body: AssignChatDto) {
|
||||||
return this.assignmentService.assignChat(body.chatId, body.userId, body.areaId);
|
return this.assignmentService.assignChat(body.chatId, body.userId, body.areaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +155,7 @@ export class WhatsappController {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
@Post('transfer')
|
@Post('transfer')
|
||||||
async transferChat(@Body() body: { chatId: string; areaId: number; userId?: number | null; note?: string | null }) {
|
async transferChat(@Body() body: TransferChatDto) {
|
||||||
return this.assignmentService.transferChat(body);
|
return this.assignmentService.transferChat(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +184,7 @@ export class WhatsappController {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
@Post('close')
|
@Post('close')
|
||||||
async closeChat(@Body() body: { chatId: string; userId?: string | number | null }) {
|
async closeChat(@Body() body: CloseChatDto) {
|
||||||
return this.assignmentService.closeChat(body.chatId, body.userId);
|
return this.assignmentService.closeChat(body.chatId, body.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +204,7 @@ export class WhatsappController {
|
|||||||
})
|
})
|
||||||
@Get('templates')
|
@Get('templates')
|
||||||
async getTemplates() {
|
async getTemplates() {
|
||||||
return this.whatsappService.getTemplates();
|
return this.whatsappTemplateService.getTemplates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@ -214,8 +225,8 @@ export class WhatsappController {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
@Post('templates')
|
@Post('templates')
|
||||||
async saveTemplate(@Body() body: { name: string; content: string; areaId?: number | null; requestedByRole?: string; category?: string }) {
|
async saveTemplate(@Body() body: CreateWhatsappTemplateDto) {
|
||||||
return this.whatsappService.saveTemplate(body.name, body.content, body.areaId, body.requestedByRole, body.category);
|
return this.whatsappTemplateService.saveTemplate(body.name, body.content, body.areaId, body.requestedByRole, body.category);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@ -224,8 +235,8 @@ export class WhatsappController {
|
|||||||
})
|
})
|
||||||
@ApiParam({ name: 'id', description: 'ID do template.' })
|
@ApiParam({ name: 'id', description: 'ID do template.' })
|
||||||
@Post('templates/update/:id')
|
@Post('templates/update/:id')
|
||||||
async updateTemplate(@Param('id') id: string, @Body() body: { name: string; content: string; areaId?: number | null; category?: string }) {
|
async updateTemplate(@Param('id') id: string, @Body() body: UpdateWhatsappTemplateDto) {
|
||||||
return this.whatsappService.updateTemplate(Number(id), body.name, body.content, body.areaId, body.category);
|
return this.whatsappTemplateService.updateTemplate(Number(id), body.name, body.content, body.areaId, body.category);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@ -235,7 +246,7 @@ export class WhatsappController {
|
|||||||
@ApiParam({ name: 'id', description: 'ID do template.' })
|
@ApiParam({ name: 'id', description: 'ID do template.' })
|
||||||
@Post('templates/approve-admin/:id')
|
@Post('templates/approve-admin/:id')
|
||||||
async approveTemplateByAdmin(@Param('id') id: string) {
|
async approveTemplateByAdmin(@Param('id') id: string) {
|
||||||
return this.whatsappService.approveTemplateByAdmin(Number(id));
|
return this.whatsappTemplateService.approveTemplateByAdmin(Number(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@ -245,7 +256,7 @@ export class WhatsappController {
|
|||||||
@ApiParam({ name: 'id', description: 'ID do template.' })
|
@ApiParam({ name: 'id', description: 'ID do template.' })
|
||||||
@Post('templates/reject-admin/:id')
|
@Post('templates/reject-admin/:id')
|
||||||
async rejectTemplateByAdmin(@Param('id') id: string) {
|
async rejectTemplateByAdmin(@Param('id') id: string) {
|
||||||
return this.whatsappService.rejectTemplateByAdmin(Number(id));
|
return this.whatsappTemplateService.rejectTemplateByAdmin(Number(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@ -255,6 +266,6 @@ export class WhatsappController {
|
|||||||
@ApiParam({ name: 'id', description: 'ID do template.' })
|
@ApiParam({ name: 'id', description: 'ID do template.' })
|
||||||
@Delete('templates/:id')
|
@Delete('templates/:id')
|
||||||
async deleteTemplate(@Param('id') id: string) {
|
async deleteTemplate(@Param('id') id: string) {
|
||||||
return this.whatsappService.deleteTemplate(Number(id));
|
return this.whatsappTemplateService.deleteTemplate(Number(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,12 @@ import { WhatsappGateway } from './whatsapp.gateway';
|
|||||||
import { WhatsappController } from './whatsapp.controller';
|
import { WhatsappController } from './whatsapp.controller';
|
||||||
import { AttendanceModule } from '../attendance/attendance.module';
|
import { AttendanceModule } from '../attendance/attendance.module';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
import { WhatsappTemplateRepository } from './repositories/whatsapp-template.repository';
|
||||||
|
import { WhatsappTemplateService } from './whatsapp-template.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AttendanceModule, AuthModule],
|
imports: [AttendanceModule, AuthModule],
|
||||||
providers: [WhatsappService, WhatsappGateway],
|
providers: [WhatsappService, WhatsappGateway, WhatsappTemplateService, WhatsappTemplateRepository],
|
||||||
controllers: [WhatsappController],
|
controllers: [WhatsappController],
|
||||||
exports: [WhatsappService],
|
exports: [WhatsappService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Client, LocalAuth, MessageMedia } from 'whatsapp-web.js';
|
|||||||
import { WhatsappGateway } from './whatsapp.gateway';
|
import { WhatsappGateway } from './whatsapp.gateway';
|
||||||
import { AttendanceAssignmentService } from '../attendance/attendance-assignment.service';
|
import { AttendanceAssignmentService } from '../attendance/attendance-assignment.service';
|
||||||
import { DatabaseService } from '../../infra/database/database.service';
|
import { DatabaseService } from '../../infra/database/database.service';
|
||||||
|
import { WhatsappTemplateService } from './whatsapp-template.service';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
@ -19,51 +20,11 @@ export class WhatsappService implements OnModuleInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly gateway: WhatsappGateway,
|
private readonly gateway: WhatsappGateway,
|
||||||
private readonly assignmentService: AttendanceAssignmentService,
|
private readonly assignmentService: AttendanceAssignmentService,
|
||||||
private readonly db: DatabaseService
|
private readonly db: DatabaseService,
|
||||||
|
private readonly whatsappTemplateService: WhatsappTemplateService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
// Inicialização da tabela de templates no banco
|
|
||||||
try {
|
|
||||||
await this.db.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS whatsapp_templates (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
category VARCHAR(40) NOT NULL DEFAULT 'UTILITY',
|
|
||||||
area_id INTEGER REFERENCES areas (id) ON DELETE SET NULL,
|
|
||||||
status VARCHAR(40) NOT NULL DEFAULT 'approved',
|
|
||||||
requested_by_role VARCHAR(40),
|
|
||||||
admin_approved_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
meta_submitted_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
meta_approved_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
await this.db.query(`
|
|
||||||
ALTER TABLE whatsapp_templates
|
|
||||||
ADD COLUMN IF NOT EXISTS category VARCHAR(40) NOT NULL DEFAULT 'UTILITY',
|
|
||||||
ADD COLUMN IF NOT EXISTS area_id INTEGER REFERENCES areas (id) ON DELETE SET NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS status VARCHAR(40) NOT NULL DEFAULT 'approved',
|
|
||||||
ADD COLUMN IF NOT EXISTS requested_by_role VARCHAR(40),
|
|
||||||
ADD COLUMN IF NOT EXISTS admin_approved_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
ADD COLUMN IF NOT EXISTS meta_submitted_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
ADD COLUMN IF NOT EXISTS meta_approved_at TIMESTAMP WITH TIME ZONE;
|
|
||||||
`);
|
|
||||||
await this.db.query(`
|
|
||||||
INSERT INTO whatsapp_templates (name, content) VALUES
|
|
||||||
('aviso_fatura', 'Olá, {nome}. Estamos entrando em contato para lembrá-lo que a sua fatura está programada para {data}.'),
|
|
||||||
('boas_vindas', 'Olá, {nome}! Obrigado por entrar em contato conosco. Como podemos te ajudar hoje?'),
|
|
||||||
('lembrete_consulta', 'Olá, {nome}. Gostaríamos de confirmar o seu agendamento para {data}. Está confirmado?'),
|
|
||||||
('suporte_tecnico', 'Olá, {nome}. Sou o atendente e irei te auxiliar no seu suporte sob protocolo {protocolo}.')
|
|
||||||
ON CONFLICT (name) DO NOTHING;
|
|
||||||
`);
|
|
||||||
this.logger.log('Tabela de templates do WhatsApp verificada/criada com sucesso no PostgreSQL!');
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error('Erro ao verificar/criar tabela de templates no PostgreSQL:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log('Inicializando WhatsApp Client...');
|
this.logger.log('Inicializando WhatsApp Client...');
|
||||||
|
|
||||||
this.client = new Client({
|
this.client = new Client({
|
||||||
@ -569,7 +530,7 @@ export class WhatsappService implements OnModuleInit {
|
|||||||
areaId?: number | null,
|
areaId?: number | null,
|
||||||
variables?: Record<string, string | null | undefined>,
|
variables?: Record<string, string | null | undefined>,
|
||||||
) {
|
) {
|
||||||
const template = await this.getTemplateById(templateId);
|
const template = await this.whatsappTemplateService.getTemplateById(templateId);
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new Error('Template de WhatsApp nao encontrado');
|
throw new Error('Template de WhatsApp nao encontrado');
|
||||||
}
|
}
|
||||||
@ -625,137 +586,4 @@ export class WhatsappService implements OnModuleInit {
|
|||||||
return `*Atendente: ${cleanSenderName}*\n\n${cleanMessage}`;
|
return `*Atendente: ${cleanSenderName}*\n\n${cleanMessage}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTemplates() {
|
|
||||||
await this.refreshMetaApprovals();
|
|
||||||
const res = await this.db.query(`
|
|
||||||
SELECT
|
|
||||||
wt.*,
|
|
||||||
a.nome AS area_nome
|
|
||||||
FROM whatsapp_templates wt
|
|
||||||
LEFT JOIN areas a ON a.id = wt.area_id
|
|
||||||
ORDER BY wt.id ASC
|
|
||||||
`);
|
|
||||||
return res.rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getTemplateById(id: number) {
|
|
||||||
await this.refreshMetaApprovals();
|
|
||||||
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, areaId?: number | null, requestedByRole = 'admin', category = 'UTILITY') {
|
|
||||||
const isSupervisor = requestedByRole === 'supervisor';
|
|
||||||
const status = isSupervisor ? 'admin_review' : 'meta_review';
|
|
||||||
const adminApprovedAt = isSupervisor ? null : 'CURRENT_TIMESTAMP';
|
|
||||||
const metaSubmittedAt = isSupervisor ? null : 'CURRENT_TIMESTAMP';
|
|
||||||
const res = await this.db.query(
|
|
||||||
`
|
|
||||||
INSERT INTO whatsapp_templates (
|
|
||||||
name,
|
|
||||||
content,
|
|
||||||
category,
|
|
||||||
area_id,
|
|
||||||
status,
|
|
||||||
requested_by_role,
|
|
||||||
admin_approved_at,
|
|
||||||
meta_submitted_at,
|
|
||||||
meta_approved_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, ${adminApprovedAt}, ${metaSubmittedAt}, NULL, CURRENT_TIMESTAMP)
|
|
||||||
ON CONFLICT (name) DO UPDATE SET
|
|
||||||
content = EXCLUDED.content,
|
|
||||||
category = EXCLUDED.category,
|
|
||||||
area_id = EXCLUDED.area_id,
|
|
||||||
status = EXCLUDED.status,
|
|
||||||
requested_by_role = EXCLUDED.requested_by_role,
|
|
||||||
admin_approved_at = EXCLUDED.admin_approved_at,
|
|
||||||
meta_submitted_at = EXCLUDED.meta_submitted_at,
|
|
||||||
meta_approved_at = NULL,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[name, content, this.normalizeTemplateCategory(category), areaId || null, status, requestedByRole]
|
|
||||||
);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTemplate(id: number, name: string, content: string, areaId?: number | null, category = 'UTILITY') {
|
|
||||||
const res = await this.db.query(
|
|
||||||
`
|
|
||||||
UPDATE whatsapp_templates
|
|
||||||
SET
|
|
||||||
name = $1,
|
|
||||||
content = $2,
|
|
||||||
category = $3,
|
|
||||||
area_id = $4,
|
|
||||||
status = 'meta_review',
|
|
||||||
admin_approved_at = CURRENT_TIMESTAMP,
|
|
||||||
meta_submitted_at = CURRENT_TIMESTAMP,
|
|
||||||
meta_approved_at = NULL,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $5
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[name, content, this.normalizeTemplateCategory(category), areaId || null, id]
|
|
||||||
);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async approveTemplateByAdmin(id: number) {
|
|
||||||
const res = await this.db.query(
|
|
||||||
`
|
|
||||||
UPDATE whatsapp_templates
|
|
||||||
SET
|
|
||||||
status = 'meta_review',
|
|
||||||
admin_approved_at = CURRENT_TIMESTAMP,
|
|
||||||
meta_submitted_at = CURRENT_TIMESTAMP,
|
|
||||||
meta_approved_at = NULL,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $1
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[id],
|
|
||||||
);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async rejectTemplateByAdmin(id: number) {
|
|
||||||
const res = await this.db.query(
|
|
||||||
`
|
|
||||||
UPDATE whatsapp_templates
|
|
||||||
SET
|
|
||||||
status = 'rejected',
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $1
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[id],
|
|
||||||
);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteTemplate(id: number) {
|
|
||||||
await this.db.query('DELETE FROM whatsapp_templates WHERE id = $1', [id]);
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
private normalizeTemplateCategory(category?: string) {
|
|
||||||
const normalized = String(category || 'UTILITY').trim().toUpperCase();
|
|
||||||
return ['UTILITY', 'MARKETING', 'AUTHENTICATION'].includes(normalized) ? normalized : 'UTILITY';
|
|
||||||
}
|
|
||||||
|
|
||||||
private async refreshMetaApprovals() {
|
|
||||||
await this.db.query(`
|
|
||||||
UPDATE whatsapp_templates
|
|
||||||
SET
|
|
||||||
status = 'approved',
|
|
||||||
meta_approved_at = COALESCE(meta_approved_at, meta_submitted_at + INTERVAL '15 minutes'),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE status = 'meta_review'
|
|
||||||
AND meta_submitted_at IS NOT NULL
|
|
||||||
AND meta_submitted_at <= CURRENT_TIMESTAMP - INTERVAL '15 minutes'
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user