REFAC: consumo do endpoint /lote, dedup de helpers e colunas por caixa

- csvService/viabilidadeService passam a consumir o endpoint /viabilidade/lote
  da API em blocos, no lugar de 1 requisição HTTP por linha.
- Helpers de detecção de formato/cabeçalho extraídos para fileFormat.js (remove
  duplicação entre os dois serviços).
- Planilha de saída: 4 colunas por caixa (Provedor, Não Dedicado, Dedicado,
  Distância Trajeto) para as 2 caixas mais próximas; Caixa 2 replica a Caixa 1
  quando não há segunda caixa / sem cobertura.
- parseCoordinate passa a aceitar direção O (Oeste => negativo) e L (Leste).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gabriel Amancio 2026-07-21 15:49:29 -03:00
parent c47a7b3a2e
commit 801d3fa90d
3 changed files with 274 additions and 249 deletions

View File

@ -1,50 +1,31 @@
const { consultarViabilidade, discoverDataType } = require('./viabilidadeService');
const { consultarViabilidadeLote, discoverDataType } = require('./viabilidadeService');
const fs = require('fs');
const path = require('path');
const XLSX = require('xlsx');
const ExcelJS = require('exceljs');
const { once } = require('events');
const {
normalizeHeader,
isExcelFile,
detectDelimiter,
hasCepHeader,
hasAddressOrNumberHeader,
hasGeoHeaders
} = require('./fileFormat');
const {
incrementProcessed,
incrementErrors,
finishJob
} = require('./jobStore.service');
const RESULT_HEADERS = ['Provedor', 'Distancia', 'Dedicado', 'Nao Dedicado', 'Erro'];
const RESULT_HEADERS = [
'Provedor (C1)', 'Nao Dedicado (C1)', 'Dedicado (C1)', 'Distancia Trajeto (C1)',
'Provedor (C2)', 'Nao Dedicado (C2)', 'Dedicado (C2)', 'Distancia Trajeto (C2)',
'Erro'
];
function normalizeHeader(value) {
return String(value || '')
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[_-]+/g, ' ')
.replace(/\s+/g, ' ');
}
function isExcelFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
if (['.xls', '.xlsx'].includes(ext)) return true;
const fileStart = fs.readFileSync(filePath).subarray(0, 512);
const signature = fileStart.subarray(0, 8);
const isZipBasedXlsx = signature[0] === 0x50 && signature[1] === 0x4b;
const isOleBasedXls = signature[0] === 0xd0
&& signature[1] === 0xcf
&& signature[2] === 0x11
&& signature[3] === 0xe0
&& signature[4] === 0xa1
&& signature[5] === 0xb1
&& signature[6] === 0x1a
&& signature[7] === 0xe1;
const startText = fileStart.toString('latin1').trimStart().toLowerCase();
const isHtmlExcel = startText.startsWith('<html')
|| startText.startsWith('<!doctype html')
|| startText.includes('<table');
return isZipBasedXlsx || isOleBasedXls || isHtmlExcel;
}
// Quantidade de itens enviados por chamada ao endpoint /lote da API.
const LOTE_CHUNK = 25;
function isXlsxFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
@ -54,13 +35,6 @@ function isXlsxFile(filePath) {
return fileStart[0] === 0x50 && fileStart[1] === 0x4b;
}
function detectDelimiter(line) {
const delimiters = [';', '\t', ','];
return delimiters
.map(delimiter => ({ delimiter, count: line.split(delimiter).length }))
.sort((a, b) => b.count - a.count)[0].delimiter;
}
function splitDelimitedLine(line, delimiter) {
const cols = [];
let current = '';
@ -88,7 +62,7 @@ function splitDelimitedLine(line, delimiter) {
}
function readDelimitedRows(filePath) {
const content = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
const content = fs.readFileSync(filePath, 'utf8').replace(/^/, '');
const lines = content.split(/\r?\n/).filter(line => line.trim());
if (!lines.length) return [];
@ -116,27 +90,6 @@ function findFirstHeaderIndex(headers, predicate) {
return headers.map(normalizeHeader).findIndex(predicate);
}
function hasHeaderAlias(headers, aliases) {
const normalizedAliases = aliases.map(normalizeHeader);
return headers.map(normalizeHeader).some(header => normalizedAliases.includes(header));
}
function hasCepHeader(headers) {
return headers.map(normalizeHeader).some(header => /\bcep\b/.test(header) || header === 'codigo postal');
}
function hasAddressOrNumberHeader(headers) {
return headers.map(normalizeHeader).some(header => ['numero', 'num', 'nº', 'n°'].includes(header)
|| header.includes('endereco')
|| header.includes('logradouro'));
}
function hasGeoHeaders(headers) {
const norm = headers.map(normalizeHeader);
return norm.some(h => /\blat(itude)?\b/.test(h))
&& norm.some(h => /\blo?n[g]?(itude)?\b/.test(h));
}
function findHeaderRowIndex(rows) {
const index = rows.findIndex(row => (hasCepHeader(row) && hasAddressOrNumberHeader(row)) || hasGeoHeaders(row));
return index >= 0 ? index : 0;
@ -153,9 +106,7 @@ function resolveColumnIndexes(headers) {
idxCep: findFirstHeaderIndex(headers, header => /\bcep\b/.test(header) || header === 'codigo postal'),
idxNumero: exactIndex(['numero', 'número', 'num', 'nº', 'n°']),
idxEndereco: findFirstHeaderIndex(headers, header => header.includes('endereco') || header.includes('logradouro')),
// Antes: exactIndex(['latitude', 'lat'])
idxLatitude: findFirstHeaderIndex(headers, header => /\blat(itude)?\b/.test(header)),
// Antes: exactIndex(['longitude', 'long', 'lng', 'lon'])
idxLongitude: findFirstHeaderIndex(headers, header => /\blo?n[g]?(itude)?\b/.test(header)),
};
}
@ -191,20 +142,25 @@ function buildCepPayload(cols, indexes) {
function parseCoordinate(value) {
const str = String(value ?? '')
.replace(/[\u00A0\u202F\u2000-\u200B\u3000]/g, '') // Bug 2: remove non-breaking spaces
.replace(/[  - ]/g, '') // remove espaços não-quebráveis
.trim()
.replace(',', '.');
// Bug 3: converte DMS "17 38 18.80 S" → -17.638556
const dms = str.match(/^(\d+)[°\s]+(\d+)['\s]+(\d+(?:\.\d+)?)["\s]*([NSEW])?$/i);
// converte DMS "17 38 18.80 S" → -17.638556. Aceita direção N/S/E/W e também
// O (Oeste=West) e L (Leste=East), usadas em pt-BR.
const dms = str.match(/^(\d+)[°\s]+(\d+)['\s]+(\d+(?:\.\d+)?)["\s]*([NSEWOL])?$/i);
if (dms) {
const decimal = parseFloat(dms[1]) + parseFloat(dms[2]) / 60 + parseFloat(dms[3]) / 3600;
const negative = /[SW]/i.test(dms[4] ?? '');
return negative ? -decimal : decimal;
const negativo = /[SWO]/i.test(dms[4] ?? ''); // Sul / West / Oeste => negativo
return negativo ? -decimal : decimal;
}
// decimal, podendo ter direção no fim (ex.: "45.55 O", "8.6 S")
const dir = str.match(/([NSEWOL])\s*$/i);
const parsed = parseFloat(str);
return Number.isFinite(parsed) ? parsed : NaN;
if (!Number.isFinite(parsed)) return NaN;
if (dir) return /[SWO]/i.test(dir[1]) ? -Math.abs(parsed) : Math.abs(parsed);
return parsed;
}
function buildGeoPayload(cols, indexes) {
@ -215,30 +171,80 @@ function buildGeoPayload(cols, indexes) {
return { latitude, longitude };
}
async function consultarComFallback(geoPayload, cepPayload) {
let lastError = null;
// Monta a lista de linhas com payload a consultar. Para cada linha, define o
// payload primário (geo se houver, senão cep) e o de fallback (cep, quando a
// linha tem ambos — preserva a semântica antiga "tenta geo, senão cep").
function buildLinhas(rows, headerRowIndex, indexes) {
const linhas = [];
for (let rowIndex = headerRowIndex + 1; rowIndex < rows.length; rowIndex++) {
const cols = rows[rowIndex];
const geo = buildGeoPayload(cols, indexes);
const cep = buildCepPayload(cols, indexes);
if (!geo && !cep) continue;
linhas.push({ rowIndex, primary: geo || cep, fallback: (geo && cep) ? cep : null });
}
return linhas;
}
if (geoPayload) {
// Avalia todas as linhas via o endpoint /lote da API, em blocos. Retorna um
// Map(rowIndex -> resultado | { __erro }). Atualiza o progresso do job.
async function avaliarLinhas(jobId, linhas) {
const resultadoPorRow = new Map();
for (let start = 0; start < linhas.length; start += LOTE_CHUNK) {
const chunk = linhas.slice(start, start + LOTE_CHUNK);
let resultados;
try {
const result = await consultarViabilidade(geoPayload);
if (!result || !result.error) return result;
lastError = new Error(result.error);
resultados = await consultarViabilidadeLote(chunk.map(l => l.primary));
} catch (err) {
lastError = err;
}
const msg = formatApiErrorResponse(err);
chunk.forEach(l => {
resultadoPorRow.set(l.rowIndex, { __erro: msg });
incrementErrors(jobId);
incrementProcessed(jobId);
});
continue;
}
if (cepPayload) {
const paraFallback = [];
chunk.forEach((linha, i) => {
const r = resultados[i];
if (r && r.erro && linha.fallback) {
paraFallback.push(linha);
} else if (r && r.erro) {
resultadoPorRow.set(linha.rowIndex, { __erro: r.erro });
} else if (r) {
resultadoPorRow.set(linha.rowIndex, r);
} else {
resultadoPorRow.set(linha.rowIndex, { __erro: 'Sem resultado para a linha' });
}
});
if (paraFallback.length) {
let fb = null;
try {
const result = await consultarViabilidade(cepPayload);
if (!result || !result.error) return result;
lastError = new Error(result.error);
fb = await consultarViabilidadeLote(paraFallback.map(l => l.fallback));
} catch (err) {
lastError = err;
const msg = formatApiErrorResponse(err);
paraFallback.forEach(l => resultadoPorRow.set(l.rowIndex, { __erro: msg }));
}
if (fb) {
paraFallback.forEach((linha, k) => {
const r = fb[k];
resultadoPorRow.set(linha.rowIndex, (r && !r.erro) ? r : { __erro: (r && r.erro) || 'Linha sem latitude/longitude ou CEP valido' });
});
}
}
throw lastError || new Error('Linha sem latitude/longitude ou CEP valido');
chunk.forEach(l => {
const r = resultadoPorRow.get(l.rowIndex);
if (r && r.__erro) incrementErrors(jobId);
incrementProcessed(jobId);
});
}
return resultadoPorRow;
}
function cleanCsvValue(value) {
@ -258,18 +264,45 @@ function formatApiErrorResponse(error) {
return error && (error.message || String(error));
}
// 4 colunas por caixa: Provedor, Nao Dedicado, Dedicado, Distancia Trajeto.
// Caixa inexistente (ex.: só 1 caixa próxima) => colunas em branco.
function caixaCols(caixa) {
if (!caixa) return ['', '', '', ''];
const viavel = v => (v ? 'Viavel' : 'Nao Viavel');
const distancia = (caixa.distancia != null && caixa.distancia !== '') ? caixa.distancia : '';
return [caixa.provedor ?? '', viavel(caixa.naoDedicado), viavel(caixa.dedicado), distancia];
}
function buildSuccessResultColumns(viab) {
const provedor = viab.provedor ?? '';
const distancia = viab.distancia ?? (viab.raw && (viab.raw.distancia || viab.raw.distance)) ?? '';
const dedicado = viab.dedicado ? 'Viavel' : 'Nao Viavel';
const naoDedicado = viab.naoDedicado ? 'Viavel' : 'Nao Viavel';
const caixas = viab.caixas || [];
const error = viab.error ? cleanCsvValue(viab.error) : '';
return [provedor, distancia, dedicado, naoDedicado, error];
let c1;
if (caixas.length === 0) {
// Sem cobertura: monta o status como "Caixa 1" (em vez de deixar em branco).
const viavel = v => (v ? 'Viavel' : 'Nao Viavel');
const distancia = (viab.distancia != null && viab.distancia !== '') ? viab.distancia : '';
c1 = [viab.provedor ?? 'Nenhum provedor disponível', viavel(viab.naoDedicado), viavel(viab.dedicado), distancia];
} else {
c1 = caixaCols(caixas[0]);
}
// Sem 2ª caixa: replica as infos da Caixa 1 (não deixa a Caixa 2 em branco).
const c2 = caixas[1] ? caixaCols(caixas[1]) : c1;
return [...c1, ...c2, error];
}
function buildErrorResultColumns(err) {
return ['', '', '', '', cleanCsvValue(formatApiErrorResponse(err))];
return ['', '', '', '', '', '', '', '', cleanCsvValue(formatApiErrorResponse(err))];
}
// Colunas de resultado para uma linha, a partir do Map de resultados do lote.
function resultColumnsFor(resultadoPorRow, rowIndex) {
const resultado = resultadoPorRow.get(rowIndex) || { __erro: 'Linha não avaliada' };
return resultado.__erro
? buildErrorResultColumns({ message: resultado.__erro })
: buildSuccessResultColumns(resultado);
}
function cloneCellStyle(cell) {
@ -297,7 +330,7 @@ function styleInsertedResultColumns(worksheet, headerRowNumber) {
});
}
async function processXlsxFile(jobId, inputPath, outputPath, rows, headerRowIndex, indexes) {
async function processXlsxFile(inputPath, outputPath, rows, headerRowIndex, indexes, resultadoPorRow) {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(inputPath);
@ -313,23 +346,11 @@ async function processXlsxFile(jobId, inputPath, outputPath, rows, headerRowInde
if (!geoPayload && !cepPayload) continue;
const row = worksheet.getRow(rowIndex + 1);
try {
const viab = await consultarComFallback(geoPayload, cepPayload);
buildSuccessResultColumns(viab).forEach((value, index) => {
resultColumnsFor(resultadoPorRow, rowIndex).forEach((value, index) => {
const cell = row.getCell(index + 1);
cell.value = value;
cell.style = cloneCellStyle(row.getCell(RESULT_HEADERS.length + 1));
});
incrementProcessed(jobId);
} catch (err) {
buildErrorResultColumns(err).forEach((value, index) => {
const cell = row.getCell(index + 1);
cell.value = value;
cell.style = cloneCellStyle(row.getCell(RESULT_HEADERS.length + 1));
});
incrementErrors(jobId);
incrementProcessed(jobId);
}
row.commit();
}
@ -427,8 +448,12 @@ async function processCsvFile(jobId, inputPath, originalName) {
const outputPath = path.join(__dirname, '..', 'outputs', outputFilename);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
// Avalia todas as linhas de uma vez (em blocos) via /lote da API.
const linhas = buildLinhas(rows, headerRowIndex, indexes);
const resultadoPorRow = await avaliarLinhas(jobId, linhas);
if (isXlsxFile(inputPath)) {
await processXlsxFile(jobId, inputPath, outputPath, rows, headerRowIndex, indexes);
await processXlsxFile(inputPath, outputPath, rows, headerRowIndex, indexes, resultadoPorRow);
finishJob(jobId, path.basename(outputPath));
return outputPath;
@ -446,15 +471,7 @@ async function processCsvFile(jobId, inputPath, originalName) {
const cepPayload = buildCepPayload(cols, indexes);
if (!geoPayload && !cepPayload) continue;
try {
const viab = await consultarComFallback(geoPayload, cepPayload);
rowResults.push({ rowIndex, values: buildSuccessResultColumns(viab) });
incrementProcessed(jobId);
} catch (err) {
rowResults.push({ rowIndex, values: buildErrorResultColumns(err) });
incrementErrors(jobId);
incrementProcessed(jobId);
}
rowResults.push({ rowIndex, values: resultColumnsFor(resultadoPorRow, rowIndex) });
}
workbook.Sheets[firstSheetName] = prependResultColumnsToWorksheet(worksheet, headerRowIndex, rowResults);
@ -465,25 +482,17 @@ async function processCsvFile(jobId, inputPath, originalName) {
}
const outStream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
outStream.write('\uFEFF');
outStream.write('');
outStream.write([...RESULT_HEADERS, ...headers].join(';') + '\n');
for (const cols of rows.slice(headerRowIndex + 1)) {
for (let rowIndex = headerRowIndex + 1; rowIndex < rows.length; rowIndex++) {
const cols = rows[rowIndex];
const geoPayload = buildGeoPayload(cols, indexes);
const cepPayload = buildCepPayload(cols, indexes);
if (!geoPayload && !cepPayload) continue;
try {
const viab = await consultarComFallback(geoPayload, cepPayload);
const outCols = [...buildSuccessResultColumns(viab), ...cols].map(cleanCsvValue);
const outCols = [...resultColumnsFor(resultadoPorRow, rowIndex), ...cols].map(cleanCsvValue);
outStream.write(outCols.join(';') + '\n');
incrementProcessed(jobId);
} catch (err) {
const outCols = [...buildErrorResultColumns(err), ...cols].map(cleanCsvValue);
outStream.write(outCols.join(';') + '\n');
incrementErrors(jobId);
incrementProcessed(jobId);
}
}
outStream.end();

72
service/fileFormat.js Normal file
View File

@ -0,0 +1,72 @@
const fs = require('fs');
const path = require('path');
// Helpers de detecção de formato/cabeçalho compartilhados entre viabilidadeService e csvService.
// Antes estavam duplicados nos dois arquivos. Todos normalizam internamente (normalizeHeader
// é idempotente), então funcionam tanto com cabeçalhos crus quanto já normalizados.
function normalizeHeader(value) {
return String(value || '')
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/[̀-ͯ]/g, '')
.replace(/[_-]+/g, ' ')
.replace(/\s+/g, ' ');
}
function isExcelFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
if (['.xls', '.xlsx'].includes(ext)) return true;
const fileStart = fs.readFileSync(filePath).subarray(0, 512);
const signature = fileStart.subarray(0, 8);
const isZipBasedXlsx = signature[0] === 0x50 && signature[1] === 0x4b;
const isOleBasedXls = signature[0] === 0xd0
&& signature[1] === 0xcf
&& signature[2] === 0x11
&& signature[3] === 0xe0
&& signature[4] === 0xa1
&& signature[5] === 0xb1
&& signature[6] === 0x1a
&& signature[7] === 0xe1;
const startText = fileStart.toString('latin1').trimStart().toLowerCase();
const isHtmlExcel = startText.startsWith('<html')
|| startText.startsWith('<!doctype html')
|| startText.includes('<table');
return isZipBasedXlsx || isOleBasedXls || isHtmlExcel;
}
function detectDelimiter(line) {
const delimiters = [';', '\t', ','];
return delimiters
.map(delimiter => ({ delimiter, count: line.split(delimiter).length }))
.sort((a, b) => b.count - a.count)[0].delimiter;
}
function hasCepHeader(headers) {
return headers.map(normalizeHeader).some(header => /\bcep\b/.test(header) || header === 'codigo postal');
}
function hasAddressOrNumberHeader(headers) {
return headers.map(normalizeHeader).some(header => ['numero', 'num', 'nº', 'n°'].includes(header)
|| header.includes('endereco')
|| header.includes('logradouro'));
}
function hasGeoHeaders(headers) {
const norm = headers.map(normalizeHeader);
return norm.some(h => /\blat(itude)?\b/.test(h))
&& norm.some(h => /\blo?n[g]?(itude)?\b/.test(h));
}
module.exports = {
normalizeHeader,
isExcelFile,
detectDelimiter,
hasCepHeader,
hasAddressOrNumberHeader,
hasGeoHeaders
};

View File

@ -1,61 +1,21 @@
const axios = require('axios');
const fs = require('fs');
const readline = require('readline');
const path = require('path');
const XLSX = require('xlsx');
const { apiConfig, apiViabilidadeUrl, apiUrlBase } = require('../config/apiConfig');
function normalizeHeader(value) {
return String(value || '')
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[_-]+/g, ' ')
.replace(/\s+/g, ' ');
}
function hasHeader(headers, aliases) {
const normalizedAliases = aliases.map(normalizeHeader);
return headers.some(header => normalizedAliases.includes(header));
}
function isExcelFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
if (['.xls', '.xlsx'].includes(ext)) return true;
const fileStart = fs.readFileSync(filePath).subarray(0, 512);
const signature = fileStart.subarray(0, 8);
const isZipBasedXlsx = signature[0] === 0x50 && signature[1] === 0x4b;
const isOleBasedXls = signature[0] === 0xd0
&& signature[1] === 0xcf
&& signature[2] === 0x11
&& signature[3] === 0xe0
&& signature[4] === 0xa1
&& signature[5] === 0xb1
&& signature[6] === 0x1a
&& signature[7] === 0xe1;
const startText = fileStart.toString('latin1').trimStart().toLowerCase();
const isHtmlExcel = startText.startsWith('<html')
|| startText.startsWith('<!doctype html')
|| startText.includes('<table');
return isZipBasedXlsx || isOleBasedXls || isHtmlExcel;
}
function detectDelimiter(line) {
const delimiters = [';', '\t', ','];
return delimiters
.map(delimiter => ({ delimiter, count: line.split(delimiter).length }))
.sort((a, b) => b.count - a.count)[0].delimiter;
}
const { apiUrlBase } = require('../config/apiConfig');
const {
normalizeHeader,
isExcelFile,
detectDelimiter,
hasCepHeader,
hasAddressOrNumberHeader,
hasGeoHeaders
} = require('./fileFormat');
function findHeaderRow(rows) {
return rows.find(row => {
const headers = row.map(normalizeHeader);
const hasCepNumero = hasCepHeader(headers) && hasAddressOrNumberHeader(headers);
const hasGeo = hasGeoHeaders(headers);
const hasCepNumero = hasCepHeader(row) && hasAddressOrNumberHeader(row);
const hasGeo = hasGeoHeaders(row);
return hasCepNumero || hasGeo;
}) || [];
}
@ -79,12 +39,10 @@ async function readDelimitedHeaders(filePath) {
const rl = readline.createInterface({ input: instream, crlfDelay: Infinity });
for await (const rawLine of rl) {
const line = rawLine.replace(/^\uFEFF/, '').replace(/\r$/, '');
const line = rawLine.replace(/^/, '').replace(/\r$/, '');
if (!line.trim()) continue;
const headers = line.split(detectDelimiter(line)).map(normalizeHeader);
const hasCepNumero = hasCepHeader(headers) && hasAddressOrNumberHeader(headers);
const hasGeo = hasGeoHeaders(headers);
if (hasCepNumero || hasGeo) {
if ((hasCepHeader(headers) && hasAddressOrNumberHeader(headers)) || hasGeoHeaders(headers)) {
rl.close();
return headers;
}
@ -94,69 +52,55 @@ async function readDelimitedHeaders(filePath) {
return [];
}
function hasCepHeader(headers) {
return headers.some(header => /\bcep\b/.test(header) || header === 'codigo postal');
}
function hasAddressOrNumberHeader(headers) {
return headers.some(header => ['numero', 'num', 'nº', 'n°'].includes(header)
|| header.includes('endereco')
|| header.includes('logradouro'));
}
function hasGeoHeaders(headers) {
return headers.some(h => /\blat(itude)?\b/.test(h))
&& headers.some(h => /\blo?n[g]?(itude)?\b/.test(h));
}
async function consultarViabilidade(data) {
try {
const dataType = await discoverDataType(data);
let endpoint = apiUrlBase;
if (dataType === 'geolocalizacao') {
endpoint += 'viabilidade/lat-long';
} else {
endpoint += 'viabilidade';
}
const response = await axios.post(endpoint, data, {
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
});
console.log('Resposta da API de viabilidade:', response.data);
return (response.data);
} catch (error) {
throw error;
}
}
// Preciso de uma função para verificar se os dados vindos são de CEP ou de geolocalização
// Verifica se os dados vindos são de CEP ou de geolocalização (aceita caminho de arquivo ou objeto).
async function discoverDataType(input) {
if (typeof input === 'string') {
const headers = isExcelFile(input)
? readExcelHeaders(input)
: await readDelimitedHeaders(input);
const hasCepNumero = hasCepHeader(headers) && hasAddressOrNumberHeader(headers);
if (hasCepNumero) {
return 'cep';
} else if (hasGeoHeaders(headers)) {
return 'geolocalizacao';
} else {
if (hasCepHeader(headers) && hasAddressOrNumberHeader(headers)) return 'cep';
if (hasGeoHeaders(headers)) return 'geolocalizacao';
return 'unknown';
}
} else if (typeof input === 'object') {
// Trata como objeto de dados
if (input.cep && input.numero) {
return 'cep';
} else if (input.latitude !== undefined && input.longitude !== undefined) {
return 'geolocalizacao';
} else {
return 'unknown';
}
} else {
return 'unknown';
}
}
module.exports = { consultarViabilidade, discoverDataType };
if (input && typeof input === 'object') {
if (input.cep && input.numero) return 'cep';
if (input.latitude !== undefined && input.longitude !== undefined) return 'geolocalizacao';
return 'unknown';
}
return 'unknown';
}
// Consulta individual (manual / uma linha). Escolhe o endpoint conforme o tipo dos dados.
async function consultarViabilidade(data) {
const dataType = await discoverDataType(data);
const endpoint = apiUrlBase + (dataType === 'geolocalizacao' ? 'viabilidade/lat-long' : 'viabilidade');
const response = await axios.post(endpoint, data, {
// A API agora faz geocode + GeoGrid + trajeto (várias chamadas externas);
// o cep-promise também pode ser lento. 30s evita timeout prematuro.
timeout: 30000,
headers: { 'Content-Type': 'application/json' }
});
return response.data;
}
// Consulta em LOTE: envia um array de itens ({cep,numero} ou {latitude,longitude}) de uma só vez.
// Retorna o array `resultados` da API, alinhado por índice ao array enviado.
async function consultarViabilidadeLote(itens, modo) {
const endpoint = apiUrlBase + 'viabilidade/lote';
const body = { itens };
if (modo) body.modo = modo;
const response = await axios.post(endpoint, body, {
timeout: 180000,
headers: { 'Content-Type': 'application/json' }
});
const data = response.data || {};
return Array.isArray(data.resultados) ? data.resultados : [];
}
module.exports = { consultarViabilidade, consultarViabilidadeLote, discoverDataType };