Files

259 lines
10 KiB
JavaScript

'use strict'
/**
* apiWhatsappTools.js — WPP-05/06/07
*
* Endpoints chamados pelo motor NewWhats (via human_api ou webhook)
* para ações que requerem acesso ao banco de dados do satélite.
*
* Autenticação: header x-internal-secret (mesmo segredo do INTERNAL_WEBHOOK_SECRET)
* ou verifyToken (admin) dependendo da rota.
*/
const express = require('express')
const router = express.Router()
const { execute, queryOne, query } = require('../database/postgres')
// Middleware interno (motor → satélite)
function internalAuth(req, res, next) {
const secret = process.env.INTERNAL_WEBHOOK_SECRET
if (!secret) { next(); return } // sem segredo configurado, deixa passar (dev)
if (req.headers['x-internal-secret'] === secret) { next(); return }
res.status(401).json({ error: 'Não autorizado' })
}
// ── WPP-05 — Cotação Rápida ────────────────────────────────────────────────────
// POST /api/whatsapp/cotacao-rapida
// Body: { phone, items: [{sku, quantidade}], forma_pagto?, tipo_entrega? }
//
// Fluxo:
// 1. Encontra cliente pelo telefone
// 2. Cria cotação em rascunho
// 3. Adiciona itens (resolve SKU → produto_id)
// 4. Recalcula total
// 5. Retorna link de acompanhamento + total
router.post('/cotacao-rapida', internalAuth, async (req, res) => {
try {
const { phone, items, forma_pagto, tipo_entrega, tenant_id = 1 } = req.body
if (!phone || !Array.isArray(items) || !items.length) {
return res.status(400).json({ error: 'phone e items[] são obrigatórios' })
}
// 1. Acha cliente pelo telefone
const digits = phone.replace(/\D/g, '').replace(/^55/, '')
let cliente = null
for (const v of [digits, digits.length === 11 ? digits.slice(0,2)+digits.slice(3) : null, digits.length === 10 ? digits.slice(0,2)+'9'+digits.slice(2) : null].filter(Boolean)) {
cliente = await queryOne(
`SELECT c.id, c.nome, c.tipo FROM clientes c
JOIN cliente_telefones ct ON ct.cliente_id = c.id
WHERE c.tenant_id = $1 AND REGEXP_REPLACE(ct.numero,'[^0-9]','','g') LIKE $2
LIMIT 1`,
[tenant_id, `%${v}`]
)
if (cliente) break
}
if (!cliente) {
return res.status(404).json({ error: 'Cliente não encontrado para este telefone. Cadastre-o primeiro.' })
}
// 2. Cria cotação
const cotacao = await queryOne(
`INSERT INTO cotacoes (tenant_id, cliente_id, status, forma_pagto, tipo_entrega)
VALUES ($1,$2,'rascunho',$3,$4) RETURNING id`,
[tenant_id, cliente.id, forma_pagto || 'pix', tipo_entrega || 'entrega']
)
// 3. Adiciona itens (resolve por SKU)
for (const item of items) {
const produto = await queryOne(
`SELECT id, nome, preco, sku,
(SELECT preco_promocional FROM promotions WHERE produto_id = p.id AND ativo=1 LIMIT 1) AS preco_promo
FROM produtos p WHERE tenant_id=$1 AND (sku=$2 OR id=$3::int) AND ativo=true LIMIT 1`,
[tenant_id, item.sku || '', item.produto_id || 0]
)
if (!produto) continue
const qty = Number(item.quantidade) || 1
const preco = Number(produto.preco)
const promo = produto.preco_promo ? Number(produto.preco_promo) : null
await execute(
`INSERT INTO cotacao_itens
(cotacao_id,produto_id,nome_produto,sku,quantidade,preco_unitario,preco_promocional,desconto_pct,subtotal)
VALUES ($1,$2,$3,$4,$5,$6,$7,0,$8)`,
[cotacao.id, produto.id, produto.nome, produto.sku, qty, preco, promo, (promo ?? preco) * qty]
)
}
// 4. Recalcula total
await execute(
`UPDATE cotacoes SET total=(SELECT COALESCE(SUM(subtotal),0) FROM cotacao_itens WHERE cotacao_id=$1), updated_at=NOW() WHERE id=$1`,
[cotacao.id]
)
const updated = await queryOne('SELECT total FROM cotacoes WHERE id=$1', [cotacao.id])
const baseUrl = process.env.APP_BASE_URL || 'https://alemaoconveniencias.com.br'
const link = `${baseUrl}/app/catalogo?cotacao=${cotacao.id}`
return res.json({
ok: true,
cotacao_id: cotacao.id,
cliente: cliente.nome,
total: `R$ ${parseFloat(updated.total).toFixed(2).replace('.',',')}`,
link,
mensagem: `🛒 Cotação criada para *${cliente.nome}*!\nTotal: *R$ ${parseFloat(updated.total).toFixed(2).replace('.',',')}*\n\nAcompanhe aqui: ${link}`,
})
} catch (err) {
console.error('[wpp.cotacao-rapida]', err.message)
return res.status(500).json({ error: 'Erro ao criar cotação rápida' })
}
})
// ── WPP-06 — Salvar contato coletado pela IA ──────────────────────────────────
// POST /api/whatsapp/salvar-contato
// Body: { phone, nome, tipo, email?, cpf_cnpj?, tenant_id? }
router.post('/salvar-contato', internalAuth, async (req, res) => {
try {
const { phone, nome, tipo = 'PF', email, cpf_cnpj, tenant_id = 1 } = req.body
if (!phone || !nome) return res.status(400).json({ error: 'phone e nome são obrigatórios' })
const digits = phone.replace(/\D/g, '').replace(/^55/, '')
const tipoVal = ['PF','PJ'].includes(tipo?.toUpperCase()) ? tipo.toUpperCase() : 'PF'
// Verifica se já existe
let cliente = null
for (const v of [digits].filter(Boolean)) {
cliente = await queryOne(
`SELECT c.id FROM clientes c
JOIN cliente_telefones ct ON ct.cliente_id = c.id
WHERE c.tenant_id=$1 AND REGEXP_REPLACE(ct.numero,'[^0-9]','','g') LIKE $2 LIMIT 1`,
[tenant_id, `%${v}`]
)
if (cliente) break
}
if (cliente) {
// Atualiza se necessário
await execute(
`UPDATE clientes SET nome=COALESCE(NULLIF($1,''),nome), email=COALESCE(NULLIF($2,''),email), updated_at=NOW() WHERE id=$3`,
[nome, email || null, cliente.id]
)
return res.json({ ok: true, acao: 'atualizado', cliente_id: cliente.id })
}
// Cria novo cliente
const novoCliente = await queryOne(
`INSERT INTO clientes (tenant_id, nome, tipo, email, cpf_cnpj, status)
VALUES ($1,$2,$3,$4,$5,'ativo') RETURNING id`,
[tenant_id, nome, tipoVal, email || null, cpf_cnpj?.replace(/\D/g,'') || null]
)
// Adiciona telefone
await execute(
`INSERT INTO cliente_telefones (cliente_id, tipo, numero, aceita_whatsapp, principal)
VALUES ($1,'whatsapp',$2,true,true)`,
[novoCliente.id, digits]
)
return res.json({ ok: true, acao: 'criado', cliente_id: novoCliente.id })
} catch (err) {
console.error('[wpp.salvar-contato]', err.message)
return res.status(500).json({ error: 'Erro ao salvar contato' })
}
})
// ── WPP-07 — Handoff: registra troca de contexto para atendente humano ─────────
// POST /api/whatsapp/handoff
// Body: { chat_id, tenant_id?, resumo, setor?, contato_nome?, contato_phone? }
router.post('/handoff', internalAuth, async (req, res) => {
try {
const { chat_id, tenant_id = 1, resumo, setor = 'atendimento', contato_nome, contato_phone } = req.body
if (!chat_id || !resumo) return res.status(400).json({ error: 'chat_id e resumo são obrigatórios' })
// Registra handoff (ativa pausa IA por 4h)
await execute(
`INSERT INTO nw_handoff (chat_id, tenant_id, human_until, updated_at)
VALUES ($1,$2,NOW() + INTERVAL '4 hours', NOW())
ON CONFLICT (chat_id, tenant_id) DO UPDATE SET human_until=NOW() + INTERVAL '4 hours', updated_at=NOW()`,
[chat_id, tenant_id]
)
// Gera mensagem de contexto para o inbox do setor
const remetente = 'Sistema IA'
const mensagem = [
`🤝 *Transferência para atendente humano*`,
`Chat: ${contato_nome || chat_id}${contato_phone ? ` (${contato_phone})` : ''}`,
``,
`*Resumo da conversa:*`,
resumo,
].join('\n')
await execute(
`INSERT INTO mensagens_internas (tenant_id, setor, remetente, mensagem)
VALUES ($1,$2,$3,$4)`,
[tenant_id, setor, remetente, mensagem]
)
return res.json({ ok: true, handoff_ativo: true, setor })
} catch (err) {
console.error('[wpp.handoff]', err.message)
return res.status(500).json({ error: 'Erro ao registrar handoff' })
}
})
// ── WPP-09 — Reactivação: lista clientes inativos para follow-up ──────────────
// GET /api/whatsapp/clientes-inativos?dias=60&tenant_id=1
// (chamado internamente pelo job de reativação)
router.get('/clientes-inativos', internalAuth, async (req, res) => {
try {
const dias = Math.min(Number(req.query.dias) || 60, 365)
const tenant_id = Number(req.query.tenant_id) || 1
const rows = await query(
`SELECT c.id AS cliente_id, c.nome, ct.numero AS telefone,
MAX(p.created_at) AS ultimo_pedido
FROM clientes c
JOIN cliente_telefones ct ON ct.cliente_id = c.id AND ct.aceita_whatsapp = true
LEFT JOIN pedidos p ON p.cliente_id = c.id AND p.tenant_id = $1
WHERE c.tenant_id = $1 AND c.status = 'ativo'
-- Não foi reativado nos últimos 30 dias
AND c.id NOT IN (
SELECT cliente_id FROM wpp_reativacao_log
WHERE tenant_id=$1 AND enviada_em > NOW() - INTERVAL '30 days'
AND cliente_id IS NOT NULL
)
GROUP BY c.id, c.nome, ct.numero
HAVING MAX(p.created_at) < NOW() - ($2 || ' days')::INTERVAL
OR MAX(p.created_at) IS NULL
ORDER BY ultimo_pedido ASC NULLS FIRST
LIMIT 50`,
[tenant_id, dias]
)
return res.json(rows)
} catch (err) {
console.error('[wpp.clientes-inativos]', err.message)
return res.status(500).json({ error: 'Erro ao listar clientes inativos' })
}
})
// ── WPP-09 — Registra envio de reativação ─────────────────────────────────────
// POST /api/whatsapp/log-reativacao
router.post('/log-reativacao', internalAuth, async (req, res) => {
try {
const { cliente_id, chat_id, mensagem, tenant_id = 1 } = req.body
await execute(
`INSERT INTO wpp_reativacao_log (tenant_id, cliente_id, chat_id, mensagem) VALUES ($1,$2,$3,$4)`,
[tenant_id, cliente_id || null, chat_id, mensagem || null]
)
return res.json({ ok: true })
} catch (err) {
return res.status(500).json({ error: 'Erro ao logar reativação' })
}
})
module.exports = router