This work already existed uncommitted in the main working tree (not authored by this branch) and is required as a runtime dependency for the campaign outreach port (zpro-client, templates, outreach routes/UI). Copied in as-is, untouched, so this feature branch has a working baseline; the original uncommitted state in the main checkout is left exactly as it was.
254 lines
8.9 KiB
JavaScript
254 lines
8.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { openDb, setSetting } from '../src/db.mjs';
|
|
import { createRouter } from '../src/server/router.mjs';
|
|
import { registerOutreachRoutes } from '../src/server/routes/outreach.mjs';
|
|
|
|
function setupRouter(db) {
|
|
const router = createRouter();
|
|
registerOutreachRoutes(router, { db });
|
|
return router;
|
|
}
|
|
|
|
function fakeJson() {
|
|
const calls = [];
|
|
const json = (status, data) => calls.push({ status, data });
|
|
return { json, calls };
|
|
}
|
|
|
|
function configurarCredenciais(db) {
|
|
setSetting(db, 'zpro.baseUrl', 'exemplo.zpro.com.br');
|
|
setSetting(db, 'zpro.apiId', 'abc123');
|
|
setSetting(db, 'zpro.bearerToken', 'token-teste');
|
|
}
|
|
|
|
test('POST /api/outreach/enviar-manual exige numero', async () => {
|
|
const db = openDb(':memory:');
|
|
const router = setupRouter(db);
|
|
const { handler } = router.match('POST', '/api/outreach/enviar-manual');
|
|
const { json, calls } = fakeJson();
|
|
|
|
await handler(null, null, { json, body: { numero: '', mensagem: 'oi' } });
|
|
|
|
assert.equal(calls[0].status, 400);
|
|
db.close();
|
|
});
|
|
|
|
test('POST /api/outreach/enviar-manual rejeita numero mal formatado antes de chamar o Z-PRO', async () => {
|
|
const db = openDb(':memory:');
|
|
configurarCredenciais(db);
|
|
const router = setupRouter(db);
|
|
const { handler } = router.match('POST', '/api/outreach/enviar-manual');
|
|
const { json, calls } = fakeJson();
|
|
|
|
const originalFetch = global.fetch;
|
|
let fetchChamado = false;
|
|
global.fetch = async () => { fetchChamado = true; return { ok: true, text: async () => '{}' }; };
|
|
try {
|
|
await handler(null, null, { json, body: { numero: '55759990917800', canal: 'nao_oficial', mensagem: 'oi' } });
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
|
|
assert.equal(calls[0].status, 400);
|
|
assert.match(calls[0].data.erro, /numero invalido/);
|
|
assert.equal(fetchChamado, false);
|
|
assert.equal(db.prepare('SELECT COUNT(*) c FROM outreach_sends').get().c, 0);
|
|
db.close();
|
|
});
|
|
|
|
test('POST /api/outreach/enviar-manual manda texto livre (nao-oficial) e registra origem=manual', async () => {
|
|
const db = openDb(':memory:');
|
|
configurarCredenciais(db);
|
|
const router = setupRouter(db);
|
|
const { handler } = router.match('POST', '/api/outreach/enviar-manual');
|
|
const { json, calls } = fakeJson();
|
|
|
|
const originalFetch = global.fetch;
|
|
global.fetch = async () => ({ ok: true, text: async () => JSON.stringify({ data: { ticketId: 999 } }) });
|
|
try {
|
|
await handler(null, null, { json, body: { numero: '5511999990001', canal: 'nao_oficial', mensagem: 'Oi, tudo bem?' } });
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
|
|
assert.deepEqual(calls[0], { status: 200, data: { ok: true, ticketId: '999' } });
|
|
const row = db.prepare('SELECT * FROM outreach_sends').get();
|
|
assert.equal(row.origem, 'manual');
|
|
assert.equal(row.lead_id, null);
|
|
assert.equal(row.numero, '5511999990001');
|
|
assert.equal(row.mensagem_enviada, 'Oi, tudo bem?');
|
|
assert.equal(row.canal, 'nao_oficial');
|
|
assert.equal(row.zpro_ticket_id, '999');
|
|
db.close();
|
|
});
|
|
|
|
test('POST /api/outreach/enviar-manual bloqueia quando limite diario acabou', async () => {
|
|
const db = openDb(':memory:');
|
|
configurarCredenciais(db);
|
|
setSetting(db, 'outreach.mensagensPorDia', '1');
|
|
db.prepare(
|
|
`INSERT INTO outreach_sends (lead_id, produto, numero, mensagem_enviada, status, canal, origem)
|
|
VALUES (NULL, '', '5511999990001', 'ja enviado', 'enviado', 'nao_oficial', 'manual')`
|
|
).run();
|
|
const router = setupRouter(db);
|
|
const { handler } = router.match('POST', '/api/outreach/enviar-manual');
|
|
const { json, calls } = fakeJson();
|
|
|
|
const originalFetch = global.fetch;
|
|
let fetchChamado = false;
|
|
global.fetch = async () => { fetchChamado = true; return { ok: true, text: async () => '{}' }; };
|
|
try {
|
|
await handler(null, null, { json, body: { numero: '5511999990002', canal: 'nao_oficial', mensagem: 'Oi' } });
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
|
|
assert.equal(calls[0].status, 429);
|
|
assert.equal(calls[0].data.ok, false);
|
|
assert.match(calls[0].data.erro, /limite diario atingido/);
|
|
assert.equal(fetchChamado, false);
|
|
assert.equal(db.prepare('SELECT COUNT(*) c FROM outreach_sends').get().c, 1);
|
|
db.close();
|
|
});
|
|
|
|
test('POST /api/outreach/enviar-manual canal oficial exige nome do template', async () => {
|
|
const db = openDb(':memory:');
|
|
configurarCredenciais(db);
|
|
const router = setupRouter(db);
|
|
const { handler } = router.match('POST', '/api/outreach/enviar-manual');
|
|
const { json, calls } = fakeJson();
|
|
|
|
await handler(null, null, { json, body: { numero: '5511999990001', canal: 'oficial' } });
|
|
|
|
assert.equal(calls[0].status, 400);
|
|
db.close();
|
|
});
|
|
|
|
test('POST /api/outreach/enviar-manual canal oficial envia multiplas variaveis do template', async () => {
|
|
const db = openDb(':memory:');
|
|
configurarCredenciais(db);
|
|
const router = setupRouter(db);
|
|
const { handler } = router.match('POST', '/api/outreach/enviar-manual');
|
|
const { json, calls } = fakeJson();
|
|
|
|
const originalFetch = global.fetch;
|
|
let payload;
|
|
global.fetch = async (url, options) => {
|
|
payload = JSON.parse(options.body);
|
|
return { ok: true, status: 200, text: async () => '{"success":true,"data":{"ticketId":321}}' };
|
|
};
|
|
try {
|
|
await handler(null, null, {
|
|
json,
|
|
body: {
|
|
numero: '5511999990001',
|
|
canal: 'oficial',
|
|
templateWabaNome: 'gti_inicio_atendimento_solicitado',
|
|
templateWabaIdioma: 'pt_BR',
|
|
templateVariaveis: ['Gustavo', 'Chamado #123', '']
|
|
}
|
|
});
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
|
|
assert.deepEqual(calls[0], { status: 200, data: { ok: true, ticketId: '321' } });
|
|
assert.deepEqual(payload.templateData.template.components, [
|
|
{ type: 'body', parameters: [{ type: 'text', text: 'Gustavo' }, { type: 'text', text: 'Chamado #123' }] }
|
|
]);
|
|
db.close();
|
|
});
|
|
|
|
test('POST /api/outreach/enviar-manual resolve variaveis internas do GChat', async () => {
|
|
const db = openDb(':memory:');
|
|
configurarCredenciais(db);
|
|
const router = setupRouter(db);
|
|
const { handler } = router.match('POST', '/api/outreach/enviar-manual');
|
|
const { json } = fakeJson();
|
|
|
|
const originalFetch = global.fetch;
|
|
let payload;
|
|
global.fetch = async (url, options) => {
|
|
payload = JSON.parse(options.body);
|
|
return { ok: true, status: 200, text: async () => '{"success":true,"data":{"ticketId":321}}' };
|
|
};
|
|
try {
|
|
await handler(null, null, {
|
|
json,
|
|
body: {
|
|
numero: '5511999990001',
|
|
canal: 'oficial',
|
|
templateWabaNome: 'gti_teste',
|
|
templateWabaIdioma: 'pt_BR',
|
|
templateVariaveis: ['{{name}}', '{{protocol}}', '{{phoneNumber}}']
|
|
}
|
|
});
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
|
|
assert.deepEqual(payload.templateData.template.components, [
|
|
{ type: 'body', parameters: [
|
|
{ type: 'text', text: 'Gustavo' },
|
|
{ type: 'text', text: 'Atendimento comercial' },
|
|
{ type: 'text', text: '5511999990001' }
|
|
] }
|
|
]);
|
|
db.close();
|
|
});
|
|
|
|
test('POST /api/outreach/enviar-manual registra erro quando o Z-PRO falha', async () => {
|
|
const db = openDb(':memory:');
|
|
configurarCredenciais(db);
|
|
const router = setupRouter(db);
|
|
const { handler } = router.match('POST', '/api/outreach/enviar-manual');
|
|
const { json, calls } = fakeJson();
|
|
|
|
const originalFetch = global.fetch;
|
|
global.fetch = async () => ({ ok: false, status: 400, text: async () => '{"erro":"numero nao existe no whatsapp"}' });
|
|
try {
|
|
await handler(null, null, { json, body: { numero: '5511999990001', canal: 'nao_oficial', mensagem: 'teste' } });
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
|
|
assert.equal(calls[0].data.ok, false);
|
|
const row = db.prepare('SELECT * FROM outreach_sends').get();
|
|
assert.equal(row.status, 'erro');
|
|
assert.match(row.erro, /numero nao existe/);
|
|
db.close();
|
|
});
|
|
|
|
test('GET /api/outreach/status inclui envios manuais em "ultimos" (LEFT JOIN, sem lead)', async () => {
|
|
const db = openDb(':memory:');
|
|
db.prepare(
|
|
`INSERT INTO outreach_sends (lead_id, produto, numero, mensagem_enviada, status, canal, origem)
|
|
VALUES (NULL, '', '5511999990001', 'oi', 'enviado', 'nao_oficial', 'manual')`
|
|
).run();
|
|
const router = setupRouter(db);
|
|
const { handler } = router.match('GET', '/api/outreach/status');
|
|
const { json, calls } = fakeJson();
|
|
|
|
await handler(null, null, { json });
|
|
|
|
const ultimo = calls[0].data.ultimos[0];
|
|
assert.equal(ultimo.origem, 'manual');
|
|
assert.equal(ultimo.numero, '5511999990001');
|
|
db.close();
|
|
});
|
|
|
|
test('GET /api/outreach/variables lista variaveis comerciais', async () => {
|
|
const db = openDb(':memory:');
|
|
const router = setupRouter(db);
|
|
const { handler } = router.match('GET', '/api/outreach/variables');
|
|
const { json, calls } = fakeJson();
|
|
|
|
await handler(null, null, { json });
|
|
|
|
assert.equal(calls[0].status, 200);
|
|
assert.ok(calls[0].data.variables.some((variable) => variable.key === 'empresa'));
|
|
assert.ok(calls[0].data.variables.some((variable) => variable.key === 'name'));
|
|
db.close();
|
|
});
|