'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