Baseline commit of the existing Maquina Comercial Pessoal project

Tracks the app that was already in place (capture, treatment, outreach,
WhatsApp integration, server, frontend, tests, docs) so future feature
work can branch and use isolated worktrees.
This commit is contained in:
Gustavo
2026-07-31 10:54:07 -03:00
parent 6cd336a41e
commit d330bb5ff0
132 changed files with 29969 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { generateDailySchedule } from '../src/outreach/campaign-schedule.mjs';
const WINDOWS = [
{ start: '09:30', end: '11:30' },
{ start: '14:00', end: '17:00' }
];
const MIN_MS = 12 * 60 * 1000;
const MAX_MS = 25 * 60 * 1000;
function schedule(overrides = {}) {
return generateDailySchedule({
date: '2026-07-27',
count: 18,
windows: WINDOWS,
minMs: MIN_MS,
maxMs: MAX_MS,
hourlyLimit: 4,
random: () => 0,
...overrides
});
}
test('gera somente em dia util e preserva a data civil local', () => {
assert.deepEqual(schedule({ date: '2026-07-25' }), []);
assert.deepEqual(schedule({ date: '2026-07-26' }), []);
const slots = schedule({ date: '2026-07-27', count: 2 });
assert.equal(slots.length, 2);
assert.deepEqual(slots.map((slot) => [
slot.getFullYear(), slot.getMonth() + 1, slot.getDate(), slot.getHours(), slot.getMinutes()
]), [
[2026, 7, 27, 9, 30],
[2026, 7, 27, 9, 42]
]);
});
test('usa random injetado para espacamento inclusivo entre 12 e 25 minutos', () => {
const minimum = schedule({ count: 3, random: () => 0 });
assert.deepEqual(minimum.map((slot) => `${slot.getHours()}:${String(slot.getMinutes()).padStart(2, '0')}`), [
'9:30', '9:42', '9:54'
]);
const maximum = schedule({ count: 3, random: () => 1 });
assert.deepEqual(maximum.map((slot) => `${slot.getHours()}:${String(slot.getMinutes()).padStart(2, '0')}`), [
'9:30', '9:55', '10:20'
]);
});
test('respeita no maximo quatro slots em qualquer hora rolante, incluindo o limite exato', () => {
const slots = schedule({ count: 8, minMs: 1, maxMs: 1 });
assert.deepEqual(slots.slice(0, 5).map((slot) => slot.getTime() - slots[0].getTime()), [
0, 1, 2, 3, 60 * 60 * 1000
]);
for (let index = 4; index < slots.length; index += 1) {
assert.ok(slots[index].getTime() - slots[index - 4].getTime() >= 60 * 60 * 1000);
}
});
test('nao agenda durante almoco, fora das janelas ou em outra data', () => {
const slots = schedule({ count: 18 });
assert.ok(slots.length > 0);
for (const slot of slots) {
const minutes = slot.getHours() * 60 + slot.getMinutes();
assert.equal(slot.getDate(), 27);
assert.ok((minutes >= 570 && minutes <= 690) || (minutes >= 840 && minutes <= 1020));
assert.ok(minutes <= 690 || minutes >= 840);
}
});
test('limita a 18 e retorna menos quando as janelas nao comportam a quantidade', () => {
assert.equal(schedule({ count: 99 }).length, 18);
const short = schedule({
count: 18,
windows: [{ start: '09:30', end: '09:50' }],
minMs: 12 * 60 * 1000,
maxMs: 12 * 60 * 1000
});
assert.deepEqual(short.map((slot) => [slot.getHours(), slot.getMinutes()]), [[9, 30], [9, 42]]);
});
test('rejeita parametros inseguros sem produzir datas invalidas', () => {
assert.throws(() => schedule({ date: '2026-02-30' }), /data/i);
assert.throws(() => schedule({ windows: [{ start: '11:30', end: '09:30' }] }), /janela/i);
assert.throws(() => schedule({ random: () => 1.1 }), /random/i);
});