Add shared-number review routes and block manual send for unreleased numbers
This commit is contained in:
@@ -1,11 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
addOptOut,
|
addOptOut,
|
||||||
|
addSharedNumberOverride,
|
||||||
countDailyOutreachUsage,
|
countDailyOutreachUsage,
|
||||||
getSetting,
|
getSetting,
|
||||||
|
isNumberSharedAcrossLeads,
|
||||||
isOptedOut,
|
isOptedOut,
|
||||||
|
isSharedNumberOverridden,
|
||||||
listMessageTemplates,
|
listMessageTemplates,
|
||||||
listOptOuts,
|
listOptOuts,
|
||||||
|
listSharedNumberGroups,
|
||||||
removeOptOut,
|
removeOptOut,
|
||||||
|
removeSharedNumberOverride,
|
||||||
reserveOutreachSend,
|
reserveOutreachSend,
|
||||||
saveWhatsAppVerification,
|
saveWhatsAppVerification,
|
||||||
finishOutreachSend,
|
finishOutreachSend,
|
||||||
@@ -86,6 +91,10 @@ export function registerOutreachRoutes(router, { db, whatsapp }) {
|
|||||||
json(409, { ok: false, erro: 'numero bloqueado por opt-out' });
|
json(409, { ok: false, erro: 'numero bloqueado por opt-out' });
|
||||||
return;
|
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);
|
const limit = dailyLimitStatus(db);
|
||||||
if (limit.restante <= 0) {
|
if (limit.restante <= 0) {
|
||||||
json(429, { ok: false, erro: `limite diario atingido (${limit.enviadosHoje}/${limit.limite})`, ...limit });
|
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 });
|
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 });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,79 @@ test('rotas opt-out listam e restringem mutacoes a admin', async () => {
|
|||||||
db.close();
|
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 () => {
|
test('envio com leadId honra destino visivel e usa contexto completo do banco', async () => {
|
||||||
const { db, router, sent } = setup();
|
const { db, router, sent } = setup();
|
||||||
createLeadInserter(db)({
|
createLeadInserter(db)({
|
||||||
|
|||||||
Reference in New Issue
Block a user