const googleService = require("../../shared/apis/googleService.js"); const hubsoftService = require("../../shared/apis/hubsoftService.js"); const logger = require('../../shared/utils/logger.js'); const apiConfig = require('../../shared/config/apiConfig.js'); const repository = require('./contratacao.repository.js'); const core = require('../viabilidade/viabilidade.core.js'); const { mapWithConcurrency } = require('../../shared/utils/concurrency.js'); const { ViabilidadeModel, ClientModelPf, ClientModelPj, ProspectModel, ViabilidadeReverseGeocodeModel } = 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; }; // Classe de erro personalizada para o serviço class ServiceError extends Error { constructor(message, statusCode = 500) { super(message); this.name = 'ServiceError'; this.statusCode = statusCode; } } // Resolve as coordenadas de um endereço a partir de CEP + número: // cep-promise (dados do endereço) -> Google Geocoding (lat/lon). async function resolverCoordenadasPorCep(rawCep, rawNumero) { let address; try { const cep = require('cep-promise'); address = await cep(rawCep); logger.info('Endereço obtido com sucesso via cep-promise', { address }); } catch (err) { logger.error('Erro ao obter endereço com cep-promise', { message: err.message, stack: err.stack, rawCep }); throw new ServiceError('Não foi possível obter endereço para o CEP informado ou o CEP é inválido.', 502); } const { street, neighborhood, city, state, cep } = address; const addressString = `${street}, ${rawNumero}, ${neighborhood}, ${city}, ${state}, ${rawCep}`; logger.info('Endereço montado para geocodificação', { addressString }); if (!street || !neighborhood || !city || !state) { logger.error('Dados de endereço incompletos para geocodificação', { address }); throw new ServiceError('Dados de endereço indefinidos:' + addressString, 422); } 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 }); return { coords, address: { street, neighborhood, city, state, cep } }; } // Função para verificar a viabilidade de um endereço. // A distância usada na regra é a REAL (trajeto); `distanciaReta` vai junto no resultado. async function verificarViabilidade(rawViabilidadeData) { const rawCep = rawViabilidadeData.cep; const rawNumero = rawViabilidadeData.numero; const source = rawViabilidadeData.source || ''; 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 { coords, address } = await resolverCoordenadasPorCep(rawCep, rawNumero); const { street, neighborhood, city, state, cep } = address; const avaliacao = await core.avaliarPorCoordenadas(coords); logger.info('Avaliação de viabilidade concluída', { avaliacao }); const viabilidadeResult = new ViabilidadeModel( rawViabilidadeData.nome, rawViabilidadeData.email, rawViabilidadeData.telefone, street, rawNumero, neighborhood, city, state, cep, avaliacao.naoDedicado, avaliacao.dedicado, avaliacao.distancia, avaliacao.provedor, avaliacao.distanciaReta ); if (source) { return viabilidadeResult; } try { await repository.insertViabilidadeData(viabilidadeResult); logger.info('Viabilidade salva com sucesso'); } catch (err) { logger.error('Erro ao salvar viabilidade', err); } return viabilidadeResult; } async function verificarGeocodeReverso(lat, lon) { const address = await googleService.reverseGeocodeWithGoogle(lat, lon); return address; } async function verificarViabilidadeLatLong(latitude, longitude) { const coords = { lat: Number(latitude), lon: Number(longitude) }; const avaliacao = await core.avaliarPorCoordenadas(coords); logger.info('Avaliação de viabilidade (lat/long) concluída', { avaliacao }); const address = await verificarGeocodeReverso(latitude, longitude); return new ViabilidadeReverseGeocodeModel( address, avaliacao.naoDedicado, avaliacao.dedicado, avaliacao.distancia, avaliacao.provedor, avaliacao.distanciaReta ); } // Formata o resultado do core para o item de resposta do lote. function formatarResultadoLote(avaliacao) { return { provedor: avaliacao.provedor, distancia: avaliacao.distancia, distanciaReta: avaliacao.distanciaReta, distanciaReal: avaliacao.distanciaReal, dedicado: avaliacao.dedicado, naoDedicado: avaliacao.naoDedicado, // detalhe por caixa (mais próxima em linha reta primeiro) — usado nas colunas da planilha caixas: (avaliacao.caixas || []).map(c => ({ provedor: c.provedor, sigla: c.sigla, distancia: c.distancia, distanciaReta: c.distanciaReta, distanciaReal: c.distanciaReal, dedicado: c.dedicado, naoDedicado: c.naoDedicado })) }; } // Verificação em LOTE (usada pelo viabiliza). Aceita itens {cep, numero} ou // {latitude, longitude}. Deduplica coordenadas/CEPs iguais, executa com // concorrência limitada e usa top-N reduzido (mais barato) para o trajeto. async function verificarViabilidadeLote(itens, { modo } = {}) { if (!Array.isArray(itens)) { throw new ServiceError('O campo "itens" deve ser um array.', 400); } const topN = apiConfig.geogridTrajetoTopNLote; const chaveDe = (item) => { if (item && item.latitude !== undefined && item.longitude !== undefined) { const lat = Number(item.latitude), lon = Number(item.longitude); if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null; return `geo:${lat.toFixed(6)},${lon.toFixed(6)}`; } if (item && item.cep && item.numero) { return `cep:${String(item.cep).replace(/\D/g, '')}-${String(item.numero).trim()}`; } return null; }; const avaliarItem = async (item) => { // Lote/planilha: mistura provedores (2 caixas mais próximas de qualquer provedor). const opts = { modo, topN, misturarProvedores: true }; if (item && item.latitude !== undefined && item.longitude !== undefined) { const a = await core.avaliarPorCoordenadas({ lat: Number(item.latitude), lon: Number(item.longitude) }, opts); return formatarResultadoLote(a); } const { coords } = await resolverCoordenadasPorCep(item.cep, item.numero); const a = await core.avaliarPorCoordenadas(coords, opts); return formatarResultadoLote(a); }; // Resolve apenas as chaves únicas (evita chamadas externas repetidas) e mapeia de volta. const chaves = itens.map(chaveDe); const unicas = [...new Set(chaves.filter(Boolean))]; const primeiroItemDaChave = unicas.map(chave => itens[chaves.indexOf(chave)]); const resultadosUnicos = await mapWithConcurrency(primeiroItemDaChave, apiConfig.geogridConcurrency, async (item) => { try { return await avaliarItem(item); } catch (e) { logger.warn('Falha ao avaliar item do lote', { message: e.message }); return { erro: e.message || 'Erro ao avaliar item.' }; } }); const porChave = {}; unicas.forEach((chave, i) => { porChave[chave] = resultadosUnicos[i]; }); return itens.map((item, index) => { const chave = chaves[index]; if (!chave) { return { index, erro: 'Item inválido: informe cep+numero ou latitude+longitude.' }; } return { index, ...porChave[chave] }; }); } async function criarProspecto(rawProspectData) { try { const payload = (rawProspectData && rawProspectData.prospectData) ? rawProspectData.prospectData : rawProspectData; 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' }, '1 Gb + Super WiFi': { id_servico: '32' }, '100 Mega + Super WiFi + IP Fixo ': { id_servico: '22' }, '200 Mega + Super WiFi + IP Fixo': { id_servico: '94' }, '500 Mega + Super WiFi + IP Fixo': { id_servico: '96' }, '700 Mega + Super WiFi + IP Fixo': { id_servico: '97' }, '1 Gb + Super WiFi + IP Fixo': { id_servico: '32' }, }; 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); } // 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); 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); } //função que troca a / da data de nascimento por - const formatDate = (dateStr) => { if (!dateStr) return ""; return dateStr.replace(/\//g, '-'); }; const dataNascimento = formatDate(getPayloadValue(payload, 'Data de nascimento') || ""); if (payload["CPF:"] === "") { // Pessoa Jurídica const prospectData = new ProspectModel( getPayloadValue(payload, 'CEP'), servico, // objeto { id_servico, valor } getPayloadValue(payload, 'Razão Social'), // nomeRazaoSocial getPayloadValue(payload, 'CNPJ'), // cpfCnpj celular, // telefone getPayloadValue(payload, 'E-mail'), // email "pj", // tipoPessoa getPayloadValue(payload, 'Bairro'), // bairro getPayloadValue(payload, 'Logradouro'), // endereco getPayloadValue(payload, 'Número'), // numero "", // nomeMae (não aplicável para PJ) getPayloadValue(payload, 'Complemento') || "", // complemento "" // dataNascimento (não aplicável) ); console.log(prospectData); // serializa para JSON plano conforme toJSON() const prospectPayload = (typeof prospectData.pjToJSON === 'function') ? prospectData.pjToJSON() : JSON.parse(JSON.stringify(prospectData)); await hubsoftService.criarProspectHubsoft(prospectPayload); return prospectPayload; } else { // Pessoa Física const prospectData = new ProspectModel( getPayloadValue(payload, 'CEP'), servico, // objeto { id_servico, valor } getPayloadValue(payload, 'Nome completo'), // nomeRazaoSocial getPayloadValue(payload, 'CPF'), // cpfCnpj celular, // telefone getPayloadValue(payload, 'E-mail'), // email "pf", // tipoPessoa getPayloadValue(payload, 'Bairro'), // bairro getPayloadValue(payload, 'Logradouro'), // endereco getPayloadValue(payload, 'Número'), // numero getPayloadValue(payload, 'Nome da mãe') || "", // nomeMae getPayloadValue(payload, 'Complemento') || "", // complemento dataNascimento // dataNascimento ); console.log(prospectData); // serializa para JSON plano conforme toJSON() const prospectPayload = (typeof prospectData.pfToJSON === 'function') ? prospectData.pfToJSON() : JSON.parse(JSON.stringify(prospectData)); // envia objeto plano ao hubsoft (o hubsoftService já monta { prospectData }) const result = await hubsoftService.criarProspectHubsoft(prospectPayload); logger.info('Criando prospecto PF', { prospectPayload }); return result; } } catch (error) { logger.error('Erro ao criar prospecto', { message: error.message, stack: error.stack }); throw new ServiceError('Erro ao criar prospecto.', 500); } } module.exports = { verificarViabilidade, criarProspecto, verificarViabilidadeLatLong, verificarViabilidadeLote, verificarGeocodeReverso, };