FEAT: Implementar modelos de dados para viabilidade e cliente, e refatorar serviços de criação de prospecto

This commit is contained in:
Gabriel Amancio 2025-12-02 17:29:20 -03:00
parent ed607df252
commit 3cf2004d66
5 changed files with 260 additions and 143 deletions

1
package-lock.json generated
View File

@ -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",

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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 });