Compare commits

..

No commits in common. "82aca0f94d1813e06043fb1f297d311b66780c3a" and "54a81db111053fac6a8c61cd17cb0e93001d759d" have entirely different histories.

4 changed files with 44 additions and 86 deletions

View File

@ -61,6 +61,7 @@ GOOGLE_API_KEY=sua_chave_google_maps_aqui
GEOGRID_API_URL=https://.../viabilidade/raio # URL COMPLETA da consulta por raio
GEOGRID_API_KEY=sua_chave_geogrid_aqui
GEOGRID_API_COOKIE=seu_cookie_geogrid_aqui
GEOGRID_AUTHORIZED_SIGLAS_PASTAS=Sigla A, Sigla B # pastas de rede própria (viram provedor "Sothis")
# Distância REAL de trajeto (opcionais — têm defaults seguros)
GEOGRID_API_BASE_URL=https://.../vale/api/v3 # base p/ o endpoint de trajeto; se ausente, derivada da URL de consulta
@ -83,19 +84,15 @@ DB_PORT=5432
DB_USER=seu_usuario_postgres
DB_PASSWORD=sua_senha_postgres
DB_NAME=contratacao_db
# Siglas de Pastas (mapeamento de provedores)
SIGLA_SOTHIS=SOTHIS
SIGLA_OUTRO_PROVEDOR=OUTRO
```
### Classificação de Pastas (provedor)
### Siglas de Pastas
O provedor é resolvido pelo **prefixo da sigla da pasta** no GeoGrid (não há configuração no `.env`):
- `s-...`**Sothis** (rede própria).
- `p-<nome>...`**parceiro**; o provedor é o nome que vem depois do `-` (ex.: `p-Vivo``Vivo`).
- **Meio de transmissão** (marcador logo após o prefixo):
- `sub` → rede **subterrânea**, prefixa `(Subterrâneo)` (ex.: `s-sub-Barueri``(Subterrâneo) Sothis`).
- `a`**Aéreo**, sufixa ` - Aéreo` (ex.: `S-A-ABC``Sothis - Aéreo`).
- `r`**Rádio**, sufixa ` - Rádio` (ex.: `P-R-Vivo``Vivo - Rádio`).
- Qualquer sigla fora dessa convenção é considerada **não elegível** (ignorada).
O sistema mapeia siglas de pastas da GeoGrid para retornar o provedor na resposta. Quando o provedor bater com as siglas configuradas no `.env`, a API retorna `"Sothis"` como provedor.
## 3. Rodando a Aplicação
@ -240,7 +237,7 @@ Verifica a viabilidade técnica de um endereço usando CEP e número.
- `dedicado`: Serviço disponível em banda dedicada (distância real ≤ 1000m)
- `distancia`: Distância **real de trajeto** em metros até a caixa mais próxima (ou "5KM+" se não houver caixa no raio). Se o trajeto falhar, cai para a distância em linha reta.
- `distanciaReta`: Distância em **linha reta** em metros (aditivo, para comparação).
- `provedor`: Identificação do provedor pela convenção de prefixo da sigla de pasta (ver "Classificação de Pastas")
- `provedor`: Identificação do provedor baseado na sigla de pasta
> **Nota:** a regra de viabilidade e o campo `distancia` passaram a usar a **distância real de trajeto** (via GeoGrid `/integracao/trajeto/cordenada`). Como a real é sempre ≥ a linha reta, endereços de borda podem deixar de viabilizar em relação ao comportamento anterior.

View File

@ -31,78 +31,48 @@ const fetchRegistros = async (lat, lon) => {
logger.info("Resposta do GeoGrid", {
totalRegistros: registros.length,
siglas: registros.map(r => r.pasta?.sigla)
siglas: registros.map(r => r.pasta?.sigla),
autorizadas: apiConfig.geogridAuthorizedSiglasPastas
});
return registros;
};
// Classificação da sigla da pasta por convenção de PREFIXO:
// "s-..." => Sothis (rede própria)
// "p-<nome>..." => Parceiro; o provedor é o nome que vem depois do "-"
// Marcador de MEIO DE TRANSMISSÃO logo após o prefixo:
// "a" => Aéreo (sufixo " - Aéreo")
// "r" => Rádio (sufixo " - Rádio")
// "sub" => Subterrâneo (prefixa "(Subterrâneo) ")
// Exemplos: "s-Barueri" => "Sothis"; "p-Vivo" => "Vivo"; "s-sub-Barueri" => "(Subterrâneo) Sothis";
// "S-A-ABC" => "Sothis - Aéreo"; "P-R-Vivo" => "Vivo - Rádio".
// Qualquer outra coisa => não elegível (tipo "nenhum").
const classificarSigla = (sigla) => {
const partes = String(sigla || '').split('-').map(p => p.trim()).filter(Boolean);
const prefixo = (partes[0] || '').toLowerCase();
let tipo;
if (prefixo === 's') tipo = 'autorizado';
else if (prefixo === 'p') tipo = 'parceiro';
else return { tipo: 'nenhum', provedor: sigla || 'Desconhecido' };
// Marcador de meio de transmissão logo após o prefixo (partes[1]).
const marcador = (partes[1] || '').toLowerCase();
const aereo = marcador === 'a';
const radio = marcador === 'r';
const subterraneo = partes.slice(1).some(p => p.toLowerCase() === 'sub');
// Nome: Sothis é fixo; parceiro = segmentos após o prefixo, sem os marcadores.
let nome = 'Sothis';
if (tipo === 'parceiro') {
const resto = partes.slice(1).filter((p, i) => {
const pl = p.toLowerCase();
if (pl === 'sub') return false;
if (i === 0 && (pl === 'a' || pl === 'r')) return false; // marcador de meio
return true;
});
nome = resto.join(' ') || 'Parceiro';
}
// Formatação: subterrâneo prefixa "(Subterrâneo)"; aéreo/rádio sufixam " - Meio".
let provedor = subterraneo ? `(Subterrâneo) ${nome}` : nome;
if (aereo) provedor = `${provedor} - Aéreo`;
else if (radio) provedor = `${provedor} - Rádio`;
return { tipo, provedor };
// Seleção de registros por prioridade: autorizados (rede própria) > parceiros > nenhum.
// Retorna a LISTA ordenada (por distância, como veio do GeoGrid) + o provedor resolvido.
// Elegibilidade de um registro: rede própria (sigla autorizada) ou parceiro.
// Siglas fora disso (sub-projetos, etc.) NÃO são consideradas viabilidade.
const ehAutorizado = (sigla) => apiConfig.geogridAuthorizedSiglasPastas.includes(sigla);
const ehParceiro = (sigla) => !!sigla && (sigla.toLowerCase().includes("parceiro -") || sigla.toLowerCase().includes("parceiros - outros"));
const ehElegivel = (registro) => {
const sigla = registro && registro.pasta && registro.pasta.sigla;
return ehAutorizado(sigla) || ehParceiro(sigla);
};
const ehAutorizado = (sigla) => classificarSigla(sigla).tipo === 'autorizado';
const ehParceiro = (sigla) => classificarSigla(sigla).tipo === 'parceiro';
const ehElegivel = (registro) => classificarSigla(registro && registro.pasta && registro.pasta.sigla).tipo !== 'nenhum';
const provedorDoRegistro = (registro) => classificarSigla(registro && registro.pasta && registro.pasta.sigla).provedor;
const selecionarRegistros = (registros) => {
const autorizados = registros.filter(r => ehAutorizado(r.pasta && r.pasta.sigla));
if (autorizados.length > 0) {
return { provedor: provedorDoRegistro(autorizados[0]), tipo: "autorizado", registros: autorizados };
return { provedor: "Sothis", tipo: "autorizado", registros: autorizados };
}
const parceiros = registros.filter(r => ehParceiro(r.pasta && r.pasta.sigla));
if (parceiros.length > 0) {
return { provedor: provedorDoRegistro(parceiros[0]), tipo: "parceiro", registros: parceiros };
return { provedor: parceiros[0].pasta.sigla, tipo: "parceiro", registros: parceiros };
}
return { provedor: "Nenhum provedor disponível", tipo: "nenhum", registros: [] };
};
// Normaliza um registro do GeoGrid para o que o core precisa: coordenadas da
// caixa (destino do trajeto) + distância em linha reta + sigla + provedor resolvido.
// caixa (destino do trajeto) + distância em linha reta + sigla da pasta.
// Resolve o provedor de UM registro: rede própria (sigla autorizada) => "Sothis";
// caso contrário, a própria sigla da pasta (parceiro/sub-projeto/etc.).
const provedorDoRegistro = (registro) => {
const sigla = registro?.pasta?.sigla;
if (apiConfig.geogridAuthorizedSiglasPastas.includes(sigla)) return "Sothis";
return sigla || "Desconhecido";
};
const mapCaixa = (registro) => {
const lat = Number(registro?.latitude);
const lon = Number(registro?.longitude);
@ -189,4 +159,4 @@ const consultaViabilidade = async (lat, lon) => {
}
};
module.exports = { consultaViabilidade, consultarCaixasProximas, consultaTrajeto, mapCaixa, classificarSigla };
module.exports = { consultaViabilidade, consultarCaixasProximas, consultaTrajeto, mapCaixa };

View File

@ -8,6 +8,7 @@ const googleApiKey = process.env.GOOGLE_API_KEY;
const geogridApiUrl = process.env.GEOGRID_API_URL; // URL completa da consulta por raio (inalterada)
const geogridApiKey = process.env.GEOGRID_API_KEY;
const geogridApiCookie = process.env.GEOGRID_API_COOKIE;
const geogridAuthorizedSiglasPastas = process.env.GEOGRID_AUTHORIZED_SIGLAS_PASTAS ? process.env.GEOGRID_AUTHORIZED_SIGLAS_PASTAS.split(',').map(s => s.trim()) : [];
// Base da API GeoGrid: usada para montar endpoints além da consulta (ex.: trajeto).
// Se GEOGRID_API_BASE_URL não for definida, deriva da URL de consulta removendo o sufixo /viabilidade/raio.
@ -35,6 +36,7 @@ module.exports = {
geogridApiBaseUrl: geogridApiBaseUrl,
geogridApiKey: geogridApiKey,
geogridApiCookie: geogridApiCookie,
geogridAuthorizedSiglasPastas: geogridAuthorizedSiglasPastas,
geogridTrajetoModo: geogridTrajetoModo,
geogridTrajetoTopN: geogridTrajetoTopN,
geogridTrajetoTopNLote: geogridTrajetoTopNLote,

View File

@ -1,3 +1,6 @@
// Define siglas autorizadas ANTES de carregar apiConfig (que lê o env no require).
process.env.GEOGRID_AUTHORIZED_SIGLAS_PASTAS = 'São Paulo - SP, TMC';
const test = require('node:test');
const assert = require('node:assert/strict');
@ -153,33 +156,19 @@ test('core retorna detalhe por caixa (caixas[]) com provedor por caixa', async (
assert.equal(r.caixas[1].naoDedicado, false); // 900 > 500
});
test('classificarSigla: convenção de prefixos s-/p-/sub', () => {
assert.deepEqual(geogridService.classificarSigla('s-Barueri'), { tipo: 'autorizado', provedor: 'Sothis' });
assert.deepEqual(geogridService.classificarSigla('p-Vivo'), { tipo: 'parceiro', provedor: 'Vivo' });
assert.deepEqual(geogridService.classificarSigla('s-sub-Barueri'), { tipo: 'autorizado', provedor: '(Subterrâneo) Sothis' });
assert.deepEqual(geogridService.classificarSigla('p-sub-Vivo'), { tipo: 'parceiro', provedor: '(Subterrâneo) Vivo' });
// meio de transmissão: aéreo / rádio (marcador logo após o prefixo)
assert.deepEqual(geogridService.classificarSigla('S-A-ABC'), { tipo: 'autorizado', provedor: 'Sothis - Aéreo' });
assert.deepEqual(geogridService.classificarSigla('P-R-Vivo'), { tipo: 'parceiro', provedor: 'Vivo - Rádio' });
assert.deepEqual(geogridService.classificarSigla('p-a-Vivo'), { tipo: 'parceiro', provedor: 'Vivo - Aéreo' });
assert.equal(geogridService.classificarSigla('Sub Alameda Santos').tipo, 'nenhum');
assert.equal(geogridService.classificarSigla('São Paulo - SP').tipo, 'nenhum'); // convenção antiga não é mais elegível
assert.equal(geogridService.classificarSigla('S-SP').provedor, 'Sothis'); // case-insensitive
});
test('seleção MISTA: só s-/p- (ignora siglas fora da convenção), com subterrâneo', async () => {
test('seleção MISTA: só autorizadas + parceiros (ignora siglas inelegíveis)', async () => {
const axios = require('axios');
const orig = axios.get;
axios.get = async () => ({ data: { registros: [
{ latitude: '0', longitude: '0', distancia: '50', pasta: { sigla: 'Sub Alameda Santos' } }, // NÃO elegível (mais próxima)
{ latitude: '1', longitude: '1', distancia: '100', pasta: { sigla: 'p-Vivo' } }, // parceiro
{ latitude: '2', longitude: '2', distancia: '200', pasta: { sigla: 's-sub-Barueri' } } // Sothis subterrâneo
{ latitude: '1', longitude: '1', distancia: '100', pasta: { sigla: 'Parceiro - X' } }, // parceiro
{ latitude: '2', longitude: '2', distancia: '200', pasta: { sigla: 'São Paulo - SP' } } // autorizada => Sothis
] } });
try {
const r = await realConsultarCaixasProximas(0, 0, { topN: 2, misturarProvedores: true });
assert.equal(r.caixas.length, 2); // sub-alameda excluída
assert.equal(r.caixas[0].provedor, 'Vivo');
assert.equal(r.caixas[1].provedor, '(Subterrâneo) Sothis');
assert.equal(r.caixas[0].provedor, 'Parceiro - X');
assert.equal(r.caixas[1].provedor, 'Sothis');
} finally {
axios.get = orig;
}
@ -189,13 +178,13 @@ test('seleção PADRÃO (WordPress): prioriza Sothis e ignora parceiro mais pró
const axios = require('axios');
const orig = axios.get;
axios.get = async () => ({ data: { registros: [
{ latitude: '1', longitude: '1', distancia: '100', pasta: { sigla: 'p-Vivo' } },
{ latitude: '2', longitude: '2', distancia: '200', pasta: { sigla: 's-Barueri' } }
{ latitude: '1', longitude: '1', distancia: '100', pasta: { sigla: 'Parceiro - X' } },
{ latitude: '2', longitude: '2', distancia: '200', pasta: { sigla: 'São Paulo - SP' } }
] } });
try {
const r = await realConsultarCaixasProximas(0, 0, { topN: 2 });
assert.equal(r.provedor, 'Sothis');
assert.equal(r.caixas.length, 1); // só a autorizada (Sothis)
assert.equal(r.caixas.length, 1); // só a autorizada
assert.equal(r.caixas[0].provedor, 'Sothis');
} finally {
axios.get = orig;