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; } // 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) { // preciso criar 2 campos: Link Dedicado e Link Não Dedicado em que o dedicado é viável até 1000m e o não dedicado até 500m if (result.dist <= 500) { return res.json({ endereco, latitude: lat, longitude: lon, distancia: result.dist, dedicado: 'Viável', naoDedicado: 'Viável' }); } else if (result.dist <= 1000) { return res.json({ endereco, latitude: lat, longitude: lon, distancia: result.dist, dedicado: 'Viável', naoDedicado: 'Não viável' }); } else { return res.json({ endereco, latitude: lat, longitude: lon, distancia: result.dist, dedicado: 'Não viável', naoDedicado: 'Não viável' }); } } } 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}`));