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
+168
View File
@@ -0,0 +1,168 @@
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import { DatabaseSync } from 'node:sqlite';
import { openDb, setSetting } from '../../src/db.mjs';
import { hashPassword } from '../../src/auth.mjs';
import { createUser } from '../../src/db.mjs';
import { createAppServer } from '../../src/server/index.mjs';
import { seedCampaigns } from '../../src/outreach/campaign-store.mjs';
const DEBUG = process.env.DEBUG_SERVER === '1';
const log = DEBUG ? (m) => process.stderr.write(`[test-server] ${m}\n`) : () => {};
function randomHex(length) {
const bytes = new Uint8Array(Math.ceil(length / 2));
crypto.getRandomValues(bytes);
return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('').slice(0, length);
}
export async function startTestServer() {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-test-'));
const dbPath = path.join(tmpDir, 'test.db');
process.env.APP_DB_PATH = dbPath;
process.env.APP_WHATSAPP_AUTH_DIR = path.join(tmpDir, 'whatsapp-auth');
const db = openDb(dbPath);
const adminPw = 'admin-senha-teste';
createUser(db, { username: 'admin', passwordHash: hashPassword(adminPw), role: 'admin' });
seedCampaigns(db);
setSetting(db, 'outreach.mensagensPorDia', '10');
const campaignIds = db.prepare('SELECT id, code FROM outreach_campaigns ORDER BY id').all();
const leadIds = [];
const empresas = {
'1': ['Hamburgueria Bella', 'Pizzaria Famosa', 'Acaiteria Tropical', 'Marmitaria Sabor', 'Lanchonete do Ze'],
'2': ['Restaurante Chefe', 'Mercado Central', 'Mercearia Sao Jose', 'Minimercado Bom', 'Padaria Pao Quente'],
'3': ['Loja de Moda', 'Material Construcao', 'Autopecas Veloz', 'Oficina Mecanica']
};
let leadIdCounter = 1;
for (const c of campaignIds) {
const names = empresas[c.code] || ['Empresa Generica'];
for (const nome of names) {
const whatsapp = `5511999${String(leadIdCounter).padStart(6, '0')}`;
const r = db.prepare(
`INSERT INTO leads (dedup_key, empresa, whatsapp, segmento, prioridade, score, classificacao, telefone_valido)
VALUES (?, ?, ?, ?, ?, ?, 'bom', 1)`
).run(
`browser-test-lead-${leadIdCounter}`,
nome,
whatsapp,
c.code === 'gdelivery-food' ? 'hamburguerias' : c.code === 'gti-food' ? 'restaurantes' : 'lojas_de_roupa',
leadIdCounter <= 5 ? 1 : 2,
100 - leadIdCounter
);
const lid = Number(r.lastInsertRowid);
leadIds.push({ id: lid, empresa: nome, whatsapp, campaignId: c.id });
db.prepare(
`INSERT INTO whatsapp_number_verifications (numero, status, numero_resolvido, checked_at)
VALUES (?, 'confirmado', ?, datetime('now'))`
).run(whatsapp, whatsapp);
leadIdCounter += 1;
}
}
const firstLead = leadIds[0];
setSetting(db, 'outreach.ativo', 'true');
const firstCampaignId = campaignIds[0].id;
const variantIds = db.prepare(
'SELECT id FROM outreach_campaign_variants WHERE campaign_id = ? ORDER BY code LIMIT 1'
).all(firstCampaignId);
if (variantIds.length > 0 && firstLead) {
const variantId = variantIds[0].id;
const produto = db.prepare('SELECT produto FROM outreach_campaigns WHERE id = ?').get(firstCampaignId).produto;
db.prepare(
`INSERT INTO outreach_campaign_queue
(campaign_id, lead_id, variant_id, produto, numero, segmento, rendered_message, selection_reason, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'preview')`
).run(firstCampaignId, firstLead.id, variantId, produto, firstLead.whatsapp, 'hamburguerias',
'Ola! Teste de mensagem renderizada para campanha.', 'browser-test');
}
const stageLeadId = leadIds.length > 2 ? leadIds[2].id : (firstLead ? firstLead.id : null);
if (stageLeadId) {
db.prepare(
`INSERT INTO outreach_lead_states (lead_id, stage, first_reply_at)
VALUES (?, 'respondeu', datetime('now'))`
).run(stageLeadId);
db.prepare(
`INSERT INTO outreach_stage_events (lead_id, campaign_id, stage, note)
VALUES (?, ?, 'respondeu', 'cliente quer saber mais sobre G-Delivery')`
).run(stageLeadId, firstCampaignId);
}
const replyLeadId = leadIds.length > 1 ? leadIds[1].id : (firstLead ? firstLead.id : null);
if (replyLeadId) {
db.prepare(
`INSERT OR IGNORE INTO outreach_inbound_replies (provider_message_id, lead_id, numero, received_at)
VALUES (?, ?, ?, datetime('now'))`
).run(`test-inbound-${Date.now()}`, replyLeadId, leadIds[1]?.whatsapp || '5511999990002');
}
let fakeSends = [];
const whatsapp = {
getStatus: () => ({ state: 'READY', connected: true }),
getQr: () => null,
connect: async () => {},
disconnect: async () => {},
checkNumber: async (number) => ({
registered: true,
numero: number,
numeroResolvido: number,
status: 'confirmado'
}),
sendText: async () => {
fakeSends.push({ at: new Date().toISOString() });
throw new Error('fake manager nao envia mensagens reais');
}
};
const jobs = { subscribe: () => null, activeJobs: () => [] };
const server = createAppServer({ db, jobs, whatsapp, log });
return new Promise((resolve, reject) => {
server.listen(3911, '127.0.0.1', () => {
log(`server listening on 3911`);
const apiRequest = async (method, path, options = {}) => {
const url = `http://127.0.0.1:3911${path}`;
const headers = { ...options.headers };
if (options.body && !headers['content-type']) {
headers['content-type'] = 'application/json';
}
const response = await fetch(url, {
method,
headers,
body: options.body ? JSON.stringify(options.body) : undefined
});
let data = null;
try {
data = await response.json();
} catch {
data = null;
}
return { status: response.status, headers: response.headers, data, raw: response };
};
const teardown = async () => {
await new Promise((r) => server.close(r));
db.close();
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
log('test server teardown complete');
};
resolve({ server, db, tmpDir, whatsapp, fakeSends, apiRequest, teardown, adminPw });
});
server.on('error', reject);
});
}