Primeiro commit

This commit is contained in:
tulioperdigao 2025-10-13 10:43:27 -03:00
commit 001a007b68
15 changed files with 7881 additions and 0 deletions

5
.env Normal file
View File

@ -0,0 +1,5 @@
GOOGLE_API_KEY="AIzaSyCTrRFGKCZSspHRmTWQiclmIEOg-LROgyo"
API_URL="https://plutao.geogridmaps.com.br/vale/api/v3/viabilidade/raio"
API_KEY="6d717e972ba17c7cf0ab731801b8bbeac2f281e5"
COKIE="PHPSESSID=6d717e972ba17c7cf0ab731801b8bbeac2f281e5"
PORT="3000"

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# Aplicação Viabilidade
Pequena aplicação Node.js para processar CSVs com endereços/lat-lon e consultar a API de viabilidade.
Como usar:
1. Abra um terminal e entre na pasta `aplicação viabilidade`.
2. Crie um virtualenv Node e instale dependências:
```powershell
npm install
```
3. Inicie o servidor:
```powershell
npm start
```
4. Abra `http://localhost:3000` no navegador. Faça o upload do CSV ou consulte manualmente por latitude/longitude.
Notas:
- Configure variáveis de ambiente `API_KEY` e `COOKIE` se quiser usar credenciais diferentes.
- O CSV de saída é gravado em `aplicação viabilidade/outputs` com ponto-e-vírgula como separador.

0
logs/server.log Normal file
View File

View File

@ -0,0 +1,51 @@
CEP;Número;Endereço;Latitude;Longitude;Não dedicado;Dedicado;Distancia;Parceiro/Sothis
09693090;;Rua Giácomo Gobato, Paulicéia, São Bernardo do Campo - SP;-23.668952;-46.578870;viável;viável;238M;Sothis
06328000;;Rua do Estádio, Conjunto Habitacional Presidente Castelo Branco, Carapicuíba - SP;-23.525912;-46.827711;viável;viável;227M;
04723003;;Avenida João Dias, Santo Amaro, São Paulo - SP;-23.645323;-46.713501;viável;viável;49M;Sothis
09635100;;Rua Londrina, Rudge Ramos, São Bernardo do Campo - SP;-23.653116;-46.570632;viável;viável;160M;Sothis
01427002;;Rua Estados Unidos, Jardim América, São Paulo - SP;-23.567089;-46.668385;viável;viável;89M;Sothis
07252312;;Estrada do Caminho Velho, Jardim Nova Cidade, Guarulhos - SP;-23.440175;-46.404492;viável;viável;44M;
72520000;;;;;;;;
07243550;;Rua São José do Paraíso, Parque das Nações, Guarulhos - SP;-23.440003;-46.409005;viável;viável;155M;
07252000;;Estrada Presidente Juscelino Kubitschek de Oliveira, Jardim Albertina, Guarulhos - SP;-23.443960;-46.408090;viável;viável;393M;
03063000;;Avenida Celso Garcia, Tatuapé, São Paulo - SP;-23.535886;-46.571523;Não viável;viável;670M;Sothis
03089000;;Rua Antônio de Barros, Tatuapé, São Paulo - SP;-23.532450;-46.559923;viável;viável;73M;Sothis
03064000;;Avenida Celso Garcia, Tatuapé, São Paulo - SP;-23.535886;-46.571523;Não viável;viável;670M;Sothis
03063000;;Avenida Celso Garcia, Tatuapé, São Paulo - SP;-23.535886;-46.571523;Não viável;viável;670M;Sothis
05724003;;Avenida Giovanni Gronchi, Vila Andrade, São Paulo - SP;-23.633665;-46.736968;viável;viável;88M;Sothis
09663070;;Rua dos Alpes, Taboão, São Bernardo do Campo - SP;-23.656249;-46.600384;viável;viável;162M;Sothis
09623010;;Rua Cândido Portinari, Rudge Ramos, São Bernardo do Campo - SP;-23.662025;-46.572399;viável;viável;60M;Sothis
09623010;;Rua Cândido Portinari, Rudge Ramos, São Bernardo do Campo - SP;-23.662025;-46.572399;viável;viável;60M;Sothis
04547130;;Alameda Vicente Pinzon, Vila Olímpia, São Paulo - SP;-23.597222;-46.686828;viável;viável;6M;Sothis
04550000;;Rua Alvorada, Vila Olímpia, São Paulo - SP;-23.601978;-46.680931;viável;viável;111M;Sothis
04013001;;Rua Cubatão, Vila Mariana, São Paulo - SP;-23.579522;-46.642049;viável;viável;119M;Sothis
04220001;;Avenida Presidente Wilson, Vila Independência, São Paulo - SP;-23.598027;-46.586724;viável;viável;311M;Sothis
04328020;;Rua Filinto Elíseo, Jardim Lourdes (Zona Sul), São Paulo - SP;-23.664197;-46.636933;viável;viável;10M;Sothis
04327020;;Rua Professor Araújo Maciel, Vila Fachini, São Paulo - SP;-23.661432;-46.634321;viável;viável;71M;Sothis
09695000;;Rua Garcia Lorca, Paulicéia, São Bernardo do Campo - SP;-23.670972;-46.573067;viável;viável;15M;Sothis
04326080;;Avenida Euclides, Vila Fachini, São Paulo - SP;-23.657608;-46.635149;viável;viável;15M;Sothis
05651002;;Avenida Giovanni Gronchi, Morumbi, São Paulo - SP;-23.598015;-46.719745;viável;viável;270M;Sothis
04119000;;Rua Afonso Celso, Vila Mariana, São Paulo - SP;-23.598109;-46.634870;viável;viável;101M;Sothis
04182001;;Avenida Marginal Direita Anchieta, Jardim Santa Cruz (Sacomã), São Paulo - SP;-23.637750;-46.594034;Não viável;Não viável;1555M;Sothis
05640004;;Avenida Doutor Guilherme Dumont Vilares, Jardim Londrina, São Paulo - SP;-23.606544;-46.745966;viável;viável;429M;Sothis
05717270;;Rua José Ramon Urtiza, Vila Andrade, São Paulo - SP;-23.634250;-46.732806;viável;viável;36M;Sothis
05501020;;Rua Pirajussara, Butantã, São Paulo - SP;-23.570436;-46.705975;viável;viável;22M;Sothis
05533000;;Avenida Eliseu de Almeida, Instituto de Previdência, São Paulo - SP;-23.578949;-46.719039;viável;viável;441M;Sothis
04423070;;Rua Lorenzo Mavilis, Jardim Melo, São Paulo - SP;-23.686448;-46.634629;Não viável;Não viável;2487M;Sothis
05651001;;Avenida Giovanni Gronchi, Morumbi, São Paulo - SP;-23.598015;-46.719745;viável;viável;270M;Sothis
04726000;;Avenida João Carlos da Silva Borges, Vila Cruzeiro, São Paulo - SP;-23.640291;-46.711730;viável;viável;20M;Sothis
05717270;;Rua José Ramon Urtiza, Vila Andrade, São Paulo - SP;-23.634250;-46.732806;viável;viável;36M;Sothis
04756010;;Rua José Abrantes, Santo Amaro, São Paulo - SP;-23.646962;-46.717362;viável;viável;18M;Sothis
05510050;;Rua Camargo, Butantã, São Paulo - SP;-23.572095;-46.709575;viável;viável;106M;Sothis
04509021;;Rua Bueno Brandão, Vila Nova Conceição, São Paulo - SP;-23.590811;-46.670515;viável;viável;15M;Sothis
04509001;;Rua Jacques Félix, Vila Nova Conceição, São Paulo - SP;-23.591721;-46.671305;viável;viável;11M;Sothis
04512900;;Rua Diogo Jácome, Vila Nova Conceição, São Paulo - SP;-23.594937;-46.666452;viável;viável;347M;Sothis
04512001;;Rua Diogo Jácome, Vila Nova Conceição, São Paulo - SP;-23.594937;-46.666452;viável;viável;347M;Sothis
04515030;;Avenida Jacutinga, Indianópolis, São Paulo - SP;-23.602405;-46.666464;viável;viável;382M;Sothis
09655000;;Avenida do Taboão, Taboão, São Bernardo do Campo - SP;-23.657319;-46.596513;viável;viável;449M;Sothis
09604000;;Avenida Senador Vergueiro, Rudge Ramos, São Bernardo do Campo - SP;-23.662056;-46.564535;viável;viável;273M;Sothis
09625055;;Rua 25 de Janeiro, Rudge Ramos, São Bernardo do Campo - SP;-23.660854;-46.567256;viável;viável;95M;Sothis
04101100;;Rua Vergueiro, Vila Mariana, São Paulo - SP;-23.586961;-46.634285;viável;viável;121M;Sothis
04524030;;Avenida Bem-te-vi, Moema, São Paulo - SP;-23.607630;-46.668614;viável;viável;14M;Sothis
09080370;;Rua das Figueiras, Campestre, Santo André - SP;-23.639401;-46.545595;viável;viável;281M;Sothis
09090521;;Rua das Monções, Jardim, Santo André - SP;-23.652560;-46.534243;viável;viável;311M;Sothis
1 CEP Número Endereço Latitude Longitude Não dedicado Dedicado Distancia Parceiro/Sothis
2 09693090 Rua Giácomo Gobato, Paulicéia, São Bernardo do Campo - SP -23.668952 -46.578870 viável viável 238M Sothis
3 06328000 Rua do Estádio, Conjunto Habitacional Presidente Castelo Branco, Carapicuíba - SP -23.525912 -46.827711 viável viável 227M
4 04723003 Avenida João Dias, Santo Amaro, São Paulo - SP -23.645323 -46.713501 viável viável 49M Sothis
5 09635100 Rua Londrina, Rudge Ramos, São Bernardo do Campo - SP -23.653116 -46.570632 viável viável 160M Sothis
6 01427002 Rua Estados Unidos, Jardim América, São Paulo - SP -23.567089 -46.668385 viável viável 89M Sothis
7 07252312 Estrada do Caminho Velho, Jardim Nova Cidade, Guarulhos - SP -23.440175 -46.404492 viável viável 44M
8 72520000
9 07243550 Rua São José do Paraíso, Parque das Nações, Guarulhos - SP -23.440003 -46.409005 viável viável 155M
10 07252000 Estrada Presidente Juscelino Kubitschek de Oliveira, Jardim Albertina, Guarulhos - SP -23.443960 -46.408090 viável viável 393M
11 03063000 Avenida Celso Garcia, Tatuapé, São Paulo - SP -23.535886 -46.571523 Não viável viável 670M Sothis
12 03089000 Rua Antônio de Barros, Tatuapé, São Paulo - SP -23.532450 -46.559923 viável viável 73M Sothis
13 03064000 Avenida Celso Garcia, Tatuapé, São Paulo - SP -23.535886 -46.571523 Não viável viável 670M Sothis
14 03063000 Avenida Celso Garcia, Tatuapé, São Paulo - SP -23.535886 -46.571523 Não viável viável 670M Sothis
15 05724003 Avenida Giovanni Gronchi, Vila Andrade, São Paulo - SP -23.633665 -46.736968 viável viável 88M Sothis
16 09663070 Rua dos Alpes, Taboão, São Bernardo do Campo - SP -23.656249 -46.600384 viável viável 162M Sothis
17 09623010 Rua Cândido Portinari, Rudge Ramos, São Bernardo do Campo - SP -23.662025 -46.572399 viável viável 60M Sothis
18 09623010 Rua Cândido Portinari, Rudge Ramos, São Bernardo do Campo - SP -23.662025 -46.572399 viável viável 60M Sothis
19 04547130 Alameda Vicente Pinzon, Vila Olímpia, São Paulo - SP -23.597222 -46.686828 viável viável 6M Sothis
20 04550000 Rua Alvorada, Vila Olímpia, São Paulo - SP -23.601978 -46.680931 viável viável 111M Sothis
21 04013001 Rua Cubatão, Vila Mariana, São Paulo - SP -23.579522 -46.642049 viável viável 119M Sothis
22 04220001 Avenida Presidente Wilson, Vila Independência, São Paulo - SP -23.598027 -46.586724 viável viável 311M Sothis
23 04328020 Rua Filinto Elíseo, Jardim Lourdes (Zona Sul), São Paulo - SP -23.664197 -46.636933 viável viável 10M Sothis
24 04327020 Rua Professor Araújo Maciel, Vila Fachini, São Paulo - SP -23.661432 -46.634321 viável viável 71M Sothis
25 09695000 Rua Garcia Lorca, Paulicéia, São Bernardo do Campo - SP -23.670972 -46.573067 viável viável 15M Sothis
26 04326080 Avenida Euclides, Vila Fachini, São Paulo - SP -23.657608 -46.635149 viável viável 15M Sothis
27 05651002 Avenida Giovanni Gronchi, Morumbi, São Paulo - SP -23.598015 -46.719745 viável viável 270M Sothis
28 04119000 Rua Afonso Celso, Vila Mariana, São Paulo - SP -23.598109 -46.634870 viável viável 101M Sothis
29 04182001 Avenida Marginal Direita Anchieta, Jardim Santa Cruz (Sacomã), São Paulo - SP -23.637750 -46.594034 Não viável Não viável 1555M Sothis
30 05640004 Avenida Doutor Guilherme Dumont Vilares, Jardim Londrina, São Paulo - SP -23.606544 -46.745966 viável viável 429M Sothis
31 05717270 Rua José Ramon Urtiza, Vila Andrade, São Paulo - SP -23.634250 -46.732806 viável viável 36M Sothis
32 05501020 Rua Pirajussara, Butantã, São Paulo - SP -23.570436 -46.705975 viável viável 22M Sothis
33 05533000 Avenida Eliseu de Almeida, Instituto de Previdência, São Paulo - SP -23.578949 -46.719039 viável viável 441M Sothis
34 04423070 Rua Lorenzo Mavilis, Jardim Melo, São Paulo - SP -23.686448 -46.634629 Não viável Não viável 2487M Sothis
35 05651001 Avenida Giovanni Gronchi, Morumbi, São Paulo - SP -23.598015 -46.719745 viável viável 270M Sothis
36 04726000 Avenida João Carlos da Silva Borges, Vila Cruzeiro, São Paulo - SP -23.640291 -46.711730 viável viável 20M Sothis
37 05717270 Rua José Ramon Urtiza, Vila Andrade, São Paulo - SP -23.634250 -46.732806 viável viável 36M Sothis
38 04756010 Rua José Abrantes, Santo Amaro, São Paulo - SP -23.646962 -46.717362 viável viável 18M Sothis
39 05510050 Rua Camargo, Butantã, São Paulo - SP -23.572095 -46.709575 viável viável 106M Sothis
40 04509021 Rua Bueno Brandão, Vila Nova Conceição, São Paulo - SP -23.590811 -46.670515 viável viável 15M Sothis
41 04509001 Rua Jacques Félix, Vila Nova Conceição, São Paulo - SP -23.591721 -46.671305 viável viável 11M Sothis
42 04512900 Rua Diogo Jácome, Vila Nova Conceição, São Paulo - SP -23.594937 -46.666452 viável viável 347M Sothis
43 04512001 Rua Diogo Jácome, Vila Nova Conceição, São Paulo - SP -23.594937 -46.666452 viável viável 347M Sothis
44 04515030 Avenida Jacutinga, Indianópolis, São Paulo - SP -23.602405 -46.666464 viável viável 382M Sothis
45 09655000 Avenida do Taboão, Taboão, São Bernardo do Campo - SP -23.657319 -46.596513 viável viável 449M Sothis
46 09604000 Avenida Senador Vergueiro, Rudge Ramos, São Bernardo do Campo - SP -23.662056 -46.564535 viável viável 273M Sothis
47 09625055 Rua 25 de Janeiro, Rudge Ramos, São Bernardo do Campo - SP -23.660854 -46.567256 viável viável 95M Sothis
48 04101100 Rua Vergueiro, Vila Mariana, São Paulo - SP -23.586961 -46.634285 viável viável 121M Sothis
49 04524030 Avenida Bem-te-vi, Moema, São Paulo - SP -23.607630 -46.668614 viável viável 14M Sothis
50 09080370 Rua das Figueiras, Campestre, Santo André - SP -23.639401 -46.545595 viável viável 281M Sothis
51 09090521 Rua das Monções, Jardim, Santo André - SP -23.652560 -46.534243 viável viável 311M Sothis

5957
outputs/enderecos_output.csv Normal file

File diff suppressed because it is too large Load Diff

1152
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "aplicacao_viabilidade",
"version": "1.0.0",
"description": "Web interface para consulta de viabilidade por CSV ou manual",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"author": "",
"license": "MIT",
"dependencies": {
"axios": "^1.0.0",
"cors": "^2.8.5",
"csv-parser": "^3.0.0",
"dotenv": "^17.2.3",
"express": "^4.18.2",
"fast-csv": "^4.3.6",
"ipaddr.js": "^2.2.0",
"multer": "*"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

56
public/index.html Normal file
View File

@ -0,0 +1,56 @@
<!doctype html>
<html lang="pt-br">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Validação Viabilidade</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="shortcut icon" href="https://sothis.com.br/wp-content/uploads/2024/11/cropped-Icon-Sothis-2020-01.png" type="image/x-icon">
</head>
<body class="p-4">
<div class="header__container">
<div class="header-title__container">
<h1 class="mb-4">Validação de Viabilidade</h1>
</div>
<div class="header-logo__container">
<img src="./assets/logo_login-sothis.png" alt="Logo Branco Sothis" width="120">
</div>
</div>
<div class="container">
<div class="card mb-3">
<div class="card-body">
<h5>Upload de CSV</h5>
<form id="uploadForm">
<div class="mb-3">
<input class="form-control" type="file" id="csvfile" accept=".csv" required />
</div>
<button class="btn btn-primary" type="submit">Enviar CSV</button>
</form>
<div id="uploadResult" class="mt-3"></div>
<div class="progress mt-2" style="height:24px; display:none;" id="progressWrap">
<div id="progressBar" class="progress-bar" role="progressbar" style="width:0%">0%</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<h5>Consulta Manual por CEP</h5>
<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="numero" class="form-control" placeholder="Número"></div>
<div class="col-md-4"><button id="btnConsultaCep" class="btn btn-secondary">Consultar CEP</button></div>
</div>
<div id="consultaResult" class="mt-3"></div>
</div>
</div>
</div>
<script src="main.js"></script>
</body>
</html>

69
public/main.js Normal file
View File

@ -0,0 +1,69 @@
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fileEl = document.getElementById('csvfile');
if (!fileEl.files.length) return;
const fd = new FormData();
fd.append('csvfile', fileEl.files[0]);
const resEl = document.getElementById('uploadResult');
resEl.innerText = 'Enviando...';
try {
const resp = await fetch('/upload', { method: 'POST', body: fd });
const data = await resp.json();
if (data.jobId) {
// show progress bar
document.getElementById('progressWrap').style.display = 'block';
pollJob(data.jobId);
resEl.innerText = `Consultando viabilidade...`;
} else if (data.error) {
resEl.innerText = 'Erro: ' + data.error;
}
} catch (e) {
resEl.innerText = 'Erro no upload';
}
});
async function pollJob(jobId) {
const resEl = document.getElementById('uploadResult');
const bar = document.getElementById('progressBar');
try {
const resp = await fetch(`/status/${jobId}`);
const j = await resp.json();
if (j.total && j.total > 0) {
const pct = Math.round((j.processed / j.total) * 100);
bar.style.width = pct + '%';
bar.innerText = `${pct}%`;
}
if (j.status === 'done') {
bar.style.width = '100%';
bar.innerText = '100%';
resEl.innerHTML = `Concluído. <a href="${j.download}">Baixar CSV processado</a>`;
return;
}
if (j.status === 'error') {
resEl.innerText = 'Erro no processamento: ' + j.error;
return;
}
// ainda processando
setTimeout(() => pollJob(jobId), 1000);
} catch (e) {
resEl.innerText = 'Erro ao consultar status do job';
}
}
document.getElementById('btnConsultaCep').addEventListener('click', async () => {
const cep = document.getElementById('cep').value;
const numero = document.getElementById('numero').value;
const el = document.getElementById('consultaResult');
el.innerText = 'Consultando...';
try {
const resp = await fetch(`/consulta-cep?cep=${encodeURIComponent(cep)}&numero=${encodeURIComponent(numero)}`);
const data = await resp.json();
if (data.distancia) {
el.innerText = `Endereço: ${data.endereco}\nLat: ${data.latitude} Lon: ${data.longitude}\nDistância: ${data.distancia}`;
} else if (data.error) {
el.innerText = 'Erro: ' + data.error;
}
} catch (e) {
el.innerText = 'Erro na consulta';
}
});

55
public/style.css Normal file
View File

@ -0,0 +1,55 @@
@charset "UTF-8";
.header__container {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #234164;
width: 100%;
padding: 20px 125px 25px 125px;
margin-bottom: 50px;
}
.header-title__container > h1 {
color: white;
margin: 0px !important;
font-size: 30px;
}
.p-4 {
padding: 0px !important;
}
button {
background-color: #31a3b5 !important;
border: none !important;
}
button:hover {
background-color: #298897 !important;
}
.progress-bar {
background-color: #31a3b5 !important;
}
.card {
margin-top: 40px;
}
@media screen and (max-width: 768px) {
.header__container {
padding-left: 20px;
padding-right: 20px;
}
.header-title__container > h1{
font-size: 16px;
}
.header-logo__container > img {
width: 80px;
}
}

436
server.js Normal file
View File

@ -0,0 +1,436 @@
require('dotenv').config();
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const csv = require('csv-parser');
const fastCsv = require('fast-csv');
const axios = require('axios');
const cors = require('cors');
const app = express();
const upload = multer({ dest: 'uploads/' });
const PORT = process.env.PORT;
app.use(cors());
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
// Configure sua API_KEY e COOKIE aqui ou via variáveis de ambiente
const API_URL = process.env.API_URL;
const API_KEY = process.env.API_KEY;
const COOKIE = process.env.COOKIE;
const HEADERS = {
'api-key': API_KEY,
'Cookie': COOKIE
};
// small fetch wrapper for external services (ViaCEP etc.) with basic rate-limiting
async function fetchJson(url, opts = {}) {
try {
const r = await axios.get(url, { timeout: 10000, ...opts });
return r.data;
} catch (e) {
console.warn(`fetchJson error ${url}: ${e.message}`);
return null;
}
}
const BASE_BACKOFF_MS = parseInt(process.env.BASE_BACKOFF_MS || '1000', 10); // backoff inicial para retry
const MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '5', 10);
const REQUEST_DELAY_MS = parseInt(process.env.REQUEST_DELAY_MS || '250', 10);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// normalize partner sigla to requested labels
function normalizePartnerSigla(sigla) {
if (!sigla) return sigla;
const s = String(sigla).trim();
if (!s) return s;
const lowered = s.toLowerCase();
// map these two specific variants to 'Sothis'
if (lowered === 'são bernardo do campo - sp' || lowered === 'sao bernardo do campo - sp' || lowered === 'sao bernardo do campo') return 'Sothis';
if (lowered === 'são paulo - sp' || lowered === 'sao paulo - sp' || lowered === 'sao paulo') return 'Sothis';
return s;
}
// Geocode using Google Geocoding API. Returns { lat, lon } or null
async function geocodeWithGoogle(address) {
const key = process.env.GOOGLE_API_KEY;
if (!key) return null;
try {
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${encodeURIComponent(key)}`;
const r = await axios.get(url, { timeout: 10000 });
if (r && r.data && Array.isArray(r.data.results) && r.data.results.length > 0) {
const loc = r.data.results[0].geometry && r.data.results[0].geometry.location;
if (loc && loc.lat !== undefined && loc.lng !== undefined) {
return { lat: Number(loc.lat), lon: Number(loc.lng) };
}
}
return null;
} catch (e) {
console.warn(`geocodeWithGoogle error for '${address}': ${e.message}`);
return null;
}
}
async function getMinDistance(lat, lon) {
// tenta várias vezes com backoff exponencial; trata 429 usando Retry-After se disponível
let attempt = 0;
while (attempt < MAX_RETRIES) {
try {
// envia também o raio (em metros) - API espera esse parâmetro em várias rotas
const resp = await axios.get(API_URL, { headers: HEADERS, params: { raio: 5000, latitude: lat, longitude: lon, "itens[]": ["caixa"], consultarPasta: "S" }, timeout: 10000 });
const data = resp.data;
const registros = (data && data.registros) ? data.registros : [];
// find registros that have a numeric distancia and keep original object for robust extraction
const candidates = registros.map(r => ({ raw: r, distanciaRaw: r && r.distancia }))
.map(o => ({ raw: o.raw, num: (o.distanciaRaw !== undefined && o.distanciaRaw !== null && o.distanciaRaw !== '') ? Number(o.distanciaRaw) : null }))
.filter(x => x.num !== null && !Number.isNaN(x.num));
if (candidates.length) {
candidates.sort((a,b) => a.num - b.num);
const best = candidates[0];
const r = best.raw || {};
// robust extraction of pasta sigla with fallbacks
let pastaSigla = null;
try {
if (r.pasta) {
if (typeof r.pasta === 'string' && r.pasta.trim()) pastaSigla = r.pasta.trim();
else if (r.pasta.sigla && String(r.pasta.sigla).trim()) pastaSigla = String(r.pasta.sigla).trim();
else if (r.pasta.cidade && r.pasta.cidade.sigla && String(r.pasta.cidade.sigla).trim()) pastaSigla = String(r.pasta.cidade.sigla).trim();
}
} catch (e) {
pastaSigla = null;
}
// if closest has no pastaSigla, try find any candidate with non-empty sigla
if (!pastaSigla) {
for (let j = 0; j < candidates.length; j++) {
const rr = candidates[j].raw || {};
try {
if (rr.pasta) {
if (typeof rr.pasta === 'string' && rr.pasta.trim()) { pastaSigla = rr.pasta.trim(); break; }
if (rr.pasta.sigla && String(rr.pasta.sigla).trim()) { pastaSigla = String(rr.pasta.sigla).trim(); break; }
if (rr.pasta.cidade && rr.pasta.cidade.sigla && String(rr.pasta.cidade.sigla).trim()) { pastaSigla = String(rr.pasta.cidade.sigla).trim(); break; }
}
} catch (e) {
// continue
}
}
}
if (!pastaSigla) console.warn(`[WARN] Nenhuma pasta.sigla encontrada para coordenadas ${lat},${lon} (closest dist ${best.num})`);
pastaSigla = normalizePartnerSigla(pastaSigla);
return { dist: best.num, pastaSigla };
}
// sem distancias válidas
return null;
} catch (err) {
attempt += 1;
// se for 429, tente respeitar Retry-After quando disponível
if (err.response && err.response.status === 429) {
const ra = err.response.headers && (err.response.headers['retry-after'] || err.response.headers['Retry-After']);
let waitMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
if (ra) {
const raSec = parseInt(ra, 10);
if (!isNaN(raSec)) waitMs = raSec * 1000;
}
console.warn(`[WARN] 429 recebido para ${lat},${lon} - aguardando ${waitMs}ms e tentando novamente (attempt ${attempt}/${MAX_RETRIES})`);
await sleep(waitMs);
continue;
}
// para outros erros de rede/timeout, aguarda backoff exponencial e tenta de novo
const waitMs = BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
console.warn(`[WARN] Erro ao consultar API para ${lat},${lon}: ${err.message} - backoff ${waitMs}ms (attempt ${attempt}/${MAX_RETRIES})`);
await sleep(waitMs);
}
}
// exauriu tentativas
console.error(`[ERROR] Exauriu retries para ${lat},${lon}`);
return null;
}
// upload CSV endpoint
const jobs = {}; // jobId -> { status, total, processed, download, error }
app.post('/upload', upload.single('csvfile'), (req, res) => {
if (!req.file) return res.status(400).json({ error: 'Nenhum arquivo enviado' });
const filePath = req.file.path;
const jobId = `${Date.now()}-${Math.random().toString(36).slice(2,8)}`;
jobs[jobId] = { status: 'queued', total: 0, processed: 0, download: null, error: null };
(async () => {
jobs[jobId].status = 'processing';
try {
const rows = [];
await new Promise((resolve, reject) => {
fs.createReadStream(filePath)
.pipe(csv({ separator: ';' }))
.on('data', (data) => rows.push(data))
.on('end', resolve)
.on('error', reject);
});
jobs[jobId].total = rows.length;
const coordCache = new Map();
const outRows = [];
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
// normalize keys to avoid duplicates caused by different headers
const norm = {};
Object.keys(row).forEach(k => {
// normalize header: lowercase, remove diacritics and non-alphanumeric
const kn = k.trim().toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]/g, '');
norm[kn] = row[k];
});
// Input columns (from normalized map)
const rawCep = norm['cep'] ? String(norm['cep']).replace(/\D/g, '') : '';
const rawNumero = norm['numero'] ? String(norm['numero']).trim() : '';
// prefer lat/lon from normalized input if available
const rawLat = norm['latitude'] || norm['lat'] || null;
const rawLon = norm['longitude'] || norm['lon'] || norm['long'] || null;
// Prefer existing lat/lon if provided from normalized fields
let lat = null, lon = null;
if (rawLat && rawLon) {
lat = Number(String(rawLat).replace(',', '.'));
lon = Number(String(rawLon).replace(',', '.'));
}
let builtAddress = '';
// If no coords, try ViaCEP -> Google
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
if (rawCep) {
const cep8 = rawCep.padStart(8, '0');
const viaCepData = await fetchJson(`https://viacep.com.br/ws/${cep8}/json/`);
if (viaCepData && !viaCepData.erro) {
const logradouro = viaCepData.logradouro || '';
const bairro = viaCepData.bairro || '';
const cidade = viaCepData.localidade || '';
const uf = viaCepData.uf || '';
if (logradouro) {
builtAddress = `${logradouro}, ${rawNumero}, ${bairro}, ${cidade} - ${uf}`.replace(/, ,/g, ',').replace(/^,\s*/, '');
} else {
// fallback: use neighborhood/city
builtAddress = `${bairro || ''} ${cidade ? ', ' + cidade : ''} ${uf ? '- ' + uf : ''}`.trim();
}
// build addressToUse (builtAddress already assembled above)
if (!process.env.GOOGLE_API_KEY) {
console.error('[ERROR] GOOGLE_API_KEY não definida. Não será possível geocodificar. Defina a chave no .env ou em process.env');
} else {
const addressToUse = builtAddress || `${cidade} ${uf} ${cep8}`;
const geo = await geocodeWithGoogle(addressToUse);
if (geo) { lat = geo.lat; lon = geo.lon; }
else console.warn(`Google Geocoding não retornou resultado para '${addressToUse}' (CEP ${cep8}, row ${i+1})`);
}
} else {
console.warn(`ViaCEP erro for CEP ${rawCep} (row ${i+1})`);
}
} else {
console.log(`Row ${i+1}: missing/invalid CEP -> '${rawCep}'`);
}
}
// Prepare explicit output row to avoid extra columns
const out = {
'CEP': rawCep || '',
'Número': rawNumero || '',
'Endereço': builtAddress || '',
// write lat/lon as strings with dot decimal and fixed precision to avoid locale swaps
'Latitude': Number.isFinite(lat) ? Number(lat).toFixed(6) : '',
'Longitude': Number.isFinite(lon) ? Number(lon).toFixed(6) : '',
'Não dedicado': '',
'Dedicado': '',
'Distancia': '',
'Parceiro/Sothis': ''
};
if (Number.isFinite(lat) && Number.isFinite(lon)) {
const coordKey = `${lat.toFixed(6)},${lon.toFixed(6)}`;
if (coordCache.has(coordKey)) {
const cached = coordCache.get(coordKey); // cached is either null or { dist, pastaSigla }
if (cached !== null) {
const d = cached.dist;
const di = Math.round(Number(d));
out['Não dedicado'] = di <= 500 ? 'viável' : 'Não viável';
out['Dedicado'] = di <= 1000 ? 'viável' : 'Não viável';
out['Distancia'] = `${di}M`;
out['Parceiro/Sothis'] = normalizePartnerSigla(cached.pastaSigla) || '';
} else {
out['Não dedicado'] = 'Não viável';
out['Dedicado'] = 'Não viável';
out['Distancia'] = '5km +';
out['Parceiro/Sothis'] = '';
}
} else {
const minResult = await getMinDistance(lat, lon); // { dist, pastaSigla } or null
coordCache.set(coordKey, minResult);
if (minResult !== null) {
const di = Math.round(Number(minResult.dist));
out['Não dedicado'] = di <= 500 ? 'viável' : 'Não viável';
out['Dedicado'] = di <= 1000 ? 'viável' : 'Não viável';
out['Distancia'] = `${di}M`;
out['Parceiro/Sothis'] = normalizePartnerSigla(minResult.pastaSigla) || '';
} else {
out['Não dedicado'] = 'Não viável';
out['Dedicado'] = 'Não viável';
out['Distancia'] = '5km +';
out['Parceiro/Sothis'] = '';
}
await sleep(REQUEST_DELAY_MS);
}
} else {
// no coords available -> keep defaults
}
outRows.push(out);
jobs[jobId].processed = i + 1;
}
// write output csv - use explicit outRows and fixed header order
const outPath = path.join(__dirname, 'outputs');
if (!fs.existsSync(outPath)) fs.mkdirSync(outPath);
const originalName = (req.file && req.file.originalname) ? req.file.originalname : `upload_${Date.now()}.csv`;
const parsed = path.parse(originalName);
let outBase = `${parsed.name}_output`;
let outFile = path.join(outPath, `${outBase}.csv`);
if (fs.existsSync(outFile)) {
outFile = path.join(outPath, `${outBase}_${Date.now()}.csv`);
}
const headers = ['CEP','Número','Endereço','Latitude','Longitude','Não dedicado','Dedicado','Distancia','Parceiro/Sothis'];
await new Promise((resolve, reject) => {
const ws = fs.createWriteStream(outFile);
ws.write('\uFEFF');
fastCsv.write(outRows, { headers: headers, delimiter: ';' }).pipe(ws).on('finish', resolve).on('error', reject);
});
try { fs.unlinkSync(filePath); } catch (e) {}
jobs[jobId].status = 'done';
jobs[jobId].download = `/download/${path.basename(outFile)}`;
} catch (err) {
console.error(err);
jobs[jobId].status = 'error';
jobs[jobId].error = String(err.message || err);
}
})();
return res.json({ jobId });
});
// download endpoint
app.get('/download/:name', (req, res) => {
const name = req.params.name;
const p = path.join(__dirname, 'outputs', name);
if (!fs.existsSync(p)) return res.status(404).send('Arquivo não encontrado');
res.download(p);
});
// job status endpoint
app.get('/status/:jobId', (req, res) => {
const job = jobs[req.params.jobId];
if (!job) return res.status(404).json({ error: 'job não encontrado' });
return res.json(job);
});
// manual query endpoint
// /consulta now accepts either latitude+longitude OR cep+numero. If cep is provided we resolve ViaCEP -> Google -> Geogrid
app.get('/consulta', async (req, res) => {
const { latitude: rawLat, longitude: rawLon, cep: rawCep, numero: rawNumero } = req.query;
// If cep provided, use ViaCEP -> Google geocoding -> Geogrid
if (rawCep) {
const cep = String(rawCep).replace(/\D/g, '');
const numero = rawNumero ? String(rawNumero).trim() : '';
try {
const viaCepData = await fetchJson(`https://viacep.com.br/ws/${cep}/json/`);
if (!viaCepData || viaCepData.erro) return res.status(404).json({ error: 'CEP não encontrado' });
const logradouro = viaCepData.logradouro || '';
const bairro = viaCepData.bairro || '';
const cidade = viaCepData.localidade || '';
const uf = viaCepData.uf || '';
const endereco = `${logradouro}, ${numero}, ${bairro}, ${cidade} - ${uf}`.replace(/, ,/g, ',').replace(/^,\s*/, '');
if (!process.env.GOOGLE_API_KEY) return res.status(500).json({ error: 'GOOGLE_API_KEY não definida no servidor' });
const geo = await geocodeWithGoogle(endereco || `${cidade} ${uf} ${cep}`);
if (!geo) return res.status(404).json({ error: 'geocode não encontrado (Google)' });
const lat = Number(geo.lat);
const lon = Number(geo.lon);
const result = await getMinDistance(lat, lon);
if (result && result.dist !== undefined) {
return res.json({ endereco, latitude: lat, longitude: lon, distancia: result.dist, parceiro: result.pastaSigla || '' });
}
return res.json({ endereco, latitude: lat, longitude: lon, distancia: '5km +' });
} catch (err) {
console.error(err);
return res.status(500).json({ error: 'Erro na consulta' });
}
}
// Otherwise require latitude+longitude
if (!rawLat || !rawLon) return res.status(400).json({ error: 'latitude e longitude são obrigatórios (ou forneça cep)' });
const latitude = Number(String(rawLat).replace(',', '.'));
const longitude = Number(String(rawLon).replace(',', '.'));
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
console.warn(`Consulta manual com parâmetros inválidos: lat='${rawLat}' lon='${rawLon}'`);
return res.status(400).json({ error: 'latitude ou longitude inválidos' });
}
try {
console.log(`Consulta manual: lat=${latitude} lon=${longitude}`);
const result = await getMinDistance(latitude, longitude);
console.log(`Resultado consulta manual: ${JSON.stringify(result)}`);
if (result && result.dist !== undefined) {
return res.json({ distancia: result.dist, parceiro: result.pastaSigla || '' });
}
return res.json({ distancia: '5km +' });
} catch (err) {
console.error(err);
return res.status(500).json({ error: 'Erro na consulta' });
}
});
// manual CEP+Numero query: resolves ViaCEP -> Nominatim -> Geogrid
app.get('/consulta-cep', async (req, res) => {
const { cep: rawCep, numero: rawNumero } = req.query;
if (!rawCep) return res.status(400).json({ error: 'cep é obrigatório' });
const cep = String(rawCep).replace(/\D/g, '');
const numero = rawNumero ? String(rawNumero).trim() : '';
try {
const viaCepData = await fetchJson(`https://viacep.com.br/ws/${cep}/json/`);
if (!viaCepData || viaCepData.erro) return res.status(404).json({ error: 'CEP não encontrado' });
const logradouro = viaCepData.logradouro || '';
const bairro = viaCepData.bairro || '';
const cidade = viaCepData.localidade || '';
const uf = viaCepData.uf || '';
const endereco = `${logradouro}, ${numero}, ${bairro}, ${cidade} - ${uf}`.replace(/, ,/g, ',').replace(/^,\s*/, '');
if (!process.env.GOOGLE_API_KEY) return res.status(500).json({ error: 'GOOGLE_API_KEY não definida no servidor' });
const geo = await geocodeWithGoogle(endereco || `${cidade} ${uf} ${cep}`);
if (!geo) return res.status(404).json({ error: 'geocode não encontrado (Google)' });
const lat = Number(geo.lat);
const lon = Number(geo.lon);
const result = await getMinDistance(lat, lon);
if (result && result.dist !== undefined) {
return res.json({ endereco, latitude: lat, longitude: lon, distancia: result.dist, parceiro: result.pastaSigla || '' });
}
return res.json({ endereco, latitude: lat, longitude: lon, distancia: '5km +' });
} catch (err) {
console.error(err);
return res.status(500).json({ error: 'erro na consulta' });
}
});
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));

0
tmp/restart.txt Normal file
View File

53
tmp_versions.json Normal file
View File

@ -0,0 +1,53 @@
[
"0.0.1",
"0.0.2",
"0.0.3",
"0.0.4",
"0.0.5",
"0.0.6",
"0.0.7",
"0.1.0",
"0.1.1",
"0.1.2",
"0.1.3",
"0.1.4",
"0.1.6",
"0.1.7",
"0.1.8",
"1.0.0",
"1.0.1",
"1.0.2",
"1.0.3",
"1.0.4",
"1.0.5",
"1.0.6",
"1.1.0",
"1.2.0",
"1.2.1",
"1.3.0",
"1.3.1",
"1.4.0",
"1.4.1",
"1.4.2",
"1.4.3",
"1.4.4-lts.1",
"1.4.4",
"1.4.5-lts.1",
"1.4.5-lts.2",
"2.0.0-alpha.1",
"2.0.0-alpha.2",
"2.0.0-alpha.3",
"2.0.0-alpha.4",
"2.0.0-alpha.5",
"2.0.0-alpha.6",
"2.0.0-alpha.7",
"2.0.0-beta.1",
"2.0.0-rc.1",
"2.0.0-rc.2",
"2.0.0-rc.3",
"2.0.0-rc.4",
"2.0.0",
"2.0.1",
"2.0.2",
"3.0.0-alpha.1"
]