Files

247 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 }