chore: import maquina-comercial-pessoal history via subtree
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: maquina-comercial-pessoal git-subtree-mainline:f5af038e19git-subtree-split:a9ef4d776d
This commit is contained in:
@@ -0,0 +1,573 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { openDb, addOptOut, createLeadInserter, setSetting } from '../src/db.mjs';
|
||||
import { createRouter } from '../src/server/router.mjs';
|
||||
import { registerOutreachRoutes } from '../src/server/routes/outreach.mjs';
|
||||
|
||||
function setup({ state = 'READY' } = {}) {
|
||||
const db = openDb(':memory:');
|
||||
setSetting(db, 'outreach.mensagensPorDia', '10');
|
||||
const sent = [];
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state, connected: state === 'READY' }),
|
||||
sendText: async (args) => {
|
||||
const context = await args.beforeSend?.();
|
||||
sent.push(args);
|
||||
const result = { messageId: 'local-123' };
|
||||
await args.afterSend?.(result, null, context);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
const router = createRouter();
|
||||
registerOutreachRoutes(router, { db, whatsapp });
|
||||
return { db, router, sent };
|
||||
}
|
||||
|
||||
async function invoke(router, method, pathname, { body, user = { role: 'comercial' } } = {}) {
|
||||
const calls = [];
|
||||
const matched = router.match(method, pathname);
|
||||
assert.ok(matched, `rota ausente: ${method} ${pathname}`);
|
||||
await matched.handler(null, null, {
|
||||
params: matched.params,
|
||||
body,
|
||||
user,
|
||||
json: (status, data) => calls.push({ status, data })
|
||||
});
|
||||
return calls[0];
|
||||
}
|
||||
|
||||
test('status de outreach informa conexao WhatsApp local', async () => {
|
||||
const { db, router } = setup();
|
||||
const response = await invoke(router, 'GET', '/api/outreach/status');
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.data.whatsapp.state, 'READY');
|
||||
assert.equal(response.data.configurado, true);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('resultado desconhecido conta no limite diario', async () => {
|
||||
const { db, router, sent } = setup();
|
||||
setSetting(db, 'outreach.mensagensPorDia', '1');
|
||||
db.prepare("INSERT INTO outreach_sends (produto, numero, mensagem_enviada, status, origem) VALUES ('ERP', '5511999990001', 'oi', 'desconhecido', 'manual')").run();
|
||||
const status = await invoke(router, 'GET', '/api/outreach/status');
|
||||
assert.equal(status.data.enviadosHoje, 1);
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5511999990002', mensagem: 'Oi' }
|
||||
});
|
||||
assert.equal(response.status, 429);
|
||||
assert.equal(sent.length, 0);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('envio manual exige READY, numero valido e mensagem', async () => {
|
||||
const disconnected = setup({ state: 'DISCONNECTED' });
|
||||
const response = await invoke(disconnected.router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5511999990001', mensagem: 'Oi' }
|
||||
});
|
||||
assert.equal(response.status, 409);
|
||||
disconnected.db.close();
|
||||
|
||||
const ready = setup();
|
||||
assert.equal((await invoke(ready.router, 'POST', '/api/outreach/enviar-manual', { body: { numero: '123', mensagem: 'Oi' } })).status, 400);
|
||||
assert.equal((await invoke(ready.router, 'POST', '/api/outreach/enviar-manual', { body: { numero: '5511999990001', mensagem: '' } })).status, 400);
|
||||
ready.db.close();
|
||||
});
|
||||
|
||||
test('envio manual renderiza lead, envia local e registra messageId generico', async () => {
|
||||
const { db, router, sent } = setup();
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: {
|
||||
numero: '+55 (11) 99999-0001',
|
||||
mensagem: 'Ola {empresa}, oferta de {produto} em {cidade}',
|
||||
lead: { empresa: 'Padaria Sol', produto_principal: 'ERP', cidade: 'Ilheus' }
|
||||
}
|
||||
});
|
||||
assert.deepEqual(response, { status: 202, data: { ok: true, status: 'aceito', messageId: 'local-123' } });
|
||||
assert.equal(sent[0].number, '5511999990001');
|
||||
assert.equal(sent[0].message, 'Ola Padaria Sol, oferta de ERP em Ilheus');
|
||||
const row = db.prepare('SELECT * FROM outreach_sends').get();
|
||||
assert.equal(row.provider_message_id, 'local-123');
|
||||
assert.equal(row.status, 'aceito');
|
||||
assert.equal(row.canal, 'local');
|
||||
assert.equal(row.origem, 'manual');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('envio manual bloqueia opt-out e limite diario', async () => {
|
||||
const opted = setup();
|
||||
addOptOut(opted.db, '5511999990001', 'nao contatar');
|
||||
const blocked = await invoke(opted.router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5511999990001', mensagem: 'Oi' }
|
||||
});
|
||||
assert.equal(blocked.status, 409);
|
||||
assert.equal(opted.sent.length, 0);
|
||||
opted.db.close();
|
||||
|
||||
const limited = setup();
|
||||
setSetting(limited.db, 'outreach.mensagensPorDia', '0');
|
||||
const response = await invoke(limited.router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5511999990001', mensagem: 'Oi' }
|
||||
});
|
||||
assert.equal(response.status, 429);
|
||||
limited.db.close();
|
||||
});
|
||||
|
||||
test('rotas opt-out listam e restringem mutacoes a admin', async () => {
|
||||
const { db, router } = setup();
|
||||
const denied = await invoke(router, 'POST', '/api/outreach/opt-outs', {
|
||||
body: { numero: '5511999990001' }, user: { role: 'comercial' }
|
||||
});
|
||||
assert.equal(denied.status, 403);
|
||||
|
||||
const created = await invoke(router, 'POST', '/api/outreach/opt-outs', {
|
||||
body: { numero: '5511999990001', motivo: 'solicitado' }, user: { role: 'admin' }
|
||||
});
|
||||
assert.equal(created.status, 201);
|
||||
const listed = await invoke(router, 'GET', '/api/outreach/opt-outs');
|
||||
assert.equal(listed.data.optOuts.length, 1);
|
||||
const removed = await invoke(router, 'DELETE', '/api/outreach/opt-outs/5511999990001', { user: { role: 'admin' } });
|
||||
assert.equal(removed.status, 200);
|
||||
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)({
|
||||
dedup_key: 'real-1', empresa: 'Empresa Real', whatsapp: '5511999990002', cidade: 'Santos',
|
||||
produto_principal: 'ERP', produtos_json: '[]', pains_json: '[]', origem: 'teste',
|
||||
telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim'
|
||||
});
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { leadId: 1, numero: '5511999990001', mensagem: 'Oi {empresa} de {cidade}' }
|
||||
});
|
||||
assert.equal(response.status, 202);
|
||||
assert.equal(sent[0].number, '5511999990001');
|
||||
assert.equal(sent[0].message, 'Oi Empresa Real de Santos');
|
||||
assert.equal(db.prepare('SELECT lead_id FROM outreach_sends').get().lead_id, 1);
|
||||
assert.equal(db.prepare('SELECT numero FROM outreach_sends').get().numero, '5511999990001');
|
||||
assert.equal(db.prepare('SELECT whatsapp FROM leads WHERE id = 1').get().whatsapp, '5511999990002');
|
||||
assert.equal((await invoke(router, 'POST', '/api/outreach/enviar-manual', { body: { leadId: 999, numero: '5511999990001', mensagem: 'Oi' } })).status, 404);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('envio manual rejeita leadId coercivel antes de cache, reserva ou envio', async () => {
|
||||
const { db, router, sent } = setup();
|
||||
createLeadInserter(db)({
|
||||
dedup_key: 'canonical-1', empresa: 'Empresa Real', whatsapp: '5511999990002', cidade: 'Santos',
|
||||
produto_principal: 'ERP', produtos_json: '[]', pains_json: '[]', origem: 'teste',
|
||||
telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1, score: 0, classificacao: 'ruim'
|
||||
});
|
||||
db.exec(`
|
||||
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
||||
BEGIN SELECT RAISE(FAIL, 'cache nao deveria ser tocado'); END;
|
||||
`);
|
||||
|
||||
for (const leadId of [true, '1', '01', '1e0', 1.5, 0, -1, NaN, 999]) {
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { leadId, numero: '5511999990001', mensagem: 'Oi' }
|
||||
});
|
||||
assert.deepEqual(response, { status: 404, data: { erro: 'lead nao encontrado' } });
|
||||
}
|
||||
|
||||
assert.equal(sent.length, 0);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_sends').get().total, 0);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) total FROM whatsapp_number_verifications').get().total, 0);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('envio manual persiste numero confirmado e devolve metadados resolvidos', async () => {
|
||||
const db = openDb(':memory:');
|
||||
setSetting(db, 'outreach.mensagensPorDia', '10');
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state: 'READY' }),
|
||||
sendText: async (args) => {
|
||||
const sendId = await args.beforeSend();
|
||||
const result = {
|
||||
messageId: 'id-1',
|
||||
numero: '5511888880000',
|
||||
status: 'erro_temporario',
|
||||
resolvedNumber: '557599091780',
|
||||
variant: 'sem_nono_digito'
|
||||
};
|
||||
await args.afterSend(result, null, sendId);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
const router = createRouter();
|
||||
registerOutreachRoutes(router, { db, whatsapp });
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5575999091780', mensagem: 'Oi' }
|
||||
});
|
||||
assert.deepEqual(response, {
|
||||
status: 202,
|
||||
data: {
|
||||
ok: true,
|
||||
status: 'aceito',
|
||||
messageId: 'id-1',
|
||||
resolvedNumber: '557599091780',
|
||||
variant: 'sem_nono_digito'
|
||||
}
|
||||
});
|
||||
assert.deepEqual(
|
||||
{ ...db.prepare('SELECT numero, status, numero_resolvido, variante FROM whatsapp_number_verifications').get() },
|
||||
{
|
||||
numero: '5575999091780',
|
||||
status: 'confirmado',
|
||||
numero_resolvido: '557599091780',
|
||||
variante: 'sem_nono_digito'
|
||||
}
|
||||
);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('numero nao cadastrado no envio retorna 422 e persiste verificacao', async () => {
|
||||
const db = openDb(':memory:');
|
||||
setSetting(db, 'outreach.mensagensPorDia', '10');
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state: 'READY' }),
|
||||
sendText: async () => {
|
||||
throw Object.assign(new Error('numero nao esta cadastrado no WhatsApp'), {
|
||||
numero: '5511888880000',
|
||||
status: 'nao_cadastrado',
|
||||
registered: false,
|
||||
resolvedNumber: null,
|
||||
variant: null
|
||||
});
|
||||
}
|
||||
};
|
||||
const router = createRouter();
|
||||
registerOutreachRoutes(router, { db, whatsapp });
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5511999990001', mensagem: 'Oi' }
|
||||
});
|
||||
assert.equal(response.status, 422);
|
||||
assert.deepEqual(
|
||||
{ ...db.prepare('SELECT numero, status FROM whatsapp_number_verifications').get() },
|
||||
{ numero: '5511999990001', status: 'nao_cadastrado' }
|
||||
);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_sends').get().total, 0);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('timeout de consulta no envio persiste erro temporario antes de retornar 504', async () => {
|
||||
const db = openDb(':memory:');
|
||||
setSetting(db, 'outreach.mensagensPorDia', '10');
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state: 'READY' }),
|
||||
sendText: async () => {
|
||||
throw Object.assign(new Error('consulta excedeu o prazo'), {
|
||||
statusCode: 504,
|
||||
code: 'WHATSAPP_LOOKUP_TIMEOUT',
|
||||
status: 'erro_temporario',
|
||||
registered: false,
|
||||
resolvedNumber: null,
|
||||
variant: null
|
||||
});
|
||||
}
|
||||
};
|
||||
const router = createRouter();
|
||||
registerOutreachRoutes(router, { db, whatsapp });
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5511999990001', mensagem: 'Oi' }
|
||||
});
|
||||
assert.equal(response.status, 504);
|
||||
assert.deepEqual(
|
||||
{ ...db.prepare('SELECT status, erro FROM whatsapp_number_verifications').get() },
|
||||
{ status: 'erro_temporario', erro: 'consulta excedeu o prazo' }
|
||||
);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('falha de cache no numero nao cadastrado preserva 422 e aviso local', async () => {
|
||||
const db = openDb(':memory:');
|
||||
setSetting(db, 'outreach.mensagensPorDia', '10');
|
||||
db.exec(`
|
||||
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
||||
BEGIN SELECT RAISE(FAIL, 'cache indisponivel'); END;
|
||||
`);
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state: 'READY' }),
|
||||
sendText: async () => {
|
||||
throw Object.assign(new Error('numero nao cadastrado'), {
|
||||
status: 'nao_cadastrado', registered: false, resolvedNumber: null, variant: null
|
||||
});
|
||||
}
|
||||
};
|
||||
const router = createRouter();
|
||||
registerOutreachRoutes(router, { db, whatsapp });
|
||||
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5511999990001', mensagem: 'Oi' }
|
||||
});
|
||||
|
||||
assert.equal(response.status, 422);
|
||||
assert.equal(response.data.ok, false);
|
||||
assert.match(response.data.erro, /nao cadastrado/);
|
||||
assert.match(response.data.aviso, /verificacao local/i);
|
||||
assert.doesNotMatch(response.data.aviso, /tente.*enviar/i);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_sends').get().total, 0);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('falha de cache no timeout de lookup preserva 504 e aviso local', async () => {
|
||||
const db = openDb(':memory:');
|
||||
setSetting(db, 'outreach.mensagensPorDia', '10');
|
||||
db.exec(`
|
||||
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
||||
BEGIN SELECT RAISE(FAIL, 'cache indisponivel'); END;
|
||||
`);
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state: 'READY' }),
|
||||
sendText: async () => {
|
||||
throw Object.assign(new Error('consulta excedeu o prazo'), {
|
||||
statusCode: 504, status: 'erro_temporario', registered: false, resolvedNumber: null, variant: null
|
||||
});
|
||||
}
|
||||
};
|
||||
const router = createRouter();
|
||||
registerOutreachRoutes(router, { db, whatsapp });
|
||||
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5511999990001', mensagem: 'Oi' }
|
||||
});
|
||||
|
||||
assert.equal(response.status, 504);
|
||||
assert.equal(response.data.ok, false);
|
||||
assert.match(response.data.erro, /prazo/);
|
||||
assert.match(response.data.aviso, /verificacao local/i);
|
||||
assert.doesNotMatch(response.data.aviso, /tente.*enviar/i);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_sends').get().total, 0);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('timeout apos reserva finaliza uma vez como desconhecido e orienta a nao reenviar', async () => {
|
||||
const db = openDb(':memory:');
|
||||
setSetting(db, 'outreach.mensagensPorDia', '10');
|
||||
db.exec(`
|
||||
CREATE TABLE outreach_send_update_audit (send_id INTEGER, status TEXT);
|
||||
CREATE TRIGGER audit_outreach_send_update AFTER UPDATE ON outreach_sends
|
||||
BEGIN
|
||||
INSERT INTO outreach_send_update_audit (send_id, status) VALUES (NEW.id, NEW.status);
|
||||
END;
|
||||
`);
|
||||
let callbackCalls = 0;
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state: 'READY' }),
|
||||
sendText: async (args) => {
|
||||
const sendId = await args.beforeSend();
|
||||
const error = Object.assign(new Error('envio excedeu o prazo'), {
|
||||
statusCode: 504,
|
||||
code: 'WHATSAPP_SEND_TIMEOUT',
|
||||
deliveryUnknown: true,
|
||||
numero: '5511888880000',
|
||||
status: 'erro_temporario',
|
||||
resolvedNumber: '557599091780',
|
||||
variant: 'sem_nono_digito'
|
||||
});
|
||||
callbackCalls += 1;
|
||||
await args.afterSend(null, error, sendId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const router = createRouter();
|
||||
registerOutreachRoutes(router, { db, whatsapp });
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5575999091780', mensagem: 'Oi' }
|
||||
});
|
||||
assert.equal(response.status, 202);
|
||||
assert.equal(response.data.status, 'resultado_desconhecido');
|
||||
assert.match(response.data.aviso, /nao reenvie/i);
|
||||
assert.equal(callbackCalls, 1);
|
||||
const row = db.prepare('SELECT status, erro FROM outreach_sends').get();
|
||||
assert.equal(row.status, 'desconhecido');
|
||||
assert.match(row.erro, /prazo/);
|
||||
assert.deepEqual(
|
||||
{ ...db.prepare('SELECT numero, status FROM whatsapp_number_verifications').get() },
|
||||
{ numero: '5575999091780', status: 'confirmado' }
|
||||
);
|
||||
assert.deepEqual(
|
||||
db.prepare('SELECT status FROM outreach_send_update_audit').all().map((audit) => audit.status),
|
||||
['desconhecido']
|
||||
);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('falha secundaria no cache preserva resposta aceita e orienta a nao reenviar', async () => {
|
||||
const db = openDb(':memory:');
|
||||
setSetting(db, 'outreach.mensagensPorDia', '10');
|
||||
db.exec(`
|
||||
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
||||
BEGIN SELECT RAISE(FAIL, 'cache indisponivel'); END;
|
||||
`);
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state: 'READY' }),
|
||||
sendText: async (args) => {
|
||||
const sendId = await args.beforeSend();
|
||||
const result = { messageId: 'aceita-1', resolvedNumber: '5511999990001', variant: 'exata' };
|
||||
await args.afterSend(result, null, sendId);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
const router = createRouter();
|
||||
registerOutreachRoutes(router, { db, whatsapp });
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5511999990001', mensagem: 'Oi' }
|
||||
});
|
||||
assert.equal(response.status, 202);
|
||||
assert.equal(response.data.status, 'aceito');
|
||||
assert.equal(response.data.messageId, 'aceita-1');
|
||||
assert.match(response.data.aviso, /verificacao local/i);
|
||||
assert.match(response.data.aviso, /nao reenvie/i);
|
||||
assert.equal(db.prepare('SELECT status FROM outreach_sends').get().status, 'aceito');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('falha secundaria no cache preserva resultado desconhecido e aviso de nao reenvio', async () => {
|
||||
const db = openDb(':memory:');
|
||||
setSetting(db, 'outreach.mensagensPorDia', '10');
|
||||
db.exec(`
|
||||
CREATE TRIGGER fail_verification_cache BEFORE INSERT ON whatsapp_number_verifications
|
||||
BEGIN SELECT RAISE(FAIL, 'cache indisponivel'); END;
|
||||
`);
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state: 'READY' }),
|
||||
sendText: async (args) => {
|
||||
const sendId = await args.beforeSend();
|
||||
const error = Object.assign(new Error('envio excedeu o prazo'), {
|
||||
deliveryUnknown: true,
|
||||
numero: '5511888880000',
|
||||
status: 'erro_temporario',
|
||||
resolvedNumber: '5511999990001',
|
||||
variant: 'exata'
|
||||
});
|
||||
await args.afterSend(null, error, sendId);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const router = createRouter();
|
||||
registerOutreachRoutes(router, { db, whatsapp });
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5511999990001', mensagem: 'Oi' }
|
||||
});
|
||||
assert.equal(response.status, 202);
|
||||
assert.equal(response.data.status, 'resultado_desconhecido');
|
||||
assert.match(response.data.aviso, /verificacao local/i);
|
||||
assert.match(response.data.aviso, /nao reenvie/i);
|
||||
assert.equal(db.prepare('SELECT status FROM outreach_sends').get().status, 'desconhecido');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('duas requisicoes manuais concorrentes respeitam limite dentro da fila', async () => {
|
||||
const { db, router } = setup();
|
||||
setSetting(db, 'outreach.mensagensPorDia', '1');
|
||||
const calls = [];
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state: 'READY' }),
|
||||
sendText: async (args) => { const context = await args.beforeSend(); const result = { messageId: 'id' }; await args.afterSend(result, null, context); return result; }
|
||||
};
|
||||
const concurrentRouter = createRouter();
|
||||
registerOutreachRoutes(concurrentRouter, { db, whatsapp });
|
||||
const responses = await Promise.all([
|
||||
invoke(concurrentRouter, 'POST', '/api/outreach/enviar-manual', { body: { numero: '5511999990001', mensagem: 'um' } }),
|
||||
invoke(concurrentRouter, 'POST', '/api/outreach/enviar-manual', { body: { numero: '5511999990002', mensagem: 'dois' } })
|
||||
]);
|
||||
calls.push(...responses.map((r) => r.status));
|
||||
assert.deepEqual(calls.sort(), [202, 429]);
|
||||
assert.equal(db.prepare("SELECT COUNT(*) total FROM outreach_sends WHERE status = 'aceito'").get().total, 1);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('aceite com falha de persistencia orienta a nao reenviar', async () => {
|
||||
const db = openDb(':memory:');
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state: 'READY' }),
|
||||
sendText: async () => {
|
||||
const error = new Error('banco indisponivel');
|
||||
error.deliverySucceeded = true;
|
||||
error.messageId = 'possibly-sent-1';
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const router = createRouter();
|
||||
registerOutreachRoutes(router, { db, whatsapp });
|
||||
const response = await invoke(router, 'POST', '/api/outreach/enviar-manual', {
|
||||
body: { numero: '5511999990001', mensagem: 'oi' }
|
||||
});
|
||||
assert.equal(response.status, 202);
|
||||
assert.equal(response.data.status, 'persistencia_incerta');
|
||||
assert.match(response.data.aviso, /nao reenvie/i);
|
||||
db.close();
|
||||
});
|
||||
Reference in New Issue
Block a user