viabilidade/services/distanceService.js

150 lines
5.0 KiB
JavaScript

const dotenv = require("dotenv");
dotenv.config();
const axios = require("axios");
const { sleep } = require("./sleepService");
const { normalizePartnerSigla } = require("./partnerSiglaService");
// 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
const BASE_BACKOFF_MS = 10; // backoff inicial para retry
const MAX_RETRIES = 5;
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;
}
module.exports = { getMinDistance };