2025-11-24 10:59:46 -03:00
|
|
|
const googleService = require("../../shared/apis/googleService.js");
|
|
|
|
|
const hubsoftService = require("../../shared/apis/hubsoftService.js");
|
2025-11-24 16:13:35 -03:00
|
|
|
const logger = require('../../shared/utils/logger.js');
|
2026-07-20 09:44:06 -03:00
|
|
|
const apiConfig = require('../../shared/config/apiConfig.js');
|
2025-12-08 15:43:20 -03:00
|
|
|
const repository = require('./contratacao.repository.js');
|
2026-07-20 09:44:06 -03:00
|
|
|
const core = require('../viabilidade/viabilidade.core.js');
|
|
|
|
|
const { mapWithConcurrency } = require('../../shared/utils/concurrency.js');
|
2026-01-15 13:43:51 -03:00
|
|
|
const { ViabilidadeModel, ClientModelPf, ClientModelPj, ProspectModel, ViabilidadeReverseGeocodeModel } = require('./contratacao.model');
|
2025-12-02 17:29:20 -03:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
};
|
2025-11-24 16:13:35 -03:00
|
|
|
|
2025-12-08 15:43:20 -03:00
|
|
|
// Classe de erro personalizada para o serviço
|
2025-11-24 10:59:46 -03:00
|
|
|
|
|
|
|
|
class ServiceError extends Error {
|
|
|
|
|
constructor(message, statusCode = 500) {
|
|
|
|
|
super(message);
|
|
|
|
|
this.name = 'ServiceError';
|
|
|
|
|
this.statusCode = statusCode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 09:44:06 -03:00
|
|
|
// 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) {
|
2026-01-08 14:01:03 -03:00
|
|
|
let address;
|
|
|
|
|
try {
|
2026-05-05 17:17:59 -03:00
|
|
|
const cep = require('cep-promise');
|
|
|
|
|
address = await cep(rawCep);
|
|
|
|
|
logger.info('Endereço obtido com sucesso via cep-promise', { address });
|
2026-01-08 14:01:03 -03:00
|
|
|
} catch (err) {
|
2026-05-05 17:17:59 -03:00
|
|
|
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);
|
2025-11-24 10:59:46 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-05 17:17:59 -03:00
|
|
|
const { street, neighborhood, city, state, cep } = address;
|
2026-07-20 09:44:06 -03:00
|
|
|
const addressString = `${street}, ${rawNumero}, ${neighborhood}, ${city}, ${state}, ${rawCep}`;
|
2026-01-08 14:01:03 -03:00
|
|
|
logger.info('Endereço montado para geocodificação', { addressString });
|
2026-01-07 09:16:42 -03:00
|
|
|
|
2026-05-05 17:17:59 -03:00
|
|
|
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);
|
|
|
|
|
}
|
2026-01-07 09:16:42 -03:00
|
|
|
|
2025-11-24 10:59:46 -03:00
|
|
|
const coords = await googleService.geocodeWithGoogle(addressString);
|
|
|
|
|
if (!coords) {
|
2025-11-24 16:13:35 -03:00
|
|
|
logger.error('Não foi possível obter coordenadas do Google Geocoding', { addressString });
|
2025-11-24 10:59:46 -03:00
|
|
|
throw new ServiceError('Não foi possível obter as coordenadas do endereço.', 500);
|
|
|
|
|
}
|
2025-11-24 16:13:35 -03:00
|
|
|
logger.info('Coordenadas obtidas com sucesso', { coords });
|
2025-11-24 10:59:46 -03:00
|
|
|
|
2026-07-20 09:44:06 -03:00
|
|
|
return { coords, address: { street, neighborhood, city, state, cep } };
|
|
|
|
|
}
|
2025-11-24 10:59:46 -03:00
|
|
|
|
2026-07-20 09:44:06 -03:00
|
|
|
// 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 || '';
|
2025-11-24 10:59:46 -03:00
|
|
|
|
2026-07-20 09:44:06 -03:00
|
|
|
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);
|
2025-11-24 10:59:46 -03:00
|
|
|
}
|
|
|
|
|
|
2026-07-20 09:44:06 -03:00
|
|
|
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 });
|
2026-01-08 14:01:03 -03:00
|
|
|
|
2026-07-20 09:44:06 -03:00
|
|
|
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
|
|
|
|
|
);
|
2025-12-08 15:43:20 -03:00
|
|
|
|
2026-01-08 14:01:03 -03:00
|
|
|
if (source) {
|
|
|
|
|
return viabilidadeResult;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await repository.insertViabilidadeData(viabilidadeResult);
|
|
|
|
|
logger.info('Viabilidade salva com sucesso');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.error('Erro ao salvar viabilidade', err);
|
|
|
|
|
}
|
2025-12-02 17:29:20 -03:00
|
|
|
|
|
|
|
|
return viabilidadeResult;
|
2025-11-24 10:59:46 -03:00
|
|
|
}
|
|
|
|
|
|
2026-01-15 13:43:51 -03:00
|
|
|
async function verificarGeocodeReverso(lat, lon) {
|
|
|
|
|
const address = await googleService.reverseGeocodeWithGoogle(lat, lon);
|
|
|
|
|
return address;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function verificarViabilidadeLatLong(latitude, longitude) {
|
2026-07-20 09:44:06 -03:00
|
|
|
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 });
|
2026-01-15 13:43:51 -03:00
|
|
|
|
|
|
|
|
const address = await verificarGeocodeReverso(latitude, longitude);
|
|
|
|
|
|
2026-07-20 09:44:06 -03:00
|
|
|
return new ViabilidadeReverseGeocodeModel(
|
|
|
|
|
address, avaliacao.naoDedicado, avaliacao.dedicado, avaliacao.distancia, avaliacao.provedor,
|
|
|
|
|
avaliacao.distanciaReta
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-15 13:43:51 -03:00
|
|
|
|
2026-07-20 09:44:06 -03:00
|
|
|
// 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] };
|
|
|
|
|
});
|
2026-01-15 13:43:51 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-11-24 16:13:35 -03:00
|
|
|
async function criarProspecto(rawProspectData) {
|
2025-11-24 10:59:46 -03:00
|
|
|
try {
|
2025-11-24 16:13:35 -03:00
|
|
|
const payload = (rawProspectData && rawProspectData.prospectData) ? rawProspectData.prospectData : rawProspectData;
|
|
|
|
|
|
2025-12-02 17:29:20 -03:00
|
|
|
const planos = {
|
2026-01-08 14:01:03 -03:00
|
|
|
'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' },
|
2026-01-27 08:59:19 -03:00
|
|
|
'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' },
|
2025-12-02 17:29:20 -03:00
|
|
|
};
|
2025-11-24 16:13:35 -03:00
|
|
|
|
2025-12-02 17:29:20 -03:00
|
|
|
const planoKey = getPayloadValue(payload, 'Descrição');
|
|
|
|
|
const servico = planos[planoKey];
|
|
|
|
|
const celular = Number(String(getPayloadValue(payload, 'Celular')).replace(/\D/g, ''));
|
2025-11-24 16:13:35 -03:00
|
|
|
|
2025-12-02 17:29:20 -03:00
|
|
|
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);
|
2025-11-24 16:13:35 -03:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 17:29:20 -03:00
|
|
|
// 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;
|
2025-11-24 16:13:35 -03:00
|
|
|
}
|
2025-12-02 17:29:20 -03:00
|
|
|
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);
|
2025-11-24 16:13:35 -03:00
|
|
|
}
|
|
|
|
|
|
2026-01-08 14:01:03 -03:00
|
|
|
//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') || "");
|
|
|
|
|
|
2025-12-02 17:29:20 -03:00
|
|
|
if (payload["CPF:"] === "") {
|
|
|
|
|
// Pessoa Jurídica
|
2026-01-07 09:16:42 -03:00
|
|
|
const prospectData = new ProspectModel(
|
|
|
|
|
getPayloadValue(payload, 'CEP'),
|
2026-01-08 14:01:03 -03:00
|
|
|
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)
|
2025-12-02 17:29:20 -03:00
|
|
|
);
|
2026-01-07 09:16:42 -03:00
|
|
|
console.log(prospectData);
|
|
|
|
|
// serializa para JSON plano conforme toJSON()
|
|
|
|
|
const prospectPayload = (typeof prospectData.pjToJSON === 'function')
|
|
|
|
|
? prospectData.pjToJSON()
|
|
|
|
|
: JSON.parse(JSON.stringify(prospectData));
|
2026-01-08 14:01:03 -03:00
|
|
|
|
2026-01-07 09:16:42 -03:00
|
|
|
await hubsoftService.criarProspectHubsoft(prospectPayload);
|
|
|
|
|
return prospectPayload;
|
2025-12-02 17:29:20 -03:00
|
|
|
} else {
|
2026-01-08 14:01:03 -03:00
|
|
|
// Pessoa Física
|
2025-12-02 17:29:20 -03:00
|
|
|
const prospectData = new ProspectModel(
|
|
|
|
|
getPayloadValue(payload, 'CEP'),
|
2026-01-08 14:01:03 -03:00
|
|
|
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
|
2025-12-02 17:29:20 -03:00
|
|
|
);
|
2026-01-07 09:16:42 -03:00
|
|
|
console.log(prospectData);
|
2025-12-02 17:29:20 -03:00
|
|
|
// serializa para JSON plano conforme toJSON()
|
2026-01-07 09:16:42 -03:00
|
|
|
const prospectPayload = (typeof prospectData.pfToJSON === 'function')
|
|
|
|
|
? prospectData.pfToJSON()
|
2025-12-02 17:29:20 -03:00
|
|
|
: JSON.parse(JSON.stringify(prospectData));
|
|
|
|
|
|
|
|
|
|
// envia objeto plano ao hubsoft (o hubsoftService já monta { prospectData })
|
2026-01-08 14:01:03 -03:00
|
|
|
const result = await hubsoftService.criarProspectHubsoft(prospectPayload);
|
2025-12-02 17:29:20 -03:00
|
|
|
|
|
|
|
|
logger.info('Criando prospecto PF', { prospectPayload });
|
2026-01-08 14:01:03 -03:00
|
|
|
return result;
|
2025-11-24 16:13:35 -03:00
|
|
|
}
|
2025-12-02 17:29:20 -03:00
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Erro ao criar prospecto', { message: error.message, stack: error.stack });
|
|
|
|
|
throw new ServiceError('Erro ao criar prospecto.', 500);
|
2025-11-24 10:59:46 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 17:29:20 -03:00
|
|
|
|
2025-11-24 10:59:46 -03:00
|
|
|
module.exports = {
|
|
|
|
|
verificarViabilidade,
|
2026-01-15 13:43:51 -03:00
|
|
|
criarProspecto,
|
|
|
|
|
verificarViabilidadeLatLong,
|
2026-07-20 09:44:06 -03:00
|
|
|
verificarViabilidadeLote,
|
2026-01-15 13:43:51 -03:00
|
|
|
verificarGeocodeReverso,
|
2025-11-24 10:59:46 -03:00
|
|
|
};
|