Compare commits
3 Commits
011f7b8c47
...
801d3fa90d
| Author | SHA1 | Date | |
|---|---|---|---|
| 801d3fa90d | |||
| c47a7b3a2e | |||
| 060e387b9f |
@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="description-content">
|
<div class="description-content">
|
||||||
<p>
|
<p>
|
||||||
A capilaridade da fibra óptica da Sothis Telecom está sempre em expansão, por isso, atualizamos essa página constantemente. Essa página é de uso interno e de canais de revenda, o resultado da consulta informa a distância em em metros da caixa mais próxima, bem como identifica se ela pertence a rede própria ou parceiro last mile. Não sem divulgue as informações desta página sem o consentimento prévio da Sothis Telecom.
|
A capilaridade da fibra óptica da Sothis Telecom está sempre em expansão, por isso, atualizamos essa página constantemente. Essa página é de uso interno e de canais de revenda, o resultado da consulta informa a distância em metros da caixa mais próxima, bem como identifica se ela pertence a rede própria ou parceiro last mile. Não divulgue as informações desta página sem o consentimento prévio da Sothis Telecom.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -37,18 +37,21 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5>Consulta em lote:</h5>
|
<h5>Consulta em lote:</h5>
|
||||||
<p>
|
<p>
|
||||||
Faça um upload de um arquivo .CSV com a primeira coluna com os CEPs e a segunda coluna com o número do logradouro. O resultado da consulta será baixado automaticamente em um arquivo .CSV em seu computador. Também é possível consultar a partir da geolocalização: faça um upload de um arquivo .CSV com a primeira coluna sendo latitude e a segunda longitude.
|
Faça um upload somente de arquivos .CSV ou .XLSX com a primeira coluna com os CEPs e a segunda coluna com o número do logradouro. O resultado da consulta será baixado automaticamente em um arquivo no seu computador. Também é possível consultar a partir da geolocalização: faça o upload somente de arquivos .CSV ou .XLSX com a primeira coluna sendo latitude e a segunda longitude. Dentro da célula, deve conter apenas números e sem espaço.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Caso precise baixar os modelos de arquivo para upload, clique em "Baixar Modelos".
|
Caso precise baixar os modelos de arquivo para upload, clique em "Baixar Modelos".
|
||||||
</p>
|
</p>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-primary button-mobile" id="card__button-download">Baixar Modelos</button>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
<form id="uploadForm">
|
<form id="uploadForm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<input class="form-control" type="file" id="csvfile" accept=".csv,.xls,.xlsx" required />
|
<input class="form-control" type="file" id="csvfile" accept=".csv,.xls,.xlsx" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-buttons__container">
|
<div class="card-buttons__container">
|
||||||
<button class="btn btn-primary button-mobile" type="submit">Enviar CSV</button>
|
<button class="btn btn-primary button-mobile" id="card__button-enviar" type="submit">Enviar</button>
|
||||||
<button type="button" class="btn btn-primary button-mobile" id="card__button-download">Baixar Modelos</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div id="uploadResult" class="mt-3"></div>
|
<div id="uploadResult" class="mt-3"></div>
|
||||||
@ -68,12 +71,23 @@
|
|||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-md-4"><input id="cep" class="form-control" placeholder="CEP (somente números)"></div>
|
<div class="col-md-4"><input id="cep" class="form-control" placeholder="CEP (somente números)"></div>
|
||||||
<div class="col-md-4"><input id="numero" class="form-control" placeholder="Número"></div>
|
<div class="col-md-4"><input id="numero" class="form-control" placeholder="Número"></div>
|
||||||
<div class="col-md-4 card-buttons__container"><button id="btnConsultaCep" class="btn btn-secondary">Consultar CEP</button></div>
|
<div class="col-md-4 consultar-cep__container"><button id="btnConsultaCep" class="btn btn-secondary">Consultar CEP</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="consultaResult" class="mt-3"></div>
|
<div id="consultaResult" class="mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="aviso-importante__container">
|
||||||
|
<h4>Aviso Importante</h4>
|
||||||
|
<p>
|
||||||
|
A consulta de viabilidade disponibilizada neste sistema possui caráter preliminar e é baseada nas informações cadastrais da rede e no endereço informado.
|
||||||
|
A confirmação da disponibilidade do serviço dependerá de análise técnica realizada antes da instalação. Nessa etapa, poderão ser identificadas limitações que inviabilizem o atendimento, como indisponibilidade de capacidade da rede, necessidade de expansão da infraestrutura, distância acima do limite técnico, obstáculos para passagem da fibra óptica, restrições de acesso ao imóvel, ausência de autorizações necessárias ou outras condições técnicas e operacionais.
|
||||||
|
Por esse motivo, o resultado desta consulta não constitui garantia de disponibilidade ou contratação do serviço, estando a instalação condicionada à aprovação da análise técnica final.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="main.js"></script>
|
<script src="main.js"></script>
|
||||||
|
|||||||
@ -35,6 +35,7 @@
|
|||||||
|
|
||||||
.card-buttons__container {
|
.card-buttons__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@ -50,7 +51,8 @@ button:hover {
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border: 1px solid #31a3b5 !important;
|
border: 1px solid #31a3b5 !important;
|
||||||
color: #31a3b5 !important;
|
color: #31a3b5 !important;
|
||||||
margin-left: 10px;
|
margin-left: 0px;
|
||||||
|
top: -5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
@ -61,6 +63,26 @@ button:hover {
|
|||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aviso-importante__container {
|
||||||
|
margin-top: 35px;
|
||||||
|
padding-top: 15px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aviso-importante__container h4 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aviso-importante__container p {
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.header__container {
|
.header__container {
|
||||||
|
|||||||
@ -1,50 +1,31 @@
|
|||||||
const { consultarViabilidade, discoverDataType } = require('./viabilidadeService');
|
const { consultarViabilidadeLote, discoverDataType } = require('./viabilidadeService');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const XLSX = require('xlsx');
|
const XLSX = require('xlsx');
|
||||||
const ExcelJS = require('exceljs');
|
const ExcelJS = require('exceljs');
|
||||||
const { once } = require('events');
|
const { once } = require('events');
|
||||||
|
const {
|
||||||
|
normalizeHeader,
|
||||||
|
isExcelFile,
|
||||||
|
detectDelimiter,
|
||||||
|
hasCepHeader,
|
||||||
|
hasAddressOrNumberHeader,
|
||||||
|
hasGeoHeaders
|
||||||
|
} = require('./fileFormat');
|
||||||
const {
|
const {
|
||||||
incrementProcessed,
|
incrementProcessed,
|
||||||
incrementErrors,
|
incrementErrors,
|
||||||
finishJob
|
finishJob
|
||||||
} = require('./jobStore.service');
|
} = 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) {
|
// Quantidade de itens enviados por chamada ao endpoint /lote da API.
|
||||||
return String(value || '')
|
const LOTE_CHUNK = 25;
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isXlsxFile(filePath) {
|
function isXlsxFile(filePath) {
|
||||||
const ext = path.extname(filePath).toLowerCase();
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
@ -54,13 +35,6 @@ function isXlsxFile(filePath) {
|
|||||||
return fileStart[0] === 0x50 && fileStart[1] === 0x4b;
|
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) {
|
function splitDelimitedLine(line, delimiter) {
|
||||||
const cols = [];
|
const cols = [];
|
||||||
let current = '';
|
let current = '';
|
||||||
@ -88,7 +62,7 @@ function splitDelimitedLine(line, delimiter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readDelimitedRows(filePath) {
|
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());
|
const lines = content.split(/\r?\n/).filter(line => line.trim());
|
||||||
if (!lines.length) return [];
|
if (!lines.length) return [];
|
||||||
|
|
||||||
@ -116,27 +90,6 @@ function findFirstHeaderIndex(headers, predicate) {
|
|||||||
return headers.map(normalizeHeader).findIndex(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) {
|
function findHeaderRowIndex(rows) {
|
||||||
const index = rows.findIndex(row => (hasCepHeader(row) && hasAddressOrNumberHeader(row)) || hasGeoHeaders(row));
|
const index = rows.findIndex(row => (hasCepHeader(row) && hasAddressOrNumberHeader(row)) || hasGeoHeaders(row));
|
||||||
return index >= 0 ? index : 0;
|
return index >= 0 ? index : 0;
|
||||||
@ -153,9 +106,7 @@ function resolveColumnIndexes(headers) {
|
|||||||
idxCep: findFirstHeaderIndex(headers, header => /\bcep\b/.test(header) || header === 'codigo postal'),
|
idxCep: findFirstHeaderIndex(headers, header => /\bcep\b/.test(header) || header === 'codigo postal'),
|
||||||
idxNumero: exactIndex(['numero', 'número', 'num', 'nº', 'n°']),
|
idxNumero: exactIndex(['numero', 'número', 'num', 'nº', 'n°']),
|
||||||
idxEndereco: findFirstHeaderIndex(headers, header => header.includes('endereco') || header.includes('logradouro')),
|
idxEndereco: findFirstHeaderIndex(headers, header => header.includes('endereco') || header.includes('logradouro')),
|
||||||
// Antes: exactIndex(['latitude', 'lat'])
|
idxLatitude: findFirstHeaderIndex(headers, header => /\blat(itude)?\b/.test(header)),
|
||||||
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)),
|
idxLongitude: findFirstHeaderIndex(headers, header => /\blo?n[g]?(itude)?\b/.test(header)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -191,20 +142,25 @@ function buildCepPayload(cols, indexes) {
|
|||||||
|
|
||||||
function parseCoordinate(value) {
|
function parseCoordinate(value) {
|
||||||
const str = String(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()
|
.trim()
|
||||||
.replace(',', '.');
|
.replace(',', '.');
|
||||||
|
|
||||||
// Bug 3: converte DMS "17 38 18.80 S" → -17.638556
|
// converte DMS "17 38 18.80 S" → -17.638556. Aceita direção N/S/E/W e também
|
||||||
const dms = str.match(/^(\d+)[°\s]+(\d+)['\s]+(\d+(?:\.\d+)?)["\s]*([NSEW])?$/i);
|
// 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) {
|
if (dms) {
|
||||||
const decimal = parseFloat(dms[1]) + parseFloat(dms[2]) / 60 + parseFloat(dms[3]) / 3600;
|
const decimal = parseFloat(dms[1]) + parseFloat(dms[2]) / 60 + parseFloat(dms[3]) / 3600;
|
||||||
const negative = /[SW]/i.test(dms[4] ?? '');
|
const negativo = /[SWO]/i.test(dms[4] ?? ''); // Sul / West / Oeste => negativo
|
||||||
return negative ? -decimal : decimal;
|
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);
|
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) {
|
function buildGeoPayload(cols, indexes) {
|
||||||
@ -215,30 +171,80 @@ function buildGeoPayload(cols, indexes) {
|
|||||||
return { latitude, longitude };
|
return { latitude, longitude };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function consultarComFallback(geoPayload, cepPayload) {
|
// Monta a lista de linhas com payload a consultar. Para cada linha, define o
|
||||||
let lastError = null;
|
// 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 {
|
try {
|
||||||
const result = await consultarViabilidade(geoPayload);
|
resultados = await consultarViabilidadeLote(chunk.map(l => l.primary));
|
||||||
if (!result || !result.error) return result;
|
|
||||||
lastError = new Error(result.error);
|
|
||||||
} catch (err) {
|
} 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) {
|
return resultadoPorRow;
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanCsvValue(value) {
|
function cleanCsvValue(value) {
|
||||||
@ -258,18 +264,45 @@ function formatApiErrorResponse(error) {
|
|||||||
return error && (error.message || String(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) {
|
function buildSuccessResultColumns(viab) {
|
||||||
const provedor = viab.provedor ?? '';
|
const caixas = viab.caixas || [];
|
||||||
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 error = viab.error ? cleanCsvValue(viab.error) : '';
|
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) {
|
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) {
|
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();
|
const workbook = new ExcelJS.Workbook();
|
||||||
await workbook.xlsx.readFile(inputPath);
|
await workbook.xlsx.readFile(inputPath);
|
||||||
|
|
||||||
@ -313,23 +346,11 @@ async function processXlsxFile(jobId, inputPath, outputPath, rows, headerRowInde
|
|||||||
if (!geoPayload && !cepPayload) continue;
|
if (!geoPayload && !cepPayload) continue;
|
||||||
|
|
||||||
const row = worksheet.getRow(rowIndex + 1);
|
const row = worksheet.getRow(rowIndex + 1);
|
||||||
try {
|
resultColumnsFor(resultadoPorRow, rowIndex).forEach((value, index) => {
|
||||||
const viab = await consultarComFallback(geoPayload, cepPayload);
|
const cell = row.getCell(index + 1);
|
||||||
buildSuccessResultColumns(viab).forEach((value, index) => {
|
cell.value = value;
|
||||||
const cell = row.getCell(index + 1);
|
cell.style = cloneCellStyle(row.getCell(RESULT_HEADERS.length + 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();
|
row.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -427,8 +448,12 @@ async function processCsvFile(jobId, inputPath, originalName) {
|
|||||||
const outputPath = path.join(__dirname, '..', 'outputs', outputFilename);
|
const outputPath = path.join(__dirname, '..', 'outputs', outputFilename);
|
||||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
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)) {
|
if (isXlsxFile(inputPath)) {
|
||||||
await processXlsxFile(jobId, inputPath, outputPath, rows, headerRowIndex, indexes);
|
await processXlsxFile(inputPath, outputPath, rows, headerRowIndex, indexes, resultadoPorRow);
|
||||||
finishJob(jobId, path.basename(outputPath));
|
finishJob(jobId, path.basename(outputPath));
|
||||||
|
|
||||||
return outputPath;
|
return outputPath;
|
||||||
@ -446,15 +471,7 @@ async function processCsvFile(jobId, inputPath, originalName) {
|
|||||||
const cepPayload = buildCepPayload(cols, indexes);
|
const cepPayload = buildCepPayload(cols, indexes);
|
||||||
if (!geoPayload && !cepPayload) continue;
|
if (!geoPayload && !cepPayload) continue;
|
||||||
|
|
||||||
try {
|
rowResults.push({ rowIndex, values: resultColumnsFor(resultadoPorRow, rowIndex) });
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
workbook.Sheets[firstSheetName] = prependResultColumnsToWorksheet(worksheet, headerRowIndex, rowResults);
|
workbook.Sheets[firstSheetName] = prependResultColumnsToWorksheet(worksheet, headerRowIndex, rowResults);
|
||||||
@ -465,25 +482,17 @@ async function processCsvFile(jobId, inputPath, originalName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const outStream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
|
const outStream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
|
||||||
outStream.write('\uFEFF');
|
outStream.write('');
|
||||||
outStream.write([...RESULT_HEADERS, ...headers].join(';') + '\n');
|
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 geoPayload = buildGeoPayload(cols, indexes);
|
||||||
const cepPayload = buildCepPayload(cols, indexes);
|
const cepPayload = buildCepPayload(cols, indexes);
|
||||||
if (!geoPayload && !cepPayload) continue;
|
if (!geoPayload && !cepPayload) continue;
|
||||||
|
|
||||||
try {
|
const outCols = [...resultColumnsFor(resultadoPorRow, rowIndex), ...cols].map(cleanCsvValue);
|
||||||
const viab = await consultarComFallback(geoPayload, cepPayload);
|
outStream.write(outCols.join(';') + '\n');
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outStream.end();
|
outStream.end();
|
||||||
|
|||||||
72
service/fileFormat.js
Normal file
72
service/fileFormat.js
Normal 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
|
||||||
|
};
|
||||||
@ -1,61 +1,21 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const readline = require('readline');
|
const readline = require('readline');
|
||||||
const path = require('path');
|
|
||||||
const XLSX = require('xlsx');
|
const XLSX = require('xlsx');
|
||||||
const { apiConfig, apiViabilidadeUrl, apiUrlBase } = require('../config/apiConfig');
|
const { apiUrlBase } = require('../config/apiConfig');
|
||||||
|
const {
|
||||||
function normalizeHeader(value) {
|
normalizeHeader,
|
||||||
return String(value || '')
|
isExcelFile,
|
||||||
.trim()
|
detectDelimiter,
|
||||||
.toLowerCase()
|
hasCepHeader,
|
||||||
.normalize('NFD')
|
hasAddressOrNumberHeader,
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
hasGeoHeaders
|
||||||
.replace(/[_-]+/g, ' ')
|
} = require('./fileFormat');
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findHeaderRow(rows) {
|
function findHeaderRow(rows) {
|
||||||
return rows.find(row => {
|
return rows.find(row => {
|
||||||
const headers = row.map(normalizeHeader);
|
const hasCepNumero = hasCepHeader(row) && hasAddressOrNumberHeader(row);
|
||||||
const hasCepNumero = hasCepHeader(headers) && hasAddressOrNumberHeader(headers);
|
const hasGeo = hasGeoHeaders(row);
|
||||||
const hasGeo = hasGeoHeaders(headers);
|
|
||||||
return hasCepNumero || hasGeo;
|
return hasCepNumero || hasGeo;
|
||||||
}) || [];
|
}) || [];
|
||||||
}
|
}
|
||||||
@ -79,12 +39,10 @@ async function readDelimitedHeaders(filePath) {
|
|||||||
const rl = readline.createInterface({ input: instream, crlfDelay: Infinity });
|
const rl = readline.createInterface({ input: instream, crlfDelay: Infinity });
|
||||||
|
|
||||||
for await (const rawLine of rl) {
|
for await (const rawLine of rl) {
|
||||||
const line = rawLine.replace(/^\uFEFF/, '').replace(/\r$/, '');
|
const line = rawLine.replace(/^/, '').replace(/\r$/, '');
|
||||||
if (!line.trim()) continue;
|
if (!line.trim()) continue;
|
||||||
const headers = line.split(detectDelimiter(line)).map(normalizeHeader);
|
const headers = line.split(detectDelimiter(line)).map(normalizeHeader);
|
||||||
const hasCepNumero = hasCepHeader(headers) && hasAddressOrNumberHeader(headers);
|
if ((hasCepHeader(headers) && hasAddressOrNumberHeader(headers)) || hasGeoHeaders(headers)) {
|
||||||
const hasGeo = hasGeoHeaders(headers);
|
|
||||||
if (hasCepNumero || hasGeo) {
|
|
||||||
rl.close();
|
rl.close();
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
@ -94,69 +52,55 @@ async function readDelimitedHeaders(filePath) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCepHeader(headers) {
|
// Verifica se os dados vindos são de CEP ou de geolocalização (aceita caminho de arquivo ou objeto).
|
||||||
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
|
|
||||||
async function discoverDataType(input) {
|
async function discoverDataType(input) {
|
||||||
if (typeof input === 'string') {
|
if (typeof input === 'string') {
|
||||||
const headers = isExcelFile(input)
|
const headers = isExcelFile(input)
|
||||||
? readExcelHeaders(input)
|
? readExcelHeaders(input)
|
||||||
: await readDelimitedHeaders(input);
|
: await readDelimitedHeaders(input);
|
||||||
|
|
||||||
const hasCepNumero = hasCepHeader(headers) && hasAddressOrNumberHeader(headers);
|
if (hasCepHeader(headers) && hasAddressOrNumberHeader(headers)) return 'cep';
|
||||||
if (hasCepNumero) {
|
if (hasGeoHeaders(headers)) return 'geolocalizacao';
|
||||||
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 {
|
|
||||||
return 'unknown';
|
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 };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user