diff --git a/service/csvService.js b/service/csvService.js index 6017e34..8ea7da2 100644 --- a/service/csvService.js +++ b/service/csvService.js @@ -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(' ({ 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']) + idxLatitude: findFirstHeaderIndex(headers, header => /\blat(itude)?\b/.test(header)), 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; } + + 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 { + fb = await consultarViabilidadeLote(paraFallback.map(l => l.fallback)); + } catch (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' }); + }); + } + } + + chunk.forEach(l => { + const r = resultadoPorRow.get(l.rowIndex); + if (r && r.__erro) incrementErrors(jobId); + incrementProcessed(jobId); + }); } - if (cepPayload) { - try { - const result = await consultarViabilidade(cepPayload); - if (!result || !result.error) return result; - lastError = new Error(result.error); - } catch (err) { - lastError = err; - } - } - - throw lastError || new Error('Linha sem latitude/longitude ou CEP valido'); + 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) => { - 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); - } + resultColumnsFor(resultadoPorRow, rowIndex).forEach((value, index) => { + const cell = row.getCell(index + 1); + cell.value = value; + cell.style = cloneCellStyle(row.getCell(RESULT_HEADERS.length + 1)); + }); 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); - 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); - } + const outCols = [...resultColumnsFor(resultadoPorRow, rowIndex), ...cols].map(cleanCsvValue); + outStream.write(outCols.join(';') + '\n'); } outStream.end(); diff --git a/service/fileFormat.js b/service/fileFormat.js new file mode 100644 index 0000000..dcf869d --- /dev/null +++ b/service/fileFormat.js @@ -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(' ({ 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 +}; diff --git a/service/viabilidadeService.js b/service/viabilidadeService.js index 560b99b..98e8f73 100644 --- a/service/viabilidadeService.js +++ b/service/viabilidadeService.js @@ -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(' ({ 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 { - 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 { + if (hasCepHeader(headers) && hasAddressOrNumberHeader(headers)) return 'cep'; + if (hasGeoHeaders(headers)) return 'geolocalizacao'; return 'unknown'; } + + 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'; } -module.exports = { consultarViabilidade, discoverDataType }; +// 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 };