FEAT: Adição do insert das viabilidades no banco.

- Model e repository alterado para o insert dos dados de viabilidade.

- Refatoração dos logs.
This commit is contained in:
Gabriel Amancio 2025-12-08 15:43:20 -03:00
parent 3cf2004d66
commit 50935d43e7
10 changed files with 255 additions and 33 deletions

147
package-lock.json generated
View File

@ -14,6 +14,7 @@
"dotenv": "^17.2.3",
"express": "^5.1.0",
"node-cron": "^4.2.1",
"pg": "^8.16.3",
"qs": "^6.11.0",
"winston": "^3.18.3",
"winston-daily-rotate-file": "^5.0.0"
@ -1216,6 +1217,95 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/pg": {
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
"pg-protocol": "^1.10.3",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.2.7"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@ -1229,6 +1319,45 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -1552,6 +1681,15 @@
"node": ">=10"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
@ -1759,6 +1897,15 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
}
}
}

View File

@ -23,6 +23,7 @@
"dotenv": "^17.2.3",
"express": "^5.1.0",
"node-cron": "^4.2.1",
"pg": "^8.16.3",
"qs": "^6.11.0",
"winston": "^3.18.3",
"winston-daily-rotate-file": "^5.0.0"
@ -32,4 +33,4 @@
"cross-env": "^10.1.0",
"nodemon": "^3.1.11"
}
}
}

View File

@ -2,16 +2,14 @@ const contratacaoService = require('./contratacao.service.js');
const logger = require('../../shared/utils/logger.js');
async function handleViabilidade(req, res) {
const { cep, numero } = req.body;
logger.info('Iniciando handleViabilidade', { cep, numero });
const rawViabilidadeData = req.body;
try {
const resultadoViabilidade = await contratacaoService.verificarViabilidade(cep, numero);
logger.info('Viabilidade verificada com sucesso', { cep, numero, resultado: resultadoViabilidade });
const resultadoViabilidade = await contratacaoService.verificarViabilidade(rawViabilidadeData);
return res.json(resultadoViabilidade);
} catch (error) {
logger.error('Erro no controller ao processar viabilidade', { message: error.message, stack: error.stack, cep, numero });
logger.error('Erro no controller ao processar viabilidade', { message: error.message, stack: error.stack, rawViabilidadeData });
const statusCode = error.statusCode || 500;
return res.status(statusCode).json({ error: error.message || "Erro interno ao processar a viabilidade." });
}
@ -19,7 +17,6 @@ async function handleViabilidade(req, res) {
async function handleCriarProspecto(req, res) {
const prospectData = req.body;
logger.info('Iniciando handleCriarProspecto', { prospectData });
try {
const resultado = await contratacaoService.criarProspecto(prospectData);

View File

@ -1,13 +1,16 @@
// classe construtor para o modelo de viabilidade
class ViabilidadeModel {
constructor(cep, numero, bairro, city, state, logradouro, naoDedicado, dedicado) {
constructor(nome, email, telefone, logradouro, numero, bairro, city, state, cep, naoDedicado, dedicado) {
// Inicialização de propriedades do modelo pode ser feita aqui
this.cep = cep;
this.nome = nome;
this.email = email;
this.telefone = telefone;
this.logradouro = logradouro;
this.numero = numero;
this.bairro = bairro;
this.cidade = city;
this.estado = state;
this.logradouro = logradouro;
this.cep = cep;
this.naoDedicado = naoDedicado;
this.dedicado = dedicado;
}

View File

@ -1,3 +1,52 @@
const dbConfig = require('../../shared/config/dbConfig.js');
async function insertViabilidadeData(viabilidadeData) {
const query = `
INSERT INTO viabilidades (
nome,
email,
telefone,
logradouro,
numero,
bairro,
cidade,
estado,
cep,
nao_dedicado,
dedicado,
data
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())
RETURNING *;
`;
const values = [
viabilidadeData.nome,
viabilidadeData.email,
viabilidadeData.telefone,
viabilidadeData.logradouro,
viabilidadeData.numero,
viabilidadeData.bairro,
viabilidadeData.cidade,
viabilidadeData.estado,
viabilidadeData.cep,
viabilidadeData.naoDedicado,
viabilidadeData.dedicado,
];
try {
const res = await dbConfig.pool.query(query, values);
return res.rows[0];
} catch (err) {
throw new Error(`Erro ao inserir dados de viabilidade: ${err.message}`);
}
}
module.exports = {
insertViabilidadeData
};
/*
DESCRIÇÃO:
Este arquivo implementa o padrão de "Repository" (Repositório) para o módulo de contratação. A camada de repositório é responsável por toda a comunicação com a fonte de dados, que geralmente é um banco de dados.

View File

@ -3,6 +3,7 @@ const googleService = require("../../shared/apis/googleService.js");
const cepRestService = require("../../shared/apis/cepRestService.js");
const hubsoftService = require("../../shared/apis/hubsoftService.js");
const logger = require('../../shared/utils/logger.js');
const repository = require('./contratacao.repository.js');
const {ViabilidadeModel, ClientModelPf, ClientModelPj, ProspectModel} = require('./contratacao.model');
// utilitário para ler chaves do payload tolerante a ":" e espaços
@ -20,7 +21,7 @@ const getPayloadValue = (obj, key) => {
return undefined;
};
// mapping de planos para IDs e valores no Hubsoft
// Classe de erro personalizada para o serviço
class ServiceError extends Error {
constructor(message, statusCode = 500) {
@ -30,15 +31,18 @@ class ServiceError extends Error {
}
}
async function verificarViabilidade(rawCep, rawNumero) {
logger.info('Iniciando verificação de viabilidade', { cep: rawCep, numero: rawNumero });
// Função para verificar a viabilidade de um endereço
async function verificarViabilidade(rawViabilidadeData) {
const rawCep = rawViabilidadeData.cep;
const rawNumero = rawViabilidadeData.numero;
if (!rawCep || !rawNumero) {
logger.warn('CEP ou número não fornecidos na verificação de viabilidade.');
throw new ServiceError('CEP e número são obrigatórios.', 400);
}
const address = await cepRestService.getConsultaCep(rawCep, rawNumero);
const address = await cepRestService.getConsultaCep(rawCep);
// FIX: Revertendo para a verificação correta da resposta do cepRestService
if (!address) {
logger.warn('Endereço não encontrado ou resposta inválida do serviço de CEP', { cep: rawCep, response: address });
@ -58,7 +62,6 @@ async function verificarViabilidade(rawCep, rawNumero) {
logger.info('Coordenadas obtidas com sucesso', { coords });
const viabilidade = await geogridService.consultaViabilidade(coords.lat, coords.lon);
logger.info('Resultado da consulta de viabilidade no GeoGrid', { viabilidade });
let naoDedicado = false;
let dedicado = false;
@ -70,16 +73,23 @@ async function verificarViabilidade(rawCep, rawNumero) {
naoDedicado = true;
dedicado = true;
} else if (distancia <= 1000) {
naoDedicado = true;
dedicado = false;
naoDedicado = false;
dedicado = true;
}
} else {
logger.warn('Resposta do GeoGrid não continha dados de viabilidade.', { cep: rawCep, numero: rawNumero });
}
const viabilidadeResult = new ViabilidadeModel(cep, rawNumero, bairro, city, state, logradouro, naoDedicado, dedicado);
const viabilidadeResult = new ViabilidadeModel(rawViabilidadeData.nome, rawViabilidadeData.email, rawViabilidadeData.telefone, logradouro, rawNumero, bairro, city, state, cep, naoDedicado, dedicado);
await repository.insertViabilidadeData(viabilidadeResult)
.then(() => {
logger.info('Dados de viabilidade inseridos no repositório com sucesso', { viabilidadeResult });
})
.catch((err) => {
logger.error('Erro ao inserir dados de viabilidade no repositório', { message: err.message, stack: err.stack });
});
logger.info('Finalizando verificação de viabilidade com sucesso', { viabilidadeResult });
return viabilidadeResult;
}
@ -111,7 +121,6 @@ async function criarProspecto(rawProspectData) {
if (!Number.isNaN(valorNum)) servico.valor = valorNum;
}
servico.id_servico = Number(servico.id_servico);
console.log(servico);
if (!servico) {
logger.warn('Plano não encontrado no mapeamento', { planoKey, disponíveis: Object.keys(planos) });
throw new ServiceError('Plano inválido ou não encontrado.', 400);

View File

@ -3,8 +3,8 @@ const logger = require('../utils/logger.js');
// Consulta o endereço completo usando a API externa de CEP
const getConsultaCep = async (rawCep, rawNumero) => {
logger.info('Iniciando consulta de CEP', { cep: rawCep, numero: rawNumero });
const getConsultaCep = async (rawCep) => {
logger.info('Iniciando consulta de CEP');
if (!rawCep) {
logger.warn('CEP não fornecido para getConsultaCep');
@ -16,12 +16,9 @@ const getConsultaCep = async (rawCep, rawNumero) => {
throw new Error("cep inválido, verifique se foram digitados apenas números");
}
// Número é opcional na consulta, mas se fornecido, deve ser uma string limpa
// const numero = rawNumero ? String(rawNumero).trim() : "";
try {
const cepRestUrl = 'https://api.cep.rest/';
logger.info('Consultando API de CEP', { url: cepRestUrl, cep });
const address = await axios.post(cepRestUrl, { cep });
if (address.data && address.data.code === 404) {
@ -39,7 +36,6 @@ const getConsultaCep = async (rawCep, rawNumero) => {
}
// Estrutura 2: { bairro, cep, ... } (direto em data)
else if (address.data.data && address.data.data.cep) {
logger.info('CEP encontrado com estrutura direta');
return address.data.data;
}
else {

View File

@ -5,7 +5,6 @@ const logger = require('../utils/logger.js');
const consultaViabilidade = async (lat, lon) => {
logger.info('Iniciando consulta de viabilidade no GeoGrid', { lat, lon });
const url = apiConfig.geogridApiUrl;
const apiKey = apiConfig.geogridApiKey;
@ -23,8 +22,8 @@ const consultaViabilidade = async (lat, lon) => {
consultarIndividual: "S"
};
const urlParams = qs.stringify(params, { arrayFormat: 'brackets' });
logger.info(`Chamada à API GeoGrid: ${url}?${urlParams}`);
// const urlParams = qs.stringify(params, { arrayFormat: 'brackets' });
// logger.info(`Chamada à API GeoGrid: ${url}?${urlParams}`);
const response = await axios.get(url, {
params,
@ -39,7 +38,6 @@ const consultaViabilidade = async (lat, lon) => {
const registros = response.data && response.data.registros;
const primeiro = Array.isArray(registros) && registros.length ? registros[0] : null;
logger.info('Resposta recebida do GeoGrid', { data: primeiro });
// Retorna no formato esperado pelo controller (viabilidade.data.distancia)
return { data: primeiro };
} catch (error) {

View File

@ -6,7 +6,6 @@ const logger = require('../utils/logger.js');
// Geocodifica um endereço usando a API do Google Maps
async function geocodeWithGoogle(address) {
logger.info('Iniciando geocodificação com Google', { address });
const key = apiConfig.googleApiKey;
if (!key) {
logger.error('Chave da API do Google não configurada.');
@ -14,7 +13,6 @@ async function geocodeWithGoogle(address) {
}
try {
const url = 'https://maps.googleapis.com/maps/api/geocode/json';
logger.info('Consultando Google Geocoding API', { url, address });
const r = await axios.get(url, {
params: { address, key },
timeout: 10000,
@ -34,7 +32,6 @@ async function geocodeWithGoogle(address) {
const lng = loc && (loc.lng !== undefined ? Number(loc.lng) : NaN);
if (!Number.isNaN(lat) && !Number.isNaN(lng)) {
const coords = { lat, lon: lng };
logger.info('Geocodificação bem-sucedida', { address, coords });
return coords;
}
}

View File

@ -0,0 +1,25 @@
const { Pool } = require('pg');
const dotenv = require('dotenv');
dotenv.config();
const dbHost = process.env.VIABILIDADES_DB_HOST;
const dbPort = process.env.VIABILIDADES_DB_PORT;
const dbUsername = process.env.VIABILIDADES_DB_USERNAME;
const dbPassword = process.env.VIABILIDADES_DB_PASSWORD;
const dbDatabase = process.env.VIABILIDADES_DB_DATABASE;
// pool de conexões:
const pool = new Pool({
host: dbHost,
port: dbPort,
user: dbUsername,
password: dbPassword,
database: dbDatabase,
max: 20, // número máximo de conexões no pool
idleTimeoutMillis: 30000, // tempo máximo que uma conexão pode ficar ociosa antes de ser fechada
connectionTimeoutMillis: 2000 // tempo máximo para tentar conectar antes de lançar um erro
});
module.exports = {pool};