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: f5af038e19
git-subtree-split: a9ef4d776d
This commit is contained in:
Gustavo
2026-09-28 15:50:17 -03:00
co-authored by Claude Sonnet 5
150 changed files with 33449 additions and 0 deletions
@@ -0,0 +1,161 @@
import fs from 'node:fs';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
import { paths } from '../../env.mjs';
import { UF_NAMES } from '../../sources/common.mjs';
import { getSource } from '../../sources/index.mjs';
import { isSqliteConstraintError } from '../../db.mjs';
function hasCnpjBaseForUf(uf) {
return fs.existsSync(path.join(paths.cnpjDir, `estabelecimentos-${uf}.csv`))
|| (uf === 'BA' && fs.existsSync(path.join(paths.cnpjDir, 'estabelecimentos-bahia.csv')));
}
export function registerCaptureRoutes(router, { db, jobs }) {
router.add('POST', '/api/capture/runs', async (req, res, { json, body }) => {
const sources = Array.isArray(body?.sources) ? body.sources.map((value) => typeof value === 'string' ? value.trim() : value) : [];
const uf = String(body?.uf || '').toUpperCase();
const cities = Array.isArray(body?.cities) ? body.cities.map((value) => typeof value === 'string' ? value.trim() : value) : [];
const segments = Array.isArray(body?.segments) ? body.segments.map((value) => typeof value === 'string' ? value.trim() : value) : [];
const limits = {
maxPerQuery: Number(body?.limits?.maxPerQuery),
queryLimit: Number(body?.limits?.queryLimit),
googlePages: body?.limits?.googlePages === undefined ? 3 : Number(body.limits.googlePages)
};
const invalidArray = (values, max) => !values.length || values.length > max
|| values.some((value) => typeof value !== 'string' || !value || value.length > 100);
if (invalidArray(sources, 10) || new Set(sources).size !== sources.length) {
json(400, { erro: 'fontes invalidas: selecione de 1 a 10 fontes unicas' });
return;
}
for (const id of sources) {
if (!getSource(id)) {
json(400, { erro: `fonte desconhecida: ${id}` });
return;
}
}
if (!UF_NAMES[uf]) {
json(400, { erro: 'informe uma UF valida' });
return;
}
if (invalidArray(cities, 1000) || (cities.includes('all') && cities.length !== 1)) {
json(400, { erro: 'cidades invalidas: selecione de 1 a 1000 municipios ou somente "all"' });
return;
}
if (!cities.includes('all')) {
const findCity = db.prepare('SELECT 1 FROM municipios WHERE uf = ? AND nome = ?');
const unknown = cities.find((city) => !findCity.get(uf, city));
if (unknown) {
json(400, { erro: `municipio invalido para ${uf}: ${unknown}` });
return;
}
}
if (invalidArray(segments, 250)
|| (segments.includes('all') && segments.some((segment) => segment !== 'all' && !segment.startsWith('cnae:')))) {
json(400, { erro: 'segmentos invalidos: selecione de 1 a 250 nichos ou somente "all"' });
return;
}
const findSegment = db.prepare('SELECT 1 FROM segments WHERE name = ? AND active = 1');
const findCnae = db.prepare('SELECT 1 FROM cnaes WHERE codigo = ?');
for (const segment of segments) {
if (segment === 'all') continue;
if (/^cnae:\d{7}$/.test(segment)) {
if (findCnae.get(segment.slice(5))) continue;
} else if (findSegment.get(segment)) {
continue;
}
json(400, { erro: `segmento ou CNAE invalido: ${segment}` });
return;
}
if (!Number.isInteger(limits.maxPerQuery) || limits.maxPerQuery < 1 || limits.maxPerQuery > 100
|| !Number.isInteger(limits.queryLimit) || limits.queryLimit < 0 || limits.queryLimit > 10000
|| !Number.isInteger(limits.googlePages) || limits.googlePages < 1 || limits.googlePages > 3) {
json(400, { erro: 'limites invalidos: maxPerQuery 1..100, queryLimit 0..10000, googlePages 1..3' });
return;
}
if ((sources.includes('cnpj-base') || sources.includes('brasilapi')) && !hasCnpjBaseForUf(uf)) {
json(409, {
erro: `a base CNPJ de ${uf} ainda nao foi extraida`,
precisaExtracao: true,
uf
});
return;
}
const running = db.prepare("SELECT id FROM capture_runs WHERE status IN ('pending','running')").get();
if (running) {
json(409, { erro: `ja existe uma captura em andamento (#${running.id}). Aguarde ou cancele.` });
return;
}
const params = { sources, uf, cities, segments, limits };
let inserted;
try {
inserted = db.prepare("INSERT INTO capture_runs (params_json, status) VALUES (?, 'pending')")
.run(JSON.stringify(params));
} catch (error) {
if (!isSqliteConstraintError(error)) throw error;
json(409, { erro: 'outra captura foi iniciada simultaneamente. Aguarde ou cancele.' });
return;
}
const runId = Number(inserted.lastInsertRowid);
const ownerToken = randomUUID();
const jobId = jobs.startJob({
type: 'captura',
script: 'src/capture/capture-job.mjs',
args: [`--run-id=${runId}`],
meta: { runId, uf, sources },
ownerToken
});
db.prepare("UPDATE capture_runs SET job_id = ?, owner_token = ?, heartbeat_at = datetime('now') WHERE id = ?")
.run(jobId, ownerToken, runId);
json(202, { runId, jobId });
});
router.add('GET', '/api/capture/runs', async (req, res, { json, query }) => {
const limit = Math.min(100, Math.max(1, Number(query.get('limit')) || 20));
const rows = db.prepare(
`SELECT id, job_id, params_json, status, progress_json, raw_count, new_count, dup_count,
error, started_at, finished_at, created_at
FROM capture_runs ORDER BY id DESC LIMIT ?`
).all(limit);
json(200, {
runs: rows.map((row) => ({
...row,
params: JSON.parse(row.params_json || '{}'),
progress: JSON.parse(row.progress_json || 'null'),
params_json: undefined,
progress_json: undefined
}))
});
});
router.add('GET', '/api/capture/runs/:id', async (req, res, { json, params }) => {
const row = db.prepare('SELECT * FROM capture_runs WHERE id = ?').get(Number(params.id));
if (!row) {
json(404, { erro: 'execucao nao encontrada' });
return;
}
json(200, {
...row,
params: JSON.parse(row.params_json || '{}'),
progress: JSON.parse(row.progress_json || 'null'),
params_json: undefined,
progress_json: undefined
});
});
router.add('POST', '/api/capture/runs/:id/cancel', async (req, res, { json, params }) => {
const row = db.prepare('SELECT job_id, status FROM capture_runs WHERE id = ?').get(Number(params.id));
if (!row) {
json(404, { erro: 'execucao nao encontrada' });
return;
}
const ok = row.job_id ? jobs.cancelJob(row.job_id) : false;
json(ok ? 202 : 409, ok ? { cancelando: true } : { erro: `nao ha job ativo para cancelar (status: ${row.status})` });
});
}