viabiliza/app.old.js

682 lines
26 KiB
JavaScript
Raw Permalink Normal View History

// 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,
// };