const axios = require('axios'); const https = require('https'); const logger = require('../utils/logger'); class TopSappAPI { constructor() { this.instances = { 'guarai_colmeia': { baseURL: 'https://131.0.192.6:9910', name: 'Guarai e Colmeia', cities: ['guarai', 'colmeia'] }, 'pedro_afonso': { baseURL: 'https://45.189.140.18:9910', name: 'Pedro Afonso', cities: ['pedro afonso', 'pedro_afonso'] } }; this.sessions = new Map(); // Armazenar sessões ativas // Configurar axios para ignorar certificados SSL auto-assinados this.axiosConfig = { httpsAgent: new https.Agent({ rejectUnauthorized: false }), timeout: 30000 // Não definir Content-Type, deixar o FormData fazer isso automaticamente }; console.log('🔗 TopSappAPI inicializado'); } /** * Determina qual instância usar baseada na cidade/região */ getInstanceByLocation(location = '') { const loc = location.toLowerCase(); // Pedro Afonso if (loc.includes('pedro') || loc.includes('afonso')) { return this.instances.pedro_afonso; } // Guarai e Colmeia (padrão) return this.instances.guarai_colmeia; } /** * Realizar login na API */ async login(instance, credentials) { try { console.log(`🔐 Fazendo login na instância: ${instance.name}`); console.log(`📍 URL: ${instance.baseURL}/Login`); console.log(`👤 Usuario: ${credentials.usuario}`); const formData = new FormData(); formData.append('usuario', credentials.usuario); formData.append('senha', credentials.senha); formData.append('identificador', credentials.identificador || 'BotTelegram'); const response = await axios.post( `${instance.baseURL}/Login`, formData, { ...this.axiosConfig, timeout: 10000 } ); console.log(`📊 Status da resposta: ${response.status}`); console.log(`📋 Dados da resposta:`, response.data); if (response.data && response.data.resultado === true) { const sessionId = response.data.sessao; const idUsuario = response.data.id_usuario; this.sessions.set(instance.baseURL, { sessionId, idUsuario, timestamp: Date.now(), instance }); console.log(`✅ Login realizado com sucesso na instância: ${instance.name}`); console.log(`🎫 Sessão: ${sessionId}`); console.log(`👤 ID do usuário: ${idUsuario}`); return { success: true, sessionId, idUsuario, instance }; } else { const errorMsg = response.data?.msg || 'Login falhou'; console.log(`❌ Erro no login: ${errorMsg}`); throw new Error(errorMsg); } } catch (error) { console.error(`❌ Erro no login da instância ${instance.name}:`, error.message); return { success: false, error: error.message, instance }; } } /** * Verificar se a sessão está válida */ isSessionValid(instanceURL) { const session = this.sessions.get(instanceURL); if (!session) return false; // Sessão expira em 30 minutos const thirtyMinutes = 30 * 60 * 1000; return (Date.now() - session.timestamp) < thirtyMinutes; } /** * Obter sessão válida ou fazer novo login */ async getValidSession(instance, credentials) { if (this.isSessionValid(instance.baseURL)) { return this.sessions.get(instance.baseURL); } const loginResult = await this.login(instance, credentials); if (loginResult.success) { return this.sessions.get(instance.baseURL); } throw new Error(`Não foi possível autenticar na instância ${instance.name}`); } /** * Fazer requisição autenticada para a API */ async makeRequest(endpoint, data = {}, instance, credentials) { try { const session = await this.getValidSession(instance, credentials); const formData = new FormData(); formData.append('sessao', session.sessionId); formData.append('identificador', credentials.identificador || 'BotTelegram'); // Adicionar parâmetros extras Object.keys(data).forEach(key => { if (data[key] !== undefined && data[key] !== null) { // Converter idCliente para inteiro se necessário const value = key === 'idCliente' ? parseInt(data[key], 10) : data[key]; formData.append(key, value.toString()); } }); // Log do FormData real que será enviado const formDataEntries = {}; for (const [k, v] of formData.entries()) { formDataEntries[k] = v; } console.log(`📡 Fazendo requisição para: ${instance.baseURL}/${endpoint}`); console.log(`📊 FormData real enviado:`, formDataEntries); const response = await axios.post( `${instance.baseURL}/${endpoint}`, formData, this.axiosConfig ); console.log(`📊 Status da resposta: ${response.status}`); console.log(`📋 Dados da resposta:`, response.data); if (response.data && (response.data.resultado === true || response.data.status === true)) { return { success: true, data: response.data, instance: instance.name }; } else { const errorMsg = response.data?.msg || 'Requisição falhou'; console.log(`❌ Erro na API: ${errorMsg}`); throw new Error(errorMsg); } } catch (error) { const responseBody = error.response?.data; console.error(`❌ Erro na requisição ${endpoint}:`, error.message); if (responseBody) { console.error(`❌ Corpo do erro ${endpoint}:`, JSON.stringify(responseBody)); } return { success: false, error: responseBody?.msg || responseBody?.mensagem || error.message, instance: instance.name }; } } /** * Buscar cliente por nome, CPF ou ID */ async buscarCliente(query, location = '', credentials) { const instance = this.getInstanceByLocation(location); try { // Obter sessão válida para pegar o idUsuario const session = await this.getValidSession(instance, credentials); console.log(`🔍 Buscando cliente: "${query}" na instância: ${instance.name}`); const searchParams = { idUsuario: session.idUsuario || 1 }; // Determinar tipo de busca const queryLimpo = query.trim(); if (/^\d+$/.test(queryLimpo)) { // Apenas números - pode ser CPF ou ID if (queryLimpo.length === 11 || queryLimpo.length === 14) { // CPF (11 dígitos) ou CNPJ (14 dígitos) console.log(`📋 Buscando por CPF/CNPJ: ${queryLimpo}`); searchParams.cpfCnpj = queryLimpo; } else { // ID do cliente console.log(`🆔 Buscando por ID: ${queryLimpo}`); searchParams.idCliente = parseInt(queryLimpo, 10); } } else { // Contém letras - é nome ou CPF formatado const apenasNumeros = queryLimpo.replace(/[^\d]/g, ''); if (apenasNumeros.length === 11 || apenasNumeros.length === 14) { // CPF/CNPJ formatado console.log(`📋 Buscando por CPF/CNPJ formatado: ${queryLimpo} -> ${apenasNumeros}`); searchParams.cpfCnpj = apenasNumeros; } else { // Nome do cliente console.log(`👤 Buscando por nome: ${queryLimpo}`); searchParams.nome = queryLimpo; // Fazer busca sem filtros específicos para pegar todos os clientes // e depois filtrar localmente por nome delete searchParams.nome; // Remove o parâmetro nome para buscar todos } } let result; // Se é busca por nome, fazer busca geral e filtrar localmente if (!searchParams.cpfCnpj && !searchParams.idCliente) { console.log(`🔄 Fazendo busca geral para filtrar por nome: "${queryLimpo}"`); // Buscar todos os clientes result = await this.makeRequest('ObterClientes', searchParams, instance, credentials); if (result.success && result.data.dados) { const todosClientes = result.data.dados; console.log(`📊 Total de clientes retornados: ${todosClientes.length}`); // Filtrar por nome localmente com diferentes estratégias const nomeQuery = queryLimpo.toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); // Remove acentos const clientesFiltrados = todosClientes.filter(cliente => { const nomeCliente = (cliente.nome || '').toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); // Remove acentos // Estratégias de busca: // 1. Nome completo exato if (nomeCliente === nomeQuery) return true; // 2. Nome contém a busca if (nomeCliente.includes(nomeQuery)) return true; // 3. Busca contém o nome (para nomes parciais) if (nomeQuery.includes(nomeCliente) && nomeCliente.length > 3) return true; // 4. Busca por palavras individuais const palavrasBusca = nomeQuery.split(/\s+/).filter(p => p.length > 2); const palavrasNome = nomeCliente.split(/\s+/); // Verificar se todas as palavras da busca estão no nome const todasPalavrasEncontradas = palavrasBusca.every(palavraBusca => palavrasNome.some(palavraNome => palavraNome.includes(palavraBusca) || palavraBusca.includes(palavraNome) ) ); if (todasPalavrasEncontradas && palavrasBusca.length > 0) return true; // 5. Busca fuzzy - verificar similaridade if (this.calcularSimilaridade(nomeCliente, nomeQuery) > 0.8) return true; return false; }); console.log(`🎯 Clientes filtrados por nome: ${clientesFiltrados.length}`); // Ordenar por relevância (nomes mais similares primeiro) clientesFiltrados.sort((a, b) => { const nomeA = (a.nome || '').toLowerCase(); const nomeB = (b.nome || '').toLowerCase(); const simA = this.calcularSimilaridade(nomeA, nomeQuery); const simB = this.calcularSimilaridade(nomeB, nomeQuery); return simB - simA; // Ordem decrescente de similaridade }); // Log dos resultados para debug if (clientesFiltrados.length > 0) { console.log(`✅ Clientes encontrados:`); clientesFiltrados.slice(0, 5).forEach((cliente, index) => { console.log(` ${index + 1}. ${cliente.nome} (ID: ${cliente.id})`); }); } else { console.log(`❌ Nenhum cliente encontrado para: "${queryLimpo}"`); // Log de amostra para debug console.log(`🔍 Amostra de nomes disponíveis:`); todosClientes.slice(0, 5).forEach((cliente, index) => { console.log(` ${index + 1}. ${cliente.nome}`); }); } return { success: true, clientes: clientesFiltrados.map(cliente => ({ ...cliente, cpf: cliente.cpfCnpj || cliente.cpf || cliente.documento || '', status: this.interpretarStatusCliente(cliente), statusOriginal: cliente.status, id_status: cliente.id_status, bloqueado: cliente.bloqueado, ativo: cliente.ativo })), instance: result.instance, debug: { totalOriginal: todosClientes.length, totalFiltrado: clientesFiltrados.length, termoBusca: queryLimpo } }; } } else { // Busca específica (CPF/ID) result = await this.makeRequest('ObterClientes', searchParams, instance, credentials); } if (result.success) { // Normalizar dados dos clientes para incluir CPF e status correto const clientesNormalizados = (result.data.dados || []).map(cliente => ({ ...cliente, cpf: cliente.cpfCnpj || cliente.cpf || cliente.documento || '', status: this.interpretarStatusCliente(cliente), statusOriginal: cliente.status, id_status: cliente.id_status, bloqueado: cliente.bloqueado, ativo: cliente.ativo })); return { success: true, clientes: clientesNormalizados, instance: result.instance }; } return result; } catch (error) { console.error(`❌ Erro na busca de cliente:`, error); return { success: false, error: error.message, instance: instance.name }; } } /** * Obter dados detalhados do cliente incluindo dados cadastrais completos * Usa apenas os dados dos serviços que já contêm informações completas do cliente */ async obterDadosCompletosCliente(idCliente, location = '', credentials) { const instance = this.getInstanceByLocation(location); try { console.log(`🔍 Obtendo dados completos do cliente ${idCliente} via serviços (evitando erro 400)`); // Usar apenas dados dos serviços que já contêm informações completas const servicosData = await this.obterServicosCliente(idCliente, location, credentials); if (!servicosData.success) { console.log(`❌ Falha ao obter serviços: ${servicosData.error}`); return servicosData; } const servicos = servicosData.data.dados || servicosData.data.servicos || []; if (servicos.length === 0) { return { success: false, error: 'Cliente não encontrado ou sem serviços' }; } // Usar dados do primeiro serviço para extrair informações do cliente const primeiroServico = servicos[0]; console.log(`✅ Extraindo dados do cliente dos serviços: ${primeiroServico.instalacao_nome}`); // Montar dados do cliente baseado nas informações dos serviços const cliente = { id: primeiroServico.id_cliente, nome: primeiroServico.instalacao_nome || 'Nome não informado', cpf: primeiroServico.cpf || '', rg: primeiroServico.rg || '', email: primeiroServico.usuario || '', telefone: primeiroServico.instalacao_contato1 || primeiroServico.instalacao_contato || '', celular: primeiroServico.instalacao_contato1 || '', endereco: primeiroServico.instalacao_endereco || '', numero: primeiroServico.instalacao_numero || '', bairro: primeiroServico.instalacao_bairro || '', cidade: primeiroServico.instalacao_cidade || '', uf: primeiroServico.instalacao_uf || '', cep: primeiroServico.instalacao_cep || '', complemento: primeiroServico.instalacao_complemento || '', data_cadastro: primeiroServico.data_cadastro || '', observacoes: primeiroServico.observacoes || '', // Determinar status baseado nos serviços status: this.determinarStatusClientePorServicos(servicos), // Dados de localização latitude: primeiroServico.latitude || '', longitude: primeiroServico.longitude || '' }; console.log(`✅ Dados do cliente extraídos com sucesso`); return { success: true, cliente: cliente, instance: instance.name, fonte: 'servicos' }; } catch (error) { console.error(`❌ Erro ao obter dados completos: ${error.message}`); return { success: false, error: error.message, instance: instance.name }; } } /** * Determinar status do cliente baseado nos serviços */ determinarStatusClientePorServicos(servicos) { // Se tem pelo menos um serviço ativo, considera cliente ativo const temServicoAtivo = servicos.some(servico => servico.id_status === 1 || servico.status === 'liberado' ); return temServicoAtivo ? 1 : 0; } /** * Obter dados detalhados do cliente */ async obterDadosCliente(idCliente, location = '', credentials) { const instance = this.getInstanceByLocation(location); return await this.makeRequest('ObterClientes', { idCliente }, instance, credentials); } /** * Obter serviços do cliente */ async obterServicosCliente(idCliente, location = '', credentials) { const instance = this.getInstanceByLocation(location); // Obter o idUsuario da sessão atual const session = await this.getValidSession(instance, credentials); console.log(`🔍 Obtendo serviços para cliente: ${idCliente} (tipo: ${typeof idCliente})`); const clienteIdInt = parseInt(idCliente, 10); // Tentar diferentes abordagens para obter serviços do cliente let response = null; let tentativas = [ // Tentativa 1: Endpoint padrão com idCliente { endpoint: 'ObterClientesServicos', data: { idCliente: clienteIdInt, idUsuario: session.idUsuario || 1 } }, // Tentativa 2: Endpoint alternativo (se existir) { endpoint: 'ObterServicosCliente', data: { idCliente: clienteIdInt, idUsuario: session.idUsuario || 1 } }, // Tentativa 3: Com parâmetros alternativos { endpoint: 'ObterClientesServicos', data: { cliente_id: clienteIdInt, idUsuario: session.idUsuario || 1 } } ]; logger.debug('Iniciando tentativas de busca de serviços', { clienteIdInt, sessionId: session.idUsuario, tentativasCount: tentativas.length }); for (let i = 0; i < tentativas.length; i++) { const tentativa = tentativas[i]; try { console.log(`📋 Tentativa ${i + 1} - Endpoint: ${tentativa.endpoint}, Dados:`, tentativa.data); logger.debug(`Tentativa ${i + 1}`, { endpoint: tentativa.endpoint, data: tentativa.data }); response = await this.makeRequest(tentativa.endpoint, tentativa.data, instance, credentials); logger.apiCall(tentativa.endpoint, tentativa.data, response); if (response.success && response.data) { console.log(`✅ Sucesso na tentativa ${i + 1} - Total de itens: ${response.data.dados?.length || response.data.length || 0}`); // Normalizar os dados de status dos serviços if (response.data.dados && Array.isArray(response.data.dados)) { response.data.dados = response.data.dados.map(servico => ({ ...servico, // Garantir que id_status está consistente id_status: this.interpretarStatusServico(servico), status: this.obterTextoStatus(this.interpretarStatusServico(servico)) })); console.log(`🔄 Status dos serviços normalizados`); } // Verificar se os dados são relevantes para o cliente const dados = response.data.dados || response.data; if (Array.isArray(dados) && dados.length > 0) { // Verificar quantos itens realmente pertencem ao cliente const servicosDoCliente = dados.filter(item => { const itemClienteId = parseInt(item.idCliente || item.cliente_id || item.id_cliente || 0, 10); return itemClienteId === clienteIdInt; }); console.log(`🎯 Serviços filtrados para cliente ${clienteIdInt}: ${servicosDoCliente.length} de ${dados.length} total`); logger.debug('Filtragem de serviços', { clienteIdBuscado: clienteIdInt, servicosEncontrados: servicosDoCliente.length, totalServicos: dados.length, primeiroItem: dados[0] ? { id_cliente: dados[0].id_cliente, idCliente: dados[0].idCliente, cliente_id: dados[0].cliente_id } : null }); // Se encontrou serviços específicos do cliente, usar essa resposta if (servicosDoCliente.length > 0 || i === tentativas.length - 1) { break; } } } } catch (error) { console.log(`❌ Falha na tentativa ${i + 1}: ${error.message}`); if (i === tentativas.length - 1) { throw error; } } } return response; } /** * Interpretar status do serviço corretamente */ interpretarStatusServico(servico) { // Verificar diferentes campos de status // Se tem id_status explícito, usar ele if (servico.id_status) { return parseInt(servico.id_status, 10); } // Interpretar pelo campo status textual if (servico.status) { const statusLower = servico.status.toLowerCase(); if (statusLower.includes('liberado') || statusLower.includes('ativo')) { return 1; // Ativo/Liberado } if (statusLower.includes('bloqueado')) { return 2; // Bloqueado } if (statusLower.includes('desativado') || statusLower.includes('inativo')) { return 3; // Desativado } } // Verificar por datas if (servico.data_desativacao && servico.data_desativacao !== '0000-00-00' && servico.data_desativacao !== null) { return 3; // Desativado } if (servico.data_bloqueio && servico.data_bloqueio !== '0000-00-00' && servico.data_bloqueio !== null) { // Verificar se foi desbloqueado depois if (!servico.data_desbloqueio || servico.data_desbloqueio === '0000-00-00' || servico.data_desbloqueio === null) { return 2; // Bloqueado } // Se tem data de desbloqueio, verificar qual é mais recente if (new Date(servico.data_desbloqueio) < new Date(servico.data_bloqueio)) { return 2; // Bloqueado (bloqueio é mais recente) } } // Por padrão, considerar ativo return 1; } /** * Obter texto descritivo do status */ obterTextoStatus(idStatus) { switch (parseInt(idStatus, 10)) { case 1: return 'liberado'; case 2: return 'bloqueado'; case 3: return 'desativado'; default: return 'desconhecido'; } } /** * Obter faturas em aberto do cliente * @param {number|string} idServico - opcional, filtra pelo serviço específico */ async obterFaturas(idCliente, location = '', credentials, idServico = null) { const instance = this.getInstanceByLocation(location); // Obter o idUsuario da sessão atual const session = await this.getValidSession(instance, credentials); const payload = { idCliente, idUsuario: session.idUsuario || 1, status: 0, situacaoFatura: 0 }; if (idServico) { payload.idServico = idServico; } return await this.makeRequest('ObterDocReceber', payload, instance, credentials); } /** * Obter dados para pagamento da fatura (linha digitável) */ async obterDadosFatura(idFatura, idCliente, location = '', credentials) { const instance = this.getInstanceByLocation(location); // Obter o idUsuario da sessão atual const session = await this.getValidSession(instance, credentials); return await this.makeRequest('ObterDocReceberDadosFatura', { idFatura, idCliente, idUsuario: session.idUsuario || 1 }, instance, credentials); } /** * Obter boleto em Base64 */ async obterBoletoBase64(idFatura, location = '', credentials) { const instance = this.getInstanceByLocation(location); return await this.makeRequest('ObterDocReceberBase64', { idFatura }, instance, credentials); } /** * Alterar status do serviço (bloquear/liberar) */ async alterarStatusServico(idServico, idCliente, status, location = '', credentials) { const instance = this.getInstanceByLocation(location); // Obter o idUsuario da sessão atual const session = await this.getValidSession(instance, credentials); // Status: 1 = Ativo, 2 = Bloqueado, 3 = Desativado return await this.makeRequest('AlterarServicoStatus', { idServico, idCliente, idUsuario: session.idUsuario || 1, idStatus: status }, instance, credentials); } /** * Bloquear serviço do cliente */ async bloquearServico(idServico, idCliente, location = '', credentials) { return await this.alterarStatusServico(idServico, idCliente, 2, location, credentials); } /** * Liberar serviço do cliente */ async liberarServico(idServico, idCliente, location = '', credentials) { return await this.alterarStatusServico(idServico, idCliente, 1, location, credentials); } /** * Obter técnicos disponíveis */ async obterTecnicos(location = '', credentials) { const instance = this.getInstanceByLocation(location); // Obter o idUsuario da sessão atual const session = await this.getValidSession(instance, credentials); return await this.makeRequest('ObterTecnicos', { idUsuario: session.idUsuario || 1 }, instance, credentials); } /** * Obter operadores disponíveis */ async obterOperadores(location = '', credentials) { const instance = this.getInstanceByLocation(location); // Obter o idUsuario da sessão atual const session = await this.getValidSession(instance, credentials); return await this.makeRequest('ObterOperadores', { idUsuario: session.idUsuario || 1 }, instance, credentials); } /** * Obter funcionários disponíveis */ async obterFuncionarios(location = '', credentials) { const instance = this.getInstanceByLocation(location); // Obter o idUsuario da sessão atual const session = await this.getValidSession(instance, credentials); return await this.makeRequest('ObterFuncionarios', { idUsuario: session.idUsuario || 1, desativado: false }, instance, credentials); } /** * Obter categorias de suporte */ async obterSuporteCategorias(location = '', credentials) { const instance = this.getInstanceByLocation(location); // Obter o idUsuario da sessão atual const session = await this.getValidSession(instance, credentials); return await this.makeRequest('ObterSuporteCategorias', { idUsuario: session.idUsuario || 1 }, instance, credentials); } /** * Obter tipos de atendimento do suporte */ async obterSuporteTiposAtendimento(location = '', credentials) { const instance = this.getInstanceByLocation(location); // Obter o idUsuario da sessão atual const session = await this.getValidSession(instance, credentials); const dados = { idUsuario: session.idUsuario || 1 }; return await this.makeRequest('ObterSuporteTiposAtendimento', dados, instance, credentials); } /** * Adicionar novo suporte */ async adicionarSuporte(dadosSuporte, location = '', credentials) { const instance = this.getInstanceByLocation(location); // Obter o idUsuario da sessão atual const session = await this.getValidSession(instance, credentials); const dados = { idUsuario: session.idUsuario || 1, idCliente: dadosSuporte.idCliente, idServico: dadosSuporte.idServico, idCategoria: dadosSuporte.idCategoria, descricao: dadosSuporte.descricao }; // Tentar com idAtendimento (técnico no sistema de suporte) se fornecido if (dadosSuporte.idAtendimento && Number.isInteger(dadosSuporte.idAtendimento)) { dados.idAtendimento = dadosSuporte.idAtendimento; console.log('🎫 API - Incluindo idAtendimento:', dadosSuporte.idAtendimento); } if (dadosSuporte.contato && dadosSuporte.contato.trim() !== '') { dados.contato = dadosSuporte.contato; } if (dadosSuporte.contatoTelefone && dadosSuporte.contatoTelefone.trim() !== '') { dados.contatoTelefone = dadosSuporte.contatoTelefone; } if (dadosSuporte.outrasInformacoes && dadosSuporte.outrasInformacoes.trim() !== '') { dados.outrasInformacoes = dadosSuporte.outrasInformacoes; } console.log('🎫 API - Dados finais para envio:', dados); // Tentativa 1: com idAtendimento const result = await this.makeRequest('AdicionarSuporte', dados, instance, credentials); if (result.success) { console.log('✅ API - Suporte criado com sucesso:', result); return result; } // Tentativa 2: sem idAtendimento (campo opcional que a API pode rejeitar) if (dados.idAtendimento) { console.log('🔄 API - Tentando criar suporte SEM idAtendimento...'); const dadosSemAtendimento = { ...dados }; delete dadosSemAtendimento.idAtendimento; const retryResult = await this.makeRequest('AdicionarSuporte', dadosSemAtendimento, instance, credentials); console.log('✅ API - Resultado sem idAtendimento:', retryResult); return retryResult; } return result; } /** * Encaminhar suporte para um técnico */ async encaminharSuporte(idSuporte, idTecnico, descricao, location = '', credentials) { const instance = this.getInstanceByLocation(location); const session = await this.getValidSession(instance, credentials); return await this.makeRequest('EncaminharSuporte', { idUsuario: session.idUsuario || 1, idSuporte: parseInt(idSuporte, 10), idTecnico: parseInt(idTecnico, 10), descricao }, instance, credentials); } /** * Obter suportes abertos de um cliente */ async obterSuportesAbertos(idCliente, location = '', credentials) { const instance = this.getInstanceByLocation(location); const session = await this.getValidSession(instance, credentials); return await this.makeRequest('ObterSuporte', { idUsuario: session.idUsuario || 1, idCliente: parseInt(idCliente, 10), status: 0 // 0 = abertos }, instance, credentials); } /** * Finalizar suporte */ async finalizarSuporte(idSuporte, descricao, location = '', credentials) { const instance = this.getInstanceByLocation(location); const session = await this.getValidSession(instance, credentials); return await this.makeRequest('FinalizarSuporte', { idUsuario: session.idUsuario || 1, idSuporte: parseInt(idSuporte, 10), descricao }, instance, credentials); } /** * Obter lista de OLTs cadastradas no TopSapp */ async obterOlts(location = '', credentials) { const instance = this.getInstanceByLocation(location); try { const session = await this.getValidSession(instance, credentials); return await this.makeRequest('ObterOlts', { idUsuario: session.idUsuario || 1 }, instance, credentials); } catch (error) { return { success: false, error: error.message }; } } /** * Atualizar dados FTTH de um serviço (vincular ONU a cliente/serviço) */ async atualizarFTTH(idCliente, idServico, ftthData, location = '', credentials) { const instance = this.getInstanceByLocation(location); try { const session = await this.getValidSession(instance, credentials); return await this.makeRequest('AlterarServico', { idUsuario: session.idUsuario || 1, idCliente: parseInt(idCliente), idServico: parseInt(idServico), ...ftthData }, instance, credentials); } catch (error) { return { success: false, error: error.message }; } } // Nota: AlterarServico não suporta campos DHCP (dhcp_username, dhcp_agent_circuit_id, dhcp_liberar). // Esses campos só existem no CadastrarClienteServico (criação de serviço novo). // A configuração IPoE é gerada pelo bot e deve ser aplicada manualmente no painel TopSapp. /** * Listar todas as instâncias disponíveis */ getInstances() { return Object.values(this.instances); } /** * Converter Base64 para PDF */ convertBase64ToPDF(base64String, filename = 'boleto.pdf') { try { // Remove header se presente (data:application/pdf;base64,) const base64Data = base64String.replace(/^data:application\/pdf;base64,/, ''); // Converte para buffer const buffer = Buffer.from(base64Data, 'base64'); return { success: true, buffer, filename, mimeType: 'application/pdf' }; } catch (error) { return { success: false, error: error.message }; } } /** * Limpar sessões expiradas */ clearExpiredSessions() { const thirtyMinutes = 30 * 60 * 1000; const now = Date.now(); for (const [instanceURL, session] of this.sessions.entries()) { if ((now - session.timestamp) >= thirtyMinutes) { this.sessions.delete(instanceURL); console.log(`🧹 Sessão expirada removida: ${session.instance.name}`); } } } /** * Interpretar status do cliente corretamente */ interpretarStatusCliente(cliente) { // Verificar diferentes campos de status que podem indicar se o cliente está ativo // Se tem campo 'ativo' explícito if (typeof cliente.ativo !== 'undefined') { return cliente.ativo === 1 || cliente.ativo === true ? 1 : 0; } // Se tem campo 'bloqueado' if (typeof cliente.bloqueado !== 'undefined') { return cliente.bloqueado === 0 || cliente.bloqueado === false ? 1 : 0; } // Verificar por id_status (alguns sistemas usam diferentes códigos) if (cliente.id_status) { // Status 1 = Ativo, 2 = Bloqueado, 3 = Desativado, etc. return cliente.id_status === 1 ? 1 : 0; } // Se tem data de desativação, está inativo if (cliente.data_desativacao && cliente.data_desativacao !== '0000-00-00' && cliente.data_desativacao !== null) { return 0; } // Se tem data de bloqueio sem data de desbloqueio, está bloqueado if (cliente.data_bloqueio && cliente.data_bloqueio !== '0000-00-00' && cliente.data_bloqueio !== null) { if (!cliente.data_desbloqueio || cliente.data_desbloqueio === '0000-00-00' || cliente.data_desbloqueio === null) { return 0; } } // Por padrão, usar o status original ou assumir ativo se não há informação de bloqueio/desativação return cliente.status || 1; } /** * Obter histórico de acessos por estação/IP (ObterClientesServicosAcessos) */ async obterAcessos(params, instance, credentials) { return this.makeRequest('ObterClientesServicosAcessos', params, instance, credentials); } /** * Calcular similaridade entre duas strings (algoritmo de Jaro-Winkler simplificado) */ calcularSimilaridade(str1, str2) { if (str1 === str2) return 1.0; if (str1.length === 0 || str2.length === 0) return 0.0; // Normalizar strings const s1 = str1.toLowerCase().trim(); const s2 = str2.toLowerCase().trim(); if (s1 === s2) return 1.0; // Calcular distância de Levenshtein simplificada const maxLen = Math.max(s1.length, s2.length); let matches = 0; for (let i = 0; i < Math.min(s1.length, s2.length); i++) { if (s1[i] === s2[i]) matches++; } // Adicionar pontos por palavras em comum const words1 = s1.split(/\s+/); const words2 = s2.split(/\s+/); let wordMatches = 0; words1.forEach(word1 => { if (words2.some(word2 => word1.includes(word2) || word2.includes(word1))) { wordMatches++; } }); const charSim = matches / maxLen; const wordSim = wordMatches / Math.max(words1.length, words2.length); // Média ponderada: 60% caracteres, 40% palavras return (charSim * 0.6) + (wordSim * 0.4); } } module.exports = TopSappAPI;