FEAT: Adiciona suporte à consulta de viabilidade por geolocalização e refatora lógica de processamento de CSV

This commit is contained in:
tulioperdigao 2026-01-15 15:16:28 -03:00
parent e5b530f9a4
commit e24db52732
5 changed files with 120 additions and 19 deletions

View File

@ -21,5 +21,6 @@ const apiConfig = {
};
const apiViabilidadeUrl = process.env.API_VIABILIDADE_URL;
const apiUrlBase = process.env.API_URL_BASE;
module.exports = { apiConfig, apiViabilidadeUrl };
module.exports = { apiConfig, apiViabilidadeUrl, apiUrlBase };

View File

@ -1,4 +1,4 @@
const { consultarViabilidade } = require('../service/viabilidadeService');
const { consultarViabilidade, discoverDataType } = require('../service/viabilidadeService');
const { processCsvFile, countValidLines } = require('../service/csvService');
const { getJob, createJob } = require('../service/jobStore.service');
const fs = require('fs');
@ -18,6 +18,21 @@ async function consultarViabilidadeController(req, res) {
}
}
// Controlador para consultar viabilidade via geolocalização
async function consultarViaGeolocalizacaoController(req, res) {
try {
const data = req.body;
data.source = 'viabiliza.sothis.com.br';
const result = await consultarViabilidade(data);
res.json(result);
} catch (error) {
console.error("Erro ao consultar viabilidade por geolocalização:", error && (error.message || error));
res.status(500).json({ error: "Erro ao consultar viabilidade por geolocalização" });
}
}
async function uploadCsvFile(req, res) {
try {
// validação simples: verifica se multer populou req.file
@ -28,6 +43,12 @@ async function uploadCsvFile(req, res) {
const filePath = req.file.path;
const originalName = req.file.originalname || req.file.filename || 'input.csv';
// Verifica o tipo de dados do CSV
const dataType = await discoverDataType(filePath);
if (dataType === 'unknown') {
return res.status(400).json({ error: 'Formato de CSV inválido. Deve conter colunas CEP e Número ou Latitude e Longitude.' });
}
// Conta as linhas válidas primeiro
const total = await countValidLines(filePath);
if (total === 0) {
@ -151,4 +172,4 @@ async function downloadModelController(req, res) {
}
}
module.exports = { consultarViabilidadeController, uploadCsvFile, getJobController, downloadCsvController, downloadModelController };
module.exports = { consultarViabilidadeController, uploadCsvFile, getJobController, downloadCsvController, downloadModelController, consultarViaGeolocalizacaoController };

View File

@ -10,6 +10,9 @@ const upload = multer({ dest: path.join(__dirname, '..', 'uploads') });
// Rota para consultar viabilidade
router.post('/viabilidade', viabilidadeController.consultarViabilidadeController);
// Rota para consultar por geolocalização
router.post('/geolocalizacao', viabilidadeController.consultarViaGeolocalizacaoController);
// rota de upload agora usa multer.single('csvfile')
router.post('/upload', upload.single('csvfile'), viabilidadeController.uploadCsvFile);

View File

@ -1,4 +1,4 @@
const { consultarViabilidade } = require('./viabilidadeService');
const { consultarViabilidade, discoverDataType } = require('./viabilidadeService');
const fs = require('fs');
const path = require('path');
const readline = require('readline');
@ -11,8 +11,9 @@ const {
failJob
} = require('./jobStore.service');
// conta linhas válidas no CSV (com CEP e Número)
// conta linhas válidas no CSV (com CEP e Número ou Latitude e Longitude)
async function countValidLines(inputPath) {
const dataType = await discoverDataType(inputPath);
const instream = fs.createReadStream(inputPath, { encoding: 'utf8' });
const rl = readline.createInterface({ input: instream, crlfDelay: Infinity });
@ -20,6 +21,8 @@ async function countValidLines(inputPath) {
let headers = [];
let idxCep = -1;
let idxNumero = -1;
let idxLatitude = -1;
let idxLongitude = -1;
let total = 0;
for await (const rawLine of rl) {
@ -31,15 +34,22 @@ async function countValidLines(inputPath) {
const lower = headers.map(h => h.toLowerCase());
idxCep = lower.indexOf('cep');
idxNumero = lower.indexOf('numero');
idxLatitude = lower.indexOf('latitude');
idxLongitude = lower.indexOf('longitude');
isHeader = false;
continue;
}
const cols = line.split(';').map(c => c.trim());
if (dataType === 'cep') {
const cep = idxCep >= 0 ? String(cols[idxCep] || '').replace(/\D/g, '') : '';
const numero = idxNumero >= 0 ? cols[idxNumero] : '';
if (cep && numero) total++;
} else if (dataType === 'geolocalizacao') {
const latitude = idxLatitude >= 0 ? parseFloat(cols[idxLatitude]) : NaN;
const longitude = idxLongitude >= 0 ? parseFloat(cols[idxLongitude]) : NaN;
if (!isNaN(latitude) && !isNaN(longitude)) total++;
}
}
return total;
@ -48,6 +58,7 @@ async function countValidLines(inputPath) {
// nova função: processa CSV linha a linha, chama consultarViabilidade e gera CSV de saída
// Recebe jobId já criado no controller
async function processCsvFile(jobId, inputPath, originalName) {
const dataType = await discoverDataType(inputPath);
const total = await countValidLines(inputPath);
// Job já criado no controller
// const jobId = createJob(total);
@ -64,6 +75,8 @@ async function processCsvFile(jobId, inputPath, originalName) {
let headers = [];
let idxCep = -1;
let idxNumero = -1;
let idxLatitude = -1;
let idxLongitude = -1;
for await (const rawLine of rl) {
const line = rawLine.replace(/\r$/, ''); // normalize CRLF
@ -78,6 +91,8 @@ async function processCsvFile(jobId, inputPath, originalName) {
const lower = headers.map(h => h.toLowerCase());
idxCep = lower.indexOf('cep');
idxNumero = lower.indexOf('numero');
idxLatitude = lower.indexOf('latitude');
idxLongitude = lower.indexOf('longitude');
// se não encontrar, tenta variações comuns
const idx = lower.indexOf('codigo postal');
@ -95,6 +110,8 @@ async function processCsvFile(jobId, inputPath, originalName) {
.map(c => c.trim())
.filter(c => c !== '');
let dataToSend = {};
if (dataType === 'cep') {
const cepRaw = (idxCep >= 0 && cols[idxCep]) ? cols[idxCep] : '';
const cep = String(cepRaw).replace(/\D/g, ''); // keep digits only
const numero = (idxNumero >= 0 && cols[idxNumero]) ? cols[idxNumero] : '';
@ -102,13 +119,25 @@ async function processCsvFile(jobId, inputPath, originalName) {
if (!cep || !numero) {
continue; // pula linha inválida
}
dataToSend = { cep, numero };
} else if (dataType === 'geolocalizacao') {
const latitude = (idxLatitude >= 0 && cols[idxLatitude]) ? parseFloat(cols[idxLatitude]) : NaN;
const longitude = (idxLongitude >= 0 && cols[idxLongitude]) ? parseFloat(cols[idxLongitude]) : NaN;
if (isNaN(latitude) || isNaN(longitude)) {
continue; // pula linha inválida
}
dataToSend = { latitude, longitude };
} else {
continue; // tipo desconhecido, pula
}
try {
const viab = await consultarViabilidade({ cep, numero });
const viab = await consultarViabilidade(dataToSend);
const distancia = viab.distancia ?? (viab.raw && (viab.raw.distancia || viab.raw.distance)) ?? '';
const endereco = `${(viab.logradouro) || ''}, ${(viab.bairro) || ''}, ${(viab.cidade) || ''}/${(viab.estado) || ''}, ${(viab.cep) || ''}`.trim().replace(/^[, ]+|[, ]+$/g, '');
const endereco = viab.endereco;
if (viab.naoDedicado) {
var naoDedicado = "Viavel";

View File

@ -1,11 +1,20 @@
const axios = require('axios');
const { apiConfig, apiViabilidadeUrl } = require('../config/apiConfig');
const fs = require('fs');
const readline = require('readline');
const { apiConfig, apiViabilidadeUrl, apiUrlBase } = require('../config/apiConfig');
const DEFAULT_TIMEOUT = (apiConfig && apiConfig.timeoutMs) || 10000;
async function consultarViabilidade(data) {
try {
const response = await axios.post(apiViabilidadeUrl, data, {
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: DEFAULT_TIMEOUT,
headers: { 'Content-Type': 'application/json' }
});
@ -16,4 +25,42 @@ async function consultarViabilidade(data) {
}
}
module.exports = { consultarViabilidade };
// Preciso de uma função para verificar se os dados vindos são de CEP ou de geolocalização
async function discoverDataType(input) {
if (typeof input === 'string') {
// Trata como filePath
const instream = fs.createReadStream(input, { encoding: 'utf8' });
const rl = readline.createInterface({ input: instream, crlfDelay: Infinity });
let headers = [];
for await (const rawLine of rl) {
const line = rawLine.replace(/\r$/, '');
if (!line.trim()) continue;
headers = line.split(';').map(h => h.trim().toLowerCase());
break;
}
rl.close();
if (headers.includes('cep') && headers.includes('numero')) {
return 'cep';
} else if (headers.includes('latitude') && headers.includes('longitude')) {
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 && input.longitude) {
return 'geolocalizacao';
} else {
return 'unknown';
}
} else {
return 'unknown';
}
}
module.exports = { consultarViabilidade, discoverDataType };