REFACTOR: Remoção de serviços obsoletos e implementação da nova funcionalidade de viabilidade pela API de contratação
- Removidos os arquivos não utilizados: fetchService, geocodeService, normalizeService e retryService. - Adicionado o viabilidadeController para gerenciar consultas de viabilidade e upload de arquivos CSV. - Criadas as viabilidadeRoutes para tratar as rotas da API relacionadas à viabilidade. - Implementado o csvService para processamento de arquivos CSV e integração com as consultas de viabilidade. - Criado o jobStore.service para gerenciamento do estado dos jobs durante o processamento do CSV. - Desenvolvido o viabilidadeService para integração com a API de viabilidade.
This commit is contained in:
parent
0de64d4024
commit
b3bca576da
723
app.js
723
app.js
@ -1,681 +1,60 @@
|
|||||||
require("dotenv").config();
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const session = require('express-session');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
const express = require("express");
|
const viabilidadeRoutes = require('./routes/viabilidadeRoutes');
|
||||||
const multer = require("multer");
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const csv = require("csv-parser");
|
|
||||||
const fastCsv = require("fast-csv");
|
|
||||||
const axios = require("axios");
|
|
||||||
const cors = require("cors");
|
|
||||||
const session = require("express-session"); // adiciona session
|
|
||||||
const { geocodeWithGoogle, addressWithGoogle } = require("./service/geocodeService");
|
|
||||||
const { fetchJson } = require("./service/fetchService");
|
|
||||||
const { BASE_BACKOFF_MS, MAX_RETRIES, REQUEST_DELAY_MS, sleep } = require("./service/retryService");
|
|
||||||
const { API_URL, HEADERS } = require("./config/apiConfig");
|
|
||||||
const { normalizePartnerSigla } = require("./service/normalizeService");
|
|
||||||
const authRoutes = require("./routes/authRoutes.js");
|
|
||||||
|
|
||||||
function createApp() {
|
const app = express();
|
||||||
const upload = multer({ dest: "uploads/" });
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
// se estiver atrás de um reverse proxy (nginx/traefik) em produção, habilite:
|
// Basic middleware
|
||||||
app.set("trust proxy", 1);
|
app.use(express.json({ limit: '5mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
app.use(cors());
|
// Session (in-memory, fine for dev/tests)
|
||||||
app.use(express.json());
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'dev-secret',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
cookie: { secure: false } // secure should be true behind HTTPS in prod
|
||||||
|
}));
|
||||||
|
|
||||||
// session TEM que vir antes das rotas que usam req.session
|
// Dev bypass for Microsoft auth / session (only when explicitly enabled)
|
||||||
app.use(
|
if (process.env.NODE_ENV === 'development' && process.env.DEV_SKIP_AUTH === 'true') {
|
||||||
session({
|
|
||||||
secret: process.env.SESSION_SECRET || "change-me",
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: false,
|
|
||||||
cookie: {
|
|
||||||
maxAge: 24 * 60 * 60 * 1000,
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
sameSite: "lax",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// -- Desenvolvimento: pular autenticação se configurado
|
|
||||||
// Para ativar: defina NODE_ENV=development e DEV_SKIP_AUTH=true no .env
|
|
||||||
if (process.env.NODE_ENV === 'development' && process.env.DEV_SKIP_AUTH === 'true') {
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
// garante que exista uma sessão autenticada para facilitar testes locais
|
|
||||||
if (req.session && (!req.session.user || !req.session.user.authenticated)) {
|
|
||||||
req.session.user = { authenticated: true, dev: true };
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// middleware que protege rotas que exigem login
|
|
||||||
function requireAuth(req, res, next) {
|
|
||||||
if (req.session?.user?.authenticated) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.xhr || req.headers.accept?.includes("application/json")) {
|
|
||||||
return res.status(401).json({ error: "not_authenticated" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.redirect("/login");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// proteger demais rotas (ex.: /upload, /consulta)
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
// permissão liberada para rotas de auth já tratadas; proteger o resto
|
if (!req.session) return next();
|
||||||
if (req.path.startsWith("/auth") || req.path === "/login") return next();
|
// mark a simple user in session so handlers that expect auth work in dev
|
||||||
return requireAuth(req, res, next);
|
req.session.user = req.session.user || { id: 'dev', name: 'developer' };
|
||||||
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// redirect raiz
|
|
||||||
app.get("/", (req, res) => {
|
|
||||||
// em dev com bypass, sirva a página diretamente (sem redirect)
|
|
||||||
if (process.env.NODE_ENV === 'development' && process.env.DEV_SKIP_AUTH === 'true') {
|
|
||||||
return res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.session?.user?.authenticated) {
|
|
||||||
return res.redirect("/public/index.html");
|
|
||||||
}
|
|
||||||
return res.redirect("/login");
|
|
||||||
});
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
async function getMinDistance(lat, lon) {
|
|
||||||
// tenta várias vezes com backoff exponencial; trata 429 usando Retry-After se disponível
|
|
||||||
let attempt = 0;
|
|
||||||
while (attempt < MAX_RETRIES) {
|
|
||||||
try {
|
|
||||||
// envia também o raio (em metros) - API espera esse parâmetro em várias rotas
|
|
||||||
const resp = await axios.get(API_URL, {
|
|
||||||
headers: HEADERS,
|
|
||||||
params: {
|
|
||||||
raio: 5000,
|
|
||||||
latitude: lat,
|
|
||||||
longitude: lon,
|
|
||||||
"itens[]": ["caixa"],
|
|
||||||
consultarPasta: "S",
|
|
||||||
},
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
const data = resp.data;
|
|
||||||
const registros = data && data.registros ? data.registros : [];
|
|
||||||
// find registros that have a numeric distancia and keep original object for robust extraction
|
|
||||||
const candidates = registros
|
|
||||||
.map((r) => ({ raw: r, distanciaRaw: r && r.distancia }))
|
|
||||||
.map((o) => ({
|
|
||||||
raw: o.raw,
|
|
||||||
num:
|
|
||||||
o.distanciaRaw !== undefined &&
|
|
||||||
o.distanciaRaw !== null &&
|
|
||||||
o.distanciaRaw !== ""
|
|
||||||
? Number(o.distanciaRaw)
|
|
||||||
: null,
|
|
||||||
}))
|
|
||||||
.filter((x) => x.num !== null && !Number.isNaN(x.num));
|
|
||||||
if (candidates.length) {
|
|
||||||
candidates.sort((a, b) => a.num - b.num);
|
|
||||||
const best = candidates[0];
|
|
||||||
const r = best.raw || {};
|
|
||||||
// robust extraction of pasta sigla with fallbacks
|
|
||||||
let pastaSigla = null;
|
|
||||||
try {
|
|
||||||
if (r.pasta) {
|
|
||||||
if (typeof r.pasta === "string" && r.pasta.trim())
|
|
||||||
pastaSigla = r.pasta.trim();
|
|
||||||
else if (r.pasta.sigla && String(r.pasta.sigla).trim())
|
|
||||||
pastaSigla = String(r.pasta.sigla).trim();
|
|
||||||
else if (
|
|
||||||
r.pasta.cidade &&
|
|
||||||
r.pasta.cidade.sigla &&
|
|
||||||
String(r.pasta.cidade.sigla).trim()
|
|
||||||
)
|
|
||||||
pastaSigla = String(r.pasta.cidade.sigla).trim();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
pastaSigla = null;
|
|
||||||
}
|
|
||||||
// if closest has no pastaSigla, try find any candidate with non-empty sigla
|
|
||||||
if (!pastaSigla) {
|
|
||||||
for (let j = 0; j < candidates.length; j++) {
|
|
||||||
const rr = candidates[j].raw || {};
|
|
||||||
try {
|
|
||||||
if (rr.pasta) {
|
|
||||||
if (typeof rr.pasta === "string" && rr.pasta.trim()) {
|
|
||||||
pastaSigla = rr.pasta.trim();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (rr.pasta.sigla && String(rr.pasta.sigla).trim()) {
|
|
||||||
pastaSigla = String(rr.pasta.sigla).trim();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
rr.pasta.cidade &&
|
|
||||||
rr.pasta.cidade.sigla &&
|
|
||||||
String(rr.pasta.cidade.sigla).trim()
|
|
||||||
) {
|
|
||||||
pastaSigla = String(rr.pasta.cidade.sigla).trim();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!pastaSigla)
|
|
||||||
console.warn(
|
|
||||||
`[WARN] Nenhuma pasta.sigla encontrada para coordenadas ${lat},${lon} (closest dist ${best.num})`
|
|
||||||
);
|
|
||||||
pastaSigla = normalizePartnerSigla(pastaSigla);
|
|
||||||
return { dist: best.num, pastaSigla };
|
|
||||||
}
|
|
||||||
// sem distancias válidas
|
|
||||||
return null;
|
|
||||||
} catch (err) {
|
|
||||||
attempt += 1;
|
|
||||||
// se for 429, tente respeitar Retry-After quando disponível
|
|
||||||
if (err.response && err.response.status === 429) {
|
|
||||||
const ra =
|
|
||||||
err.response.headers &&
|
|
||||||
(err.response.headers["retry-after"] ||
|
|
||||||
err.response.headers["Retry-After"]);
|
|
||||||
let waitMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
|
|
||||||
if (ra) {
|
|
||||||
const raSec = parseInt(ra, 10);
|
|
||||||
if (!isNaN(raSec)) waitMs = raSec * 1000;
|
|
||||||
}
|
|
||||||
console.warn(
|
|
||||||
`[WARN] 429 recebido para ${lat},${lon} - aguardando ${waitMs}ms e tentando novamente (attempt ${attempt}/${MAX_RETRIES})`
|
|
||||||
);
|
|
||||||
await sleep(waitMs);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// para outros erros de rede/timeout, aguarda backoff exponencial e tenta de novo
|
|
||||||
const waitMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
|
|
||||||
console.warn(
|
|
||||||
`[WARN] Erro ao consultar API para ${lat},${lon}: ${err.message} - backoff ${waitMs}ms (attempt ${attempt}/${MAX_RETRIES})`
|
|
||||||
);
|
|
||||||
await sleep(waitMs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// exauriu tentativas
|
|
||||||
console.error(`[ERROR] Exauriu retries para ${lat},${lon}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// upload CSV endpoint
|
|
||||||
const jobs = {}; // jobId -> { status, total, processed, download, error }
|
|
||||||
|
|
||||||
app.post("/upload", upload.single("csvfile"), (req, res) => {
|
|
||||||
if (!req.file)
|
|
||||||
return res.status(400).json({ error: "Nenhum arquivo enviado" });
|
|
||||||
const filePath = req.file.path;
|
|
||||||
const jobId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
||||||
jobs[jobId] = {
|
|
||||||
status: "queued",
|
|
||||||
total: 0,
|
|
||||||
processed: 0,
|
|
||||||
download: null,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
jobs[jobId].status = "processing";
|
|
||||||
try {
|
|
||||||
const rows = [];
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
fs.createReadStream(filePath)
|
|
||||||
.pipe(csv({ separator: ";" }))
|
|
||||||
.on("data", (data) => {
|
|
||||||
// ignora linhas totalmente vazias (todos os campos nulos/undefined/strings vazias)
|
|
||||||
const values = Object.values(data);
|
|
||||||
const allEmpty =
|
|
||||||
values.length === 0 ||
|
|
||||||
values.every(
|
|
||||||
(v) => v === null || v === undefined || String(v).trim() === ""
|
|
||||||
);
|
|
||||||
if (!allEmpty) rows.push(data);
|
|
||||||
})
|
|
||||||
.on("end", resolve)
|
|
||||||
.on("error", reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
jobs[jobId].total = rows.length;
|
|
||||||
|
|
||||||
const coordCache = new Map();
|
|
||||||
|
|
||||||
const outRows = [];
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
|
||||||
const row = rows[i];
|
|
||||||
// normalize keys to avoid duplicates caused by different headers
|
|
||||||
const norm = {};
|
|
||||||
Object.keys(row).forEach((k) => {
|
|
||||||
// normalize header: lowercase, remove diacritics and non-alphanumeric
|
|
||||||
const kn = k
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.normalize("NFKD")
|
|
||||||
.replace(/[\u0300-\u036f]/g, "")
|
|
||||||
.replace(/[^a-z0-9]/g, "");
|
|
||||||
norm[kn] = row[k];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Input columns (from normalized map)
|
|
||||||
const rawCep = norm["cep"]
|
|
||||||
? String(norm["cep"]).replace(/\D/g, "")
|
|
||||||
: "";
|
|
||||||
const rawNumero = norm["numero"] ? String(norm["numero"]).trim() : "";
|
|
||||||
// prefer lat/lon from normalized input if available
|
|
||||||
const rawLat = norm["latitude"] || norm["lat"] || null;
|
|
||||||
const rawLon =
|
|
||||||
norm["longitude"] || norm["lon"] || norm["long"] || null;
|
|
||||||
|
|
||||||
// Prefer existing lat/lon if provided from normalized fields
|
|
||||||
let lat = null,
|
|
||||||
lon = null;
|
|
||||||
if (rawLat && rawLon) {
|
|
||||||
lat = Number(String(rawLat).replace(",", "."));
|
|
||||||
lon = Number(String(rawLon).replace(",", "."));
|
|
||||||
}
|
|
||||||
|
|
||||||
let builtAddress = "";
|
|
||||||
|
|
||||||
const googleGeocodeAddress = await addressWithGoogle(lat, lon);
|
|
||||||
if (googleGeocodeAddress) {
|
|
||||||
builtAddress = googleGeocodeAddress;
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
`Google Reverse Geocoding não retornou resultado para coords ${lat},${lon} (row ${i + 1})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Row ${i + 1}: CEP='${rawCep}' Número='${rawNumero}' Lat='${lat}' Lon='${lon}' BuiltAddress='${builtAddress}'`);
|
|
||||||
|
|
||||||
// If no coords, try ViaCEP -> Google
|
|
||||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
|
||||||
if (rawCep) {
|
|
||||||
const cep8 = rawCep.padStart(8, "0");
|
|
||||||
const cepRestData = await fetch(
|
|
||||||
'https://api.cep.rest/', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ cep: cep8 })
|
|
||||||
}
|
|
||||||
).then(r => r.json());
|
|
||||||
if (cepRestData && !cepRestData.erro) {
|
|
||||||
const logradouro = cepRestData.data.logradouro || "";
|
|
||||||
const bairro = cepRestData.data.bairro || "";
|
|
||||||
const cidade = cepRestData.data.localidade || "";
|
|
||||||
const uf = cepRestData.data.uf || "";
|
|
||||||
if (logradouro) {
|
|
||||||
builtAddress =
|
|
||||||
`${logradouro}, ${rawNumero}, ${bairro}, ${cidade} - ${uf}`
|
|
||||||
.replace(/, ,/g, ",")
|
|
||||||
.replace(/^,\s*/, "");
|
|
||||||
} else {
|
|
||||||
// fallback: use neighborhood/city
|
|
||||||
builtAddress = `${bairro || ""} ${
|
|
||||||
cidade ? ", " + cidade : ""
|
|
||||||
} ${uf ? "- " + uf : ""}`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// build addressToUse (builtAddress already assembled above)
|
|
||||||
if (!process.env.GOOGLE_API_KEY) {
|
|
||||||
console.error(
|
|
||||||
"[ERROR] GOOGLE_API_KEY não definida. Não será possível geocodificar. Defina a chave no .env ou em process.env"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const addressToUse =
|
|
||||||
builtAddress || `${cidade} ${uf} ${cep8}`;
|
|
||||||
const geo = await geocodeWithGoogle(addressToUse);
|
|
||||||
if (geo) {
|
|
||||||
lat = geo.lat;
|
|
||||||
lon = geo.lon;
|
|
||||||
} else
|
|
||||||
console.warn(
|
|
||||||
`Google Geocoding não retornou resultado para '${addressToUse}' (CEP ${cep8}, row ${
|
|
||||||
i + 1
|
|
||||||
})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(`ViaCEP erro for CEP ${rawCep} (row ${i + 1})`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`Row ${i + 1}: missing/invalid CEP -> '${rawCep}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare explicit output row to avoid extra columns
|
|
||||||
const out = {
|
|
||||||
CEP: rawCep || "",
|
|
||||||
Número: rawNumero || "",
|
|
||||||
Endereço: builtAddress || "",
|
|
||||||
// write lat/lon as strings with dot decimal and fixed precision to avoid locale swaps
|
|
||||||
Latitude: Number.isFinite(lat) ? Number(lat).toFixed(6) : "",
|
|
||||||
Longitude: Number.isFinite(lon) ? Number(lon).toFixed(6) : "",
|
|
||||||
"Não dedicado": "",
|
|
||||||
Dedicado: "",
|
|
||||||
Distancia: "",
|
|
||||||
"Parceiro/Sothis": "",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Number.isFinite(lat) && Number.isFinite(lon)) {
|
|
||||||
const coordKey = `${lat.toFixed(6)},${lon.toFixed(6)}`;
|
|
||||||
if (coordCache.has(coordKey)) {
|
|
||||||
const cached = coordCache.get(coordKey); // cached is either null or { dist, pastaSigla }
|
|
||||||
if (cached !== null) {
|
|
||||||
const d = cached.dist;
|
|
||||||
const di = Math.round(Number(d));
|
|
||||||
out["Não dedicado"] = di <= 500 ? "viável" : "Não viável";
|
|
||||||
out["Dedicado"] = di <= 1000 ? "viável" : "Não viável";
|
|
||||||
out["Distancia"] = `${di}M`;
|
|
||||||
out["Parceiro/Sothis"] =
|
|
||||||
normalizePartnerSigla(cached.pastaSigla) || "";
|
|
||||||
} else {
|
|
||||||
out["Não dedicado"] = "Não viável";
|
|
||||||
out["Dedicado"] = "Não viável";
|
|
||||||
out["Distancia"] = "5km +";
|
|
||||||
out["Parceiro/Sothis"] = "";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const minResult = await getMinDistance(lat, lon); // { dist, pastaSigla } or null
|
|
||||||
coordCache.set(coordKey, minResult);
|
|
||||||
if (minResult !== null) {
|
|
||||||
const di = Math.round(Number(minResult.dist));
|
|
||||||
out["Não dedicado"] = di <= 500 ? "viável" : "Não viável";
|
|
||||||
out["Dedicado"] = di <= 1000 ? "viável" : "Não viável";
|
|
||||||
out["Distancia"] = `${di}M`;
|
|
||||||
out["Parceiro/Sothis"] =
|
|
||||||
normalizePartnerSigla(minResult.pastaSigla) || "";
|
|
||||||
} else {
|
|
||||||
out["Não dedicado"] = "Não viável";
|
|
||||||
out["Dedicado"] = "Não viável";
|
|
||||||
out["Distancia"] = "5km +";
|
|
||||||
out["Parceiro/Sothis"] = "";
|
|
||||||
}
|
|
||||||
await sleep(REQUEST_DELAY_MS);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no coords available -> keep defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
outRows.push(out);
|
|
||||||
jobs[jobId].processed = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write output csv - use explicit outRows and fixed header order
|
|
||||||
const outPath = path.join(__dirname, "outputs");
|
|
||||||
if (!fs.existsSync(outPath)) fs.mkdirSync(outPath);
|
|
||||||
const originalName =
|
|
||||||
req.file && req.file.originalname
|
|
||||||
? req.file.originalname
|
|
||||||
: `upload_${Date.now()}.csv`;
|
|
||||||
const parsed = path.parse(originalName);
|
|
||||||
let outBase = `${parsed.name}_output`;
|
|
||||||
let outFile = path.join(outPath, `${outBase}.csv`);
|
|
||||||
if (fs.existsSync(outFile)) {
|
|
||||||
outFile = path.join(outPath, `${outBase}_${Date.now()}.csv`);
|
|
||||||
}
|
|
||||||
const headers = [
|
|
||||||
"CEP",
|
|
||||||
"Número",
|
|
||||||
"Endereço",
|
|
||||||
"Latitude",
|
|
||||||
"Longitude",
|
|
||||||
"Não dedicado",
|
|
||||||
"Dedicado",
|
|
||||||
"Distancia",
|
|
||||||
"Parceiro/Sothis",
|
|
||||||
];
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const ws = fs.createWriteStream(outFile);
|
|
||||||
ws.write("\uFEFF");
|
|
||||||
fastCsv
|
|
||||||
.write(outRows, { headers: headers, delimiter: ";" })
|
|
||||||
.pipe(ws)
|
|
||||||
.on("finish", resolve)
|
|
||||||
.on("error", reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
jobs[jobId].status = "done";
|
|
||||||
jobs[jobId].download = `/download/${path.basename(outFile)}`;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
jobs[jobId].status = "error";
|
|
||||||
jobs[jobId].error = String(err.message || err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return res.json({ jobId });
|
|
||||||
});
|
|
||||||
|
|
||||||
// download model endpoint
|
|
||||||
app.get("/download-model/:name", (req, res) => {
|
|
||||||
const name = req.params.name;
|
|
||||||
const safeName = path.basename(name); // evita ../
|
|
||||||
const filePath = path.join(__dirname, "models", safeName);
|
|
||||||
if (!fs.existsSync(filePath)) return res.status(404).send("Arquivo não encontrado");
|
|
||||||
return res.download(filePath, safeName);
|
|
||||||
});
|
|
||||||
|
|
||||||
// lista os arquivos disponíveis em /models
|
|
||||||
app.get("/download-models/list", (req, res) => {
|
|
||||||
const modelDir = path.join(__dirname, "models");
|
|
||||||
if (!fs.existsSync(modelDir)) return res.status(404).json({ error: "Pasta de modelos não encontrada" });
|
|
||||||
const files = fs.readdirSync(modelDir).filter((f) => fs.statSync(path.join(modelDir, f)).isFile());
|
|
||||||
return res.json({ files });
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// download result endpoint
|
|
||||||
app.get("/download/:name", (req, res) => {
|
|
||||||
const name = req.params.name;
|
|
||||||
const p = path.join(__dirname, "outputs", name);
|
|
||||||
if (!fs.existsSync(p))
|
|
||||||
return res.status(404).send("Arquivo não encontrado");
|
|
||||||
res.download(p);
|
|
||||||
});
|
|
||||||
|
|
||||||
// job status endpoint
|
|
||||||
app.get("/status/:jobId", (req, res) => {
|
|
||||||
const job = jobs[req.params.jobId];
|
|
||||||
if (!job) return res.status(404).json({ error: "job não encontrado" });
|
|
||||||
return res.json(job);
|
|
||||||
});
|
|
||||||
|
|
||||||
// manual query endpoint
|
|
||||||
// /consulta now accepts either latitude+longitude OR cep+numero. If cep is provided we resolve ViaCEP -> Google -> Geogrid
|
|
||||||
app.get("/consulta", async (req, res) => {
|
|
||||||
const {
|
|
||||||
latitude: rawLat,
|
|
||||||
longitude: rawLon,
|
|
||||||
cep: rawCep,
|
|
||||||
numero: rawNumero,
|
|
||||||
} = req.query;
|
|
||||||
|
|
||||||
// If cep provided, use ViaCEP -> Google geocoding -> Geogrid
|
|
||||||
if (rawCep) {
|
|
||||||
const cep = String(rawCep).replace(/\D/g, "");
|
|
||||||
const numero = rawNumero ? String(rawNumero).trim() : "";
|
|
||||||
try {
|
|
||||||
const viaCepData = await fetch(
|
|
||||||
'https://api.cep.rest/', { method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ cep })
|
|
||||||
}
|
|
||||||
).then(r => r.json());
|
|
||||||
if (!viaCepData || viaCepData.erro)
|
|
||||||
return res.status(404).json({ error: "CEP não encontrado" });
|
|
||||||
const logradouro = viaCepData.data.logradouro || "";
|
|
||||||
const bairro = viaCepData.data.bairro || "";
|
|
||||||
const cidade = viaCepData.data.localidade || "";
|
|
||||||
const uf = viaCepData.data.uf || "";
|
|
||||||
const endereco =
|
|
||||||
`${logradouro}, ${numero}, ${bairro}, ${cidade} - ${uf}`
|
|
||||||
.replace(/, ,/g, ",")
|
|
||||||
.replace(/^,\s*/, "");
|
|
||||||
|
|
||||||
if (!process.env.GOOGLE_API_KEY)
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.json({ error: "GOOGLE_API_KEY não definida no servidor" });
|
|
||||||
const geo = await geocodeWithGoogle(
|
|
||||||
endereco || `${cidade} ${uf} ${cep}`
|
|
||||||
);
|
|
||||||
if (!geo)
|
|
||||||
return res
|
|
||||||
.status(404)
|
|
||||||
.json({ error: "geocode não encontrado (Google)" });
|
|
||||||
const lat = Number(geo.lat);
|
|
||||||
const lon = Number(geo.lon);
|
|
||||||
const result = await getMinDistance(lat, lon);
|
|
||||||
if (result && result.dist !== undefined) {
|
|
||||||
return res.json({
|
|
||||||
endereco,
|
|
||||||
latitude: lat,
|
|
||||||
longitude: lon,
|
|
||||||
distancia: result.dist,
|
|
||||||
parceiro: result.pastaSigla || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.json({
|
|
||||||
endereco,
|
|
||||||
latitude: lat,
|
|
||||||
longitude: lon,
|
|
||||||
distancia: "5km +",
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return res.status(500).json({ error: "Erro na consulta" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise require latitude+longitude
|
|
||||||
if (!rawLat || !rawLon)
|
|
||||||
return res.status(400).json({
|
|
||||||
error: "latitude e longitude são obrigatórios (ou forneça cep)",
|
|
||||||
});
|
|
||||||
const latitude = Number(String(rawLat).replace(",", "."));
|
|
||||||
const longitude = Number(String(rawLon).replace(",", "."));
|
|
||||||
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
|
||||||
console.warn(
|
|
||||||
`Consulta manual com parâmetros inválidos: lat='${rawLat}' lon='${rawLon}'`
|
|
||||||
);
|
|
||||||
return res.status(400).json({ error: "latitude ou longitude inválidos" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`Consulta manual: lat=${latitude} lon=${longitude}`);
|
|
||||||
const result = await getMinDistance(latitude, longitude);
|
|
||||||
console.log(`Resultado consulta manual: ${JSON.stringify(result)}`);
|
|
||||||
if (result && result.dist !== undefined) {
|
|
||||||
return res.json({
|
|
||||||
distancia: result.dist,
|
|
||||||
parceiro: result.pastaSigla || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.json({ distancia: "5km +" });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return res.status(500).json({ error: "Erro na consulta" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// manual CEP+Numero query: resolves ViaCEP -> Nominatim -> Geogrid
|
|
||||||
app.get("/consulta-cep", async (req, res) => {
|
|
||||||
const { cep: rawCep, numero: rawNumero } = req.query;
|
|
||||||
if (!rawCep) return res.status(400).json({ error: "cep é obrigatório" });
|
|
||||||
const cep = String(rawCep).replace(/\D/g, "");
|
|
||||||
const numero = rawNumero ? String(rawNumero).trim() : "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cepRestData = await fetch(
|
|
||||||
'https://api.cep.rest/', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ cep })
|
|
||||||
}
|
|
||||||
).then(r => r.json());
|
|
||||||
if (!cepRestData || cepRestData.erro)
|
|
||||||
return res.status(404).json({ error: "CEP não encontrado" });
|
|
||||||
const logradouro = cepRestData.data.logradouro || "";
|
|
||||||
const bairro = cepRestData.data.bairro || "";
|
|
||||||
const cidade = cepRestData.data.localidade || "";
|
|
||||||
const uf = cepRestData.data.uf || "";
|
|
||||||
const endereco = `${logradouro}, ${numero}, ${bairro}, ${cidade} - ${uf}`
|
|
||||||
.replace(/, ,/g, ",")
|
|
||||||
.replace(/^,\s*/, "");
|
|
||||||
|
|
||||||
if (!process.env.GOOGLE_API_KEY)
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.json({ error: "GOOGLE_API_KEY não definida no servidor" });
|
|
||||||
const geo = await geocodeWithGoogle(endereco || `${cidade} ${uf} ${cep}`);
|
|
||||||
if (!geo)
|
|
||||||
return res
|
|
||||||
.status(404)
|
|
||||||
.json({ error: "geocode não encontrado (Google)" });
|
|
||||||
const lat = Number(geo.lat);
|
|
||||||
const lon = Number(geo.lon);
|
|
||||||
const result = await getMinDistance(lat, lon);
|
|
||||||
if (result && result.dist !== undefined) {
|
|
||||||
return res.json({
|
|
||||||
endereco,
|
|
||||||
latitude: lat,
|
|
||||||
longitude: lon,
|
|
||||||
distancia: result.dist,
|
|
||||||
parceiro: result.pastaSigla || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.json({
|
|
||||||
endereco,
|
|
||||||
latitude: lat,
|
|
||||||
longitude: lon,
|
|
||||||
distancia: "5km +",
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return res.status(500).json({ error: "erro na consulta" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// Servir arquivos estáticos (index.html)
|
|
||||||
// app.use("/public", express.static(path.join(__dirname, "public")));
|
|
||||||
|
|
||||||
// Usa as rotas de autenticação
|
|
||||||
app.use("/", authRoutes);
|
|
||||||
|
|
||||||
// servir arquivos estáticos da pasta public (rotas protegidas já são tratadas pelo middleware global)
|
|
||||||
app.use(express.static(path.join(__dirname, "public")));
|
|
||||||
|
|
||||||
// rota protegida que serve o index.html
|
|
||||||
app.get("/app", requireAuth, (req, res) => {
|
|
||||||
res.sendFile(path.join(__dirname, "public", "index.html"));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
// Serve static assets (UI)
|
||||||
createApp,
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
};
|
|
||||||
|
// Mount API routes
|
||||||
|
app.use('/', viabilidadeRoutes);
|
||||||
|
|
||||||
|
// Health endpoint
|
||||||
|
app.get('/health', (req, res) => res.json({ ok: true }));
|
||||||
|
|
||||||
|
// 404
|
||||||
|
app.use((req, res) => res.status(404).json({ error: 'Not found' }));
|
||||||
|
|
||||||
|
// Error handler
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err && (err.stack || err.message) || err);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
const port = parseInt(process.env.PORT, 10) || 3000;
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server listening on port ${port} (env=${process.env.NODE_ENV || 'production'})`);
|
||||||
|
if (process.env.NODE_ENV === 'development' && process.env.DEV_SKIP_AUTH === 'true') {
|
||||||
|
console.log('[START-NOAUTH] DEV_SKIP_AUTH=true — authentication is bypassed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
681
app.old.js
Normal file
681
app.old.js
Normal file
@ -0,0 +1,681 @@
|
|||||||
|
// require("dotenv").config();
|
||||||
|
|
||||||
|
// const express = require("express");
|
||||||
|
// const multer = require("multer");
|
||||||
|
// const fs = require("fs");
|
||||||
|
// const path = require("path");
|
||||||
|
// const csv = require("csv-parser");
|
||||||
|
// const fastCsv = require("fast-csv");
|
||||||
|
// const axios = require("axios");
|
||||||
|
// const cors = require("cors");
|
||||||
|
// const session = require("express-session"); // adiciona session
|
||||||
|
// const { geocodeWithGoogle, addressWithGoogle } = require("./service/geocodeService");
|
||||||
|
// const { fetchJson } = require("./service/fetchService");
|
||||||
|
// const { BASE_BACKOFF_MS, MAX_RETRIES, REQUEST_DELAY_MS, sleep } = require("./service/retryService");
|
||||||
|
// const { API_URL, HEADERS } = require("./config/apiConfig");
|
||||||
|
// const { normalizePartnerSigla } = require("./service/normalizeService");
|
||||||
|
// const authRoutes = require("./routes/authRoutes.js");
|
||||||
|
|
||||||
|
// function createApp() {
|
||||||
|
// const upload = multer({ dest: "uploads/" });
|
||||||
|
// const app = express();
|
||||||
|
|
||||||
|
// // se estiver atrás de um reverse proxy (nginx/traefik) em produção, habilite:
|
||||||
|
// app.set("trust proxy", 1);
|
||||||
|
|
||||||
|
// app.use(cors());
|
||||||
|
// app.use(express.json());
|
||||||
|
|
||||||
|
// // session TEM que vir antes das rotas que usam req.session
|
||||||
|
// app.use(
|
||||||
|
// session({
|
||||||
|
// secret: process.env.SESSION_SECRET || "change-me",
|
||||||
|
// resave: false,
|
||||||
|
// saveUninitialized: false,
|
||||||
|
// cookie: {
|
||||||
|
// maxAge: 24 * 60 * 60 * 1000,
|
||||||
|
// secure: process.env.NODE_ENV === "production",
|
||||||
|
// sameSite: "lax",
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // -- Desenvolvimento: pular autenticação se configurado
|
||||||
|
// // Para ativar: defina NODE_ENV=development e DEV_SKIP_AUTH=true no .env
|
||||||
|
// if (process.env.NODE_ENV === 'development' && process.env.DEV_SKIP_AUTH === 'true') {
|
||||||
|
// app.use((req, res, next) => {
|
||||||
|
// // garante que exista uma sessão autenticada para facilitar testes locais
|
||||||
|
// if (req.session && (!req.session.user || !req.session.user.authenticated)) {
|
||||||
|
// req.session.user = { authenticated: true, dev: true };
|
||||||
|
// }
|
||||||
|
// next();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // middleware que protege rotas que exigem login
|
||||||
|
// function requireAuth(req, res, next) {
|
||||||
|
// if (req.session?.user?.authenticated) {
|
||||||
|
// return next();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (req.xhr || req.headers.accept?.includes("application/json")) {
|
||||||
|
// return res.status(401).json({ error: "not_authenticated" });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return res.redirect("/login");
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// // proteger demais rotas (ex.: /upload, /consulta)
|
||||||
|
// app.use((req, res, next) => {
|
||||||
|
// // permissão liberada para rotas de auth já tratadas; proteger o resto
|
||||||
|
// if (req.path.startsWith("/auth") || req.path === "/login") return next();
|
||||||
|
// return requireAuth(req, res, next);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // redirect raiz
|
||||||
|
// app.get("/", (req, res) => {
|
||||||
|
// // em dev com bypass, sirva a página diretamente (sem redirect)
|
||||||
|
// if (process.env.NODE_ENV === 'development' && process.env.DEV_SKIP_AUTH === 'true') {
|
||||||
|
// return res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (req.session?.user?.authenticated) {
|
||||||
|
// return res.redirect("/public/index.html");
|
||||||
|
// }
|
||||||
|
// return res.redirect("/login");
|
||||||
|
// });
|
||||||
|
|
||||||
|
// /////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// async function getMinDistance(lat, lon) {
|
||||||
|
// // tenta várias vezes com backoff exponencial; trata 429 usando Retry-After se disponível
|
||||||
|
// let attempt = 0;
|
||||||
|
// while (attempt < MAX_RETRIES) {
|
||||||
|
// try {
|
||||||
|
// // envia também o raio (em metros) - API espera esse parâmetro em várias rotas
|
||||||
|
// const resp = await axios.get(API_URL, {
|
||||||
|
// headers: HEADERS,
|
||||||
|
// params: {
|
||||||
|
// raio: 5000,
|
||||||
|
// latitude: lat,
|
||||||
|
// longitude: lon,
|
||||||
|
// "itens[]": ["caixa"],
|
||||||
|
// consultarPasta: "S",
|
||||||
|
// },
|
||||||
|
// timeout: 10000,
|
||||||
|
// });
|
||||||
|
// const data = resp.data;
|
||||||
|
// const registros = data && data.registros ? data.registros : [];
|
||||||
|
// // find registros that have a numeric distancia and keep original object for robust extraction
|
||||||
|
// const candidates = registros
|
||||||
|
// .map((r) => ({ raw: r, distanciaRaw: r && r.distancia }))
|
||||||
|
// .map((o) => ({
|
||||||
|
// raw: o.raw,
|
||||||
|
// num:
|
||||||
|
// o.distanciaRaw !== undefined &&
|
||||||
|
// o.distanciaRaw !== null &&
|
||||||
|
// o.distanciaRaw !== ""
|
||||||
|
// ? Number(o.distanciaRaw)
|
||||||
|
// : null,
|
||||||
|
// }))
|
||||||
|
// .filter((x) => x.num !== null && !Number.isNaN(x.num));
|
||||||
|
// if (candidates.length) {
|
||||||
|
// candidates.sort((a, b) => a.num - b.num);
|
||||||
|
// const best = candidates[0];
|
||||||
|
// const r = best.raw || {};
|
||||||
|
// // robust extraction of pasta sigla with fallbacks
|
||||||
|
// let pastaSigla = null;
|
||||||
|
// try {
|
||||||
|
// if (r.pasta) {
|
||||||
|
// if (typeof r.pasta === "string" && r.pasta.trim())
|
||||||
|
// pastaSigla = r.pasta.trim();
|
||||||
|
// else if (r.pasta.sigla && String(r.pasta.sigla).trim())
|
||||||
|
// pastaSigla = String(r.pasta.sigla).trim();
|
||||||
|
// else if (
|
||||||
|
// r.pasta.cidade &&
|
||||||
|
// r.pasta.cidade.sigla &&
|
||||||
|
// String(r.pasta.cidade.sigla).trim()
|
||||||
|
// )
|
||||||
|
// pastaSigla = String(r.pasta.cidade.sigla).trim();
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// pastaSigla = null;
|
||||||
|
// }
|
||||||
|
// // if closest has no pastaSigla, try find any candidate with non-empty sigla
|
||||||
|
// if (!pastaSigla) {
|
||||||
|
// for (let j = 0; j < candidates.length; j++) {
|
||||||
|
// const rr = candidates[j].raw || {};
|
||||||
|
// try {
|
||||||
|
// if (rr.pasta) {
|
||||||
|
// if (typeof rr.pasta === "string" && rr.pasta.trim()) {
|
||||||
|
// pastaSigla = rr.pasta.trim();
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// if (rr.pasta.sigla && String(rr.pasta.sigla).trim()) {
|
||||||
|
// pastaSigla = String(rr.pasta.sigla).trim();
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// if (
|
||||||
|
// rr.pasta.cidade &&
|
||||||
|
// rr.pasta.cidade.sigla &&
|
||||||
|
// String(rr.pasta.cidade.sigla).trim()
|
||||||
|
// ) {
|
||||||
|
// pastaSigla = String(rr.pasta.cidade.sigla).trim();
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// // continue
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (!pastaSigla)
|
||||||
|
// console.warn(
|
||||||
|
// `[WARN] Nenhuma pasta.sigla encontrada para coordenadas ${lat},${lon} (closest dist ${best.num})`
|
||||||
|
// );
|
||||||
|
// pastaSigla = normalizePartnerSigla(pastaSigla);
|
||||||
|
// return { dist: best.num, pastaSigla };
|
||||||
|
// }
|
||||||
|
// // sem distancias válidas
|
||||||
|
// return null;
|
||||||
|
// } catch (err) {
|
||||||
|
// attempt += 1;
|
||||||
|
// // se for 429, tente respeitar Retry-After quando disponível
|
||||||
|
// if (err.response && err.response.status === 429) {
|
||||||
|
// const ra =
|
||||||
|
// err.response.headers &&
|
||||||
|
// (err.response.headers["retry-after"] ||
|
||||||
|
// err.response.headers["Retry-After"]);
|
||||||
|
// let waitMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
|
||||||
|
// if (ra) {
|
||||||
|
// const raSec = parseInt(ra, 10);
|
||||||
|
// if (!isNaN(raSec)) waitMs = raSec * 1000;
|
||||||
|
// }
|
||||||
|
// console.warn(
|
||||||
|
// `[WARN] 429 recebido para ${lat},${lon} - aguardando ${waitMs}ms e tentando novamente (attempt ${attempt}/${MAX_RETRIES})`
|
||||||
|
// );
|
||||||
|
// await sleep(waitMs);
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // para outros erros de rede/timeout, aguarda backoff exponencial e tenta de novo
|
||||||
|
// const waitMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
|
||||||
|
// console.warn(
|
||||||
|
// `[WARN] Erro ao consultar API para ${lat},${lon}: ${err.message} - backoff ${waitMs}ms (attempt ${attempt}/${MAX_RETRIES})`
|
||||||
|
// );
|
||||||
|
// await sleep(waitMs);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// // exauriu tentativas
|
||||||
|
// console.error(`[ERROR] Exauriu retries para ${lat},${lon}`);
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // upload CSV endpoint
|
||||||
|
// const jobs = {}; // jobId -> { status, total, processed, download, error }
|
||||||
|
|
||||||
|
// app.post("/upload", upload.single("csvfile"), (req, res) => {
|
||||||
|
// if (!req.file)
|
||||||
|
// return res.status(400).json({ error: "Nenhum arquivo enviado" });
|
||||||
|
// const filePath = req.file.path;
|
||||||
|
// const jobId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
// jobs[jobId] = {
|
||||||
|
// status: "queued",
|
||||||
|
// total: 0,
|
||||||
|
// processed: 0,
|
||||||
|
// download: null,
|
||||||
|
// error: null,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// (async () => {
|
||||||
|
// jobs[jobId].status = "processing";
|
||||||
|
// try {
|
||||||
|
// const rows = [];
|
||||||
|
// await new Promise((resolve, reject) => {
|
||||||
|
// fs.createReadStream(filePath)
|
||||||
|
// .pipe(csv({ separator: ";" }))
|
||||||
|
// .on("data", (data) => {
|
||||||
|
// // ignora linhas totalmente vazias (todos os campos nulos/undefined/strings vazias)
|
||||||
|
// const values = Object.values(data);
|
||||||
|
// const allEmpty =
|
||||||
|
// values.length === 0 ||
|
||||||
|
// values.every(
|
||||||
|
// (v) => v === null || v === undefined || String(v).trim() === ""
|
||||||
|
// );
|
||||||
|
// if (!allEmpty) rows.push(data);
|
||||||
|
// })
|
||||||
|
// .on("end", resolve)
|
||||||
|
// .on("error", reject);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// jobs[jobId].total = rows.length;
|
||||||
|
|
||||||
|
// const coordCache = new Map();
|
||||||
|
|
||||||
|
// const outRows = [];
|
||||||
|
// for (let i = 0; i < rows.length; i++) {
|
||||||
|
// const row = rows[i];
|
||||||
|
// // normalize keys to avoid duplicates caused by different headers
|
||||||
|
// const norm = {};
|
||||||
|
// Object.keys(row).forEach((k) => {
|
||||||
|
// // normalize header: lowercase, remove diacritics and non-alphanumeric
|
||||||
|
// const kn = k
|
||||||
|
// .trim()
|
||||||
|
// .toLowerCase()
|
||||||
|
// .normalize("NFKD")
|
||||||
|
// .replace(/[\u0300-\u036f]/g, "")
|
||||||
|
// .replace(/[^a-z0-9]/g, "");
|
||||||
|
// norm[kn] = row[k];
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Input columns (from normalized map)
|
||||||
|
// const rawCep = norm["cep"]
|
||||||
|
// ? String(norm["cep"]).replace(/\D/g, "")
|
||||||
|
// : "";
|
||||||
|
// const rawNumero = norm["numero"] ? String(norm["numero"]).trim() : "";
|
||||||
|
// // prefer lat/lon from normalized input if available
|
||||||
|
// const rawLat = norm["latitude"] || norm["lat"] || null;
|
||||||
|
// const rawLon =
|
||||||
|
// norm["longitude"] || norm["lon"] || norm["long"] || null;
|
||||||
|
|
||||||
|
// // Prefer existing lat/lon if provided from normalized fields
|
||||||
|
// let lat = null,
|
||||||
|
// lon = null;
|
||||||
|
// if (rawLat && rawLon) {
|
||||||
|
// lat = Number(String(rawLat).replace(",", "."));
|
||||||
|
// lon = Number(String(rawLon).replace(",", "."));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let builtAddress = "";
|
||||||
|
|
||||||
|
// const googleGeocodeAddress = await addressWithGoogle(lat, lon);
|
||||||
|
// if (googleGeocodeAddress) {
|
||||||
|
// builtAddress = googleGeocodeAddress;
|
||||||
|
// } else {
|
||||||
|
// console.warn(
|
||||||
|
// `Google Reverse Geocoding não retornou resultado para coords ${lat},${lon} (row ${i + 1})`
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// console.log(`Row ${i + 1}: CEP='${rawCep}' Número='${rawNumero}' Lat='${lat}' Lon='${lon}' BuiltAddress='${builtAddress}'`);
|
||||||
|
|
||||||
|
// // If no coords, try ViaCEP -> Google
|
||||||
|
// if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
||||||
|
// if (rawCep) {
|
||||||
|
// const cep8 = rawCep.padStart(8, "0");
|
||||||
|
// const cepRestData = await fetch(
|
||||||
|
// 'https://api.cep.rest/', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
|
// body: JSON.stringify({ cep: cep8 })
|
||||||
|
// }
|
||||||
|
// ).then(r => r.json());
|
||||||
|
// if (cepRestData && !cepRestData.erro) {
|
||||||
|
// const logradouro = cepRestData.data.logradouro || "";
|
||||||
|
// const bairro = cepRestData.data.bairro || "";
|
||||||
|
// const cidade = cepRestData.data.localidade || "";
|
||||||
|
// const uf = cepRestData.data.uf || "";
|
||||||
|
// if (logradouro) {
|
||||||
|
// builtAddress =
|
||||||
|
// `${logradouro}, ${rawNumero}, ${bairro}, ${cidade} - ${uf}`
|
||||||
|
// .replace(/, ,/g, ",")
|
||||||
|
// .replace(/^,\s*/, "");
|
||||||
|
// } else {
|
||||||
|
// // fallback: use neighborhood/city
|
||||||
|
// builtAddress = `${bairro || ""} ${
|
||||||
|
// cidade ? ", " + cidade : ""
|
||||||
|
// } ${uf ? "- " + uf : ""}`.trim();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // build addressToUse (builtAddress already assembled above)
|
||||||
|
// if (!process.env.GOOGLE_API_KEY) {
|
||||||
|
// console.error(
|
||||||
|
// "[ERROR] GOOGLE_API_KEY não definida. Não será possível geocodificar. Defina a chave no .env ou em process.env"
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// const addressToUse =
|
||||||
|
// builtAddress || `${cidade} ${uf} ${cep8}`;
|
||||||
|
// const geo = await geocodeWithGoogle(addressToUse);
|
||||||
|
// if (geo) {
|
||||||
|
// lat = geo.lat;
|
||||||
|
// lon = geo.lon;
|
||||||
|
// } else
|
||||||
|
// console.warn(
|
||||||
|
// `Google Geocoding não retornou resultado para '${addressToUse}' (CEP ${cep8}, row ${
|
||||||
|
// i + 1
|
||||||
|
// })`
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// console.warn(`ViaCEP erro for CEP ${rawCep} (row ${i + 1})`);
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// console.log(`Row ${i + 1}: missing/invalid CEP -> '${rawCep}'`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Prepare explicit output row to avoid extra columns
|
||||||
|
// const out = {
|
||||||
|
// CEP: rawCep || "",
|
||||||
|
// Número: rawNumero || "",
|
||||||
|
// Endereço: builtAddress || "",
|
||||||
|
// // write lat/lon as strings with dot decimal and fixed precision to avoid locale swaps
|
||||||
|
// Latitude: Number.isFinite(lat) ? Number(lat).toFixed(6) : "",
|
||||||
|
// Longitude: Number.isFinite(lon) ? Number(lon).toFixed(6) : "",
|
||||||
|
// "Não dedicado": "",
|
||||||
|
// Dedicado: "",
|
||||||
|
// Distancia: "",
|
||||||
|
// "Parceiro/Sothis": "",
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (Number.isFinite(lat) && Number.isFinite(lon)) {
|
||||||
|
// const coordKey = `${lat.toFixed(6)},${lon.toFixed(6)}`;
|
||||||
|
// if (coordCache.has(coordKey)) {
|
||||||
|
// const cached = coordCache.get(coordKey); // cached is either null or { dist, pastaSigla }
|
||||||
|
// if (cached !== null) {
|
||||||
|
// const d = cached.dist;
|
||||||
|
// const di = Math.round(Number(d));
|
||||||
|
// out["Não dedicado"] = di <= 500 ? "viável" : "Não viável";
|
||||||
|
// out["Dedicado"] = di <= 1000 ? "viável" : "Não viável";
|
||||||
|
// out["Distancia"] = `${di}M`;
|
||||||
|
// out["Parceiro/Sothis"] =
|
||||||
|
// normalizePartnerSigla(cached.pastaSigla) || "";
|
||||||
|
// } else {
|
||||||
|
// out["Não dedicado"] = "Não viável";
|
||||||
|
// out["Dedicado"] = "Não viável";
|
||||||
|
// out["Distancia"] = "5km +";
|
||||||
|
// out["Parceiro/Sothis"] = "";
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// const minResult = await getMinDistance(lat, lon); // { dist, pastaSigla } or null
|
||||||
|
// coordCache.set(coordKey, minResult);
|
||||||
|
// if (minResult !== null) {
|
||||||
|
// const di = Math.round(Number(minResult.dist));
|
||||||
|
// out["Não dedicado"] = di <= 500 ? "viável" : "Não viável";
|
||||||
|
// out["Dedicado"] = di <= 1000 ? "viável" : "Não viável";
|
||||||
|
// out["Distancia"] = `${di}M`;
|
||||||
|
// out["Parceiro/Sothis"] =
|
||||||
|
// normalizePartnerSigla(minResult.pastaSigla) || "";
|
||||||
|
// } else {
|
||||||
|
// out["Não dedicado"] = "Não viável";
|
||||||
|
// out["Dedicado"] = "Não viável";
|
||||||
|
// out["Distancia"] = "5km +";
|
||||||
|
// out["Parceiro/Sothis"] = "";
|
||||||
|
// }
|
||||||
|
// await sleep(REQUEST_DELAY_MS);
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // no coords available -> keep defaults
|
||||||
|
// }
|
||||||
|
|
||||||
|
// outRows.push(out);
|
||||||
|
// jobs[jobId].processed = i + 1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // write output csv - use explicit outRows and fixed header order
|
||||||
|
// const outPath = path.join(__dirname, "outputs");
|
||||||
|
// if (!fs.existsSync(outPath)) fs.mkdirSync(outPath);
|
||||||
|
// const originalName =
|
||||||
|
// req.file && req.file.originalname
|
||||||
|
// ? req.file.originalname
|
||||||
|
// : `upload_${Date.now()}.csv`;
|
||||||
|
// const parsed = path.parse(originalName);
|
||||||
|
// let outBase = `${parsed.name}_output`;
|
||||||
|
// let outFile = path.join(outPath, `${outBase}.csv`);
|
||||||
|
// if (fs.existsSync(outFile)) {
|
||||||
|
// outFile = path.join(outPath, `${outBase}_${Date.now()}.csv`);
|
||||||
|
// }
|
||||||
|
// const headers = [
|
||||||
|
// "CEP",
|
||||||
|
// "Número",
|
||||||
|
// "Endereço",
|
||||||
|
// "Latitude",
|
||||||
|
// "Longitude",
|
||||||
|
// "Não dedicado",
|
||||||
|
// "Dedicado",
|
||||||
|
// "Distancia",
|
||||||
|
// "Parceiro/Sothis",
|
||||||
|
// ];
|
||||||
|
// await new Promise((resolve, reject) => {
|
||||||
|
// const ws = fs.createWriteStream(outFile);
|
||||||
|
// ws.write("\uFEFF");
|
||||||
|
// fastCsv
|
||||||
|
// .write(outRows, { headers: headers, delimiter: ";" })
|
||||||
|
// .pipe(ws)
|
||||||
|
// .on("finish", resolve)
|
||||||
|
// .on("error", reject);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// fs.unlinkSync(filePath);
|
||||||
|
// } catch (e) {}
|
||||||
|
|
||||||
|
// jobs[jobId].status = "done";
|
||||||
|
// jobs[jobId].download = `/download/${path.basename(outFile)}`;
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error(err);
|
||||||
|
// jobs[jobId].status = "error";
|
||||||
|
// jobs[jobId].error = String(err.message || err);
|
||||||
|
// }
|
||||||
|
// })();
|
||||||
|
|
||||||
|
// return res.json({ jobId });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // download model endpoint
|
||||||
|
// app.get("/download-model/:name", (req, res) => {
|
||||||
|
// const name = req.params.name;
|
||||||
|
// const safeName = path.basename(name); // evita ../
|
||||||
|
// const filePath = path.join(__dirname, "models", safeName);
|
||||||
|
// if (!fs.existsSync(filePath)) return res.status(404).send("Arquivo não encontrado");
|
||||||
|
// return res.download(filePath, safeName);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // lista os arquivos disponíveis em /models
|
||||||
|
// app.get("/download-models/list", (req, res) => {
|
||||||
|
// const modelDir = path.join(__dirname, "models");
|
||||||
|
// if (!fs.existsSync(modelDir)) return res.status(404).json({ error: "Pasta de modelos não encontrada" });
|
||||||
|
// const files = fs.readdirSync(modelDir).filter((f) => fs.statSync(path.join(modelDir, f)).isFile());
|
||||||
|
// return res.json({ files });
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// // download result endpoint
|
||||||
|
// app.get("/download/:name", (req, res) => {
|
||||||
|
// const name = req.params.name;
|
||||||
|
// const p = path.join(__dirname, "outputs", name);
|
||||||
|
// if (!fs.existsSync(p))
|
||||||
|
// return res.status(404).send("Arquivo não encontrado");
|
||||||
|
// res.download(p);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // job status endpoint
|
||||||
|
// app.get("/status/:jobId", (req, res) => {
|
||||||
|
// const job = jobs[req.params.jobId];
|
||||||
|
// if (!job) return res.status(404).json({ error: "job não encontrado" });
|
||||||
|
// return res.json(job);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // manual query endpoint
|
||||||
|
// // /consulta now accepts either latitude+longitude OR cep+numero. If cep is provided we resolve ViaCEP -> Google -> Geogrid
|
||||||
|
// app.get("/consulta", async (req, res) => {
|
||||||
|
// const {
|
||||||
|
// latitude: rawLat,
|
||||||
|
// longitude: rawLon,
|
||||||
|
// cep: rawCep,
|
||||||
|
// numero: rawNumero,
|
||||||
|
// } = req.query;
|
||||||
|
|
||||||
|
// // If cep provided, use ViaCEP -> Google geocoding -> Geogrid
|
||||||
|
// if (rawCep) {
|
||||||
|
// const cep = String(rawCep).replace(/\D/g, "");
|
||||||
|
// const numero = rawNumero ? String(rawNumero).trim() : "";
|
||||||
|
// try {
|
||||||
|
// const viaCepData = await fetch(
|
||||||
|
// 'https://api.cep.rest/', { method: 'POST',
|
||||||
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
|
// body: JSON.stringify({ cep })
|
||||||
|
// }
|
||||||
|
// ).then(r => r.json());
|
||||||
|
// if (!viaCepData || viaCepData.erro)
|
||||||
|
// return res.status(404).json({ error: "CEP não encontrado" });
|
||||||
|
// const logradouro = viaCepData.data.logradouro || "";
|
||||||
|
// const bairro = viaCepData.data.bairro || "";
|
||||||
|
// const cidade = viaCepData.data.localidade || "";
|
||||||
|
// const uf = viaCepData.data.uf || "";
|
||||||
|
// const endereco =
|
||||||
|
// `${logradouro}, ${numero}, ${bairro}, ${cidade} - ${uf}`
|
||||||
|
// .replace(/, ,/g, ",")
|
||||||
|
// .replace(/^,\s*/, "");
|
||||||
|
|
||||||
|
// if (!process.env.GOOGLE_API_KEY)
|
||||||
|
// return res
|
||||||
|
// .status(500)
|
||||||
|
// .json({ error: "GOOGLE_API_KEY não definida no servidor" });
|
||||||
|
// const geo = await geocodeWithGoogle(
|
||||||
|
// endereco || `${cidade} ${uf} ${cep}`
|
||||||
|
// );
|
||||||
|
// if (!geo)
|
||||||
|
// return res
|
||||||
|
// .status(404)
|
||||||
|
// .json({ error: "geocode não encontrado (Google)" });
|
||||||
|
// const lat = Number(geo.lat);
|
||||||
|
// const lon = Number(geo.lon);
|
||||||
|
// const result = await getMinDistance(lat, lon);
|
||||||
|
// if (result && result.dist !== undefined) {
|
||||||
|
// return res.json({
|
||||||
|
// endereco,
|
||||||
|
// latitude: lat,
|
||||||
|
// longitude: lon,
|
||||||
|
// distancia: result.dist,
|
||||||
|
// parceiro: result.pastaSigla || "",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// return res.json({
|
||||||
|
// endereco,
|
||||||
|
// latitude: lat,
|
||||||
|
// longitude: lon,
|
||||||
|
// distancia: "5km +",
|
||||||
|
// });
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error(err);
|
||||||
|
// return res.status(500).json({ error: "Erro na consulta" });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Otherwise require latitude+longitude
|
||||||
|
// if (!rawLat || !rawLon)
|
||||||
|
// return res.status(400).json({
|
||||||
|
// error: "latitude e longitude são obrigatórios (ou forneça cep)",
|
||||||
|
// });
|
||||||
|
// const latitude = Number(String(rawLat).replace(",", "."));
|
||||||
|
// const longitude = Number(String(rawLon).replace(",", "."));
|
||||||
|
// if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
||||||
|
// console.warn(
|
||||||
|
// `Consulta manual com parâmetros inválidos: lat='${rawLat}' lon='${rawLon}'`
|
||||||
|
// );
|
||||||
|
// return res.status(400).json({ error: "latitude ou longitude inválidos" });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// console.log(`Consulta manual: lat=${latitude} lon=${longitude}`);
|
||||||
|
// const result = await getMinDistance(latitude, longitude);
|
||||||
|
// console.log(`Resultado consulta manual: ${JSON.stringify(result)}`);
|
||||||
|
// if (result && result.dist !== undefined) {
|
||||||
|
// return res.json({
|
||||||
|
// distancia: result.dist,
|
||||||
|
// parceiro: result.pastaSigla || "",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// return res.json({ distancia: "5km +" });
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error(err);
|
||||||
|
// return res.status(500).json({ error: "Erro na consulta" });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // manual CEP+Numero query: resolves ViaCEP -> Nominatim -> Geogrid
|
||||||
|
// app.get("/consulta-cep", async (req, res) => {
|
||||||
|
// const { cep: rawCep, numero: rawNumero } = req.query;
|
||||||
|
// if (!rawCep) return res.status(400).json({ error: "cep é obrigatório" });
|
||||||
|
// const cep = String(rawCep).replace(/\D/g, "");
|
||||||
|
// const numero = rawNumero ? String(rawNumero).trim() : "";
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const cepRestData = await fetch(
|
||||||
|
// 'https://api.cep.rest/', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
|
// body: JSON.stringify({ cep })
|
||||||
|
// }
|
||||||
|
// ).then(r => r.json());
|
||||||
|
// if (!cepRestData || cepRestData.erro)
|
||||||
|
// return res.status(404).json({ error: "CEP não encontrado" });
|
||||||
|
// const logradouro = cepRestData.data.logradouro || "";
|
||||||
|
// const bairro = cepRestData.data.bairro || "";
|
||||||
|
// const cidade = cepRestData.data.localidade || "";
|
||||||
|
// const uf = cepRestData.data.uf || "";
|
||||||
|
// const endereco = `${logradouro}, ${numero}, ${bairro}, ${cidade} - ${uf}`
|
||||||
|
// .replace(/, ,/g, ",")
|
||||||
|
// .replace(/^,\s*/, "");
|
||||||
|
|
||||||
|
// if (!process.env.GOOGLE_API_KEY)
|
||||||
|
// return res
|
||||||
|
// .status(500)
|
||||||
|
// .json({ error: "GOOGLE_API_KEY não definida no servidor" });
|
||||||
|
// const geo = await geocodeWithGoogle(endereco || `${cidade} ${uf} ${cep}`);
|
||||||
|
// if (!geo)
|
||||||
|
// return res
|
||||||
|
// .status(404)
|
||||||
|
// .json({ error: "geocode não encontrado (Google)" });
|
||||||
|
// const lat = Number(geo.lat);
|
||||||
|
// const lon = Number(geo.lon);
|
||||||
|
// const result = await getMinDistance(lat, lon);
|
||||||
|
// if (result && result.dist !== undefined) {
|
||||||
|
// return res.json({
|
||||||
|
// endereco,
|
||||||
|
// latitude: lat,
|
||||||
|
// longitude: lon,
|
||||||
|
// distancia: result.dist,
|
||||||
|
// parceiro: result.pastaSigla || "",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// return res.json({
|
||||||
|
// endereco,
|
||||||
|
// latitude: lat,
|
||||||
|
// longitude: lon,
|
||||||
|
// distancia: "5km +",
|
||||||
|
// });
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error(err);
|
||||||
|
// return res.status(500).json({ error: "erro na consulta" });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// ////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// // Servir arquivos estáticos (index.html)
|
||||||
|
// // app.use("/public", express.static(path.join(__dirname, "public")));
|
||||||
|
|
||||||
|
// // Usa as rotas de autenticação
|
||||||
|
// app.use("/", authRoutes);
|
||||||
|
|
||||||
|
// // servir arquivos estáticos da pasta public (rotas protegidas já são tratadas pelo middleware global)
|
||||||
|
// app.use(express.static(path.join(__dirname, "public")));
|
||||||
|
|
||||||
|
// // rota protegida que serve o index.html
|
||||||
|
// app.get("/app", requireAuth, (req, res) => {
|
||||||
|
// res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
// /////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// return app;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// module.exports = {
|
||||||
|
// createApp,
|
||||||
|
// };
|
||||||
@ -19,4 +19,7 @@ const apiConfig = {
|
|||||||
COOKIE,
|
COOKIE,
|
||||||
HEADERS
|
HEADERS
|
||||||
};
|
};
|
||||||
module.exports = apiConfig;
|
|
||||||
|
const apiViabilidadeUrl = process.env.API_VIABILIDADE_URL;
|
||||||
|
|
||||||
|
module.exports = { apiConfig, apiViabilidadeUrl };
|
||||||
54
controller/viabilidadeController.js
Normal file
54
controller/viabilidadeController.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
const { consultarViabilidade } = require('../service/viabilidadeService');
|
||||||
|
const { processCsvFile } = require('../service/csvService');
|
||||||
|
const { getJob } = require('../service/jobStore.service');
|
||||||
|
|
||||||
|
|
||||||
|
// Controlador para consultar viabilidade
|
||||||
|
async function consultarViabilidadeController(req, res) {
|
||||||
|
try {
|
||||||
|
const data = req.body;
|
||||||
|
const result = await consultarViabilidade(data);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao consultar viabilidade:", error && (error.message || error));
|
||||||
|
res.status(500).json({ error: "Erro ao consultar viabilidade" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadCsvFile(req, res) {
|
||||||
|
try {
|
||||||
|
// validação simples: verifica se multer populou req.file
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'Nenhum arquivo enviado. Campo do form deve ser "csvfile".' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = req.file.path;
|
||||||
|
const originalName = req.file.originalname || req.file.filename || 'input.csv';
|
||||||
|
|
||||||
|
const out = await processCsvFile(filePath, originalName);
|
||||||
|
|
||||||
|
// normaliza retorno (processCsvFile pode retornar string ou objeto)
|
||||||
|
const outputPath = (typeof out === 'string') ? out : (out && out.outputPath) || null;
|
||||||
|
|
||||||
|
return res.json({ outputPath });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao processar CSV:", error && (error.message || error));
|
||||||
|
return res.status(500).json({ error: 'Erro ao processar CSV' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJobController(req, res) {
|
||||||
|
try {
|
||||||
|
const jobId = req.params.jobId;
|
||||||
|
const job = getJob(jobId);
|
||||||
|
if (!job) {
|
||||||
|
return res.status(404).json({ error: 'Job não encontrado' });
|
||||||
|
}
|
||||||
|
res.json(job);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao obter job:", error && (error.message || error));
|
||||||
|
return res.status(500).json({ error: 'Erro ao obter job' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { consultarViabilidadeController, uploadCsvFile, getJobController };
|
||||||
@ -56,15 +56,31 @@ document.getElementById('btnConsultaCep').addEventListener('click', async () =>
|
|||||||
const el = document.getElementById('consultaResult');
|
const el = document.getElementById('consultaResult');
|
||||||
el.innerText = 'Consultando...';
|
el.innerText = 'Consultando...';
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`/consulta-cep?cep=${encodeURIComponent(cep)}&numero=${encodeURIComponent(numero)}`);
|
// usar fetch ao invés de axios para evitar dependência no cliente
|
||||||
|
const resp = await fetch('/viabilidade', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ cep, numero })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
const text = await resp.text().catch(() => '');
|
||||||
|
throw new Error(`Status ${resp.status} ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
||||||
if (data.distancia) {
|
if (data.distancia) {
|
||||||
el.innerText = `Endereço: ${data.endereco}\nLat: ${data.latitude} Lon: ${data.longitude}\nDistância: ${data.distancia}`;
|
el.innerText = `Endereço: ${data.logradouro}, ${data.bairro}, ${data.cidade}/${data.estado}, ${data.cep}\nDistância: ${data.distancia}`;
|
||||||
} else if (data.error) {
|
} else if (data.error) {
|
||||||
el.innerText = 'Erro: ' + data.error;
|
el.innerText = 'Erro: ' + data.error;
|
||||||
|
} else {
|
||||||
|
el.innerText = 'Resposta inesperada da API';
|
||||||
|
console.log('Resposta completa:', data);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
el.innerText = 'Erro na consulta';
|
console.error(e);
|
||||||
|
el.innerText = 'Erro na consulta: ' + (e.message || e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
24
routes/viabilidadeRoutes.js
Normal file
24
routes/viabilidadeRoutes.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const path = require('path');
|
||||||
|
const multer = require('multer');
|
||||||
|
const viabilidadeController = require('../controller/viabilidadeController');
|
||||||
|
|
||||||
|
// grava uploads temporários em /uploads
|
||||||
|
const upload = multer({ dest: path.join(__dirname, '..', 'uploads') });
|
||||||
|
|
||||||
|
// Rota para consultar viabilidade
|
||||||
|
router.post('/viabilidade', viabilidadeController.consultarViabilidadeController);
|
||||||
|
|
||||||
|
// rota de upload agora usa multer.single('csvfile')
|
||||||
|
router.post('/upload', upload.single('csvfile'), viabilidadeController.uploadCsvFile);
|
||||||
|
|
||||||
|
router.get('/status/:jobId', (req, res) => {
|
||||||
|
const job = viabilidadeController.getJobController(req.params.jobId);
|
||||||
|
if (!job) {
|
||||||
|
return res.status(404).json({ error: 'Job não encontrado' });
|
||||||
|
}
|
||||||
|
res.json(job);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
40
server.js
40
server.js
@ -1,17 +1,31 @@
|
|||||||
require('dotenv').config();
|
const http = require('http');
|
||||||
const { createApp } = require('./app');
|
const app = require('./app');
|
||||||
|
|
||||||
const app = createApp();
|
const port = parseInt(process.env.PORT, 10) || 3000;
|
||||||
// garante fallback caso Plesk não exponha PORT
|
const server = http.createServer(app);
|
||||||
const port = process.env.PORT || 3000;
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
server.listen(port, () => {
|
||||||
console.log(`Server running on http://localhost:${port} (NODE_ENV=${process.env.NODE_ENV || 'development'})`);
|
console.log(`Server listening on port ${port} (env=${process.env.NODE_ENV || 'production'})`);
|
||||||
|
if (process.env.NODE_ENV === 'development' && process.env.DEV_SKIP_AUTH === 'true') {
|
||||||
|
console.log('[START-NOAUTH] DEV_SKIP_AUTH=true — authentication is bypassed');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('uncaughtException', err => {
|
function shutdown(signal) {
|
||||||
console.error('uncaughtException', err);
|
console.log(`Received ${signal}, shutting down...`);
|
||||||
});
|
server.close(() => {
|
||||||
process.on('unhandledRejection', err => {
|
console.log('Server closed.');
|
||||||
console.error('unhandledRejection', err);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
// force exit after 10s
|
||||||
|
setTimeout(() => {
|
||||||
|
console.error('Forcing shutdown.');
|
||||||
|
process.exit(1);
|
||||||
|
}, 10000).unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
|
|
||||||
|
// export for tests / scripts
|
||||||
|
module.exports = server;
|
||||||
145
service/csvService.js
Normal file
145
service/csvService.js
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
const { consultarViabilidade } = require('./viabilidadeService');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const readline = require('readline');
|
||||||
|
const { once } = require('events');
|
||||||
|
const {
|
||||||
|
createJob,
|
||||||
|
incrementProcessed,
|
||||||
|
incrementErrors,
|
||||||
|
finishJob,
|
||||||
|
failJob
|
||||||
|
} = require('./jobStore.service');
|
||||||
|
|
||||||
|
// conta linhas válidas no CSV (com CEP e Número)
|
||||||
|
async function countValidLines(inputPath) {
|
||||||
|
const instream = fs.createReadStream(inputPath, { encoding: 'utf8' });
|
||||||
|
const rl = readline.createInterface({ input: instream, crlfDelay: Infinity });
|
||||||
|
|
||||||
|
let isHeader = true;
|
||||||
|
let headers = [];
|
||||||
|
let idxCep = -1;
|
||||||
|
let idxNumero = -1;
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for await (const rawLine of rl) {
|
||||||
|
const line = rawLine.replace(/\r$/, '');
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
|
if (isHeader) {
|
||||||
|
headers = line.split(';').map(h => h.trim());
|
||||||
|
const lower = headers.map(h => h.toLowerCase());
|
||||||
|
idxCep = lower.indexOf('cep');
|
||||||
|
idxNumero = lower.indexOf('numero');
|
||||||
|
isHeader = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cols = line.split(';').map(c => c.trim());
|
||||||
|
const cep = idxCep >= 0 ? String(cols[idxCep] || '').replace(/\D/g, '') : '';
|
||||||
|
const numero = idxNumero >= 0 ? cols[idxNumero] : '';
|
||||||
|
|
||||||
|
if (cep && numero) total++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nova função: processa CSV linha a linha, chama consultarViabilidade e gera CSV de saída
|
||||||
|
async function processCsvFile(inputPath) {
|
||||||
|
const total = await countValidLines(inputPath);
|
||||||
|
const jobId = createJob(total);
|
||||||
|
const baseName = path.parse(inputPath).name;
|
||||||
|
const outputFilename = `processed_${Date.now()}_${baseName}.csv`;
|
||||||
|
const outputPath = path.join(__dirname, '..', 'outputs', outputFilename);
|
||||||
|
|
||||||
|
const instream = fs.createReadStream(inputPath, { encoding: 'utf8' });
|
||||||
|
const rl = readline.createInterface({ input: instream, crlfDelay: Infinity });
|
||||||
|
const outStream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
|
||||||
|
outStream.write('\uFEFF');
|
||||||
|
|
||||||
|
let isHeader = true;
|
||||||
|
let headers = [];
|
||||||
|
let idxCep = -1;
|
||||||
|
let idxNumero = -1;
|
||||||
|
|
||||||
|
for await (const rawLine of rl) {
|
||||||
|
const line = rawLine.replace(/\r$/, ''); // normalize CRLF
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
|
if (isHeader) {
|
||||||
|
headers = line
|
||||||
|
.split(';')
|
||||||
|
.map(h => h.trim())
|
||||||
|
.filter(h => h !== '');
|
||||||
|
|
||||||
|
const lower = headers.map(h => h.toLowerCase());
|
||||||
|
idxCep = lower.indexOf('cep');
|
||||||
|
idxNumero = lower.indexOf('numero');
|
||||||
|
|
||||||
|
// se não encontrar, tenta variações comuns
|
||||||
|
const idx = lower.indexOf('codigo postal');
|
||||||
|
if (idx !== -1) idxCep = idx;
|
||||||
|
|
||||||
|
|
||||||
|
const outHeaders = [...headers, 'Distancia', 'Endereco', 'Não Dedicado', 'Dedicado', 'Erro'];
|
||||||
|
outStream.write(outHeaders.join(';') + '\n');
|
||||||
|
isHeader = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cols = line
|
||||||
|
.split(';')
|
||||||
|
.map(c => c.trim())
|
||||||
|
.filter(c => c !== '');
|
||||||
|
|
||||||
|
const cepRaw = (idxCep >= 0 && cols[idxCep]) ? cols[idxCep] : '';
|
||||||
|
const cep = String(cepRaw).replace(/\D/g, ''); // keep digits only
|
||||||
|
const numero = (idxNumero >= 0 && cols[idxNumero]) ? cols[idxNumero] : '';
|
||||||
|
|
||||||
|
if (!cep || !numero) {
|
||||||
|
continue; // pula linha inválida
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const viab = await consultarViabilidade({ cep, numero });
|
||||||
|
|
||||||
|
const distancia = viab.distancia ?? (viab.raw && (viab.raw.distancia || viab.raw.distance)) ?? '';
|
||||||
|
|
||||||
|
const endereco = `${(viab.logradouro) || ''}, ${(viab.bairro) || ''}, ${(viab.cidade) || ''}/${(viab.estado) || ''}, ${(viab.cep) || ''}`.trim().replace(/^[, ]+|[, ]+$/g, '');
|
||||||
|
|
||||||
|
if (viab.naoDedicado) {
|
||||||
|
var naoDedicado = "Viavel";
|
||||||
|
} else {
|
||||||
|
var naoDedicado = "Não Viavel";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viab.dedicado) {
|
||||||
|
var dedicado = "Viavel";
|
||||||
|
} else {
|
||||||
|
var dedicado = "Não Viavel";
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = viab.error ? String(viab.error).replace(/[\r\n;]/g, ' ') : '';
|
||||||
|
|
||||||
|
const outCols = [...cols, distancia, endereco, naoDedicado, dedicado, error];
|
||||||
|
outStream.write(outCols.join(';') + '\n');
|
||||||
|
incrementProcessed(jobId);
|
||||||
|
} catch (err) {
|
||||||
|
const errMsg = (err && (err.message || String(err))).replace(/[\r\n;]/g, ' ');
|
||||||
|
const outCols = [...cols, '', '', '', '', '', '', errMsg];
|
||||||
|
outStream.write(outCols.join(';') + '\n');
|
||||||
|
incrementErrors(jobId);
|
||||||
|
incrementProcessed(jobId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outStream.end();
|
||||||
|
await once(outStream, 'finish');
|
||||||
|
|
||||||
|
finishJob(jobId, `/download/${path.basename(outputPath)}`);
|
||||||
|
|
||||||
|
return { jobId, outputPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { processCsvFile };
|
||||||
@ -1,14 +0,0 @@
|
|||||||
const axios = require('axios');
|
|
||||||
|
|
||||||
// small fetch wrapper for external services (ViaCEP etc.) with basic rate-limiting
|
|
||||||
async function fetchJson(url, opts = {}) {
|
|
||||||
try {
|
|
||||||
const r = await axios.get(url, { timeout: 10000, ...opts });
|
|
||||||
return r.data;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`fetchJson error ${url}: ${e.message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { fetchJson };
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
const dotenv = require('dotenv');
|
|
||||||
const axios = require('axios');
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
// Geocode using Google Geocoding API. Returns { lat, lon } or null
|
|
||||||
async function geocodeWithGoogle(address) {
|
|
||||||
const key = process.env.GOOGLE_API_KEY;
|
|
||||||
if (!key) return null;
|
|
||||||
try {
|
|
||||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${encodeURIComponent(key)}`;
|
|
||||||
const r = await axios.get(url, { timeout: 10000 });
|
|
||||||
if (r && r.data && Array.isArray(r.data.results) && r.data.results.length > 0) {
|
|
||||||
const loc = r.data.results[0].geometry && r.data.results[0].geometry.location;
|
|
||||||
if (loc && loc.lat !== undefined && loc.lng !== undefined) {
|
|
||||||
return { lat: Number(loc.lat), lon: Number(loc.lng) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`geocodeWithGoogle error for '${address}': ${e.message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addressWithGoogle(lat, lon) {
|
|
||||||
const key = process.env.GOOGLE_API_KEY;
|
|
||||||
if (!key) return null;
|
|
||||||
try {
|
|
||||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${encodeURIComponent(lat)},${encodeURIComponent(lon)}&key=${encodeURIComponent(key)}`;
|
|
||||||
const r = await axios.get(url, { timeout: 10000 });
|
|
||||||
if (r && r.data && Array.isArray(r.data.results) && r.data.results.length > 0) {
|
|
||||||
console.log(`Google Reverse Geocoding result for ${lat},${lon}: ${r.data.results[0].formatted_address}`);
|
|
||||||
return r.data.results[0].formatted_address || null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`addressWithGoogle error for '${lat},${lon}': ${e.message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { geocodeWithGoogle, addressWithGoogle };
|
|
||||||
57
service/jobStore.service.js
Normal file
57
service/jobStore.service.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const jobs = new Map();
|
||||||
|
|
||||||
|
function createJob(total) {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
jobs.set(id, {
|
||||||
|
id,
|
||||||
|
status: 'processing',
|
||||||
|
total,
|
||||||
|
processed: 0,
|
||||||
|
errors: 0,
|
||||||
|
download: null,
|
||||||
|
startedAt: new Date()
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementProcessed(id) {
|
||||||
|
const job = jobs.get(id);
|
||||||
|
if (job) job.processed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementErrors(id) {
|
||||||
|
const job = jobs.get(id);
|
||||||
|
if (job) job.errors++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishJob(id, download) {
|
||||||
|
const job = jobs.get(id);
|
||||||
|
if (job) {
|
||||||
|
job.status = 'done';
|
||||||
|
job.download = download;
|
||||||
|
job.finishedAt = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function failJob(id, error) {
|
||||||
|
const job = jobs.get(id);
|
||||||
|
if (job) {
|
||||||
|
job.status = 'error';
|
||||||
|
job.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJob(id) {
|
||||||
|
return jobs.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createJob,
|
||||||
|
incrementProcessed,
|
||||||
|
incrementErrors,
|
||||||
|
finishJob,
|
||||||
|
failJob,
|
||||||
|
getJob
|
||||||
|
};
|
||||||
@ -1,13 +0,0 @@
|
|||||||
// normalize partner sigla to requested labels
|
|
||||||
function normalizePartnerSigla(sigla) {
|
|
||||||
if (!sigla) return sigla;
|
|
||||||
const s = String(sigla).trim();
|
|
||||||
if (!s) return s;
|
|
||||||
const lowered = s.toLowerCase();
|
|
||||||
// map these two specific variants to 'Sothis'
|
|
||||||
if (lowered === 'são bernardo do campo - sp' || lowered === 'sao bernardo do campo - sp' || lowered === 'sao bernardo do campo') return 'Sothis';
|
|
||||||
if (lowered === 'são paulo - sp' || lowered === 'sao paulo - sp' || lowered === 'sao paulo') return 'Sothis';
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { normalizePartnerSigla };
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
const dotenv = require('dotenv');
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const BASE_BACKOFF_MS = parseInt(process.env.BASE_BACKOFF_MS || '1000', 10); // backoff inicial para retry
|
|
||||||
const MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '5', 10);
|
|
||||||
const REQUEST_DELAY_MS = parseInt(process.env.REQUEST_DELAY_MS || '250', 10);
|
|
||||||
|
|
||||||
function sleep(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
BASE_BACKOFF_MS,
|
|
||||||
MAX_RETRIES,
|
|
||||||
REQUEST_DELAY_MS,
|
|
||||||
sleep
|
|
||||||
};
|
|
||||||
55
service/viabilidadeService.js
Normal file
55
service/viabilidadeService.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const { apiConfig, apiViabilidadeUrl } = require('../config/apiConfig');
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT = (apiConfig && apiConfig.timeoutMs) || 10000;
|
||||||
|
const MAX_RETRIES = (apiConfig && apiConfig.maxRetries) || 3;
|
||||||
|
const BASE_BACKOFF_MS = (apiConfig && apiConfig.baseBackoffMs) || 500;
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((r) => setTimeout(r, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeResponse(body) {
|
||||||
|
if (!body) return { error: 'Resposta vazia', raw: body };
|
||||||
|
// Caso já venha com distancia/distance direto
|
||||||
|
if (typeof body === 'object') {
|
||||||
|
if (body.distancia || body.distance) {
|
||||||
|
return {
|
||||||
|
distancia: body.distancia || body.distance,
|
||||||
|
raw: body
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// formato antigo com array registros
|
||||||
|
if (Array.isArray(body.registros) && body.registros.length) {
|
||||||
|
// seleciona o registro com menor distancia (se existir)
|
||||||
|
let min = null;
|
||||||
|
for (const r of body.registros) {
|
||||||
|
const d = r.distancia ?? r.distance ?? null;
|
||||||
|
if (d == null) continue;
|
||||||
|
if (min == null || d < min.d) min = { d, r };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
distancia: min ? min.d : null,
|
||||||
|
parceiro: min ? (min.r.pastaSigla || min.r.parceiro || null) : null,
|
||||||
|
raw: body
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// fallback: retorna body bruto para inspeção
|
||||||
|
return { raw: body };
|
||||||
|
}
|
||||||
|
return { raw: body };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function consultarViabilidade(data) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(apiViabilidadeUrl, data, {
|
||||||
|
timeout: DEFAULT_TIMEOUT,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
return (response.data);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { consultarViabilidade };
|
||||||
Loading…
Reference in New Issue
Block a user