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.
277 lines
11 KiB
JavaScript
277 lines
11 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { openDb, createLeadInserter, setSetting, seedMessageTemplates, updateMessageTemplate } from '../src/db.mjs';
|
|
import { renderTemplate } from '../src/outreach/templates.mjs';
|
|
import { runOutreachBatch } from '../src/outreach/run.mjs';
|
|
|
|
function baseLead(overrides) {
|
|
return {
|
|
legal_name: '', ddd: '', email: '', segmento: '', segmento_label: '',
|
|
interesse: '', cnpj: '', cnae: '', endereco: '', website: '', pains_json: '[]',
|
|
origem: 'cnpj-base', telefone_valido: 1, cnpj_valido: 0, empresa_valida: 1,
|
|
score: 80, classificacao: 'bom', motivo_classificacao: '', capture_run_id: null,
|
|
captured_at: null, exported_at: null, prioridade: 1,
|
|
produtos_json: JSON.stringify([{ nome: 'ERP', estrelas: 5 }, { nome: 'G-Chat', estrelas: 3 }]),
|
|
produto_principal: 'ERP',
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
// libera a checagem de horario comercial em qualquer hora que o teste rode
|
|
function ativarOutreach(db, extra = {}) {
|
|
setSetting(db, 'outreach.ativo', 'true');
|
|
setSetting(db, 'outreach.horaInicio', '00:00');
|
|
setSetting(db, 'outreach.horaFim', '23:59');
|
|
setSetting(db, 'outreach.mensagensPorDia', '1');
|
|
setSetting(db, 'outreach.tentarProximoProduto', 'false');
|
|
setSetting(db, 'outreach.diasEntreProdutos', '5');
|
|
for (const [key, value] of Object.entries(extra)) setSetting(db, key, value);
|
|
}
|
|
|
|
function fakeZproClient(overrides = {}) {
|
|
const calls = { sendText: [], bulkIndividual: [], addTagContact: [], createTag: [], sendTemplateWaba: [] };
|
|
return {
|
|
calls,
|
|
sendText: async (args) => {
|
|
calls.sendText.push(args);
|
|
return { data: { ticketId: 123 } };
|
|
},
|
|
bulkIndividual: async (args) => {
|
|
calls.sendText.push(args);
|
|
return { data: { ticketId: 123 } };
|
|
},
|
|
sendTemplateWaba: async (args) => {
|
|
calls.sendTemplateWaba.push(args);
|
|
return { data: { ticketId: 456 } };
|
|
},
|
|
listTags: async () => ({ data: [] }),
|
|
createTag: async (args) => {
|
|
calls.createTag.push(args);
|
|
return { id: calls.createTag.length };
|
|
},
|
|
addTagContact: async (args) => {
|
|
calls.addTagContact.push(args);
|
|
return {};
|
|
},
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
test('renderTemplate substitui {empresa}/{cidade}/{segmento}', () => {
|
|
const lead = { empresa: 'Padaria Sol', cidade: 'Ilheus', segmento_label: 'Padarias' };
|
|
const texto = renderTemplate('Ola {empresa} de {cidade} ({segmento})', lead);
|
|
assert.equal(texto, 'Ola Padaria Sol de Ilheus (Padarias)');
|
|
});
|
|
|
|
test('renderTemplate usa fallback quando o lead nao tem o campo', () => {
|
|
const texto = renderTemplate('Ola {empresa} de {cidade}', {});
|
|
assert.equal(texto, 'Ola sua empresa de sua regiao');
|
|
});
|
|
|
|
test('renderTemplate substitui {produto}', () => {
|
|
const texto = renderTemplate('Oferta: {produto}', { produto: 'ERP' });
|
|
assert.equal(texto, 'Oferta: ERP');
|
|
});
|
|
|
|
test('renderTemplate substitui variaveis internas do GChat', () => {
|
|
const texto = renderTemplate('{{greeting}} {{name}} {{protocol}} {{businessName}} {{phoneNumber}} {{user}}', {
|
|
empresa: 'Padaria Sol', produto: 'ERP', whatsapp: '5511999990001'
|
|
});
|
|
assert.equal(texto, 'Ola Padaria Sol ERP Padaria Sol 5511999990001 Grupo GTI');
|
|
});
|
|
|
|
test('runOutreachBatch nao faz nada quando outreach.ativo=false', async () => {
|
|
const db = openDb(':memory:');
|
|
const insert = createLeadInserter(db);
|
|
insert(baseLead({ lead_uid: 'A', dedup_key: 'k1', empresa: 'A', whatsapp: '5511999990001', cidade: 'Salvador', uf: 'BA' }));
|
|
seedMessageTemplates(db, ['ERP']);
|
|
setSetting(db, 'outreach.ativo', 'false');
|
|
|
|
const zproClient = fakeZproClient();
|
|
const resumo = await runOutreachBatch({ db, zproClient, log: () => {} });
|
|
|
|
assert.deepEqual(resumo, { tentados: 0, enviados: 0, erros: 0, pulados: 0 });
|
|
assert.equal(zproClient.calls.sendText.length, 0);
|
|
db.close();
|
|
});
|
|
|
|
test('runOutreachBatch manda o produto principal, registra o envio e aplica tags', async () => {
|
|
const db = openDb(':memory:');
|
|
const insert = createLeadInserter(db);
|
|
insert(baseLead({ lead_uid: 'A', dedup_key: 'k1', empresa: 'Padaria Sol', whatsapp: '5511999990001', cidade: 'Ilheus', uf: 'BA' }));
|
|
seedMessageTemplates(db, ['ERP', 'G-Chat']);
|
|
ativarOutreach(db);
|
|
|
|
const zproClient = fakeZproClient();
|
|
const resumo = await runOutreachBatch({ db, zproClient, log: () => {} });
|
|
|
|
assert.equal(resumo.enviados, 1);
|
|
assert.equal(resumo.erros, 0);
|
|
assert.equal(zproClient.calls.sendText.length, 1);
|
|
assert.equal(zproClient.calls.sendText[0].number, '5511999990001');
|
|
assert.match(zproClient.calls.sendText[0].message, /Padaria Sol/);
|
|
assert.equal(zproClient.calls.addTagContact.length, 1);
|
|
assert.deepEqual(zproClient.calls.addTagContact[0].number, '5511999990001');
|
|
|
|
const row = db.prepare("SELECT * FROM outreach_sends WHERE status = 'enviado'").get();
|
|
assert.equal(row.produto, 'ERP');
|
|
assert.equal(row.lead_id, 1);
|
|
|
|
db.close();
|
|
});
|
|
|
|
test('runOutreachBatch nao manda de novo o mesmo produto pro mesmo lead (mesmo com orcamento sobrando)', async () => {
|
|
const db = openDb(':memory:');
|
|
const insert = createLeadInserter(db);
|
|
insert(baseLead({ lead_uid: 'A', dedup_key: 'k1', empresa: 'A', whatsapp: '5511999990001', cidade: 'Salvador', uf: 'BA' }));
|
|
seedMessageTemplates(db, ['ERP']);
|
|
ativarOutreach(db);
|
|
|
|
// ja existe um envio 'enviado' pra esse lead+produto, simulando uma rodada anterior
|
|
// (sem chamar runOutreachBatch de novo, que dispararia o intervalo real de 3-8s).
|
|
db.prepare(
|
|
`INSERT INTO outreach_sends (lead_id, produto, mensagem_enviada, status) VALUES (1, 'ERP', 'msg', 'enviado')`
|
|
).run();
|
|
setSetting(db, 'outreach.mensagensPorDia', '10');
|
|
|
|
const zproClient = fakeZproClient();
|
|
const resumo = await runOutreachBatch({ db, zproClient, log: () => {} });
|
|
|
|
assert.equal(zproClient.calls.sendText.length, 0);
|
|
assert.equal(resumo.enviados, 0);
|
|
assert.equal(resumo.pulados, 1);
|
|
db.close();
|
|
});
|
|
|
|
test('runOutreachBatch ignora leads ja exportados', async () => {
|
|
const db = openDb(':memory:');
|
|
const insert = createLeadInserter(db);
|
|
insert(baseLead({
|
|
lead_uid: 'A', dedup_key: 'k1', empresa: 'A', whatsapp: '5511999990001', cidade: 'Salvador', uf: 'BA',
|
|
exported_at: new Date().toISOString()
|
|
}));
|
|
seedMessageTemplates(db, ['ERP']);
|
|
ativarOutreach(db);
|
|
|
|
const zproClient = fakeZproClient();
|
|
const resumo = await runOutreachBatch({ db, zproClient, log: () => {} });
|
|
|
|
assert.equal(resumo.enviados, 0);
|
|
assert.equal(zproClient.calls.sendText.length, 0);
|
|
db.close();
|
|
});
|
|
|
|
test('runOutreachBatch respeita o limite diario', async () => {
|
|
const db = openDb(':memory:');
|
|
const insert = createLeadInserter(db);
|
|
insert(baseLead({ lead_uid: 'A', dedup_key: 'k1', empresa: 'A', whatsapp: '5511999990001', cidade: 'Salvador', uf: 'BA' }));
|
|
seedMessageTemplates(db, ['ERP']);
|
|
ativarOutreach(db);
|
|
setSetting(db, 'outreach.mensagensPorDia', '0');
|
|
|
|
const zproClient = fakeZproClient();
|
|
const resumo = await runOutreachBatch({ db, zproClient, log: () => {} });
|
|
|
|
assert.deepEqual(resumo, { tentados: 0, enviados: 0, erros: 0, pulados: 0 });
|
|
db.close();
|
|
});
|
|
|
|
test('runOutreachBatch pula lead cujo template do produto esta inativo', async () => {
|
|
const db = openDb(':memory:');
|
|
const insert = createLeadInserter(db);
|
|
insert(baseLead({ lead_uid: 'A', dedup_key: 'k1', empresa: 'A', whatsapp: '5511999990001', cidade: 'Salvador', uf: 'BA' }));
|
|
seedMessageTemplates(db, ['ERP']);
|
|
updateMessageTemplate(db, 'ERP', { ativo: false });
|
|
ativarOutreach(db);
|
|
|
|
const zproClient = fakeZproClient();
|
|
const resumo = await runOutreachBatch({ db, zproClient, log: () => {} });
|
|
|
|
assert.equal(resumo.enviados, 0);
|
|
assert.equal(resumo.pulados, 1);
|
|
assert.equal(zproClient.calls.sendText.length, 0);
|
|
db.close();
|
|
});
|
|
|
|
test('runOutreachBatch registra erro e segue quando o Z-PRO falha', async () => {
|
|
const db = openDb(':memory:');
|
|
const insert = createLeadInserter(db);
|
|
insert(baseLead({ lead_uid: 'A', dedup_key: 'k1', empresa: 'A', whatsapp: '5511999990001', cidade: 'Salvador', uf: 'BA' }));
|
|
seedMessageTemplates(db, ['ERP']);
|
|
ativarOutreach(db);
|
|
|
|
const zproClient = fakeZproClient({
|
|
sendText: async () => {
|
|
throw new Error('falha simulada');
|
|
}
|
|
});
|
|
const resumo = await runOutreachBatch({ db, zproClient, log: () => {} });
|
|
|
|
assert.equal(resumo.erros, 1);
|
|
assert.equal(resumo.enviados, 0);
|
|
const row = db.prepare("SELECT * FROM outreach_sends WHERE status = 'erro'").get();
|
|
assert.match(row.erro, /falha simulada/);
|
|
db.close();
|
|
});
|
|
|
|
test('runOutreachBatch usa sendTemplateWaba (nao texto livre) quando o canal e oficial', async () => {
|
|
const db = openDb(':memory:');
|
|
const insert = createLeadInserter(db);
|
|
insert(baseLead({ lead_uid: 'A', dedup_key: 'k1', empresa: 'A', whatsapp: '5511999990001', cidade: 'Salvador', uf: 'BA' }));
|
|
seedMessageTemplates(db, ['ERP']);
|
|
updateMessageTemplate(db, 'ERP', {
|
|
canal: 'oficial',
|
|
templateWabaNome: 'promo_erp',
|
|
templateWabaIdioma: 'pt_BR',
|
|
templateWabaVariaveis: ['{empresa}', 'Assunto: {produto} em {cidade}']
|
|
});
|
|
ativarOutreach(db);
|
|
|
|
const zproClient = fakeZproClient();
|
|
const resumo = await runOutreachBatch({ db, zproClient, log: () => {} });
|
|
|
|
assert.equal(resumo.enviados, 1);
|
|
assert.equal(zproClient.calls.sendText.length, 0);
|
|
assert.equal(zproClient.calls.sendTemplateWaba.length, 1);
|
|
assert.deepEqual(zproClient.calls.sendTemplateWaba[0], {
|
|
number: '5511999990001', templateName: 'promo_erp', languageCode: 'pt_BR', variables: ['A', 'Assunto: ERP em Salvador']
|
|
});
|
|
|
|
const row = db.prepare("SELECT * FROM outreach_sends WHERE status = 'enviado'").get();
|
|
assert.equal(row.canal, 'oficial');
|
|
db.close();
|
|
});
|
|
|
|
test('runOutreachBatch pula lead quando o canal e oficial mas o template nao foi configurado', async () => {
|
|
const db = openDb(':memory:');
|
|
const insert = createLeadInserter(db);
|
|
insert(baseLead({ lead_uid: 'A', dedup_key: 'k1', empresa: 'A', whatsapp: '5511999990001', cidade: 'Salvador', uf: 'BA' }));
|
|
seedMessageTemplates(db, ['ERP']);
|
|
updateMessageTemplate(db, 'ERP', { canal: 'oficial' }); // sem templateWabaNome
|
|
ativarOutreach(db);
|
|
|
|
const zproClient = fakeZproClient();
|
|
const resumo = await runOutreachBatch({ db, zproClient, log: () => {} });
|
|
|
|
assert.equal(resumo.enviados, 0);
|
|
assert.equal(resumo.pulados, 1);
|
|
assert.equal(zproClient.calls.sendTemplateWaba.length, 0);
|
|
assert.equal(zproClient.calls.sendText.length, 0);
|
|
db.close();
|
|
});
|
|
|
|
test('runOutreachBatch grava canal=nao_oficial por padrao', async () => {
|
|
const db = openDb(':memory:');
|
|
const insert = createLeadInserter(db);
|
|
insert(baseLead({ lead_uid: 'A', dedup_key: 'k1', empresa: 'A', whatsapp: '5511999990001', cidade: 'Salvador', uf: 'BA' }));
|
|
seedMessageTemplates(db, ['ERP']);
|
|
ativarOutreach(db);
|
|
|
|
const zproClient = fakeZproClient();
|
|
await runOutreachBatch({ db, zproClient, log: () => {} });
|
|
|
|
const row = db.prepare("SELECT * FROM outreach_sends WHERE status = 'enviado'").get();
|
|
assert.equal(row.canal, 'nao_oficial');
|
|
db.close();
|
|
});
|