PERF: Adição de Logs estruturados

This commit is contained in:
Rafael Alves Lopes 2025-11-24 16:13:35 -03:00
parent 038b9eb3d4
commit d1448404e8
7 changed files with 221 additions and 31 deletions

View File

@ -8,12 +8,31 @@ const routes = require('./routes/routes.js');
const logger = require('./shared/utils/logger.js');
const app = express();
app.set('trust proxy', 1); // Confia no primeiro proxy reverso na frente da aplicação
const PORT = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
// Logar todas as requisições
app.use((req, res, next) => {
logger.info(`Requisição recebida: ${req.method} ${req.originalUrl} - IP: ${req.ip}`);
next();
});
app.use('/api', routes);
// Tratamento de erros
app.use((err, req, res, next) => {
logger.error(`Erro não tratado: ${err.message}`, {
stack: err.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip
});
res.status(500).send('Ocorreu um erro interno no servidor.');
});
app.listen(PORT, () => {
logger.info(`🚀 Servidor API rodando na porta ${PORT} em modo ${process.env.NODE_ENV}`);
});

View File

@ -1,14 +1,17 @@
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 });
try {
const resultadoViabilidade = await contratacaoService.verificarViabilidade(cep, numero);
logger.info('Viabilidade verificada com sucesso', { cep, numero, resultado: resultadoViabilidade });
return res.json(resultadoViabilidade);
} catch (error) {
console.error("Erro no controller ao processar viabilidade:", error.message);
logger.error('Erro no controller ao processar viabilidade', { message: error.message, stack: error.stack, cep, numero });
const statusCode = error.statusCode || 500;
return res.status(statusCode).json({ error: error.message || "Erro interno ao processar a viabilidade." });
}
@ -16,13 +19,15 @@ 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);
logger.info('Prospecto criado com sucesso', { resultado });
return res.json({ message: 'Prospecto criado com sucesso', data: resultado });
} catch (error) {
console.error("Erro no controller ao criar prospecto:", error.message);
logger.error('Erro no controller ao criar prospecto', { message: error.message, stack: error.stack });
const statusCode = error.statusCode || 500;
return res.status(statusCode).json({ error: error.message || "Erro interno ao criar o prospecto." });
}

View File

@ -2,6 +2,17 @@ const geogridService = require("../../shared/apis/geogridService.js");
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');
// mapping de planos para IDs e valores no Hubsoft
const PLANOS_CONFIG = {
'100-mega': { servicoId: '22', servicoValor: '149' },
'200-mega': { servicoId: '94', servicoValor: '159.9' },
'300-mega': { servicoId: '95', servicoValor: '239.9' },
'500-mega': { servicoId: '96', servicoValor: '379.9' },
'700-mega': { servicoId: '97', servicoValor: '499.9' },
};
class ServiceError extends Error {
constructor(message, statusCode = 500) {
@ -12,30 +23,41 @@ class ServiceError extends Error {
}
async function verificarViabilidade(rawCep, rawNumero) {
logger.info('Iniciando verificação de viabilidade', { cep: rawCep, numero: rawNumero });
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);
// FIX: Revertendo para a verificação correta da resposta do cepRestService
if (!address || !address.success || !address.data) {
logger.warn('Endereço não encontrado ou resposta inválida do serviço de CEP', { cep: rawCep, response: address });
throw new ServiceError('Endereço não encontrado para o CEP fornecido.', 404);
}
logger.info('Endereço obtido com sucesso via CEP', { data: address.data });
const { logradouro, bairro, localidade: city, uf: state, cep } = address.data;
const { logradouro, bairro, localidade: city, uf: state, cep } = address.data; // FIX: Usar address.data para desestruturar
const addressString = `${logradouro}, ${rawNumero}, ${bairro}, ${city}, ${state}, ${cep}`;
logger.info('String de endereço montada para geocodificação', { addressString });
const coords = await googleService.geocodeWithGoogle(addressString);
if (!coords) {
logger.error('Não foi possível obter coordenadas do Google Geocoding', { addressString });
throw new ServiceError('Não foi possível obter as coordenadas do endereço.', 500);
}
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;
if (viabilidade.data) {
const distancia = viabilidade.data.distancia;
logger.info(`Distância para o ponto de presença: ${distancia}m`);
if (distancia <= 500) {
naoDedicado = true;
dedicado = true;
@ -43,9 +65,11 @@ async function verificarViabilidade(rawCep, rawNumero) {
naoDedicado = true;
dedicado = false;
}
} else {
logger.warn('Resposta do GeoGrid não continha dados de viabilidade.', { cep: rawCep, numero: rawNumero });
}
return {
const resultadoFinal = {
bairro,
cidade: city,
estado: state,
@ -53,14 +77,98 @@ async function verificarViabilidade(rawCep, rawNumero) {
naoDedicado,
dedicado,
};
logger.info('Finalizando verificação de viabilidade com sucesso', { resultadoFinal });
return resultadoFinal;
}
async function criarProspecto(prospectData) {
async function criarProspecto(rawProspectData) {
try {
console.log("Service: Dados recebidos para criação de prospecto:", prospectData);
// Normaliza payload (alguns callers enviam { prospectData: { ... } })
const payload = (rawProspectData && rawProspectData.prospectData) ? rawProspectData.prospectData : rawProspectData;
logger.info('Payload usado para criação de prospecto', { payload });
// helper: busca valor por múltiplas chaves possíveis, normalizando nomes
const normalize = s => String(s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
const find = (obj, variants) => {
if (!obj) return undefined;
const map = {};
for (const k of Object.keys(obj)) map[normalize(k)] = obj[k];
for (const v of variants) {
const val = map[normalize(v)];
if (val !== undefined) return val;
}
return undefined;
};
// --- Camada de Transformação e Validação de Dados ---
const cep = find(payload, ['CEP:', 'CEP', 'cep']);
if (!cep) {
throw new ServiceError('O campo "CEP:" é obrigatório.', 400);
}
const addressDetails = await cepRestService.getConsultaCep(cep);
if (!addressDetails || !addressDetails.success || !addressDetails.data) {
logger.error('Não foi possível obter detalhes do endereço (bairro) para o CEP fornecido.', { cep });
throw new ServiceError('Endereço inválido ou não encontrado. Não foi possível determinar o bairro a partir do CEP.', 400);
}
// localizar plano com mais variações de chave
const planoKey = find(payload, ['Escolha o plano:', 'Escolha o Plano:', 'plano', 'Plano', 'escolha o plano']);
const planoConfig = PLANOS_CONFIG[planoKey];
if (!planoConfig) {
logger.error(`Plano desconhecido selecionado: ${planoKey}`);
throw new ServiceError(`O plano selecionado '${planoKey}' é inválido.`, 400);
}
const tipoPlano = find(payload, ['Escolha o tipo de plano:', 'tipo de plano', 'Tipo de plano', 'tipoPlano', 'tipo']);
// tipoPlano esperado: 'plano-residencia' -> pf, else -> pj
const tipoPessoa = (tipoPlano && tipoPlano.toString().toLowerCase().includes('resid')) ? 'pf' : 'pj';
const prospectData = {
cep: cep,
servico: {
id_servico: Number(planoConfig.servicoId),
valor: Number(planoConfig.servicoValor)
},
cpf_cnpj: find(payload, ['CPF/CNPJ:', 'CPF/CNPJ', 'cpf_cnpj', 'cpf']),
telefone: (find(payload, ['Telefone:', 'Telefone', 'telefone']) || '').toString().replace(/\D/g, ''), // só números
nome_razaosocial: find(payload, ['Nome Completo:', 'Nome', 'nome', 'nome_razaosocial']),
tipo_pessoa: tipoPessoa,
bairro: addressDetails.data.bairro,
endereco: addressDetails.data.logradouro || find(payload, ['Endereço:', 'Endereço', 'endereco', 'logradouro']),
numero: Number(find(payload, ['Número:', 'Numero:', 'numero', 'número']))
};
// validações adicionais específicas
if (!prospectData.servico || !prospectData.servico.id_servico || !prospectData.servico.valor) {
throw new ServiceError('É necessário selecionar um plano válido com valor.', 400);
}
const requiredFields = ['cep', 'numero', 'endereco', 'bairro', 'nome_razaosocial', 'telefone', 'tipo_pessoa'];
for (const field of requiredFields) {
if (prospectData[field] === undefined || prospectData[field] === null || prospectData[field] === '') {
const formField = Object.keys(payload).find(key => key.toLowerCase().includes(field)) || field;
throw new ServiceError(`O campo obrigatório '${formField}' está ausente ou é inválido.`, 400);
}
}
// --- Fim da Camada de Transformação ---
logger.info('Dados do prospecto mapeados. Enviando para o Hubsoft.', { prospectData });
const resultado = await hubsoftService.criarProspectHubsoft(prospectData);
if (resultado && resultado.status === 'error') {
logger.error('Hubsoft retornou um erro de negócio ao criar prospecto', { resposta: resultado });
throw new ServiceError(resultado.msg || 'Erro ao criar prospecto no Hubsoft', 400);
}
logger.info("Prospecto criado com sucesso no Hubsoft", { resultado });
return resultado;
} catch (error) {
if (error instanceof ServiceError) {
logger.error(`Erro de serviço ao criar prospecto: ${error.message}`);
throw error;
}
logger.error("Erro inesperado ao criar prospecto no Hubsoft", { message: error.message, stack: error.stack });
throw new ServiceError(error.message || 'Erro ao comunicar com o serviço de prospectos.', 500);
}
}

View File

@ -1,13 +1,18 @@
const axios = require("axios");
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 });
if (!rawCep) {
logger.warn('CEP não fornecido para getConsultaCep');
throw new Error("cep é obrigatório");
}
const cep = String(rawCep).trim().replace(/\D/g, "");
if (cep.length !== 8) {
logger.warn('CEP inválido fornecido para getConsultaCep', { cepBruto: rawCep, cepLimpo: cep });
throw new Error("cep inválido, verifique se foram digitados apenas números");
}
@ -16,18 +21,22 @@ const getConsultaCep = async (rawCep, rawNumero) => {
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) {
logger.warn('CEP não encontrado na API externa', { cep, response: address.data });
return null; // Controller tratará como 'Endereço não encontrado'
} else if (address.data && address.data.code) {
logger.error('API de CEP retornou um código de erro', { cep, response: address.data });
throw new Error("Erro ao consultar o CEP na API externa");
} else {
if (numero) address.data.numero = numero;
logger.info('Endereço obtido com sucesso da API de CEP', { response: address.data });
return address.data;
}
} catch (error) {
console.error("Erro ao consultar o CEP:", error);
logger.error("Erro na chamada da API de CEP", { message: error.message, stack: error.stack, cep });
throw new Error("Erro ao consultar o CEP");
}
};

View File

@ -1,9 +1,11 @@
const apiConfig = require("../config/apiConfig.js")
const axios = require("axios");
const qs = require("qs");
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;
@ -21,9 +23,8 @@ const consultaViabilidade = async (lat, lon) => {
consultarIndividual: "S"
};
// Console log com curl equivalente para depuração
const curlParams = qs.stringify(params, { arrayFormat: 'brackets' });
console.log(`CURL equivalente: curl -X GET "${url}?${curlParams}" -H "api-key: ${apiKey}" -H "Cookie: ${apiCookie}"`);
const urlParams = qs.stringify(params, { arrayFormat: 'brackets' });
logger.info(`Chamada à API GeoGrid: ${url}?${urlParams}`);
const response = await axios.get(url, {
params,
@ -38,10 +39,11 @@ 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) {
console.error("Erro ao consultar viabilidade:", error && error.message ? error.message : error);
logger.error("Erro ao consultar viabilidade no GeoGrid", { message: error.message, stack: error.stack, lat, lon });
throw new Error("Erro ao consultar viabilidade");
}
};

View File

@ -1,14 +1,20 @@
const apiConfig = require("../config/apiConfig.js")
const axios = require("axios");
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) return null;
if (!key) {
logger.error('Chave da API do Google não configurada.');
return null;
}
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,
@ -16,7 +22,7 @@ async function geocodeWithGoogle(address) {
// Valida a resposta e extrai as coordenadas
if (!r || r.status !== 200) {
console.warn(`geocodeWithGoogle unexpected status for '${address}': ${r && r.status}`);
logger.warn(`Resposta inesperada do Google Geocoding API para o endereço '${address}'`, { status: r ? r.status : 'undefined' });
return null;
}
@ -27,12 +33,16 @@ async function geocodeWithGoogle(address) {
const lat = loc && (loc.lat !== undefined ? Number(loc.lat) : NaN);
const lng = loc && (loc.lng !== undefined ? Number(loc.lng) : NaN);
if (!Number.isNaN(lat) && !Number.isNaN(lng)) {
return { lat, lon: lng };
const coords = { lat, lon: lng };
logger.info('Geocodificação bem-sucedida', { address, coords });
return coords;
}
}
logger.warn('Nenhum resultado de geocodificação válido encontrado para o endereço', { address, responseData: r.data });
return null;
} catch (e) {
console.warn(`geocodeWithGoogle error for '${address}': ${e && e.message}`, e && e.stack);
logger.error(`Erro na chamada ao Google Geocoding API para o endereço '${address}'`, { message: e.message, stack: e.stack });
return null;
}
}

View File

@ -1,9 +1,11 @@
const apiConfig = require('../config/apiConfig');
const axios = require('axios');
const qs = require('qs');
const logger = require('../utils/logger.js');
// Função para autenticar e obter o token de acesso do Hubsoft
const hubsoftToken = async () => {
logger.info('Iniciando autenticação no Hubsoft');
try {
const authBody = {
client_id: apiConfig.hubsoftClientId,
@ -18,10 +20,16 @@ const hubsoftToken = async () => {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
return response.data && response.data.access_token;
const token = response.data && response.data.access_token;
if (token) {
logger.info('Token de acesso do Hubsoft obtido com sucesso.');
} else {
logger.warn('A resposta de autenticação do Hubsoft não continha um token de acesso.');
}
return token;
}
catch (error) {
console.error('Erro ao autenticar no Hubsoft:', error.response?.data || error.message);
logger.error('Erro ao autenticar no Hubsoft', { message: error.message, response: error.response?.data });
throw new Error('Erro ao autenticar no Hubsoft');
}
}
@ -30,36 +38,65 @@ const hubsoftToken = async () => {
const criarProspectHubsoft = async (prospectData) => {
try {
const token = await hubsoftToken();
if (!token) {
throw new Error('Não foi possível obter o token do Hubsoft para criar o prospecto.');
}
// Normaliza leitura dos campos para aceitar snake_case, camelCase ou objeto servico nested
const servId = prospectData?.servico?.id_servico ?? prospectData?.servicoId ?? prospectData?.servico?.idServico ?? prospectData?.servico?.id;
const servValor = prospectData?.servico?.valor ?? prospectData?.servicoValor ?? prospectData?.servico?.valorServico ?? prospectData?.servico?.valor;
const tipoPessoa = prospectData?.tipo_pessoa ?? prospectData?.tipoPessoa;
const nomeRazao = prospectData?.nome_razaosocial ?? prospectData?.nomeRazaoSocial;
const cpfCnpj = prospectData?.cpf_cnpj ?? prospectData?.cpfCnpj;
const email = prospectData?.email ?? prospectData?.e_mail ?? null;
const numero = prospectData?.numero ?? prospectData?.numeroEndereco ?? prospectData?.numeroEnderecoString;
const endereco = prospectData?.endereco;
const bairro = prospectData?.bairro;
const telefone = prospectData?.telefone ?? null;
const payload = {
cep: prospectData.cep,
servico: {
id_servico: prospectData.servicoId,
valor: prospectData.servicoValor
id_servico: servId !== undefined ? Number(servId) : undefined,
valor: servValor !== undefined ? Number(servValor) : undefined
},
numero: prospectData.numero,
endereco: prospectData.endereco,
bairro: prospectData.bairro,
tipo_pessoa: prospectData.tipoPessoa,
nome_razaosocial: prospectData.nomeRazaoSocial,
cpf_cnpj: prospectData.cpfCnpj,
email: prospectData.email,
telefone: prospectData.telefone || null
numero: numero !== undefined ? String(numero) : undefined,
endereco: endereco,
bairro: bairro,
tipo_pessoa: tipoPessoa,
nome_razaosocial: nomeRazao,
cpf_cnpj: cpfCnpj,
email: email,
telefone: telefone
};
// Envia o body JSON corretamente (sem "params" aninhado)
const response = await axios.post(`${apiConfig.hubsoftUrl}/api/v1/integracao/prospecto`, payload, {
// Remover propriedades undefined para evitar envio de campos vazios
Object.keys(payload).forEach(k => {
if (payload[k] === undefined) delete payload[k];
});
if (payload.servico) {
Object.keys(payload.servico).forEach(k => {
if (payload.servico[k] === undefined) delete payload.servico[k];
});
if (Object.keys(payload.servico).length === 0) delete payload.servico;
}
const url = `${apiConfig.hubsoftUrl}/api/v1/integracao/prospecto`;
logger.info('Enviando dados para criar prospecto no Hubsoft', { url, payload });
const response = await axios.post(url, payload, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('Resposta do Hubsoft:', response.data);
logger.info('Resposta da criação de prospecto no Hubsoft', { response: response.data });
return response.data;
}
catch (error) {
console.error('Erro ao criar prospecto no Hubsoft:', error.response?.data || error.message);
logger.error('Erro ao criar prospecto no Hubsoft', { message: error.message, response: error.response?.data });
throw new Error('Erro ao criar prospecto no Hubsoft');
}
}