From 3cf2004d669d7767301e0c4f078c65d128f33666 Mon Sep 17 00:00:00 2001 From: "gabriel.amancio" Date: Tue, 2 Dec 2025 17:29:20 -0300 Subject: [PATCH] =?UTF-8?q?FEAT:=20Implementar=20modelos=20de=20dados=20pa?= =?UTF-8?q?ra=20viabilidade=20e=20cliente,=20e=20refatorar=20servi=C3=A7os?= =?UTF-8?q?=20de=20cria=C3=A7=C3=A3o=20de=20prospecto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1 - src/modules/contratacao/contratacao.model.js | 111 +++++++++ .../contratacao/contratacao.service.js | 228 ++++++++++-------- src/shared/apis/cepRestService.js | 24 +- src/shared/apis/hubsoftService.js | 39 +-- 5 files changed, 260 insertions(+), 143 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2aa16b8..f8cb1b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1705,7 +1705,6 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", diff --git a/src/modules/contratacao/contratacao.model.js b/src/modules/contratacao/contratacao.model.js index d2eecbb..e16748c 100644 --- a/src/modules/contratacao/contratacao.model.js +++ b/src/modules/contratacao/contratacao.model.js @@ -1,3 +1,114 @@ +// classe construtor para o modelo de viabilidade +class ViabilidadeModel { + constructor(cep, numero, bairro, city, state, logradouro, naoDedicado, dedicado) { + // Inicialização de propriedades do modelo pode ser feita aqui + this.cep = cep; + this.numero = numero; + this.bairro = bairro; + this.cidade = city; + this.estado = state; + this.logradouro = logradouro; + this.naoDedicado = naoDedicado; + this.dedicado = dedicado; + } +} + +// classe construtor para o modelo de cliente pessoa física e jurídica +class ClientModelPf { + constructor(nome, email, cep, numero, endereco, bairro, cidade, estado, servico, plano, cpf, telefone, tipoPessoa, enderecoCobrança, nomeMae, dataNascimento, diaCobranca, formaPagamento, banco, agencia, conta, emailCobrança) { + this.nome = nome; + this.email = email; + this.cep = cep; + this.numero = numero; + this.endereco = endereco; + this.bairro = bairro; + this.cidade = cidade; + this.estado = estado; + this.servico = servico; + this.plano = plano; + this.cpf = cpf; + this.telefone = telefone; + this.tipoPessoa = tipoPessoa; + this.enderecoCobrança = enderecoCobrança; + this.nomeMae = nomeMae; + this.dataNascimento = dataNascimento; + this.diaCobranca = diaCobranca; + this.formaPagamento = formaPagamento; + this.banco = banco; + this.agencia = agencia; + this.conta = conta; + this.emailCobrança = emailCobrança; + + } +} + +class ClientModelPj { + constructor(razaoSocial, email, cep, numero, endereco, bairro, cidade, estado, servico, plano, cnpj, telefone, tipoPessoa, enderecoCobrança, diaCobranca, formaPagamento, banco, agencia, conta, emailCobrança) { + this.razaoSocial = razaoSocial; + this.email = email; + this.cep = cep; + this.numero = numero; + this.endereco = endereco; + this.bairro = bairro; + this.cidade = cidade; + this.estado = estado; + this.servico = servico; + this.plano = plano; + this.cnpj = cnpj; + this.telefone = telefone; + this.tipoPessoa = tipoPessoa; + this.enderecoCobrança = enderecoCobrança; + this.diaCobranca = diaCobranca; + this.formaPagamento = formaPagamento; + this.banco = banco; + this.agencia = agencia; + this.conta = conta; + this.emailCobrança = emailCobrança; + + } +} + +class ProspectModel { + constructor(cep, servico, nomeRazaoSocial, cpfCnpj, telefone, email, tipoPessoa, bairro, endereco, numero, nomeMae) { + this.cep = cep; + this.servico = servico; + this.cpf_cnpj = cpfCnpj; + this.telefone = telefone; + this.nome_razaosocial = nomeRazaoSocial; + this.tipoPessoa = tipoPessoa; + this.bairro = bairro; + this.endereco = endereco; + this.numero = numero; + this.email = email; + this.nomeMae = nomeMae; + } + + toJSON() { + return { + cep: this.cep, + servico: (this.servico && typeof this.servico === 'object') ? { + id_servico: this.servico.id_servico ?? this.servico.id ?? null, + valor: this.servico.valor ?? null + } : null, + cpf_cnpj: this.cpf_cnpj, + telefone: this.telefone, + nome_razaosocial: this.nome_razaosocial, + tipo_pessoa: this.tipoPessoa, + bairro: this.bairro, + numero: this.numero, + endereco: this.endereco, + email: this.email, + // nomeMae não está no exemplo esperado; remova ou deixe se necessário + }; + } +} + + + +module.exports = {ViabilidadeModel, ClientModelPf, ClientModelPj, ProspectModel}; + + + /* DESCRIÇÃO: Este arquivo é destinado a definir o "Model" ou o esquema de dados para o módulo de contratação. diff --git a/src/modules/contratacao/contratacao.service.js b/src/modules/contratacao/contratacao.service.js index 4653d3a..e6c45e7 100644 --- a/src/modules/contratacao/contratacao.service.js +++ b/src/modules/contratacao/contratacao.service.js @@ -3,16 +3,24 @@ 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 {ViabilidadeModel, ClientModelPf, ClientModelPj, ProspectModel} = require('./contratacao.model'); + +// utilitário para ler chaves do payload tolerante a ":" e espaços +const getPayloadValue = (obj, key) => { + if (!obj) return undefined; + // variações diretas + const variants = [key, `${key}:`, `${key}: `]; + for (const v of variants) if (Object.prototype.hasOwnProperty.call(obj, v)) return obj[v]; + // tentativa de correspondência normalizada (ignora ":" e case) + const nk = String(key).trim().toLowerCase(); + for (const k of Object.keys(obj)) { + const kn = String(k).trim().replace(/:$/, '').toLowerCase(); + if (kn === nk) return obj[k]; + } + return undefined; +}; // 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) { @@ -29,16 +37,16 @@ async function verificarViabilidade(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) { + if (!address) { 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 }); + logger.info('Endereço obtido com sucesso via CEP', { data: address }); - const { logradouro, bairro, localidade: city, uf: state, cep } = address.data; // FIX: Usar address.data para desestruturar + const { logradouro, bairro, localidade: city, uf: state, cep } = address; // 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 }); @@ -69,110 +77,132 @@ async function verificarViabilidade(rawCep, rawNumero) { logger.warn('Resposta do GeoGrid não continha dados de viabilidade.', { cep: rawCep, numero: rawNumero }); } - const resultadoFinal = { - bairro, - cidade: city, - estado: state, - logradouro, - naoDedicado, - dedicado, - }; - logger.info('Finalizando verificação de viabilidade com sucesso', { resultadoFinal }); - return resultadoFinal; + const viabilidadeResult = new ViabilidadeModel(cep, rawNumero, bairro, city, state, logradouro, naoDedicado, dedicado); + + logger.info('Finalizando verificação de viabilidade com sucesso', { viabilidadeResult }); + return viabilidadeResult; } async function criarProspecto(rawProspectData) { try { - // 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; + const planos = { + '100 Mega + Super WiFi': { id_servico: '22'}, + '200 Mega + Super WiFi': { id_servico: '94'}, + '500 Mega + Super WiFi': { id_servico: '96'}, + '700 Mega + Super WiFi': { id_servico: '97'}, }; - // --- 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 planoKey = getPayloadValue(payload, 'Descrição'); + const servico = planos[planoKey]; + const celular = Number(String(getPayloadValue(payload, 'Celular')).replace(/\D/g, '')); + + 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); } - 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); + // parse do valor mensal de forma robusta (aceita "R$ 119,90", "119,90", etc) + const rawValor = getPayloadValue(payload, 'Valor (mensal)') || getPayloadValue(payload, 'Valor (mensal):') || getPayloadValue(payload, 'Valor'); + if (rawValor) { + const sanitized = String(rawValor).replace(/[^\d,.-]/g, '').replace(/\.(?=\d{3})/g, '').replace(',', '.'); + const valorNum = Number(sanitized); + 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); } - // 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); + if (payload["CPF:"] === "") { + // Pessoa Jurídica + const clientData = new ClientModelPj( + payload["Razão Social:"], + payload["E-mail:"], + payload["CEP:"], + payload["Número:"], + payload["Logradouro:"], + payload["Bairro:"], + payload["Cidade:"], + payload["Estado:"], + servico, + payload["Telefone:"], + "pj", + payload["Endereço de cobrança"], + payload["CNPJ:"], + payload["Selecione o dia de vencimento:"], + payload["Escolha a forma de pagamento:"], + payload["Banco:"], + `${payload["Agência:"]}-${payload["Dígito (agência):"]}`, + payload["Conta:"], + payload["E-mail de cobrança:"], + ); + logger.info('Criando prospecto PJ no Hubsoft', { clientData }); + const resultado = await hubsoftService.criarProspectoHubsoft(clientData); + logger.info('Prospecto PJ criado com sucesso no Hubsoft', { resultado }); + return resultado; + } else { + // Pessoa Física + const clientData = new ClientModelPf( + payload["Nome completo:"], + payload["E-mail:"], + payload["CEP:"], + payload["Número:"], + payload["Logradouro:"], + payload["Bairro:"], + payload["Cidade:"], + payload["Estado:"], + servico, + payload["Plano:"], + payload["CPF:"], + celular, + "pf", + payload["Endereço de cobrança"], + payload["Nome da mãe:"], + payload["Data de nascimento:"], + payload["Selecione o dia de vencimento:"], + payload["Escolha a forma de pagamento:"], + payload["Banco:"], + `${payload["Agência:"]}-${payload["Dígito (agência):"]}`, + payload["Conta:"], + payload["E-mail de cobrança:"], + ); + + const prospectData = new ProspectModel( + getPayloadValue(payload, 'CEP'), + servico, // passa o objeto { id_servico, valor } + getPayloadValue(payload, 'Nome completo'), + getPayloadValue(payload, 'CPF'), + celular, + getPayloadValue(payload, 'E-mail'), + "pf", + getPayloadValue(payload, 'Bairro'), + getPayloadValue(payload, 'Logradouro'), + getPayloadValue(payload, 'Número'), + getPayloadValue(payload, 'Nome da mãe') || "" + ); + + // serializa para JSON plano conforme toJSON() + const prospectPayload = (typeof prospectData.toJSON === 'function') + ? prospectData.toJSON() + : JSON.parse(JSON.stringify(prospectData)); + + // envia objeto plano ao hubsoft (o hubsoftService já monta { prospectData }) + await hubsoftService.criarProspectHubsoft(prospectPayload); + + logger.info('Criando prospecto PF', { prospectPayload }); + return prospectPayload; } - - 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); + logger.error('Erro ao criar prospecto', { message: error.message, stack: error.stack }); + throw new ServiceError('Erro ao criar prospecto.', 500); } } + module.exports = { verificarViabilidade, criarProspecto diff --git a/src/shared/apis/cepRestService.js b/src/shared/apis/cepRestService.js index d07a703..40e1091 100644 --- a/src/shared/apis/cepRestService.js +++ b/src/shared/apis/cepRestService.js @@ -18,7 +18,7 @@ const getConsultaCep = async (rawCep, rawNumero) => { // Número é opcional na consulta, mas se fornecido, deve ser uma string limpa - const numero = rawNumero ? String(rawNumero).trim() : ""; + // const numero = rawNumero ? String(rawNumero).trim() : ""; try { const cepRestUrl = 'https://api.cep.rest/'; logger.info('Consultando API de CEP', { url: cepRestUrl, cep }); @@ -31,9 +31,21 @@ const getConsultaCep = async (rawCep, rawNumero) => { 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; + // Estrutura 1: { "cep:09662000": { bairro, ... } } + const cepKey = `cep:${cep}`; + if (address.data.data && address.data.data[cepKey]) { + logger.info('CEP encontrado com chave aninhada', { cepKey }); + return address.data.data[cepKey]; + } + // 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 { + logger.warn('Estrutura de resposta não reconhecida', { response: address.data }); + return address.data.data || address.data; + } } } catch (error) { logger.error("Erro na chamada da API de CEP", { message: error.message, stack: error.stack, cep }); @@ -55,7 +67,9 @@ module.exports = { getConsultaCep }; 5. Trata a resposta da API: - Se o CEP não for encontrado (código 404), retorna `null`. - Se houver outro erro na API, lança uma exceção. - - Se for bem-sucedido, anexa o número ao objeto de dados e retorna os dados do endereço. + - Se for bem-sucedido, verifica qual estrutura a API retornou: + * Estrutura aninhada: `{ data: { "cep:09662000": { ... } } }` → extrai a chave dinâmica + * Estrutura direta: `{ data: { bairro, cep, ... } }` → retorna data direto 6. Em caso de erro na comunicação, loga o erro e lança uma exceção genérica. Este serviço abstrai a complexidade da comunicação com a API de CEP, fornecendo uma interface simples para outras partes da aplicação obterem dados de endereço. diff --git a/src/shared/apis/hubsoftService.js b/src/shared/apis/hubsoftService.js index 6e771dc..4063860 100644 --- a/src/shared/apis/hubsoftService.js +++ b/src/shared/apis/hubsoftService.js @@ -42,44 +42,7 @@ const criarProspectHubsoft = async (prospectData) => { 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: servId !== undefined ? Number(servId) : undefined, - valor: servValor !== undefined ? Number(servValor) : undefined - }, - numero: numero !== undefined ? String(numero) : undefined, - endereco: endereco, - bairro: bairro, - tipo_pessoa: tipoPessoa, - nome_razaosocial: nomeRazao, - cpf_cnpj: cpfCnpj, - email: email, - telefone: telefone - }; - - // 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 payload = prospectData; const url = `${apiConfig.hubsoftUrl}/api/v1/integracao/prospecto`; logger.info('Enviando dados para criar prospecto no Hubsoft', { url, payload });