diff --git a/src/server/routes/outreach.mjs b/src/server/routes/outreach.mjs index 4ba388c..da77260 100644 --- a/src/server/routes/outreach.mjs +++ b/src/server/routes/outreach.mjs @@ -1,11 +1,16 @@ import { addOptOut, + addSharedNumberOverride, countDailyOutreachUsage, getSetting, + isNumberSharedAcrossLeads, isOptedOut, + isSharedNumberOverridden, listMessageTemplates, listOptOuts, + listSharedNumberGroups, removeOptOut, + removeSharedNumberOverride, reserveOutreachSend, saveWhatsAppVerification, finishOutreachSend, @@ -86,6 +91,10 @@ export function registerOutreachRoutes(router, { db, whatsapp }) { json(409, { ok: false, erro: 'numero bloqueado por opt-out' }); return; } + if (isNumberSharedAcrossLeads(db, number) && !isSharedNumberOverridden(db, number)) { + json(409, { ok: false, erro: 'numero compartilhado por varios leads - revise em Configuracoes antes de enviar' }); + return; + } const limit = dailyLimitStatus(db); if (limit.restante <= 0) { json(429, { ok: false, erro: `limite diario atingido (${limit.enviadosHoje}/${limit.limite})`, ...limit }); @@ -220,4 +229,27 @@ export function registerOutreachRoutes(router, { db, whatsapp }) { } json(200, { ok: true }); }); + + router.add('GET', '/api/outreach/shared-numbers', async (req, res, { json }) => { + json(200, { groups: listSharedNumberGroups(db) }); + }); + + router.add('POST', '/api/outreach/shared-numbers/:numero/liberar', async (req, res, { json, params, body, user }) => { + if (!requireAdmin(user, json)) return; + const motivo = String(body?.motivo || '').trim(); + if (!motivo) { + json(400, { erro: 'motivo obrigatorio para liberar numero compartilhado' }); + return; + } + json(201, { override: addSharedNumberOverride(db, params.numero, motivo) }); + }); + + router.add('DELETE', '/api/outreach/shared-numbers/:numero/liberar', async (req, res, { json, params, user }) => { + if (!requireAdmin(user, json)) return; + if (!removeSharedNumberOverride(db, params.numero)) { + json(404, { erro: 'override nao encontrado' }); + return; + } + json(200, { ok: true }); + }); } diff --git a/tests/outreach-routes.test.mjs b/tests/outreach-routes.test.mjs index f560553..6e0364d 100644 --- a/tests/outreach-routes.test.mjs +++ b/tests/outreach-routes.test.mjs @@ -130,6 +130,79 @@ test('rotas opt-out listam e restringem mutacoes a admin', async () => { db.close(); }); +test('envio manual bloqueia numero compartilhado por leads distintos ate liberacao', async () => { + const { db, router } = setup(); + const insert = createLeadInserter(db); + insert({ + dedup_key: 'shared-a', empresa: 'Empresa A', whatsapp: '5511999990009', cidade: 'Salvador', + produtos_json: '[]', pains_json: '[]', origem: 'teste', + telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim' + }); + insert({ + dedup_key: 'shared-b', empresa: 'Empresa B', whatsapp: '5511999990009', cidade: 'Salvador', + produtos_json: '[]', pains_json: '[]', origem: 'teste', + telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim' + }); + + const blocked = await invoke(router, 'POST', '/api/outreach/enviar-manual', { + body: { numero: '5511999990009', mensagem: 'Oi' } + }); + assert.equal(blocked.status, 409); + + await invoke(router, 'POST', '/api/outreach/shared-numbers/5511999990009/liberar', { + body: { motivo: 'confirmado contato direto' }, user: { role: 'admin' } + }); + const allowed = await invoke(router, 'POST', '/api/outreach/enviar-manual', { + body: { numero: '5511999990009', mensagem: 'Oi' } + }); + assert.equal(allowed.status, 202); + db.close(); +}); + +test('rotas de numeros compartilhados listam e restringem liberacao a admin', async () => { + const { db, router } = setup(); + const insert = createLeadInserter(db); + insert({ + dedup_key: 'shared-a', empresa: 'Empresa A', whatsapp: '5511999990009', cidade: 'Salvador', + produtos_json: '[]', pains_json: '[]', origem: 'teste', + telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim' + }); + insert({ + dedup_key: 'shared-b', empresa: 'Empresa B', whatsapp: '5511999990009', cidade: 'Salvador', + produtos_json: '[]', pains_json: '[]', origem: 'teste', + telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim' + }); + + const listed = await invoke(router, 'GET', '/api/outreach/shared-numbers'); + assert.equal(listed.data.groups.length, 1); + assert.equal(listed.data.groups[0].leads.length, 2); + + const denied = await invoke(router, 'POST', '/api/outreach/shared-numbers/5511999990009/liberar', { + body: { motivo: 'teste' }, user: { role: 'comercial' } + }); + assert.equal(denied.status, 403); + + const missingMotivo = await invoke(router, 'POST', '/api/outreach/shared-numbers/5511999990009/liberar', { + body: {}, user: { role: 'admin' } + }); + assert.equal(missingMotivo.status, 400); + + const created = await invoke(router, 'POST', '/api/outreach/shared-numbers/5511999990009/liberar', { + body: { motivo: 'confirmado' }, user: { role: 'admin' } + }); + assert.equal(created.status, 201); + + const removeDenied = await invoke(router, 'DELETE', '/api/outreach/shared-numbers/5511999990009/liberar', { user: { role: 'comercial' } }); + assert.equal(removeDenied.status, 403); + + const removed = await invoke(router, 'DELETE', '/api/outreach/shared-numbers/5511999990009/liberar', { user: { role: 'admin' } }); + assert.equal(removed.status, 200); + + const removedAgain = await invoke(router, 'DELETE', '/api/outreach/shared-numbers/5511999990009/liberar', { user: { role: 'admin' } }); + assert.equal(removedAgain.status, 404); + db.close(); +}); + test('envio com leadId honra destino visivel e usa contexto completo do banco', async () => { const { db, router, sent } = setup(); createLeadInserter(db)({