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,476 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { openDb, setSetting } from '../src/db.mjs';
|
||||
import { listCampaigns, saveAppointment, seedCampaigns, setLeadCommercialStage } from '../src/outreach/campaign-store.mjs';
|
||||
import { createRouter } from '../src/server/router.mjs';
|
||||
import { registerCampaignRoutes } from '../src/server/routes/campaigns.mjs';
|
||||
|
||||
function setup({
|
||||
state = 'READY',
|
||||
now = new Date(2026, 6, 29, 12),
|
||||
random = () => 0
|
||||
} = {}) {
|
||||
const db = openDb(':memory:');
|
||||
seedCampaigns(db);
|
||||
setSetting(db, 'outreach.mensagensPorDia', '7');
|
||||
let checks = 0;
|
||||
let sends = 0;
|
||||
const whatsapp = {
|
||||
getStatus: () => ({ state, connected: state === 'READY' }),
|
||||
checkNumber: async (number) => {
|
||||
checks += 1;
|
||||
return { registered: true, numero: number, numero_resolvido: number };
|
||||
},
|
||||
sendText: async () => {
|
||||
sends += 1;
|
||||
throw new Error('rota de campanha nao pode enviar mensagens');
|
||||
}
|
||||
};
|
||||
const router = createRouter();
|
||||
registerCampaignRoutes(router, { db, whatsapp, random, now: () => now });
|
||||
return { db, router, whatsapp, counts: () => ({ checks, sends }) };
|
||||
}
|
||||
|
||||
async function invoke(router, method, pathname, {
|
||||
body,
|
||||
user = { role: 'admin' },
|
||||
query = new URLSearchParams()
|
||||
} = {}) {
|
||||
const matched = router.match(method, pathname);
|
||||
assert.ok(matched, `rota ausente: ${method} ${pathname}`);
|
||||
const calls = [];
|
||||
await matched.handler(null, null, {
|
||||
params: matched.params,
|
||||
body,
|
||||
user,
|
||||
query,
|
||||
json: (status, data) => calls.push({ status, data })
|
||||
});
|
||||
assert.equal(calls.length, 1);
|
||||
return calls[0];
|
||||
}
|
||||
|
||||
function campaignIds(db) {
|
||||
return db.prepare('SELECT id FROM outreach_campaigns ORDER BY id').all().map((row) => row.id);
|
||||
}
|
||||
|
||||
function addPreview(db, campaignId, suffix) {
|
||||
const campaign = db.prepare('SELECT produto FROM outreach_campaigns WHERE id = ?').get(campaignId);
|
||||
const variant = db.prepare('SELECT id FROM outreach_campaign_variants WHERE campaign_id = ? ORDER BY code LIMIT 1').get(campaignId);
|
||||
const lead = db.prepare(
|
||||
`INSERT INTO leads (dedup_key, empresa, whatsapp, segmento, prioridade, score)
|
||||
VALUES (?, ?, ?, 'restaurantes', 1, 90)`
|
||||
).run(`lead-${campaignId}-${suffix}`, `Empresa ${suffix}`, `551199999${String(suffix).padStart(4, '0')}`);
|
||||
return Number(db.prepare(
|
||||
`INSERT INTO outreach_campaign_queue
|
||||
(campaign_id, lead_id, variant_id, produto, numero, segmento, rendered_message, selection_reason, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'restaurantes', 'mensagem aprovada', 'teste', 'preview')`
|
||||
).run(campaignId, Number(lead.lastInsertRowid), variant.id, campaign.produto, `551199999${String(suffix).padStart(4, '0')}`).lastInsertRowid);
|
||||
}
|
||||
|
||||
function schedulePreview(db, queueId, hour = 9, minute = 30) {
|
||||
const scheduledAt = new Date(2026, 6, 29, hour, minute).toISOString();
|
||||
db.prepare("UPDATE outreach_campaign_queue SET status = 'scheduled', scheduled_at = ? WHERE id = ?")
|
||||
.run(scheduledAt, queueId);
|
||||
}
|
||||
|
||||
test('lista, detalha e filtra fila sem misturar uso manual e automatico', async () => {
|
||||
const { db, router } = setup();
|
||||
const [campaignId] = campaignIds(db);
|
||||
addPreview(db, campaignId, 1);
|
||||
db.prepare(
|
||||
`INSERT INTO outreach_sends (produto, numero, mensagem_enviada, status, origem)
|
||||
VALUES ('G-Delivery', '5511999999001', 'manual', 'aceito', 'manual')`
|
||||
).run();
|
||||
for (const [index, status] of ['pendente', 'enviado', 'entregue', 'lido', 'desconhecido', 'erro'].entries()) {
|
||||
db.prepare(
|
||||
`INSERT INTO outreach_sends (produto, numero, mensagem_enviada, status, origem)
|
||||
VALUES ('G-Delivery', ?, 'manual', ?, 'manual')`
|
||||
).run(`55118888000${index}`, status);
|
||||
}
|
||||
const automaticSendId = Number(db.prepare(
|
||||
`INSERT INTO outreach_sends (produto, numero, mensagem_enviada, status, origem)
|
||||
VALUES ('G-Delivery', '5511999999002', 'campanha', 'aceito', 'automatico')`
|
||||
).run().lastInsertRowid);
|
||||
const queued = addPreview(db, campaignId, 2);
|
||||
schedulePreview(db, queued);
|
||||
db.prepare("UPDATE outreach_campaign_queue SET status = 'reserved' WHERE id = ?").run(queued);
|
||||
db.prepare("UPDATE outreach_campaign_queue SET status = 'sent', outreach_send_id = ? WHERE id = ?")
|
||||
.run(automaticSendId, queued);
|
||||
|
||||
const listed = await invoke(router, 'GET', '/api/campaigns', { user: { role: 'comercial' } });
|
||||
assert.equal(listed.status, 200);
|
||||
assert.equal(listed.data.campaigns.length, 3);
|
||||
const campaign = listed.data.campaigns.find((item) => item.id === campaignId);
|
||||
assert.deepEqual(campaign.usage, {
|
||||
manual: { limit: 7, used: 6, remaining: 1 },
|
||||
campaign: { limit: 10, used: 1, remaining: 9 }
|
||||
});
|
||||
|
||||
const detail = await invoke(router, 'GET', `/api/campaigns/${campaignId}`, { user: { role: 'comercial' } });
|
||||
assert.equal(detail.data.campaign.id, campaignId);
|
||||
assert.deepEqual(detail.data.usage, campaign.usage);
|
||||
const queue = await invoke(router, 'GET', `/api/campaigns/${campaignId}/queue`, {
|
||||
user: { role: 'comercial' }, query: new URLSearchParams({ status: 'preview' })
|
||||
});
|
||||
assert.deepEqual(queue.data.queue.map((row) => row.status), ['preview']);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('todas as mutacoes rejeitam usuario nao-admin antes de alterar estado', async () => {
|
||||
const { db, router } = setup();
|
||||
const [campaignId] = campaignIds(db);
|
||||
const queueId = addPreview(db, campaignId, 3);
|
||||
const requests = [
|
||||
['POST', `/api/campaigns/${campaignId}/prepare`, {}],
|
||||
['POST', `/api/campaigns/${campaignId}/approve`, { queueIds: [queueId], date: '2026-07-27' }],
|
||||
['DELETE', `/api/campaigns/${campaignId}/queue/${queueId}`, undefined],
|
||||
['POST', `/api/campaigns/${campaignId}/start`, {}],
|
||||
['POST', `/api/campaigns/${campaignId}/pause`, { reason: 'revisao' }],
|
||||
['PUT', `/api/campaigns/${campaignId}/week`, { week: 2 }]
|
||||
];
|
||||
for (const [method, path, body] of requests) {
|
||||
const response = await invoke(router, method, path, { body, user: { role: 'comercial' } });
|
||||
assert.equal(response.status, 403, `${method} ${path}`);
|
||||
}
|
||||
assert.equal(db.prepare('SELECT status FROM outreach_campaign_queue WHERE id = ?').get(queueId).status, 'preview');
|
||||
assert.equal(db.prepare('SELECT week_number FROM outreach_campaigns WHERE id = ?').get(campaignId).week_number, 1);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('preparacao usa verificacao gradual e nunca chama envio', async () => {
|
||||
const { db, router, counts } = setup();
|
||||
const [campaignId] = campaignIds(db);
|
||||
db.prepare(
|
||||
`INSERT INTO leads (dedup_key, empresa, whatsapp, segmento, prioridade, score)
|
||||
VALUES ('eligible', 'Hamburgueria Sol', '5511999990001', 'hamburguerias', 1, 100)`
|
||||
).run();
|
||||
const response = await invoke(router, 'POST', `/api/campaigns/${campaignId}/prepare`, { body: {} });
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.data.summary.previewed, 1);
|
||||
assert.deepEqual(counts(), { checks: 1, sends: 0 });
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('aprovacao exige IDs explicitos da propria campanha e faz transicao atomica', async () => {
|
||||
const { db, router } = setup();
|
||||
const [campaignId, otherCampaignId] = campaignIds(db);
|
||||
const selected = addPreview(db, campaignId, 10);
|
||||
const removed = addPreview(db, campaignId, 11);
|
||||
const foreign = addPreview(db, otherCampaignId, 12);
|
||||
const originalPayload = db.prepare(
|
||||
'SELECT variant_id, rendered_message FROM outreach_campaign_queue WHERE id = ?'
|
||||
).get(selected);
|
||||
|
||||
const missing = await invoke(router, 'POST', `/api/campaigns/${campaignId}/approve`, {
|
||||
body: { date: '2026-07-27' }
|
||||
});
|
||||
assert.equal(missing.status, 400);
|
||||
|
||||
const mixed = await invoke(router, 'POST', `/api/campaigns/${campaignId}/approve`, {
|
||||
body: { queueIds: [selected, foreign], date: '2026-07-27' }
|
||||
});
|
||||
assert.equal(mixed.status, 409);
|
||||
assert.deepEqual(db.prepare('SELECT status FROM outreach_campaign_queue WHERE id IN (?, ?) ORDER BY id').all(selected, removed).map((row) => row.status), ['preview', 'preview']);
|
||||
|
||||
const approved = await invoke(router, 'POST', `/api/campaigns/${campaignId}/approve`, {
|
||||
body: { queueIds: [selected], date: '2026-07-27' }
|
||||
});
|
||||
assert.equal(approved.status, 200);
|
||||
assert.equal(approved.data.scheduled, 1);
|
||||
const rows = db.prepare(
|
||||
'SELECT id, status, variant_id, rendered_message, scheduled_at, cancel_reason FROM outreach_campaign_queue WHERE campaign_id = ? ORDER BY id'
|
||||
).all(campaignId);
|
||||
assert.equal(rows[0].status, 'scheduled');
|
||||
assert.match(rows[0].scheduled_at, /^2026-07-27T/);
|
||||
assert.equal(rows[0].variant_id, originalPayload.variant_id);
|
||||
assert.equal(rows[0].rendered_message, originalPayload.rendered_message);
|
||||
const { variant_id: ignoredVariant, rendered_message: ignoredMessage, ...removedRow } = rows[1];
|
||||
void ignoredVariant;
|
||||
void ignoredMessage;
|
||||
assert.deepEqual({ ...removedRow }, { id: removed, status: 'cancelled', scheduled_at: null, cancel_reason: 'removed_by_operator' });
|
||||
assert.equal(db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campaignId).status, 'ready');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('aprovacao reverte tudo quando a data ou janelas nao fornecem slots suficientes', async () => {
|
||||
const { db, router } = setup();
|
||||
const [campaignId] = campaignIds(db);
|
||||
const first = addPreview(db, campaignId, 20);
|
||||
const second = addPreview(db, campaignId, 21);
|
||||
db.prepare("UPDATE outreach_campaigns SET windows_json = '[{\"start\":\"09:30\",\"end\":\"09:30\"}]' WHERE id = ?").run(campaignId);
|
||||
const response = await invoke(router, 'POST', `/api/campaigns/${campaignId}/approve`, {
|
||||
body: { queueIds: [first, second], date: '2026-07-27' }
|
||||
});
|
||||
assert.equal(response.status, 409);
|
||||
assert.deepEqual(db.prepare('SELECT status FROM outreach_campaign_queue WHERE campaign_id = ? ORDER BY id').all(campaignId).map((row) => row.status), ['preview', 'preview']);
|
||||
assert.equal(db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campaignId).status, 'paused');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('aprovacao rejeita excesso somando fila ja ativa no mesmo dia local', async () => {
|
||||
const { db, router } = setup();
|
||||
const [campaignId] = campaignIds(db);
|
||||
const existing = Array.from({ length: 10 }, (_, index) => addPreview(db, campaignId, 100 + index));
|
||||
existing.forEach((queueId, index) => schedulePreview(db, queueId, 9, 30 + index));
|
||||
const selected = addPreview(db, campaignId, 111);
|
||||
|
||||
const response = await invoke(router, 'POST', `/api/campaigns/${campaignId}/approve`, {
|
||||
body: { queueIds: [selected], date: '2026-07-29' }
|
||||
});
|
||||
assert.equal(response.status, 409);
|
||||
assert.equal(db.prepare('SELECT status FROM outreach_campaign_queue WHERE id = ?').get(selected).status, 'preview');
|
||||
assert.equal(db.prepare("SELECT COUNT(*) total FROM outreach_campaign_queue WHERE campaign_id = ? AND status = 'scheduled'").get(campaignId).total, 10);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('remocao cancela somente item mutavel pertencente a campanha', async () => {
|
||||
const { db, router } = setup();
|
||||
const [campaignId, otherCampaignId] = campaignIds(db);
|
||||
const removable = addPreview(db, campaignId, 30);
|
||||
const foreign = addPreview(db, otherCampaignId, 31);
|
||||
const sent = addPreview(db, campaignId, 32);
|
||||
schedulePreview(db, sent);
|
||||
db.prepare("UPDATE outreach_campaign_queue SET status = 'reserved' WHERE id = ?").run(sent);
|
||||
db.prepare("UPDATE outreach_campaign_queue SET status = 'sent' WHERE id = ?").run(sent);
|
||||
|
||||
assert.equal((await invoke(router, 'DELETE', `/api/campaigns/${campaignId}/queue/${foreign}`)).status, 404);
|
||||
assert.equal((await invoke(router, 'DELETE', `/api/campaigns/${campaignId}/queue/${sent}`)).status, 409);
|
||||
assert.equal((await invoke(router, 'DELETE', `/api/campaigns/${campaignId}/queue/${removable}`)).status, 200);
|
||||
assert.deepEqual({ ...db.prepare('SELECT status, cancel_reason FROM outreach_campaign_queue WHERE id = ?').get(removable) }, {
|
||||
status: 'cancelled', cancel_reason: 'removed_by_operator'
|
||||
});
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('remocao de item agendado preserva timestamp imutavel de auditoria', async () => {
|
||||
const { db, router } = setup();
|
||||
const [campaignId] = campaignIds(db);
|
||||
const queueId = addPreview(db, campaignId, 33);
|
||||
schedulePreview(db, queueId);
|
||||
const before = db.prepare('SELECT scheduled_at FROM outreach_campaign_queue WHERE id = ?').get(queueId).scheduled_at;
|
||||
|
||||
const response = await invoke(router, 'DELETE', `/api/campaigns/${campaignId}/queue/${queueId}`);
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual({ ...db.prepare('SELECT status, scheduled_at, cancel_reason FROM outreach_campaign_queue WHERE id = ?').get(queueId) }, {
|
||||
status: 'cancelled', scheduled_at: before, cancel_reason: 'removed_by_operator'
|
||||
});
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('inicio exige fila agendada, WhatsApp READY e estado ready ou paused', async () => {
|
||||
const noSchedule = setup();
|
||||
const [campaignId] = campaignIds(noSchedule.db);
|
||||
assert.equal((await invoke(noSchedule.router, 'POST', `/api/campaigns/${campaignId}/start`, { body: {} })).status, 409);
|
||||
noSchedule.db.close();
|
||||
|
||||
const disconnected = setup({ state: 'DISCONNECTED' });
|
||||
const [disconnectedId] = campaignIds(disconnected.db);
|
||||
const queueId = addPreview(disconnected.db, disconnectedId, 40);
|
||||
disconnected.db.prepare("UPDATE outreach_campaign_queue SET status = 'scheduled', scheduled_at = '2026-07-27T09:30:00.000' WHERE id = ?").run(queueId);
|
||||
assert.equal((await invoke(disconnected.router, 'POST', `/api/campaigns/${disconnectedId}/start`, { body: {} })).status, 409);
|
||||
assert.equal(disconnected.db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(disconnectedId).status, 'paused');
|
||||
disconnected.db.close();
|
||||
|
||||
const ready = setup();
|
||||
const [readyId] = campaignIds(ready.db);
|
||||
const scheduled = addPreview(ready.db, readyId, 41);
|
||||
ready.db.prepare("UPDATE outreach_campaign_queue SET status = 'scheduled', scheduled_at = '2026-07-27T09:30:00.000' WHERE id = ?").run(scheduled);
|
||||
const started = await invoke(ready.router, 'POST', `/api/campaigns/${readyId}/start`, { body: {} });
|
||||
assert.equal(started.status, 200);
|
||||
assert.equal(started.data.campaign.status, 'running');
|
||||
ready.db.close();
|
||||
});
|
||||
|
||||
test('inicio rejeita dia local com tentativas ativas acima do limite aprovado', async () => {
|
||||
const { db, router } = setup();
|
||||
const [campaignId] = campaignIds(db);
|
||||
const queueIds = Array.from({ length: 11 }, (_, index) => addPreview(db, campaignId, 200 + index));
|
||||
queueIds.forEach((queueId, index) => schedulePreview(db, queueId, 9, 30 + index));
|
||||
const response = await invoke(router, 'POST', `/api/campaigns/${campaignId}/start`, { body: {} });
|
||||
assert.equal(response.status, 409);
|
||||
assert.equal(db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campaignId).status, 'paused');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('reducao de semana rejeita limite abaixo da fila ativa do dia sem alterar controles', async () => {
|
||||
const { db, router } = setup();
|
||||
const [campaignId] = campaignIds(db);
|
||||
db.prepare('UPDATE outreach_campaigns SET week_number = 4, approved_daily_limit = 18 WHERE id = ?').run(campaignId);
|
||||
const queueIds = Array.from({ length: 11 }, (_, index) => addPreview(db, campaignId, 300 + index));
|
||||
queueIds.forEach((queueId, index) => schedulePreview(db, queueId, 9, 30 + index));
|
||||
|
||||
const response = await invoke(router, 'PUT', `/api/campaigns/${campaignId}/week`, { body: { week: 1 } });
|
||||
assert.equal(response.status, 409);
|
||||
assert.deepEqual({ ...db.prepare('SELECT week_number, approved_daily_limit FROM outreach_campaigns WHERE id = ?').get(campaignId) }, {
|
||||
week_number: 4, approved_daily_limit: 18
|
||||
});
|
||||
assert.equal(db.prepare("SELECT COUNT(*) total FROM outreach_campaign_queue WHERE campaign_id = ? AND status = 'scheduled'").get(campaignId).total, 11);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('pausa exige motivo e semana muda somente pelo mapa explicito', async () => {
|
||||
const { db, router } = setup();
|
||||
const [campaignId] = campaignIds(db);
|
||||
assert.equal((await invoke(router, 'POST', `/api/campaigns/${campaignId}/pause`, { body: {} })).status, 400);
|
||||
const paused = await invoke(router, 'POST', `/api/campaigns/${campaignId}/pause`, { body: { reason: 'revisao diaria' } });
|
||||
assert.equal(paused.status, 200);
|
||||
assert.equal(paused.data.campaign.pause_reason, 'revisao diaria');
|
||||
|
||||
assert.equal((await invoke(router, 'PUT', `/api/campaigns/${campaignId}/week`, { body: {} })).status, 400);
|
||||
assert.equal(db.prepare('SELECT week_number FROM outreach_campaigns WHERE id = ?').get(campaignId).week_number, 1);
|
||||
for (const [week, limit] of [[1, 10], [2, 12], [3, 15], [4, 18]]) {
|
||||
const changed = await invoke(router, 'PUT', `/api/campaigns/${campaignId}/week`, { body: { week } });
|
||||
assert.equal(changed.status, 200);
|
||||
assert.equal(changed.data.campaign.week_number, week);
|
||||
assert.equal(changed.data.campaign.approved_daily_limit, limit);
|
||||
}
|
||||
assert.equal((await invoke(router, 'PUT', `/api/campaigns/${campaignId}/week`, { body: { week: 5 } })).status, 400);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('pausa aceita somente ready, running ou paused e preserva estados terminais', async () => {
|
||||
const { db, router } = setup();
|
||||
const [, draftId, completedId] = campaignIds(db);
|
||||
db.prepare("UPDATE outreach_campaigns SET status = 'draft' WHERE id = ?").run(draftId);
|
||||
db.prepare("UPDATE outreach_campaigns SET status = 'completed' WHERE id = ?").run(completedId);
|
||||
for (const campaignId of [draftId, completedId]) {
|
||||
const response = await invoke(router, 'POST', `/api/campaigns/${campaignId}/pause`, {
|
||||
body: { reason: 'nao deve alterar' }
|
||||
});
|
||||
assert.equal(response.status, 409);
|
||||
}
|
||||
assert.equal(db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(draftId).status, 'draft');
|
||||
assert.equal(db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(completedId).status, 'completed');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('falha operacional retorna 500 generico sem vazar mensagem SQLite', async () => {
|
||||
const { db, router } = setup();
|
||||
const [campaignId] = campaignIds(db);
|
||||
db.exec(`
|
||||
CREATE TRIGGER campaign_operational_failure
|
||||
BEFORE UPDATE ON outreach_campaigns
|
||||
BEGIN SELECT RAISE(FAIL, 'sqlite-secret-detail'); END;
|
||||
`);
|
||||
const response = await invoke(router, 'PUT', `/api/campaigns/${campaignId}/week`, { body: { week: 2 } });
|
||||
assert.deepEqual(response, { status: 500, data: { erro: 'falha interna ao atualizar campanha' } });
|
||||
assert.doesNotMatch(JSON.stringify(response), /sqlite-secret-detail/i);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('aprovacao separa validacao conhecida de falha inesperada do agendador', async () => {
|
||||
const invalid = setup();
|
||||
const [invalidCampaignId] = campaignIds(invalid.db);
|
||||
const invalidQueueId = addPreview(invalid.db, invalidCampaignId, 500);
|
||||
const validation = await invoke(invalid.router, 'POST', `/api/campaigns/${invalidCampaignId}/approve`, {
|
||||
body: { queueIds: [invalidQueueId], date: '2026-02-30' }
|
||||
});
|
||||
assert.equal(validation.status, 400);
|
||||
assert.match(validation.data.erro, /data local invalida/i);
|
||||
invalid.db.close();
|
||||
|
||||
const secret = 'SCHEDULE-SENTINEL-SECRET';
|
||||
const unexpected = setup({ random: () => { throw new Error(secret); } });
|
||||
const [campaignId] = campaignIds(unexpected.db);
|
||||
const first = addPreview(unexpected.db, campaignId, 501);
|
||||
const second = addPreview(unexpected.db, campaignId, 502);
|
||||
const failure = await invoke(unexpected.router, 'POST', `/api/campaigns/${campaignId}/approve`, {
|
||||
body: { queueIds: [first, second], date: '2026-07-29' }
|
||||
});
|
||||
assert.deepEqual(failure, { status: 500, data: { erro: 'falha interna ao atualizar campanha' } });
|
||||
assert.doesNotMatch(JSON.stringify(failure), new RegExp(secret, 'i'));
|
||||
assert.deepEqual(unexpected.db.prepare('SELECT status FROM outreach_campaign_queue ORDER BY id').all().map((row) => row.status), ['preview', 'preview']);
|
||||
unexpected.db.close();
|
||||
});
|
||||
|
||||
test('GET /api/campaigns/replies/pending retorna lista de leads que responderam', async () => {
|
||||
const { db, router } = setup();
|
||||
const campaigns = listCampaigns(db);
|
||||
const leadId1 = Number(db.prepare(
|
||||
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-pending-1', 'Lead Respondeu', '5511999991001')"
|
||||
).run().lastInsertRowid);
|
||||
const leadId2 = Number(db.prepare(
|
||||
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-pending-2', 'Lead Avancado', '5511999991002')"
|
||||
).run().lastInsertRowid);
|
||||
setLeadCommercialStage(db, { leadId: leadId1, campaignId: campaigns[0].id, stage: 'respondeu', note: 'quer info' });
|
||||
setLeadCommercialStage(db, { leadId: leadId2, campaignId: campaigns[0].id, stage: 'enviado' });
|
||||
|
||||
const response = await invoke(router, 'GET', '/api/campaigns/replies/pending');
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.data.pending.length, 1);
|
||||
assert.equal(response.data.pending[0].lead_id, leadId1);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('PUT /api/campaigns/leads/:leadId/stage atualiza etapa comercial', async () => {
|
||||
const { db, router } = setup();
|
||||
const leadId = Number(db.prepare(
|
||||
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-stage-1', 'Lead Stage', '5511999991010')"
|
||||
).run().lastInsertRowid);
|
||||
|
||||
const response = await invoke(router, 'PUT', `/api/campaigns/leads/${leadId}/stage`, {
|
||||
body: { stage: 'interessado_gti_vendas', note: 'demonstracao marcada' }
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.data.state.stage, 'interessado_gti_vendas');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('PUT /api/campaigns/leads/:leadId/stage rejeita etapa invalida', async () => {
|
||||
const { db, router } = setup();
|
||||
const leadId = Number(db.prepare(
|
||||
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-stage-2', 'Lead Inv', '5511999991020')"
|
||||
).run().lastInsertRowid);
|
||||
|
||||
const response = await invoke(router, 'PUT', `/api/campaigns/leads/${leadId}/stage`, {
|
||||
body: { stage: 'etapa_inexistente' }
|
||||
});
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(JSON.stringify(response.data), /etapa comercial invalida/i);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('POST /api/campaigns/leads/:leadId/appointments cria agendamento', async () => {
|
||||
const { db, router } = setup();
|
||||
const campaigns = listCampaigns(db);
|
||||
const leadId = Number(db.prepare(
|
||||
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-app-1', 'Lead App', '5511999991030')"
|
||||
).run().lastInsertRowid);
|
||||
|
||||
const response = await invoke(router, 'POST', `/api/campaigns/leads/${leadId}/appointments`, {
|
||||
body: {
|
||||
campaignId: campaigns[0].id,
|
||||
kind: 'demonstracao',
|
||||
participantName: 'Maria',
|
||||
companyName: 'Empresa App',
|
||||
preferredDay: '2026-08-10',
|
||||
preferredTime: '15:00'
|
||||
}
|
||||
});
|
||||
assert.equal(response.status, 201);
|
||||
assert.equal(response.data.appointment.kind, 'demonstracao');
|
||||
assert.equal(response.data.appointment.participant_name, 'Maria');
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('POST /api/campaigns/leads/:leadId/appointments rejeita demonstracao sem campos obrigatorios', async () => {
|
||||
const { db, router } = setup();
|
||||
const leadId = Number(db.prepare(
|
||||
"INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES ('route-app-2', 'Lead App Fail', '5511999991040')"
|
||||
).run().lastInsertRowid);
|
||||
|
||||
const response = await invoke(router, 'POST', `/api/campaigns/leads/${leadId}/appointments`, {
|
||||
body: { kind: 'demonstracao' }
|
||||
});
|
||||
assert.equal(response.status, 400);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('GET /api/campaigns/replies/pending funciona sem autenticacao de admin', async () => {
|
||||
const { db, router } = setup();
|
||||
const response = await invoke(router, 'GET', '/api/campaigns/replies/pending', { user: { role: 'comercial' } });
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(Array.isArray(response.data.pending));
|
||||
db.close();
|
||||
});
|
||||
Reference in New Issue
Block a user