From 8b85a32086a70d6480dd97e859a5766d8a33617e Mon Sep 17 00:00:00 2001 From: tulioperdigao <116309232+tulioperdigao@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:23:37 -0300 Subject: [PATCH] =?UTF-8?q?REFACTOR:=20Reorganizando=20a=20aplica=C3=A7?= =?UTF-8?q?=C3=A3o=20em=20pastas=20e=20arquivos=20separados.=20Services=20?= =?UTF-8?q?para=20regras=20de=20neg=C3=B3cio,=20app=20para=20execu=C3=A7?= =?UTF-8?q?=C3=A3o=20da=20aplica=C3=A7=C3=A3o=20e=20server=20para=20inicia?= =?UTF-8?q?liza=C3=A7=C3=A3o.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 88 ++++++++++++++ server.js | 197 +------------------------------- services/distanceService.js | 149 ++++++++++++++++++++++++ services/geocodeService.js | 35 ++++++ services/jsonService.js | 15 +++ services/partnerSiglaService.js | 23 ++++ services/sleepService.js | 5 + 7 files changed, 320 insertions(+), 192 deletions(-) create mode 100644 app.js create mode 100644 services/distanceService.js create mode 100644 services/geocodeService.js create mode 100644 services/jsonService.js create mode 100644 services/partnerSiglaService.js create mode 100644 services/sleepService.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..8b1f902 --- /dev/null +++ b/app.js @@ -0,0 +1,88 @@ +const express = require("express"); +const path = require("path"); +const cors = require("cors"); +const { fetchJson } = require("./services/jsonService"); +const { getMinDistance } = require("./services/distanceService"); +const { geocodeWithGoogle } = require("./services/geocodeService"); + +function createApp() { + const app = express(); + + app.use(cors()); + app.use(express.static(path.join(__dirname, "public"))); + app.use(express.json()); + + // 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" }); + } + }); + + return app; +} + +module.exports = createApp; diff --git a/server.js b/server.js index cac4d0b..e31c8c5 100644 --- a/server.js +++ b/server.js @@ -1,196 +1,9 @@ require('dotenv').config(); +const createApp = require('./app'); -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 = createApp(); +const PORT = process.env.PORT || 3000; -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}`) }); - -app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); - - \ No newline at end of file diff --git a/services/distanceService.js b/services/distanceService.js new file mode 100644 index 0000000..0dde7cc --- /dev/null +++ b/services/distanceService.js @@ -0,0 +1,149 @@ +const dotenv = require("dotenv"); + +dotenv.config(); + +const axios = require("axios"); +const { sleep } = require("./sleepService"); +const { normalizePartnerSigla } = require("./partnerSiglaService"); + + + // 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 + + const BASE_BACKOFF_MS = 10; // backoff inicial para retry + const MAX_RETRIES = 5; + +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; +} + +module.exports = { getMinDistance }; diff --git a/services/geocodeService.js b/services/geocodeService.js new file mode 100644 index 0000000..0de4be3 --- /dev/null +++ b/services/geocodeService.js @@ -0,0 +1,35 @@ +const dotenv = require("dotenv"); + +dotenv.config(); + +const axios = require("axios"); + +// 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; + } +} + +module.exports = { geocodeWithGoogle }; diff --git a/services/jsonService.js b/services/jsonService.js new file mode 100644 index 0000000..2e1cce4 --- /dev/null +++ b/services/jsonService.js @@ -0,0 +1,15 @@ +const axios = require('axios'); + +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; + } +} + +module.exports = { + fetchJson +}; \ No newline at end of file diff --git a/services/partnerSiglaService.js b/services/partnerSiglaService.js new file mode 100644 index 0000000..990e514 --- /dev/null +++ b/services/partnerSiglaService.js @@ -0,0 +1,23 @@ +// 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; +} + +module.exports = { normalizePartnerSigla }; diff --git a/services/sleepService.js b/services/sleepService.js new file mode 100644 index 0000000..773c0f2 --- /dev/null +++ b/services/sleepService.js @@ -0,0 +1,5 @@ +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +module.exports = { sleep }; \ No newline at end of file