sothis-contratacao-api/src/modules/viabilidade/viabilidade.core.js

96 lines
4.4 KiB
JavaScript
Raw Normal View History

const geogridService = require('../../shared/apis/geogridService.js');
const apiConfig = require('../../shared/config/apiConfig.js');
const logger = require('../../shared/utils/logger.js');
const { mapWithConcurrency } = require('../../shared/utils/concurrency.js');
// Regra de negócio PURA: dada uma distância (em metros), define quais serviços
// estão disponíveis. É a mesma regra de sempre — o que muda é a distância que
// alimenta ela (agora a real de trajeto). Isolada e testável.
function classificarPorDistancia(distancia) {
if (distancia == null || typeof distancia !== 'number' || Number.isNaN(distancia)) {
return { dedicado: false, naoDedicado: false };
}
if (distancia <= 500) return { dedicado: true, naoDedicado: true };
if (distancia <= 1000) return { dedicado: true, naoDedicado: false };
return { dedicado: false, naoDedicado: false }; // 10015000m: caixa existe, mas fora do alcance
}
// Núcleo de orquestração: a partir de coordenadas de origem (endereço),
// 1) acha as N caixas mais próximas em linha reta (GeoGrid),
// 2) calcula a distância REAL de trajeto até cada uma e pega a menor,
// 3) classifica a viabilidade sobre a distância real (com fallback p/ reta).
//
// Retorna um resultado de domínio NEUTRO (sem formato de resposta HTTP):
// { distancia, distanciaReta, distanciaReal, dedicado, naoDedicado, provedor }
// - `distancia` é a EFETIVA usada na regra: real quando disponível, senão reta.
async function avaliarPorCoordenadas(origem, { modo = apiConfig.geogridTrajetoModo, topN, misturarProvedores = false } = {}) {
const { provedor, caixas } = await geogridService.consultarCaixasProximas(origem.lat, origem.lon, { topN, misturarProvedores });
// Sem caixa no raio: mantém o comportamento histórico ("5KM+", nada viável).
if (!caixas || caixas.length === 0) {
return {
distancia: "5KM+",
distanciaReta: null,
distanciaReal: null,
dedicado: false,
naoDedicado: false,
provedor,
caixas: []
};
}
// Para cada caixa (na ordem do GeoGrid = por distância em linha reta),
// calcula a distância real de trajeto e classifica individualmente.
const detalhes = await mapWithConcurrency(caixas, apiConfig.geogridConcurrency, async (caixa) => {
let distanciaReal = null;
if (caixa.lat != null && caixa.lon != null) {
try {
const trajeto = await geogridService.consultaTrajeto(origem, caixa, modo);
const d = Number(trajeto && trajeto.distancia);
distanciaReal = Number.isFinite(d) ? d : null;
} catch (err) {
logger.warn('Trajeto falhou para uma caixa, usando linha reta nesta candidata', {
message: err.message,
status: err.response?.status,
data: err.response?.data, // corpo do erro do GeoGrid (motivo do 400)
url: err.config?.url,
caixa: { lat: caixa.lat, lon: caixa.lon }
});
}
}
const distancia = distanciaReal ?? caixa.distanciaReta; // efetiva por caixa (real, senão reta)
return {
provedor: caixa.provedor,
sigla: caixa.sigla,
lat: caixa.lat,
lon: caixa.lon,
distanciaReta: caixa.distanciaReta,
distanciaReal,
distancia,
...classificarPorDistancia(distancia)
};
});
// Agregado (compatibilidade WordPress): menor distância efetiva entre as caixas.
const distanciaReta = detalhes[0].distanciaReta ?? null; // caixa mais próxima em linha reta
const reais = detalhes.map(d => d.distanciaReal).filter(d => d != null);
const distanciaReal = reais.length ? Math.min(...reais) : null;
if (distanciaReal == null) {
logger.warn('Nenhum trajeto disponível; usando distância em linha reta como fallback', { origem, distanciaReta });
}
const distanciaEfetiva = distanciaReal ?? distanciaReta;
return {
distancia: distanciaEfetiva,
distanciaReta,
distanciaReal,
...classificarPorDistancia(distanciaEfetiva),
provedor,
caixas: detalhes // detalhe por caixa (mais próxima em linha reta primeiro)
};
}
module.exports = { classificarPorDistancia, avaliarPorCoordenadas };