REFACTOR: Separando funções em serviços específicos. Deixando server como um arquivo de execução e o app como o corpo da aplicação.
This commit is contained in:
parent
660fe32e23
commit
77ab072a40
545
app.js
Normal file
545
app.js
Normal file
@ -0,0 +1,545 @@
|
||||
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 { geocodeWithGoogle } = 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");
|
||||
|
||||
function createApp() {
|
||||
const upload = multer({ dest: "uploads/" });
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
app.use(express.json());
|
||||
|
||||
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) => 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 = "";
|
||||
|
||||
// If no coords, try ViaCEP -> Google
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
||||
if (rawCep) {
|
||||
const cep8 = rawCep.padStart(8, "0");
|
||||
const viaCepData = await fetchJson(
|
||||
`https://viacep.com.br/ws/${cep8}/json/`
|
||||
);
|
||||
if (viaCepData && !viaCepData.erro) {
|
||||
const logradouro = viaCepData.logradouro || "";
|
||||
const bairro = viaCepData.bairro || "";
|
||||
const cidade = viaCepData.localidade || "";
|
||||
const uf = viaCepData.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 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 fetchJson(
|
||||
`https://viacep.com.br/ws/${cep}/json/`
|
||||
);
|
||||
if (!viaCepData || viaCepData.erro)
|
||||
return res.status(404).json({ error: "CEP não encontrado" });
|
||||
const logradouro = viaCepData.logradouro || "";
|
||||
const bairro = viaCepData.bairro || "";
|
||||
const cidade = viaCepData.localidade || "";
|
||||
const uf = viaCepData.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 viaCepData = await fetchJson(
|
||||
`https://viacep.com.br/ws/${cep}/json/`
|
||||
);
|
||||
if (!viaCepData || viaCepData.erro)
|
||||
return res.status(404).json({ error: "CEP não encontrado" });
|
||||
const logradouro = viaCepData.logradouro || "";
|
||||
const bairro = viaCepData.bairro || "";
|
||||
const cidade = viaCepData.localidade || "";
|
||||
const uf = viaCepData.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" });
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createApp,
|
||||
};
|
||||
22
config/apiConfig.js
Normal file
22
config/apiConfig.js
Normal file
@ -0,0 +1,22 @@
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Configure sua API_KEY e COOKIE aqui ou via variáveis de ambiente
|
||||
|
||||
const API_URL = process.env.API_URL;
|
||||
const API_KEY = process.env.API_KEY;
|
||||
const COOKIE = process.env.COOKIE;
|
||||
|
||||
const HEADERS = {
|
||||
'api-key': API_KEY,
|
||||
'Cookie': COOKIE
|
||||
};
|
||||
|
||||
const apiConfig = {
|
||||
API_URL,
|
||||
API_KEY,
|
||||
COOKIE,
|
||||
HEADERS
|
||||
};
|
||||
module.exports = apiConfig;
|
||||
5957
outputs/enderecos_output_1760639035809.csv
Normal file
5957
outputs/enderecos_output_1760639035809.csv
Normal file
File diff suppressed because it is too large
Load Diff
5957
outputs/enderecos_output_1760639580273.csv
Normal file
5957
outputs/enderecos_output_1760639580273.csv
Normal file
File diff suppressed because it is too large
Load Diff
439
server.js
439
server.js
@ -1,436 +1,9 @@
|
||||
require('dotenv').config();
|
||||
require('dotenv').config();
|
||||
const { createApp } = require('./app');
|
||||
|
||||
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 app = createApp();
|
||||
const port = process.env.PORT;
|
||||
|
||||
const app = express();
|
||||
const upload = multer({ dest: 'uploads/' });
|
||||
const PORT = process.env.PORT;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(express.json());
|
||||
|
||||
// Configure sua API_KEY e COOKIE aqui ou via variáveis de ambiente
|
||||
const API_URL = process.env.API_URL;
|
||||
const API_KEY = process.env.API_KEY;
|
||||
const COOKIE = process.env.COOKIE;
|
||||
|
||||
const HEADERS = {
|
||||
'api-key': API_KEY,
|
||||
'Cookie': COOKIE
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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 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) => 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 = '';
|
||||
|
||||
// If no coords, try ViaCEP -> Google
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
||||
if (rawCep) {
|
||||
const cep8 = rawCep.padStart(8, '0');
|
||||
const viaCepData = await fetchJson(`https://viacep.com.br/ws/${cep8}/json/`);
|
||||
if (viaCepData && !viaCepData.erro) {
|
||||
const logradouro = viaCepData.logradouro || '';
|
||||
const bairro = viaCepData.bairro || '';
|
||||
const cidade = viaCepData.localidade || '';
|
||||
const uf = viaCepData.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 });
|
||||
app.listen(port, () => {
|
||||
console.log(`Server running on http://localhost:${port}`)
|
||||
});
|
||||
|
||||
// download 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 fetchJson(`https://viacep.com.br/ws/${cep}/json/`);
|
||||
if (!viaCepData || viaCepData.erro) return res.status(404).json({ error: 'CEP não encontrado' });
|
||||
const logradouro = viaCepData.logradouro || '';
|
||||
const bairro = viaCepData.bairro || '';
|
||||
const cidade = viaCepData.localidade || '';
|
||||
const uf = viaCepData.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 viaCepData = await fetchJson(`https://viacep.com.br/ws/${cep}/json/`);
|
||||
if (!viaCepData || viaCepData.erro) return res.status(404).json({ error: 'CEP não encontrado' });
|
||||
const logradouro = viaCepData.logradouro || '';
|
||||
const bairro = viaCepData.bairro || '';
|
||||
const cidade = viaCepData.localidade || '';
|
||||
const uf = viaCepData.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' });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
|
||||
|
||||
|
||||
14
service/fetchService.js
Normal file
14
service/fetchService.js
Normal file
@ -0,0 +1,14 @@
|
||||
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 };
|
||||
25
service/geocodeService.js
Normal file
25
service/geocodeService.js
Normal file
@ -0,0 +1,25 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { geocodeWithGoogle };
|
||||
13
service/normalizeService.js
Normal file
13
service/normalizeService.js
Normal file
@ -0,0 +1,13 @@
|
||||
// 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 };
|
||||
18
service/retryService.js
Normal file
18
service/retryService.js
Normal file
@ -0,0 +1,18 @@
|
||||
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
|
||||
};
|
||||
5957
uploads/7593fbedff3e5b01f7ae753efad026fa
Normal file
5957
uploads/7593fbedff3e5b01f7ae753efad026fa
Normal file
File diff suppressed because it is too large
Load Diff
5957
uploads/d374ebe3f1b6061384889bc26985aa4f
Normal file
5957
uploads/d374ebe3f1b6061384889bc26985aa4f
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user