247 lines
10 KiB
JavaScript
247 lines
10 KiB
JavaScript
'use strict'
|
||
|
||
/**
|
||
* sync-knowledge.js
|
||
* Constrói o nó de conhecimento da Secretária IA com dados reais do banco
|
||
* e sincroniza com o motor NewWhats via API REST.
|
||
*
|
||
* Fluxo:
|
||
* 1. Lê dados do DB Alemão (clubes, planos, parceiros, promoções, sorteio, vagas)
|
||
* 2. Monta texto estruturado do knowledge node
|
||
* 3. Chama o motor NewWhats para atualizar o nó (ou cria se não existir)
|
||
* 4. Retorna { ok, preview, agent_id, node_id }
|
||
*/
|
||
|
||
const { query, queryOne } = require('../../database/postgres')
|
||
|
||
// ── Busca configurações do plugin no banco ────────────────────────────────────
|
||
async function getPluginConfig() {
|
||
const rows = await query(
|
||
`SELECT key, value FROM plugin_configs WHERE plugin_id = 'newwhats'`
|
||
)
|
||
const cfg = {}
|
||
for (const r of rows) cfg[r.key] = r.value
|
||
return cfg
|
||
}
|
||
|
||
// ── Chama a API interna do motor NewWhats (/api/secretaria/*) ─────────────────
|
||
// Nota: as rotas /api/secretaria/* são internas do motor (não passam pelo proxy
|
||
// /api/ext/v1). São acessadas diretamente pela URL do motor configurada no plugin.
|
||
async function motorFetch(motorUrl, _integKey, method, path, body) {
|
||
const url = `${motorUrl}/api/secretaria${path}`
|
||
const opts = {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
}
|
||
if (body) opts.body = JSON.stringify(body)
|
||
const res = await fetch(url, opts)
|
||
if (!res.ok) {
|
||
const txt = await res.text()
|
||
throw new Error(`Motor ${method} /api/secretaria${path} → ${res.status}: ${txt}`)
|
||
}
|
||
return res.json()
|
||
}
|
||
|
||
// ── Monta o texto do knowledge node ──────────────────────────────────────────
|
||
async function buildKnowledgeText() {
|
||
const lines = []
|
||
|
||
// ── Empresa ──────────────────────────────────────────────────────────────
|
||
lines.push(`=== ALEMÃO CONVENIÊNCIAS ===`)
|
||
lines.push(`Somos uma empresa de conveniências com foco em benefícios para nossos clientes.`)
|
||
lines.push(`Site: alemaoconveniencias.com | Atendimento via WhatsApp`)
|
||
lines.push(``)
|
||
|
||
// ── Clubes de Benefícios ─────────────────────────────────────────────────
|
||
const clubes = await query(
|
||
`SELECT id, name, slug, specialty, conditions, rules
|
||
FROM benefit_clubs
|
||
WHERE active = true
|
||
ORDER BY id ASC`
|
||
)
|
||
|
||
if (clubes.length) {
|
||
lines.push(`=== CLUBES DE BENEFÍCIOS ===`)
|
||
for (const clube of clubes) {
|
||
lines.push(`\n• ${clube.name} (${clube.specialty})`)
|
||
|
||
// Planos do clube
|
||
const planos = await query(
|
||
`SELECT name, price_monthly, max_dependents, features
|
||
FROM club_plans
|
||
WHERE club_id = $1 AND active = true
|
||
ORDER BY price_monthly ASC`,
|
||
[clube.id]
|
||
)
|
||
if (planos.length) {
|
||
lines.push(` Planos disponíveis:`)
|
||
for (const p of planos) {
|
||
const deps = p.max_dependents > 0 ? ` (até ${p.max_dependents} dependente${p.max_dependents > 1 ? 's' : ''})` : ' (titular)'
|
||
lines.push(` - ${p.name}${deps}: R$ ${Number(p.price_monthly).toFixed(2).replace('.', ',')}/mês`)
|
||
let features = []
|
||
try { features = typeof p.features === 'string' ? JSON.parse(p.features) : (p.features || []) } catch {}
|
||
if (features.length) lines.push(` Incluído: ${features.slice(0, 4).join(', ')}`)
|
||
}
|
||
}
|
||
|
||
// Parceiros do clube
|
||
const parceiros = await query(
|
||
`SELECT name, specialty, phone, address, discount_percent
|
||
FROM club_partners
|
||
WHERE club_id = $1 AND active = true
|
||
ORDER BY name ASC
|
||
LIMIT 8`,
|
||
[clube.id]
|
||
)
|
||
if (parceiros.length) {
|
||
lines.push(` Parceiros credenciados:`)
|
||
for (const p of parceiros) {
|
||
const desc = p.discount_percent ? ` — ${p.discount_percent}% desconto` : ''
|
||
const tel = p.phone ? ` | Tel: ${p.phone}` : ''
|
||
lines.push(` - ${p.name} (${p.specialty})${desc}${tel}`)
|
||
if (p.address) lines.push(` Endereço: ${p.address}`)
|
||
}
|
||
}
|
||
|
||
if (clube.conditions) lines.push(` Condições: ${clube.conditions}`)
|
||
if (clube.rules) lines.push(` Regras: ${clube.rules}`)
|
||
}
|
||
lines.push(``)
|
||
}
|
||
|
||
// ── Sorteio Ativo ─────────────────────────────────────────────────────────
|
||
const sorteio = await queryOne(
|
||
`SELECT titulo, descricao, preco_numero, total_numeros FROM raffles WHERE ativo = 1 LIMIT 1`
|
||
)
|
||
if (sorteio) {
|
||
const countRow = await queryOne(
|
||
`SELECT COUNT(*) AS total FROM tickets t
|
||
JOIN raffles r ON r.id = t.raffle_id
|
||
WHERE r.ativo = 1 AND (t.status = 'sold' OR (t.status = 'reserved' AND t.reserved_until > NOW()))`
|
||
)
|
||
const vendidos = parseInt(countRow?.total ?? 0, 10)
|
||
const disponiveis = sorteio.total_numeros - vendidos
|
||
lines.push(`=== SORTEIO ATIVO ===`)
|
||
lines.push(`Nome: ${sorteio.titulo}`)
|
||
if (sorteio.descricao) lines.push(`Descrição: ${sorteio.descricao}`)
|
||
lines.push(`Preço por número: R$ ${Number(sorteio.preco_numero).toFixed(2).replace('.', ',')}`)
|
||
lines.push(`Números disponíveis: ${disponiveis} de ${sorteio.total_numeros}`)
|
||
lines.push(`Como comprar: acesse nosso site e escolha seus números favoritos.`)
|
||
lines.push(``)
|
||
}
|
||
|
||
// ── Promoções Ativas ──────────────────────────────────────────────────────
|
||
const promos = await query(
|
||
`SELECT nome, descricao, preco_original, preco_promocional
|
||
FROM promotions WHERE ativo = TRUE ORDER BY id ASC`
|
||
)
|
||
if (promos.length) {
|
||
lines.push(`=== PROMOÇÕES ATIVAS ===`)
|
||
for (const p of promos) {
|
||
const economia = p.preco_original && p.preco_promocional
|
||
? ` (de R$ ${Number(p.preco_original).toFixed(2).replace('.', ',')} por R$ ${Number(p.preco_promocional).toFixed(2).replace('.', ',')})`
|
||
: ''
|
||
lines.push(`• ${p.nome}${economia}`)
|
||
if (p.descricao) lines.push(` ${p.descricao}`)
|
||
}
|
||
lines.push(``)
|
||
}
|
||
|
||
// ── Vagas de Emprego ──────────────────────────────────────────────────────
|
||
const vagas = await query(
|
||
`SELECT title, category, work_mode, job_type, location, salary_hide, salary_min, salary_max
|
||
FROM jobs
|
||
WHERE active = true AND (expires_at IS NULL OR expires_at > NOW())
|
||
ORDER BY published_at DESC
|
||
LIMIT 10`
|
||
)
|
||
if (vagas.length) {
|
||
const modeMap = { presencial: 'Presencial', hibrido: 'Híbrido', remoto: 'Remoto' }
|
||
const typeMap = { clt: 'CLT', pj: 'PJ', freelance: 'Freelance', estagio: 'Estágio', temporario: 'Temporário' }
|
||
lines.push(`=== VAGAS DE EMPREGO DISPONÍVEIS ===`)
|
||
lines.push(`(Temos ${vagas.length} vaga${vagas.length > 1 ? 's' : ''} abertas no momento)`)
|
||
for (const v of vagas) {
|
||
const modo = modeMap[v.work_mode] || v.work_mode
|
||
const tipo = typeMap[v.job_type] || v.job_type
|
||
const sal = v.salary_hide || (!v.salary_min && !v.salary_max)
|
||
? 'A combinar'
|
||
: v.salary_min && v.salary_max
|
||
? `R$ ${Number(v.salary_min).toLocaleString('pt-BR', { maximumFractionDigits: 0 })}–${Number(v.salary_max).toLocaleString('pt-BR', { maximumFractionDigits: 0 })}`
|
||
: v.salary_min
|
||
? `A partir de R$ ${Number(v.salary_min).toLocaleString('pt-BR', { maximumFractionDigits: 0 })}`
|
||
: `Até R$ ${Number(v.salary_max).toLocaleString('pt-BR', { maximumFractionDigits: 0 })}`
|
||
const loc = v.location ? ` | ${v.location}` : ''
|
||
lines.push(`• ${v.title} — ${modo} | ${tipo}${loc} | ${sal}`)
|
||
}
|
||
lines.push(`Para se candidatar, acesse nosso site na seção Vagas.`)
|
||
lines.push(``)
|
||
}
|
||
|
||
// ── Informações Gerais ────────────────────────────────────────────────────
|
||
lines.push(`=== ATENDIMENTO ===`)
|
||
lines.push(`Horário: Consulte pelo WhatsApp ou site para horário atualizado.`)
|
||
lines.push(`Carteirinha digital: disponível na área do cliente em nosso site.`)
|
||
lines.push(`Dúvidas sobre pagamento, cancelamento ou benefícios: informe que vai encaminhar para nossa equipe.`)
|
||
|
||
return lines.join('\n')
|
||
}
|
||
|
||
// ── Endpoint principal: sincroniza knowledge node no motor ────────────────────
|
||
async function syncKnowledge() {
|
||
const cfg = await getPluginConfig()
|
||
const motorUrl = cfg.newwhats_url
|
||
const integKey = cfg.integration_key
|
||
|
||
if (!motorUrl || !integKey) {
|
||
throw new Error('Plugin NewWhats não configurado (newwhats_url ou integration_key ausentes)')
|
||
}
|
||
|
||
// 1. Monta o texto
|
||
const knowledgeText = await buildKnowledgeText()
|
||
|
||
// 2. Busca agentes no motor
|
||
const agents = await motorFetch(motorUrl, integKey, 'GET', '/agents')
|
||
if (!agents.length) throw new Error('Nenhum agente encontrado no motor. Crie um agente na Secretária IA.')
|
||
|
||
const agent = agents.find(a => a.active) || agents[0]
|
||
|
||
// 3. Busca nós do agente
|
||
const nodes = await motorFetch(motorUrl, integKey, 'GET', `/agents/${agent.id}/nodes`)
|
||
|
||
// 4. Encontra o nó de knowledge (ou cria)
|
||
const knowledgeNode = nodes.find(n => n.type === 'knowledge')
|
||
|
||
let nodeId
|
||
if (knowledgeNode) {
|
||
// Atualiza nó existente
|
||
await motorFetch(motorUrl, integKey, 'PUT', `/nodes/${knowledgeNode.id}`, {
|
||
title: 'Base de Conhecimento — Alemão Conveniências',
|
||
content: knowledgeText,
|
||
active: true,
|
||
sort_order: knowledgeNode.sort_order ?? 3,
|
||
})
|
||
nodeId = knowledgeNode.id
|
||
} else {
|
||
// Cria nó novo
|
||
const created = await motorFetch(motorUrl, integKey, 'POST', `/agents/${agent.id}/nodes`, {
|
||
type: 'knowledge',
|
||
title: 'Base de Conhecimento — Alemão Conveniências',
|
||
content: knowledgeText,
|
||
sort_order: 3,
|
||
})
|
||
nodeId = created.id
|
||
}
|
||
|
||
return {
|
||
ok: true,
|
||
agent_id: agent.id,
|
||
agent: agent.name,
|
||
node_id: nodeId,
|
||
preview: knowledgeText,
|
||
chars: knowledgeText.length,
|
||
synced_at: new Date().toISOString(),
|
||
}
|
||
}
|
||
|
||
module.exports = { syncKnowledge, buildKnowledgeText }
|