From 828b6e720935fc04ab011b358d0db4b59168576c Mon Sep 17 00:00:00 2001 From: Rodrigo Bertoldo de Sousa Martins Date: Wed, 14 Oct 2026 15:10:30 -0300 Subject: [PATCH] sgp - pedro afonso --- .env.example | 132 +- src/bot/handler.js | 4 + src/sgp/api.js | 718 ++++++++++ src/topsapp/api.js | 2136 +++++++++++++++-------------- src/topsapp/config.js | 227 ++-- src/topsapp/manager.js | 2880 ++++++++++++++++++++-------------------- 6 files changed, 3432 insertions(+), 2665 deletions(-) create mode 100644 src/sgp/api.js diff --git a/.env.example b/.env.example index 0029b2a..a8bb0fe 100644 --- a/.env.example +++ b/.env.example @@ -1,60 +1,72 @@ -# Variáveis de ambiente -TELEGRAM_BOT_TOKEN=seu_token_do_bot_aqui - -# Configurações SSH (exemplo) -DEFAULT_SSH_HOST=192.168.1.1 -DEFAULT_SSH_USER=admin -DEFAULT_SSH_PASSWORD=password - -# Configurações do bot -BOT_DEBUG=true - -# IDs do Telegram com acesso a TODAS as cidades/provedores (admin) -ALLOWED_USERS=123456789,987654321 - -# ========================================================== -# Acesso por cidade/provedor (IDs do Telegram, separados por vírgula) -# Um usuário pode estar em mais de uma lista; se estiver em mais de -# uma cidade, o bot exibirá um menu para escolher qual atender. -# ========================================================== -GUARAI_ALLOWED_USERS= -PEDRO_AFONSO_ALLOWED_USERS= - -# ========================================================== -# Concentrador Juniper - Guaraí -# ========================================================== -JUNIPER_GUARAI_HOST=131.0.192.254 -JUNIPER_GUARAI_USER=rodrigo -JUNIPER_GUARAI_PASS= - -# ========================================================== -# Concentrador Juniper - Pedro Afonso -# ========================================================== -JUNIPER_PEDRO_AFONSO_HOST=45.189.140.2 -JUNIPER_PEDRO_AFONSO_USER=rodrigo -JUNIPER_PEDRO_AFONSO_PASS= - -# Configurações TopSapp - Instância Guarai/Colmeia -TOPSAPP_GUARAI_USER=seu_usuario_guarai -TOPSAPP_GUARAI_PASS=sua_senha_guarai - -# Configurações TopSapp - Instância Pedro Afonso -TOPSAPP_PEDRO_AFONSO_USER=seu_usuario_pedro_afonso -TOPSAPP_PEDRO_AFONSO_PASS=sua_senha_pedro_afonso - -# ========================================================== -# Configurações GenieACS (TR-069 ACS) -# Porta padrão NBI: 7557 -# ========================================================== - -# Instância Guarai - Clicknet -GENIEACS_GUARAI_URL=http://g.clicknetto.com.br -GENIEACS_GUARAI_NBI_PORT=7557 -GENIEACS_GUARAI_USER=api -GENIEACS_GUARAI_PASS=sua_senha_genieacs_guarai - -# Exemplo de nova instância futura: -# GENIEACS_PEDRO_AFONSO_URL=http://genieacs.exemplo.com.br -# GENIEACS_PEDRO_AFONSO_NBI_PORT=7557 -# GENIEACS_PEDRO_AFONSO_USER=api -# GENIEACS_PEDRO_AFONSO_PASS=sua_senha +# Variáveis de ambiente +TELEGRAM_BOT_TOKEN=seu_token_do_bot_aqui + +# Configurações SSH (exemplo) +DEFAULT_SSH_HOST=192.168.1.1 +DEFAULT_SSH_USER=admin +DEFAULT_SSH_PASSWORD=password + +# Configurações do bot +BOT_DEBUG=true + +# IDs do Telegram com acesso a TODAS as cidades/provedores (admin) +ALLOWED_USERS=123456789,987654321 + +# ========================================================== +# Acesso por cidade/provedor (IDs do Telegram, separados por vírgula) +# Um usuário pode estar em mais de uma lista; se estiver em mais de +# uma cidade, o bot exibirá um menu para escolher qual atender. +# ========================================================== +GUARAI_ALLOWED_USERS= +PEDRO_AFONSO_ALLOWED_USERS= + +# ========================================================== +# Concentrador Juniper - Guaraí +# ========================================================== +JUNIPER_GUARAI_HOST=131.0.192.254 +JUNIPER_GUARAI_USER=rodrigo +JUNIPER_GUARAI_PASS= + +# ========================================================== +# Concentrador Juniper - Pedro Afonso +# ========================================================== +JUNIPER_PEDRO_AFONSO_HOST=45.189.140.2 +JUNIPER_PEDRO_AFONSO_USER=rodrigo +JUNIPER_PEDRO_AFONSO_PASS= + +# Configurações TopSapp - Instância Guarai/Colmeia +TOPSAPP_GUARAI_USER=seu_usuario_guarai +TOPSAPP_GUARAI_PASS=sua_senha_guarai + +# Configurações SGP - Pedro Afonso (substitui o TopSapp nesta cidade) +# Autenticação: app + token (Sistema -> Ferramentas -> Painel Admin -> Tokens) +# ou, alternativamente, usuário + senha do SGP (Basic Auth) +SGP_PEDRO_AFONSO_URL=https://seu-sgp.exemplo.com.br +SGP_PEDRO_AFONSO_APP=nome_da_aplicacao +SGP_PEDRO_AFONSO_TOKEN=token_da_aplicacao +#SGP_PEDRO_AFONSO_USER= +#SGP_PEDRO_AFONSO_PASS= +# Opcional: id da OLT no SGP -> id_ap legado (2 = Pedro Afonso, 4 = Canavieiras). +# Se omitido, é deduzido pelo nome da OLT no SGP. +#SGP_PEDRO_AFONSO_OLT_MAP=12:2,15:4 +# Opcional: ids de status de contrato do SGP (padrão: 1 = Ativo, 4 = Suspenso) +#SGP_STATUS_ATIVO=1 +#SGP_STATUS_BLOQUEADO=4 +#SGP_INSECURE_TLS=false + +# ========================================================== +# Configurações GenieACS (TR-069 ACS) +# Porta padrão NBI: 7557 +# ========================================================== + +# Instância Guarai - Clicknet +GENIEACS_GUARAI_URL=http://g.clicknetto.com.br +GENIEACS_GUARAI_NBI_PORT=7557 +GENIEACS_GUARAI_USER=api +GENIEACS_GUARAI_PASS=sua_senha_genieacs_guarai + +# Exemplo de nova instância futura: +# GENIEACS_PEDRO_AFONSO_URL=http://genieacs.exemplo.com.br +# GENIEACS_PEDRO_AFONSO_NBI_PORT=7557 +# GENIEACS_PEDRO_AFONSO_USER=api +# GENIEACS_PEDRO_AFONSO_PASS=sua_senha diff --git a/src/bot/handler.js b/src/bot/handler.js index e973f7d..79b008b 100644 --- a/src/bot/handler.js +++ b/src/bot/handler.js @@ -6899,6 +6899,10 @@ Digite /help para ver todos os comandos disponíveis! dadosSuporte.outrasInformacoes = `Criado via Bot Telegram\nData original: ${dados.dataHoraTexto}`; + // Usados pelo SGP (técnico responsável e agendamento são definidos na abertura do chamado) + dadosSuporte.idTecnico = dados.tecnicoSelecionado?.id; + dadosSuporte.dataHoraAgendamento = dados.dataHora; + const resultado = await this.topsapp.adicionarSuporte(dadosSuporte, location); if (resultado.success) { diff --git a/src/sgp/api.js b/src/sgp/api.js new file mode 100644 index 0000000..3bb3475 --- /dev/null +++ b/src/sgp/api.js @@ -0,0 +1,718 @@ +const axios = require('axios'); +const https = require('https'); +const logger = require('../utils/logger'); + +/** + * Cliente da API do SGP (https://documenter.getpostman.com/view/6682240/2sB34hHg2V). + * + * Expõe a mesma superfície que o TopSappAPI usa no TopSappManager e devolve os + * dados no mesmo formato ({ success, data: { dados: [...] }, instance }), de + * modo que o restante do bot não precise saber qual sistema está por trás. + * + * Convenções de mapeamento: + * - idCliente -> id do cliente no SGP + * - id_servico -> id do CONTRATO no SGP (bloqueio/liberação são feitos por contrato) + * - id da fatura -> id do título no SGP + */ + +// Status de contrato no SGP (documentação pública) +const SGP_STATUS_ATIVO = parseInt(process.env.SGP_STATUS_ATIVO || '1', 10); +const SGP_STATUS_BLOQUEADO = parseInt(process.env.SGP_STATUS_BLOQUEADO || '4', 10); // Suspenso + +class SgpAPI { + constructor() { + this.instances = { + pedro_afonso: { + key: 'pedro_afonso', + name: 'Pedro Afonso (SGP)', + cities: ['pedro afonso', 'pedro_afonso'] + } + }; + + this.httpsAgent = new https.Agent({ + rejectUnauthorized: process.env.SGP_INSECURE_TLS !== 'true' + }); + this.tituloCache = new Map(); // id título -> { at, titulo } + this.tecnicoCache = new Map(); // id técnico -> username + + console.log('🔗 SgpAPI inicializado'); + } + + // ------------------------------------------------------------------ infra + + getInstanceByLocation() { + return this.instances.pedro_afonso; + } + + getInstances() { + return Object.values(this.instances); + } + + clearExpiredSessions() { + const now = Date.now(); + for (const [id, entry] of this.tituloCache.entries()) { + if (now - entry.at > 2 * 60 * 1000) this.tituloCache.delete(id); + } + } + + _name(credentials) { + return this.getInstanceByLocation().name; + } + + /** + * Requisição genérica ao SGP. Autentica com token+app (corpo/query) ou Basic Auth. + */ + async _request(credentials, method, path, params = {}) { + if (!credentials || !credentials.baseURL) { + throw new Error('SGP não configurado (URL ausente)'); + } + + const clean = {}; + Object.keys(params).forEach(k => { + if (params[k] !== undefined && params[k] !== null && params[k] !== '') clean[k] = params[k]; + }); + + const config = { + method, + url: `${credentials.baseURL.replace(/\/$/, '')}${path}`, + httpsAgent: this.httpsAgent, + timeout: 30000 + }; + + if (credentials.app && credentials.token) { + clean.app = credentials.app; + clean.token = credentials.token; + } else if (credentials.usuario && credentials.senha) { + config.auth = { username: credentials.usuario, password: credentials.senha }; + } else { + throw new Error('SGP sem credenciais (app/token ou usuário/senha)'); + } + + if (method === 'GET') { + config.params = clean; + } else { + const body = new URLSearchParams(); + Object.keys(clean).forEach(k => body.append(k, String(clean[k]))); + config.data = body.toString(); + config.headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; + } + + const response = await axios(config); + return response.data; + } + + /** + * Envolve uma chamada no formato { success, data, instance } / { success:false, error } + */ + async _wrap(credentials, fn) { + try { + const data = await fn(); + return { success: true, data, instance: this._name(credentials) }; + } catch (error) { + const body = error.response?.data; + const msg = (body && (body.msg || body.mensagem || body.detail || body.message || body.erro)) + || error.message; + logger.warn('Erro na API SGP', { error: msg, status: error.response?.status }); + console.error(`❌ Erro SGP: ${msg}`); + return { success: false, error: typeof msg === 'string' ? msg : JSON.stringify(msg), instance: this._name(credentials) }; + } + } + + // --------------------------------------------------------------- helpers + + _formatarData(data) { + if (!data) return ''; + const m = String(data).match(/^(\d{4})-(\d{2})-(\d{2})/); + return m ? `${m[3]}/${m[2]}/${m[1]}` : String(data); + } + + _statusId(texto) { + const t = String(texto || '').toLowerCase() + .normalize('NFD').replace(/[̀-ͯ]/g, '').trim(); + if (/^ativo/.test(t)) return 1; + if (/suspens|bloquead/.test(t)) return 2; + if (/cancelad|inativ|desativ/.test(t)) return 3; + return 5; // aguardando / inviabilidade / outros + } + + _textoStatus(idStatus) { + switch (idStatus) { + case 1: return 'liberado'; + case 2: return 'bloqueado'; + case 3: return 'desativado'; + default: return 'desconhecido'; + } + } + + /** id_ap legado (2 = Pedro Afonso, 4 = Canavieiras) a partir da OLT do SGP */ + _idApLegado(onu) { + if (!onu) return ''; + const mapa = {}; + (process.env.SGP_PEDRO_AFONSO_OLT_MAP || '').split(',').forEach(par => { + const [sgpId, legado] = par.split(':').map(s => s && s.trim()); + if (sgpId && legado) mapa[sgpId] = legado; + }); + if (onu.olt_id !== undefined && mapa[String(onu.olt_id)]) return mapa[String(onu.olt_id)]; + const nome = String(onu.olt_nome || '').toLowerCase(); + if (/canavieiras/.test(nome)) return '4'; + if (/pedro|afonso/.test(nome)) return '2'; + return onu.olt_id !== undefined ? String(onu.olt_id) : ''; + } + + _clienteId(c) { + return c.id ?? c.clienteId ?? c.cliente_id ?? c.idCliente ?? null; + } + + _mapCliente(c) { + const contratos = c.contratos || []; + const ativo = contratos.some(ct => this._statusId(ct.status) === 1); + const contatos = c.contatos || c.telefones || []; + const primeiroContato = Array.isArray(contatos) && contatos.length + ? (typeof contatos[0] === 'string' ? contatos[0] : (contatos[0].contato || contatos[0].numero || '')) + : ''; + const end = c.endereco || {}; + return { + id: this._clienteId(c), + nome: c.nome, + cpf: c.cpfcnpj || '', + cpfCnpj: c.cpfcnpj || '', + email: c.email || (Array.isArray(c.emails) ? c.emails[0] : '') || '', + telefone: c.telefone || c.celular || primeiroContato, + celular: c.celular || c.telefone || primeiroContato, + endereco: end.logradouro || '', + numero: end.numero || '', + bairro: end.bairro || '', + cidade: end.cidade || '', + uf: end.uf || '', + cep: end.cep || '', + complemento: end.complemento || '', + observacoes: c.observacao_cliente || '', + data_cadastro: c.dataCadastro || '', + status: ativo ? 1 : 0, + statusOriginal: contratos.map(ct => ct.status).join(', '), + id_status: ativo ? 1 : 0 + }; + } + + _mapServicos(c) { + const idCliente = this._clienteId(c); + const end = c.endereco || {}; + const out = []; + + (c.contratos || []).forEach(ct => { + const idStatus = this._statusId(ct.status); + (ct.servicos && ct.servicos.length ? ct.servicos : [{}]).forEach(srv => { + const onu = srv.onu || {}; + const conexao = onu.conexao || srv.conexao || {}; + const e = srv.endereco || end; + out.push({ + id: ct.contrato, + id_servico: ct.contrato, + servico_id: srv.id, + id_cliente: idCliente, + idCliente, + descricao: srv.plano?.descricao || srv.tipo || 'Serviço', + nome: srv.plano?.descricao || srv.tipo || 'Serviço', + plano_nome: srv.plano?.descricao || '', + tipo_servico: srv.tipo || '', + id_status: idStatus, + status: this._textoStatus(idStatus), + motivo_status: ct.motivo_status || '', + data_cadastro: ct.dataCadastro || '', + + usuario: srv.login || onu.login || '', + login: srv.login || onu.login || '', + pppoe_login: srv.login || '', + senha: srv.senha || '', + ip: srv.ip || '', + mac: srv.mac || '', + + // Dados do cliente (o TopSapp os devolvia embutidos no serviço) + instalacao_nome: c.nome, + cpf: c.cpfcnpj || '', + instalacao_endereco: e.logradouro || '', + instalacao_numero: e.numero || '', + instalacao_bairro: e.bairro || '', + instalacao_cidade: e.cidade || '', + instalacao_uf: e.uf || '', + instalacao_cep: e.cep || '', + instalacao_complemento: e.complemento || '', + latitude: e.latitude || '', + longitude: e.longitude || '', + + // FTTH + tecnologia: 'FTTH', + base: onu.olt_nome || '', + id_ap: this._idApLegado(onu), + id_olt_sgp: onu.olt_id, + pon: onu.pon !== undefined ? `${onu.slot ?? ''}/${onu.pon}` : '', + perfil: '', + serial: onu.serial || '', + serviceport: '', + ultimo_rx_power: onu.rx || '', + ultimo_tx_power: onu.tx || '', + data_ultimo_sinal: '', + hora_ultimo_sinal: '', + conexao: conexao.status || '', + ultimo_acesso: conexao.data_conexao || '', + ultimo_ip: conexao.ip || '', + + dhcp_username: '', + dhcp_agent_circuit_id: '', + dhcp_client_id: '', + dhcp_liberar: undefined + }); + }); + }); + + return out; + } + + async _listarClientes(credentials, params) { + const todos = []; + let offset = 0; + for (let pagina = 0; pagina < 20; pagina++) { + const data = await this._request(credentials, 'POST', '/api/ura/clientes/', { + omitir_titulos: 1, + limit: 100, + offset, + ...params + }); + const lote = data.clientes || []; + todos.push(...lote); + const total = data.paginacao?.total ?? lote.length; + offset += lote.length; + if (lote.length === 0 || offset >= total) break; + } + return todos; + } + + // --------------------------------------------------------------- clientes + + async buscarCliente(query, location = '', credentials) { + const instance = this.getInstanceByLocation(); + try { + const q = String(query || '').trim(); + const params = {}; + const digitos = q.replace(/\D/g, ''); + + if (/^\d+$/.test(q) && q.length !== 11 && q.length !== 14) { + params.cliente_id = parseInt(q, 10); + } else if (digitos.length === 11 || digitos.length === 14) { + params.cpfcnpj = digitos; + } else { + params.cliente_nome = q; + } + + console.log(`🔍 [SGP] Buscando cliente:`, params); + const clientes = await this._listarClientes(credentials, params); + + return { + success: true, + clientes: clientes.map(c => this._mapCliente(c)), + instance: instance.name + }; + } catch (error) { + console.error('❌ [SGP] Erro na busca de cliente:', error.message); + return { success: false, error: error.response?.data?.msg || error.message, instance: instance.name }; + } + } + + async obterServicosCliente(idCliente, location = '', credentials) { + return this._wrap(credentials, async () => { + const id = parseInt(idCliente, 10); + if (isNaN(id)) throw new Error(`ID do cliente inválido: ${idCliente}`); + const clientes = await this._listarClientes(credentials, { cliente_id: id }); + const dados = []; + clientes.forEach(c => dados.push(...this._mapServicos({ ...c, id: this._clienteId(c) ?? id }))); + return { dados }; + }); + } + + async obterDadosCompletosCliente(idCliente, location = '', credentials) { + const instance = this.getInstanceByLocation(); + try { + const id = parseInt(idCliente, 10); + const clientes = await this._listarClientes(credentials, { cliente_id: id }); + if (!clientes.length) { + return { success: false, error: 'Cliente não encontrado', instance: instance.name }; + } + const c = { ...clientes[0], id: this._clienteId(clientes[0]) ?? id }; + const cliente = this._mapCliente(c); + const primeiro = this._mapServicos(c)[0] || {}; + cliente.latitude = primeiro.latitude || ''; + cliente.longitude = primeiro.longitude || ''; + return { success: true, cliente, instance: instance.name, fonte: 'sgp' }; + } catch (error) { + return { success: false, error: error.response?.data?.msg || error.message, instance: instance.name }; + } + } + + async obterDadosCliente(idCliente, location = '', credentials) { + const r = await this.obterDadosCompletosCliente(idCliente, location, credentials); + if (!r.success) return r; + return { success: true, data: { clientes: [r.cliente] }, instance: r.instance }; + } + + interpretarStatusCliente(cliente) { + return cliente.status === 1 || cliente.id_status === 1 ? 1 : 0; + } + + interpretarStatusServico(servico) { + return servico.id_status || this._statusId(servico.status); + } + + obterTextoStatus(idStatus) { + return this._textoStatus(parseInt(idStatus, 10)); + } + + // ----------------------------------------------------------------- faturas + + _mapTitulo(t, idCliente = null) { + const pago = t.status === 'pago' || !!t.dataPagamento; + return { + id: t.id, + valor: t.valorCorrigido ?? t.valor, + valor_original: t.valor, + vencimento: this._formatarData(t.dataVencimento), + pago: pago ? 'SIM' : 'NAO', + historico: t.demonstrativo || `Fatura ${t.numeroDocumento || t.id}`, + descricao: t.demonstrativo || 'Mensalidade', + periodo_inicio: '', + periodo_fim: '', + codigo_cliente: idCliente, + id_servico: t.clienteContrato, + numero_documento: t.numeroDocumento, + link: t.link, + linha_digitavel: t.linhaDigitavel || '', + codigo_barras: t.codigoBarras || '', + qrcode: t.codigoPix || '' + }; + } + + async obterFaturas(idCliente, location = '', credentials, idServico = null) { + return this._wrap(credentials, async () => { + const params = { status: 'abertos' }; + if (idCliente) params.cliente_id = parseInt(idCliente, 10); + if (idServico) params.contrato = parseInt(idServico, 10); + + const dados = []; + let offset = 0; + for (let pagina = 0; pagina < 20; pagina++) { + const data = await this._request(credentials, 'POST', '/api/ura/titulos/', { + ...params, limit: 250, offset + }); + const lote = data.titulos || []; + lote.forEach(t => { + this.tituloCache.set(String(t.id), { at: Date.now(), titulo: t }); + dados.push(this._mapTitulo(t, idCliente)); + }); + const total = data.paginacao?.total ?? lote.length; + offset += lote.length; + if (lote.length === 0 || offset >= total) break; + } + return { dados }; + }); + } + + async _obterTitulo(idFatura, credentials) { + const cached = this.tituloCache.get(String(idFatura)); + if (cached && Date.now() - cached.at < 2 * 60 * 1000) return cached.titulo; + + const data = await this._request(credentials, 'POST', '/api/ura/titulos/', { + titulo_id: parseInt(idFatura, 10) + }); + const titulo = (data.titulos || [])[0]; + if (!titulo) throw new Error('Fatura não encontrada'); + this.tituloCache.set(String(idFatura), { at: Date.now(), titulo }); + return titulo; + } + + async obterDadosFatura(idFatura, idCliente, location = '', credentials) { + return this._wrap(credentials, async () => { + const titulo = await this._obterTitulo(idFatura, credentials); + const f = this._mapTitulo(titulo, idCliente); + return { fatura: f }; + }); + } + + /** + * Baixa o boleto em PDF e devolve em Base64 (mesmo contrato do TopSapp). + */ + async obterBoletoBase64(idFatura, location = '', credentials) { + return this._wrap(credentials, async () => { + const titulo = await this._obterTitulo(idFatura, credentials); + let link = null; + + try { + const seg = await this._request(credentials, 'POST', '/api/ura/fatura2via/', { + contrato: titulo.clienteContrato, + numero_documento: titulo.numeroDocumento, + link_pdf: 1, + nao_gerar_os: 1 + }); + link = (seg.links || []).find(l => String(l.fatura) === String(idFatura))?.link || seg.link; + } catch (error) { + logger.warn('SGP fatura2via falhou, usando link do título', { error: error.message }); + } + link = link || titulo.link; + if (!link) throw new Error('Boleto sem link disponível'); + + const resp = await axios.get(link, { + responseType: 'arraybuffer', + httpsAgent: this.httpsAgent, + timeout: 30000 + }); + const tipo = String(resp.headers['content-type'] || ''); + if (!/pdf/i.test(tipo)) throw new Error(`O link do boleto não retornou PDF (${tipo || 'desconhecido'}): ${link}`); + + return { arquivo: Buffer.from(resp.data).toString('base64'), link }; + }); + } + + convertBase64ToPDF(base64String, filename = 'boleto.pdf') { + try { + const base64Data = base64String.replace(/^data:application\/pdf;base64,/, ''); + return { success: true, buffer: Buffer.from(base64Data, 'base64'), filename, mimeType: 'application/pdf' }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + // ------------------------------------------------------ bloqueio/liberação + + async _alterarStatusContrato(idContrato, status, credentials) { + return this._wrap(credentials, () => this._request(credentials, 'POST', '/api/ura/contrato/status/edit/', { + contrato: parseInt(idContrato, 10), + status + })); + } + + async alterarStatusServico(idServico, idCliente, status, location = '', credentials) { + // status no formato TopSapp: 1 = ativo, 2 = bloqueado + return this._alterarStatusContrato(idServico, status === 1 ? SGP_STATUS_ATIVO : SGP_STATUS_BLOQUEADO, credentials); + } + + async bloquearServico(idServico, idCliente, location = '', credentials) { + return this._alterarStatusContrato(idServico, SGP_STATUS_BLOQUEADO, credentials); + } + + async liberarServico(idServico, idCliente, location = '', credentials) { + return this._alterarStatusContrato(idServico, SGP_STATUS_ATIVO, credentials); + } + + // ----------------------------------------------------------------- suporte + + async obterTecnicos(location = '', credentials) { + return this._wrap(credentials, async () => { + const lista = await this._request(credentials, 'POST', '/api/ura/tecnicos/'); + (lista || []).forEach(t => this.tecnicoCache.set(String(t.id), t.username)); + return { dados: (lista || []).map(t => ({ id: t.id, nome: t.nome, username: t.username })) }; + }); + } + + /** Equivalente aos "operadores" do TopSapp: o bot só usa id_tecnico e tecnico */ + async obterOperadores(location = '', credentials) { + return this._wrap(credentials, async () => { + const lista = await this._request(credentials, 'POST', '/api/ura/tecnicos/'); + (lista || []).forEach(t => this.tecnicoCache.set(String(t.id), t.username)); + return { dados: (lista || []).map(t => ({ id: t.id, id_tecnico: t.id, tecnico: t.nome, username: t.username })) }; + }); + } + + async obterFuncionarios(location = '', credentials) { + return this.obterTecnicos(location, credentials); + } + + async obterSuporteCategorias(location = '', credentials) { + return this._wrap(credentials, async () => { + const lista = await this._request(credentials, 'GET', '/api/os/ocorrencia/tipo/list/'); + // "ocorrenciatipo" do chamado usa o código do tipo + return { dados: (lista || []).map(t => ({ id: t.codigo ?? t.id, descricao: t.descricao })) }; + }); + } + + async obterSuporteTiposAtendimento(location = '', credentials) { + return this._wrap(credentials, async () => { + const lista = await this._request(credentials, 'GET', '/api/ura/ocorrencia/metodo/list/'); + return { dados: (lista || []).map(m => ({ id: m.codigo, descricao: m.descricao })) }; + }); + } + + /** + * Abre chamado (ocorrência + O.S.). idServico = contrato. + */ + async adicionarSuporte(dadosSuporte, location = '', credentials) { + return this._wrap(credentials, async () => { + const params = { + contrato: parseInt(dadosSuporte.idServico, 10), + conteudo: dadosSuporte.descricao, + conteudolimpo: 1, + ocorrenciatipo: dadosSuporte.idCategoria, + observacao: dadosSuporte.outrasInformacoes, + contato_nome: dadosSuporte.contato, + contato_telefone: dadosSuporte.contatoTelefone + }; + + if (dadosSuporte.idTecnico) { + const username = this.tecnicoCache.get(String(dadosSuporte.idTecnico)); + if (username) params.responsavel = username; + } + if (dadosSuporte.dataHoraAgendamento) { + const d = new Date(dadosSuporte.dataHoraAgendamento); + if (!isNaN(d.getTime())) { + const p = n => String(n).padStart(2, '0'); + params.data_hora_agendamento = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; + } + } + + const data = await this._request(credentials, 'POST', '/api/ura/chamado/', params); + if (data.status !== undefined && Number(data.status) !== 1) { + throw new Error(data.msg || 'Não foi possível abrir o chamado'); + } + return { protocolo: data.protocolo, ...data }; + }); + } + + /** + * O técnico já é definido na abertura do chamado (campo "responsavel"). + */ + async encaminharSuporte(idSuporte, idTecnico, descricao, location = '', credentials) { + return { success: true, data: { msg: 'Técnico definido na abertura do chamado' }, instance: this._name(credentials) }; + } + + async obterSuportesAbertos(idCliente, location = '', credentials) { + return this._wrap(credentials, async () => { + const lista = await this._request(credentials, 'POST', '/api/os/list/', { + cliente_id: parseInt(idCliente, 10) + }); + const dados = (lista || []) + .filter(os => Number(os.os_status) !== 1 && !os.os_data_finalizacao) + .map(os => ({ + id: os.os_id, + protocolo: os.os_protocolo, + defeito: os.os_conteudo || os.os_motivo_descricao || '', + endereco: os.endereco_logradouro || '', + numero: os.endereco_numero || '', + bairro: os.endereco_bairro || '', + cidade: os.endereco_cidade || '', + data: os.os_data_agendamento || os.os_data_cadastro || '' + })); + return { dados }; + }); + } + + async finalizarSuporte(idSuporte, descricao, location = '', credentials) { + return this._wrap(credentials, () => { + const d = new Date(); + const p = n => String(n).padStart(2, '0'); + const agora = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`; + return this._request(credentials, 'POST', `/api/os/update/id/${parseInt(idSuporte, 10)}/`, { + os_status: 1, + os_servicoprestado: descricao, + os_data_finalizacao: agora + }); + }); + } + + // -------------------------------------------------------------------- FTTH + + /** + * Lista OLTs no formato que o fluxo de vínculo de ONU espera (nome/ssid/portas/perfis). + * O SGP não exige porta/perfil para vincular uma ONU já autorizada, então + * são devolvidos valores neutros apenas para o fluxo seguir. + */ + async obterOlts(location = '', credentials) { + return this._wrap(credentials, async () => { + const lista = await this._request(credentials, 'GET', '/api/fttx/olt/list/'); + return { + dados: (lista || []).map(o => ({ + id: o.id, + nome: o.name, + ssid: o.host || '', + portas: [{ id: 0, alias: 'SGP' }], + perfis: [{ id: 0, identificacao: 'SGP' }] + })) + }; + }); + } + + /** + * Vincula uma ONU a um serviço: localiza a ONU pelo serial no SGP e associa ao login do contrato. + */ + async atualizarFTTH(idCliente, idServico, ftthData, location = '', credentials) { + return this._wrap(credentials, async () => { + const serial = String(ftthData.serial || '').toUpperCase(); + if (!serial) throw new Error('Serial da ONU não informado'); + + const servicos = await this.obterServicosCliente(idCliente, location, credentials); + if (!servicos.success) throw new Error(servicos.error); + const srv = servicos.data.dados.find(s => String(s.id_servico) === String(idServico)); + if (!srv) throw new Error(`Contrato #${idServico} não encontrado para o cliente`); + if (!srv.login) throw new Error('Contrato sem login de serviço para vincular a ONU'); + + const onus = await this._request(credentials, 'GET', '/api/fttx/onu/list/'); + const onu = (onus || []).find(o => String(o.phy_addr || '').toUpperCase() === serial); + if (!onu) { + throw new Error(`ONU ${serial} não está cadastrada/autorizada no SGP. Autorize-a no SGP e tente novamente.`); + } + + return this._request(credentials, 'POST', `/api/fttx/onu/${onu.id}/edit/`, { + onu_update: 'service', + service: srv.login + }); + }); + } + + // ------------------------------------------------------------------ RADIUS + + /** + * Sessões PPPoE (equivalente a ObterClientesServicosAcessos). + * params: { status: 'on' } ou { dataInicial, dataFinal } (DD/MM/AAAA) + */ + async obterAcessos(params, instance, credentials) { + return this._wrap(credentials, async () => { + const query = { limit: 1000 }; + if (params.status === 'on') query.online = 1; + + const iso = d => { + const m = String(d || '').match(/^(\d{2})\/(\d{2})\/(\d{4})$/); + return m ? `${m[3]}-${m[2]}-${m[1]}` : null; + }; + const ini = iso(params.dataInicial); + const fim = iso(params.dataFinal); + + const sessoes = []; + let offset = 0; + for (let pagina = 0; pagina < 20; pagina++) { + const data = await this._request(credentials, 'POST', '/ws/radius/radacct/list/all/', { ...query, offset }); + const lote = data.result || []; + lote.forEach(item => { + (item.radacct || []).forEach(r => { + const inicio = String(r.acctstarttime || ''); + if (ini && fim && inicio) { + const dia = inicio.slice(0, 10); + const termDia = r.acctstoptime ? String(r.acctstoptime).slice(0, 10) : '9999-12-31'; + if (dia > fim || termDia < ini) return; + } + sessoes.push({ + id_cliente: -1, + usuario: r.username || item.pppoe_login, + ip: r.framedipaddress || item.ip || '', + inicio, + termino: r.acctstoptime || '', + nas_ip: r.nasipaddress || '', + nome: item.nome + }); + }); + }); + const total = data.paggination?.total ?? lote.length; + offset += lote.length; + if (lote.length === 0 || offset >= total) break; + } + return { dados: sessoes }; + }); + } +} + +module.exports = SgpAPI; diff --git a/src/topsapp/api.js b/src/topsapp/api.js index 6e7e597..9458fba 100644 --- a/src/topsapp/api.js +++ b/src/topsapp/api.js @@ -1,1073 +1,1063 @@ -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) { - // idUsuario é obrigatório neste endpoint — extrair da sessão ativa - const session = await this.getValidSession(instance, credentials); - return this.makeRequest('ObterClientesServicosAcessos', { - idUsuario: session.idUsuario, - ...params - }, instance, credentials); - } - - /** - * Listar conexões ativas (ObterClientesServicos com conexao: "online") - */ - async obterConexoesAtivas(instance, credentials) { - const session = await this.getValidSession(instance, credentials); - return this.makeRequest('ObterClientesServicos', { - idUsuario: session.idUsuario, - conexao: 'online', - calcDadosAcesso: true - }, 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; +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'] + } + }; + + 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(); + + // 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) { + // idUsuario é obrigatório neste endpoint — extrair da sessão ativa + const session = await this.getValidSession(instance, credentials); + return this.makeRequest('ObterClientesServicosAcessos', { + idUsuario: session.idUsuario, + ...params + }, instance, credentials); + } + + /** + * Listar conexões ativas (ObterClientesServicos com conexao: "online") + */ + async obterConexoesAtivas(instance, credentials) { + const session = await this.getValidSession(instance, credentials); + return this.makeRequest('ObterClientesServicos', { + idUsuario: session.idUsuario, + conexao: 'online', + calcDadosAcesso: true + }, 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; diff --git a/src/topsapp/config.js b/src/topsapp/config.js index 39f5b0c..b8f3efe 100644 --- a/src/topsapp/config.js +++ b/src/topsapp/config.js @@ -1,100 +1,127 @@ -/** - * Configurações do TopSapp - */ -class TopSappConfig { - constructor() { - this.credentials = { - guarai_colmeia: { - usuario: process.env.TOPSAPP_GUARAI_USER || '', - senha: process.env.TOPSAPP_GUARAI_PASS || '', - identificador: process.env.TOPSAPP_GUARAI_ID || 'BotTelegram', - idUsuario: process.env.TOPSAPP_GUARAI_USERID || '1' - }, - pedro_afonso: { - usuario: process.env.TOPSAPP_PEDRO_AFONSO_USER || '', - senha: process.env.TOPSAPP_PEDRO_AFONSO_PASS || '', - identificador: process.env.TOPSAPP_PEDRO_AFONSO_ID || 'BotTelegram', - idUsuario: process.env.TOPSAPP_PEDRO_AFONSO_USERID || '1' - } - }; - - this.isConfigured = this.validateConfiguration(); - } - - /** - * Validar se as configurações estão completas - */ - validateConfiguration() { - const required = ['usuario', 'senha']; - - for (const instance in this.credentials) { - const creds = this.credentials[instance]; - for (const field of required) { - if (!creds[field]) { - console.warn(`⚠️ TopSapp: Campo ${field} não configurado para ${instance}`); - return false; - } - } - } - - console.log('✅ TopSapp: Configurações validadas'); - return true; - } - - /** - * Obter credenciais para uma instância - */ - getCredentials(instanceKey) { - return this.credentials[instanceKey] || null; - } - - /** - * Obter credenciais a partir do site ativo (cidade/provedor) - */ - getCredentialsBySite(siteId) { - const sites = require('../config/sites'); - const site = sites.getSite(siteId); - if (!site) return this.credentials.guarai_colmeia; - return this.credentials[site.topsappKey] || null; - } - - /** - * Obter credenciais por localização - */ - getCredentialsByLocation(location = '') { - const loc = location.toLowerCase(); - - if (loc.includes('pedro') || loc.includes('afonso')) { - return this.credentials.pedro_afonso; - } - - return this.credentials.guarai_colmeia; - } - - /** - * Verificar se o TopSapp está configurado - */ - isReady() { - return this.isConfigured; - } - - /** - * Obter status da configuração - */ - getStatus() { - const status = {}; - - for (const instance in this.credentials) { - const creds = this.credentials[instance]; - status[instance] = { - configured: !!(creds.usuario && creds.senha), - usuario: creds.usuario ? '✅' : '❌', - senha: creds.senha ? '✅' : '❌' - }; - } - - return status; - } -} - -module.exports = TopSappConfig; +/** + * Configurações do TopSapp + */ +class TopSappConfig { + constructor() { + this.credentials = { + guarai_colmeia: { + usuario: process.env.TOPSAPP_GUARAI_USER || '', + senha: process.env.TOPSAPP_GUARAI_PASS || '', + identificador: process.env.TOPSAPP_GUARAI_ID || 'BotTelegram', + idUsuario: process.env.TOPSAPP_GUARAI_USERID || '1' + }, + }; + + // Pedro Afonso migrou do TopSapp para o SGP + this.sgpCredentials = { + pedro_afonso: { + baseURL: process.env.SGP_PEDRO_AFONSO_URL || '', + app: process.env.SGP_PEDRO_AFONSO_APP || '', + token: process.env.SGP_PEDRO_AFONSO_TOKEN || '', + usuario: process.env.SGP_PEDRO_AFONSO_USER || '', + senha: process.env.SGP_PEDRO_AFONSO_PASS || '' + } + }; + + this.isConfigured = this.validateConfiguration(); + } + + /** + * Validar se as configurações estão completas + */ + validateConfiguration() { + const required = ['usuario', 'senha']; + + for (const instance in this.credentials) { + const creds = this.credentials[instance]; + for (const field of required) { + if (!creds[field]) { + console.warn(`⚠️ TopSapp: Campo ${field} não configurado para ${instance}`); + return false; + } + } + } + + console.log('✅ TopSapp: Configurações validadas'); + return true; + } + + /** + * SGP configurado quando há URL e (app+token ou usuário+senha) + */ + isSgpConfigured(key = 'pedro_afonso') { + const c = this.sgpCredentials[key]; + return !!(c && c.baseURL && ((c.app && c.token) || (c.usuario && c.senha))); + } + + /** + * Obter credenciais para uma instância + */ + getCredentials(instanceKey) { + return this.credentials[instanceKey] || null; + } + + /** + * Obter credenciais a partir do site ativo (cidade/provedor) + */ + getCredentialsBySite(siteId) { + const sites = require('../config/sites'); + const site = sites.getSite(siteId); + if (!site) return this.credentials.guarai_colmeia; + return this.credentials[site.topsappKey] || null; + } + + /** + * Obter credenciais por localização + */ + getCredentialsByLocation(location = '') { + if (this.usesSgp(location)) { + return this.sgpCredentials.pedro_afonso; + } + return this.credentials.guarai_colmeia; + } + + /** + * Pedro Afonso é atendido pelo SGP; as demais localidades pelo TopSapp + */ + usesSgp(location = '') { + const loc = String(location || '').toLowerCase(); + return loc.includes('pedro') || loc.includes('afonso'); + } + + /** + * Verificar se o TopSapp está configurado + */ + isReady(location) { + if (location === undefined) { + return this.isConfigured || this.isSgpConfigured(); + } + return this.usesSgp(location) ? this.isSgpConfigured() : this.isConfigured; + } + + /** + * Obter status da configuração + */ + getStatus() { + const status = {}; + + for (const instance in this.credentials) { + const creds = this.credentials[instance]; + status[instance] = { + configured: !!(creds.usuario && creds.senha), + usuario: creds.usuario ? '✅' : '❌', + senha: creds.senha ? '✅' : '❌' + }; + } + + status.pedro_afonso_sgp = { + configured: this.isSgpConfigured(), + usuario: this.sgpCredentials.pedro_afonso.baseURL ? '✅' : '❌', + senha: (this.sgpCredentials.pedro_afonso.token || this.sgpCredentials.pedro_afonso.senha) ? '✅' : '❌' + }; + + return status; + } +} + +module.exports = TopSappConfig; diff --git a/src/topsapp/manager.js b/src/topsapp/manager.js index b9e9f15..b42e618 100644 --- a/src/topsapp/manager.js +++ b/src/topsapp/manager.js @@ -1,1432 +1,1448 @@ -const TopSappAPI = require('./api'); -const TopSappConfig = require('./config'); -const logger = require('../utils/logger'); -const fs = require('fs').promises; -const path = require('path'); - -class TopSappManager { - constructor() { - this.api = new TopSappAPI(); - this.config = new TopSappConfig(); - - logger.info('TopSappManager inicializado'); - - // Limpar sessões expiradas a cada 10 minutos - setInterval(() => { - this.api.clearExpiredSessions(); - }, 10 * 60 * 1000); - - console.log('🎯 TopSappManager inicializado'); - } - - /** - * Verificar se o TopSapp está configurado e pronto - */ - isReady() { - return this.config.isReady(); - } - - /** - * Obter status de configuração - */ - getConfigStatus() { - return this.config.getStatus(); - } - - /** - * Buscar cliente por nome, CPF ou ID - */ - async buscarCliente(query, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado. Verifique as credenciais no arquivo .env' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - const result = await this.api.buscarCliente(query, location, credentials); - - if (result.success && result.clientes) { - return { - ...result, - total: result.clientes.length, - instancia: result.instance || location - }; - } - - return result; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Obter dados cadastrais completos do cliente - */ - async obterDadosCadastrais(idCliente, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - - console.log(`🔧 Obtendo dados cadastrais do cliente ${idCliente} (usando apenas serviços para evitar erro 400)`); - - // Buscar dados completos do cliente e serviços em paralelo - const [dadosCliente, servicos] = await Promise.all([ - this.api.obterDadosCompletosCliente(idCliente, location, credentials), - this.api.obterServicosCliente(idCliente, location, credentials) - ]); - - if (!dadosCliente.success) { - console.log(`❌ Falha ao obter dados do cliente: ${dadosCliente.error}`); - return dadosCliente; - } - - console.log(`✅ Dados do cliente obtidos com sucesso: ${dadosCliente.cliente.nome}`); - - const resultado = { - success: true, - cliente: dadosCliente.cliente, - servicos: [], - instancia: dadosCliente.instance - }; - - if (servicos.success && servicos.data) { - // Normalizar dados dos serviços - const servicosData = servicos.data.dados || servicos.data.servicos || []; - resultado.servicos = servicosData.map(servico => ({ - // Dados básicos do serviço - id_servico: servico.id_servico, - id_cliente: servico.id_cliente, - statusFormatado: this.getStatusServicoDetalhado(servico.id_status), - planoNome: servico.descricao || servico.servico_nome || servico.nome || 'Não informado', - - // Dados de acesso - login: servico.usuario || servico.login || servico.pppoe_login || '', - senha: servico.senha || servico.pppoe_senha || '', - ip: servico.ip || servico.ip_fixo || '', - - // Dados financeiros - vencimento: servico.vencimento || servico.dia_vencimento || '', - valor: servico.servico_valor || servico.valor || servico.preco || 0, - - // Dados técnicos - tecnologia: servico.tecnologia || 'FTTH', - velocidade_down: servico.servico_download || servico.velocidade_down || servico.down || '', - velocidade_up: servico.servico_upload || servico.velocidade_up || servico.up || '', - - // Dados FTTH específicos - base: servico.base || '', - id_ap: servico.id_ap || '', - pon: servico.pon || servico.porta_pon || '', - perfil: servico.perfil || '', - serial: servico.serial || servico.serial_onu || servico.mac || '', - serviceport: servico.serviceport || '', - - // Sinais ópticos - ultimo_rx_power: servico.ultimo_rx_power || servico.sinal_rx || servico.rx || '', - ultimo_tx_power: servico.ultimo_tx_power || servico.sinal_tx || servico.tx || '', - data_ultimo_sinal: servico.data_ultimo_sinal || '', - hora_ultimo_sinal: servico.hora_ultimo_sinal || '', - - // Status de conexão - conexao: servico.conexao || '', - ultimo_acesso: servico.ultimo_acesso || '', - ultimo_ip: servico.ultimo_ip || '', - - // Dados de instalação - instalacao_endereco: servico.instalacao_endereco || '', - instalacao_numero: servico.instalacao_numero || '', - instalacao_bairro: servico.instalacao_bairro || '', - instalacao_cidade: servico.instalacao_cidade || '', - instalacao_uf: servico.instalacao_uf || '', - instalacao_cep: servico.instalacao_cep || '', - instalacao_complemento: servico.instalacao_complemento || '', - - // Coordenadas - latitude: servico.latitude || '', - longitude: servico.longitude || '', - - // Dados DHCP / IPoE - dhcp_username: servico.dhcp_username || '', - dhcp_agent_circuit_id: servico.dhcp_agent_circuit_id || '', - dhcp_client_id: servico.dhcp_client_id || '', - dhcp_liberar: servico.dhcp_liberar, - - // Datas importantes - data_cadastro: servico.data_cadastro || '', - data_ativacao: servico.data_ativacao || '', - data_bloqueio: servico.data_bloqueio || '', - data_desativacao: servico.data_desativacao || '', - - // Status original - id_status: servico.id_status, - status: servico.status - })); - } - - return resultado; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Obter status detalhado do serviço - */ - getStatusServicoDetalhado(status) { - switch (parseInt(status)) { - case 1: return '🟢 Ativo'; - case 2: return '🔴 Bloqueado'; - case 3: return '⚫ Desativado'; - case 4: return '🟡 Suspenso'; - case 5: return '🔵 Aguardando Ativação'; - default: return `❓ Status ${status}`; - } - } - - /** - * Obter dados completos do cliente - */ - async obterDadosCompletos(idCliente, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - - // Buscar dados do cliente, serviços e faturas em paralelo - const [dadosCliente, servicos, faturas] = await Promise.all([ - this.api.obterDadosCliente(idCliente, location, credentials), - this.api.obterServicosCliente(idCliente, location, credentials), - this.api.obterFaturas(idCliente, location, credentials) - ]); - - const resultado = { - success: true, - cliente: null, - servicos: [], - faturas: [], - instancia: location - }; - - if (dadosCliente.success && dadosCliente.data.clientes) { - resultado.cliente = dadosCliente.data.clientes[0]; - } - - if (servicos.success && servicos.data.servicos) { - resultado.servicos = servicos.data.servicos; - } - - if (faturas.success && faturas.data.faturas) { - resultado.faturas = faturas.data.faturas; - } - - return resultado; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Obter faturas organizadas por serviço - */ - async obterFaturasDetalhadas(idCliente, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - - // Buscar serviços do cliente primeiro - const servicos = await this.api.obterServicosCliente(idCliente, location, credentials); - - if (!servicos.success) { - return servicos; - } - - const servicosData = servicos.data.dados || servicos.data.servicos || []; - - // Organizar faturas por serviço buscando cada serviço individualmente - const faturasPorServico = []; - - for (const servico of servicosData) { - const faturasDoServico = []; - - // Buscar faturas filtradas pelo id_servico diretamente na API - const faturasServico = await this.api.obterFaturas( - idCliente, location, credentials, servico.id_servico - ); - - const faturasData = (faturasServico.success && faturasServico.data) - ? (faturasServico.data.dados || []) - : []; - - for (const fatura of faturasData) { - // Obter dados detalhados da fatura (linha digitável e PIX) - const dadosFatura = await this.api.obterDadosFatura( - fatura.id, - fatura.codigo_cliente || idCliente, - location, - credentials - ); - - const faturaDetalhada = { - id: fatura.id, - valor: fatura.valor, - vencimento: fatura.vencimento, - status: fatura.pago === 'SIM' ? 'Pago' : 'Pendente', - descricao: fatura.historico || fatura.descricao || 'Mensalidade', - periodo: this.formatarPeriodo(fatura.periodo_inicio, fatura.periodo_fim), - id_servico: fatura.id_servico || servico.id_servico, - linhaDigitavel: null, - pixCopiaCola: null - }; - - // Adicionar dados de pagamento se disponíveis - if (dadosFatura.success && dadosFatura.data) { - const dadosReturn = dadosFatura.data.fatura || dadosFatura.data.dados || dadosFatura.data; - - if (dadosReturn.linha_digitavel) { - faturaDetalhada.linhaDigitavel = dadosReturn.linha_digitavel; - } - - faturaDetalhada.pixCopiaCola = dadosReturn.qrcode - || dadosReturn.pix_copia_cola - || dadosReturn.codigo_pix - || null; - } - - faturasDoServico.push(faturaDetalhada); - } - - // Se o serviço tem faturas, adicionar à lista - if (faturasDoServico.length > 0) { - faturasPorServico.push({ - servico: { - id: servico.id_servico, - nome: servico.descricao || servico.plano_nome || 'Serviço', - base: servico.base || 'Base', - tecnologia: servico.tecnologia || 'FTTH', - valor_mensal: servico.servico_valor || servico.valor || 0, - vencimento_dia: servico.vencimento || 'Não definido', - status: servico.status || 'Não definido', - login: servico.usuario || servico.login || servico.pppoe_login || '', - endereco: servico.instalacao_endereco || servico.end_rua || '' - }, - faturas: faturasDoServico - }); - } - } - - return { - success: true, - faturasPorServico, - totalServicos: faturasPorServico.length, - totalFaturas: faturasPorServico.reduce((total, item) => total + item.faturas.length, 0), - instancia: location - }; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Obter boletos do cliente - */ - async obterBoletos(idCliente, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - const faturas = await this.api.obterFaturas(idCliente, location, credentials); - - if (!faturas.success) { - return faturas; - } - - const boletos = []; - - for (const fatura of faturas.data.dados || []) { - // Obter dados da fatura (linha digitável) - const dadosFatura = await this.api.obterDadosFatura(fatura.id, fatura.codigo_cliente, location, credentials); - - const boleto = { - id: fatura.id, - valor: fatura.valor, - vencimento: fatura.vencimento, - status: fatura.pago === 'SIM' ? 'Pago' : 'Pendente', - descricao: fatura.historico, - periodo: this.formatarPeriodo(fatura.periodo_inicio, fatura.periodo_fim), - linhaDigitavel: null, - pixCopiaCola: null - }; - - // Adicionar linha digitável e PIX copia e cola se obtida com sucesso - if (dadosFatura.success && dadosFatura.data.fatura) { - if (dadosFatura.data.fatura.linha_digitavel) { - boleto.linhaDigitavel = dadosFatura.data.fatura.linha_digitavel; - } - if (dadosFatura.data.fatura.qrcode) { - boleto.pixCopiaCola = dadosFatura.data.fatura.qrcode; - } - } - - boletos.push(boleto); - } - - return { - success: true, - boletos, - total: boletos.length, - instancia: faturas.instance - }; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Gerar PDF do boleto - */ - async gerarBoletoPDF(idFatura, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - const boletoBase64 = await this.api.obterBoletoBase64(idFatura, location, credentials); - - if (!boletoBase64.success) { - return boletoBase64; - } - - // Converter Base64 para PDF - const pdfResult = this.api.convertBase64ToPDF( - boletoBase64.data.arquivo, - `boleto_${idFatura}.pdf` - ); - - if (pdfResult.success) { - return { - success: true, - filename: pdfResult.filename, - buffer: pdfResult.buffer, - mimeType: pdfResult.mimeType, - instancia: boletoBase64.instance - }; - } - - return pdfResult; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Salvar PDF do boleto em arquivo - */ - async salvarBoletoPDF(idFatura, location = '', outputDir = './boletos') { - try { - const resultado = await this.gerarBoletoPDF(idFatura, location); - - if (!resultado.success) { - return resultado; - } - - // Criar diretório se não existir - await fs.mkdir(outputDir, { recursive: true }); - - const filepath = path.join(outputDir, resultado.filename); - await fs.writeFile(filepath, resultado.buffer); - - return { - success: true, - filepath, - filename: resultado.filename, - instancia: resultado.instancia - }; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Bloquear cliente - */ - async bloquearCliente(idCliente, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - - // Obter serviços do cliente - const servicos = await this.api.obterServicosCliente(idCliente, location, credentials); - - if (!servicos.success) { - return servicos; - } - - const resultados = []; - - // Bloquear todos os serviços ativos - for (const servico of servicos.data.dados || []) { - if (servico.status === 1 || servico.estado === 'liberado') { // Apenas serviços ativos - const resultado = await this.api.bloquearServico(servico.id, idCliente, location, credentials); - resultados.push({ - servicoId: servico.id, - descricao: servico.descricao || servico.nome, - resultado - }); - } - } - - return { - success: true, - servicosBloqueados: resultados, - total: resultados.length, - instancia: servicos.instance - }; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Liberar cliente - */ - async liberarCliente(idCliente, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - - // Obter serviços do cliente - const servicos = await this.api.obterServicosCliente(idCliente, location, credentials); - - if (!servicos.success) { - return servicos; - } - - const resultados = []; - - // Liberar todos os serviços bloqueados - for (const servico of servicos.data.dados || []) { - if (servico.status === 2 || servico.estado === 'bloqueado') { // Apenas serviços bloqueados - const resultado = await this.api.liberarServico(servico.id, idCliente, location, credentials); - resultados.push({ - servicoId: servico.id, - descricao: servico.descricao || servico.nome, - resultado - }); - } - } - - return { - success: true, - servicosLiberados: resultados, - total: resultados.length, - instancia: servicos.instance - }; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Obter lista de serviços do cliente - */ - async obterServicosCliente(idCliente, location = '') { - logger.debug('obterServicosCliente chamado', { - idCliente, - location, - tipoIdCliente: typeof idCliente - }); - - if (!this.isReady()) { - logger.warn('TopSapp não está configurado'); - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - logger.debug('Credenciais obtidas', { location, hasCredentials: !!credentials }); - - const servicos = await this.api.obterServicosCliente(idCliente, location, credentials); - logger.debug('Resposta da API obterServicosCliente', { - success: servicos.success, - dataLength: servicos.data?.dados?.length, - error: servicos.error - }); - - if (!servicos.success) { - logger.warn('API retornou erro', servicos.error); - return servicos; - } - - // Converter idCliente para número para comparação - console.log(`🔍 Recebido idCliente:`, idCliente, `(tipo: ${typeof idCliente})`); - logger.debug('Processando idCliente', { - originalValue: idCliente, - originalType: typeof idCliente - }); - - const clienteId = parseInt(idCliente, 10); - logger.debug('idCliente convertido', { - clienteId, - isValid: !isNaN(clienteId) - }); - console.log(`🔍 Convertido para clienteId:`, clienteId, `(tipo: ${typeof clienteId})`); - - // Verificar se a conversão foi bem-sucedida - if (isNaN(clienteId)) { - console.log(`❌ ERRO: idCliente inválido. Valor original:`, idCliente); - return { - success: false, - error: `ID do cliente inválido: ${idCliente}` - }; - } - - console.log(`🔍 Manager - Filtrando serviços para cliente ID: ${clienteId}`); - - // Acessar os dados da resposta - let dadosServicos = servicos.data?.dados || servicos.data || []; - - // Se não é array, tentar converter - if (!Array.isArray(dadosServicos)) { - console.log(`⚠️ Dados retornados não são um array:`, typeof dadosServicos); - dadosServicos = []; - } - - console.log(`📊 Total de registros retornados pela API: ${dadosServicos.length}`); - - if (dadosServicos.length > 100) { - console.log(`⚠️ ALERTA: API retornou ${dadosServicos.length} registros - muito mais que o esperado para um cliente`); - - // Log dos primeiros registros para debug - const amostra = dadosServicos.slice(0, 5).map(item => ({ - id: item.id, - idCliente: item.idCliente || item.cliente_id || item.id_cliente, - nome: item.nome || item.descricao - })); - console.log(`🔍 Amostra dos primeiros registros:`, amostra); - } - - // Filtrar RIGOROSAMENTE apenas os serviços do cliente específico - const servicosDoCliente = dadosServicos.filter(servico => { - // Tentar diferentes campos para o ID do cliente - const possiveisIds = [ - servico.idCliente, - servico.cliente_id, - servico.id_cliente, - servico.clienteId, - servico.cod_cliente, - servico.codigo_cliente - ]; - - // Verificar se algum dos IDs corresponde ao cliente - for (const id of possiveisIds) { - if (id !== undefined && id !== null) { - const servicoClienteId = parseInt(id, 10); - if (servicoClienteId === clienteId) { - return true; - } - } - } - - return false; - }); - - console.log(`🎯 Serviços filtrados para o cliente ${clienteId}: ${servicosDoCliente.length}`); - - // Debug: mostrar um exemplo de serviço filtrado - if (servicosDoCliente.length > 0) { - console.log(`📋 Exemplo de serviço encontrado:`, { - id: servicosDoCliente[0].id || servicosDoCliente[0].id_servico, - descricao: servicosDoCliente[0].descricao, - id_cliente: servicosDoCliente[0].id_cliente, - status: servicosDoCliente[0].status - }); - } - - // Se ainda assim temos muitos resultados, algo está errado - if (servicosDoCliente.length > 50) { - console.log(`❌ ERRO: Ainda temos ${servicosDoCliente.length} serviços após filtro - possível problema na API`); - - // Log detalhado para investigação - const idsEncontrados = servicosDoCliente.map(s => ({ - id: s.id, - idCliente: s.idCliente || s.cliente_id || s.id_cliente, - nome: s.nome || s.descricao - })).slice(0, 10); - - console.log(`🔍 IDs encontrados (primeiros 10):`, idsEncontrados); - } - - const servicosFormatados = servicosDoCliente.map(servico => ({ - id: servico.id || servico.id_servico, - id_servico: servico.id_servico || servico.id, - nome: servico.nome || servico.descricao || 'Serviço sem nome', - descricao: servico.descricao || servico.nome || 'Serviço sem descrição', - status: servico.status || servico.estado || (servico.id_status === 1 ? 'liberado' : servico.id_status === 2 ? 'bloqueado' : 'desativado'), - id_status: servico.id_status, - statusNumerico: servico.id_status || servico.status, - valorMensal: servico.valor_mensal || servico.servico_valor || '0.00', - tipoServico: servico.tipo_servico || 'Não informado', - dataAtivacao: servico.data_ativacao, - observacoes: servico.observacoes, - clienteId: servico.idCliente || servico.cliente_id || servico.id_cliente, - id_cliente: servico.id_cliente || servico.idCliente || servico.cliente_id, - // Manter todos os campos originais também - ...servico - })); - - return { - success: true, - servicos: servicosFormatados, - total: servicosFormatados.length, - instancia: servicos.instance, - debug: { - totalRetornadoAPI: dadosServicos.length, - totalFiltrado: servicosFormatados.length, - clienteId: clienteId - } - }; - - } catch (error) { - console.error(`❌ Erro ao obter serviços do cliente ${idCliente}:`, error); - return { - success: false, - error: error.message - }; - } - } - - /** - * Bloquear serviço específico - */ - async bloquearServicoEspecifico(idCliente, idServico, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - const resultado = await this.api.bloquearServico(idServico, idCliente, location, credentials); - - return { - ...resultado, - instancia: location - }; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Liberar serviço específico - */ - async liberarServicoEspecifico(idCliente, idServico, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - const resultado = await this.api.liberarServico(idServico, idCliente, location, credentials); - - return { - ...resultado, - instancia: location - }; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Alterar status de um serviço específico - */ - async alterarStatusServico(idServico, acao, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - - // Determinar o status numérico baseado na ação - let status; - if (acao === 'bloquear') { - status = 2; // Bloqueado - } else if (acao === 'liberar') { - status = 1; // Ativo - } else { - return { - success: false, - error: 'Ação inválida. Use "bloquear" ou "liberar"' - }; - } - - // Primeiro, obter informações do serviço para saber o idCliente - const servicos = await this.api.obterServicosCliente(null, location, credentials); - - if (!servicos.success) { - return { - success: false, - error: 'Erro ao obter informações do serviço' - }; - } - - // Procurar o serviço específico - const servicoEncontrado = (servicos.data.dados || []).find(s => - s.id_servico == idServico || s.id == idServico - ); - - if (!servicoEncontrado) { - return { - success: false, - error: 'Serviço não encontrado' - }; - } - - const idCliente = servicoEncontrado.id_cliente; - - logger.debug('Alterando status do serviço', { - idServico, - idCliente, - status, - acao - }); - - const resultado = await this.api.alterarStatusServico(idServico, idCliente, status, location, credentials); - - return { - ...resultado, - instancia: location - }; - - } catch (error) { - logger.error('Erro ao alterar status do serviço', error); - return { - success: false, - error: error.message - }; - } - } - - /** - * Obter tipos de atendimento do suporte - */ - async obterSuporteTiposAtendimento(location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - const result = await this.api.obterSuporteTiposAtendimento(location, credentials); - - return result; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Obter técnicos disponíveis - */ - async obterTecnicos(location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - const result = await this.api.obterTecnicos(location, credentials); - - return result; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Obter operadores disponíveis - */ - async obterOperadores(location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - const result = await this.api.obterOperadores(location, credentials); - - return result; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Obter funcionários disponíveis - */ - async obterFuncionarios(location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - const result = await this.api.obterFuncionarios(location, credentials); - - return result; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Obter categorias de suporte - */ - async obterSuporteCategorias(location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - const result = await this.api.obterSuporteCategorias(location, credentials); - - return result; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Adicionar novo suporte - */ - async adicionarSuporte(dadosSuporte, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - const result = await this.api.adicionarSuporte(dadosSuporte, location, credentials); - - if (result.success) { - console.log(`✅ Suporte criado com sucesso. Protocolo: ${result.data.protocolo || 'N/A'}`); - } - - return result; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Encaminhar suporte para um técnico - */ - async encaminharSuporte(idSuporte, idTecnico, descricao, location = '') { - if (!this.isReady()) { - return { success: false, error: 'TopSapp não está configurado' }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - return await this.api.encaminharSuporte(idSuporte, idTecnico, descricao, location, credentials); - } catch (error) { - return { success: false, error: error.message }; - } - } - - /** - * Obter instâncias disponíveis - */ - getInstances() { - return this.api.getInstances(); - } - - /** - * Formatar período de datas para DD/MM/YYYY - */ - formatarPeriodo(dataInicio, dataFim) { - const formatarData = (data) => { - if (!data) return ''; - - // Se já está no formato DD/MM/YYYY, retornar como está - if (data.match(/^\d{2}\/\d{2}\/\d{4}$/)) { - return data; - } - - // Se está no formato YYYY-MM-DD, converter - if (data.match(/^\d{4}-\d{2}-\d{2}$/)) { - const [ano, mes, dia] = data.split('-'); - return `${dia}/${mes}/${ano}`; - } - - // Tentar converter outros formatos - try { - const dataObj = new Date(data); - if (!isNaN(dataObj.getTime())) { - const dia = String(dataObj.getDate()).padStart(2, '0'); - const mes = String(dataObj.getMonth() + 1).padStart(2, '0'); - const ano = dataObj.getFullYear(); - return `${dia}/${mes}/${ano}`; - } - } catch (error) { - console.warn('Erro ao formatar data:', data, error); - } - - return data; - }; - - const inicio = formatarData(dataInicio); - const fim = formatarData(dataFim); - - if (inicio && fim) { - return `${inicio} a ${fim}`; - } else if (inicio) { - return inicio; - } else if (fim) { - return fim; - } else { - return 'Período não informado'; - } - } - - /** - * Alias para obterServicosCliente - */ - async obterServicos(idCliente, location = '') { - return await this.obterServicosCliente(idCliente, location); - } - - /** - * Alias para bloquearServicoEspecifico - */ - async bloquearServico(idServico, idCliente, location = '') { - return await this.bloquearServicoEspecifico(idCliente, idServico, location); - } - - /** - * Alias para liberarServicoEspecifico - */ - async liberarServico(idServico, idCliente, location = '') { - return await this.liberarServicoEspecifico(idCliente, idServico, location); - } - - /** - * Obter faturas do cliente - */ - async obterFaturas(idCliente, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - return await this.api.obterFaturas(idCliente, location, credentials); - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Obter dados completos de uma fatura (linha digitável, PIX, etc) - */ - async obterDadosFatura(idFatura, location = '') { - if (!this.isReady()) { - return { - success: false, - error: 'TopSapp não está configurado' - }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - - // Primeiro obter a lista de faturas para pegar o idCliente - const faturas = await this.api.obterFaturas(null, location, credentials); - - if (!faturas.success || !faturas.data || !faturas.data.dados) { - return { - success: false, - error: 'Erro ao buscar faturas' - }; - } - - // Encontrar a fatura específica - const fatura = faturas.data.dados.find(f => f.id == idFatura); - - if (!fatura) { - return { - success: false, - error: 'Fatura não encontrada' - }; - } - - const idCliente = fatura.codigo_cliente || fatura.id_cliente; - - // Obter dados completos da fatura - return await this.api.obterDadosFatura(idFatura, idCliente, location, credentials); - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - /** - * Obter suportes abertos de um cliente - */ - async obterSuportesAbertos(idCliente, location = '') { - if (!this.isReady()) return { success: false, error: 'TopSapp não configurado' }; - const credentials = this.config.getCredentialsByLocation(location); - return await this.api.obterSuportesAbertos(idCliente, location, credentials); - } - - /** - * Finalizar suporte - */ - async finalizarSuporte(idSuporte, descricao, location = '') { - if (!this.isReady()) return { success: false, error: 'TopSapp não configurado' }; - const credentials = this.config.getCredentialsByLocation(location); - return await this.api.finalizarSuporte(idSuporte, descricao, location, credentials); - } - - /** - * Obter lista de OLTs cadastradas no TopSapp - */ - async obterOlts(location = '') { - if (!this.isReady()) return { success: false, error: 'TopSapp não configurado' }; - const credentials = this.config.getCredentialsByLocation(location); - return await this.api.obterOlts(location, credentials); - } - - /** - * Atualizar dados FTTH de um serviço (vincular ONU a cliente/serviço) - */ - async atualizarFTTH(idCliente, idServico, ftthData, location = '') { - if (!this.isReady()) return { success: false, error: 'TopSapp não configurado' }; - const credentials = this.config.getCredentialsByLocation(location); - return await this.api.atualizarFTTH(idCliente, idServico, ftthData, location, credentials); - } - - /** - * Mapeamento de base/OLT para prefixo do Circuit-ID IPoE - */ - _resolverPrefixoIPoE(baseInfo) { - if (!baseInfo) return null; - const b = String(baseInfo).toUpperCase(); - if (/INTERNET\s+FIBRA\s+OPTICA/i.test(b)) return 'FH_GRI'; - if (/COLMEIA/i.test(b)) return 'CLM01'; - if (/DITO/i.test(b)) return 'DIT'; - if (/CANAA\s*2/i.test(b)) return 'CAN02'; - if (/CANAA/i.test(b)) return 'CAN01'; - if (/PEDRO\s*AFONSO\s*2|CANAVIEIRAS/i.test(b)) return 'PA02'; - if (/PEDRO\s*AFONSO/i.test(b)) return 'PA01'; - return null; - } - - /** - * Mudar serviço para IPoE (DHCP Option 82) - * Determina automaticamente o prefixo do Circuit-ID pela OLT/base - */ - async mudarParaIPoE(idCliente, idServico, location = '') { - if (!this.isReady()) { - return { success: false, error: 'TopSapp não está configurado' }; - } - - try { - const credentials = this.config.getCredentialsByLocation(location); - - // Obter serviços do cliente para descobrir serial e OLT/base - const servicosResult = await this.api.obterServicosCliente(idCliente, location, credentials); - if (!servicosResult.success) { - return { success: false, error: 'Não foi possível obter serviços do cliente' }; - } - - const dados = servicosResult.data?.dados || servicosResult.data || []; - const idServicoNum = parseInt(idServico, 10); - const servico = dados.find(s => parseInt(s.id_servico, 10) === idServicoNum); - - if (!servico) { - return { success: false, error: `Serviço #${idServico} não encontrado` }; - } - - const serial = servico.serial || servico.mac || ''; - if (!serial) { - return { success: false, error: 'Serviço não possui serial de ONU cadastrado' }; - } - - const baseInfo = servico.base || ''; - const prefixo = this._resolverPrefixoIPoE(baseInfo); - if (!prefixo) { - return { success: false, error: `Não foi possível identificar a OLT/base deste serviço (base: "${baseInfo}"). Configure o mapeamento.` }; - } - - const circuitId = `${prefixo}:${serial}`; - const login = servico.usuario || servico.pppoe_login || servico.login || ''; - - logger.info(`[IPoE] Dados gerados para serviço ${idServico} do cliente ${idCliente}: circuit_id=${circuitId}`); - - // A API AlterarServico não suporta campos DHCP. - // Retornamos os dados calculados para configuração manual no painel TopSapp. - return { - success: true, - circuitId, - prefixo, - serial, - baseInfo, - login, - servico: servico.descricao || servico.nome || 'N/A' - }; - } catch (error) { - return { success: false, error: error.message }; - } - } - - /** - * Busca sessão ativa com determinado IP privado CGNAT. - * Usa status:'on' — somente sessões ativas têm o campo `ip` preenchido pelo RADIUS. - */ - async buscarConexaoAtivaPorIp(privateIP, location = '') { - if (!this.isReady()) { - return { success: false, error: 'TopSapp não está configurado' }; - } - try { - const credentials = this.config.getCredentialsByLocation(location); - const instance = this.api.getInstanceByLocation(location); - const result = await this.api.obterAcessos({ status: 'on' }, instance, credentials); - - if (!result.success) return result; - - const todos = result.data?.dados || []; - console.log(`🔍 CGNAT ativas: ${todos.length} sessões ativas retornadas`); - - const encontrados = todos.filter(s => s.ip === privateIP); - return { - success: true, - dados: encontrados, - total: encontrados.length, - totalGeral: todos.length, - instance: result.instance - }; - } catch (error) { - return { success: false, error: error.message }; - } - } - - /** - * Busca qual sessão estava ativa num determinado horário (histórico). - * Como sessões encerradas têm ip:'', filtramos por janela de tempo: - * sessões que iniciaram antes do horário e encerraram depois (ou ainda ativas). - * @param {string} privateIP - * @param {string} dataInicial DD/MM/YYYY - * @param {string} dataFinal DD/MM/YYYY - * @param {string} horaRef HH:MM (opcional) — filtra sessões ativas naquele momento - * @param {string} location - */ - async buscarSessaoPorHorario(privateIP, dataInicial, dataFinal, horaRef = null, location = '') { - if (!this.isReady()) { - return { success: false, error: 'TopSapp não está configurado' }; - } - try { - const credentials = this.config.getCredentialsByLocation(location); - const instance = this.api.getInstanceByLocation(location); - const params = {}; - if (dataInicial) params.dataInicial = dataInicial; - if (dataFinal) params.dataFinal = dataFinal; - const result = await this.api.obterAcessos(params, instance, credentials); - - if (!result.success) return result; - - const todos = result.data?.dados || []; - console.log(`🔍 CGNAT histórico: ${todos.length} registros retornados para o período`); - - // Sessões com IP preenchido (ativas durante o período) - const comIp = todos.filter(a => a.ip === privateIP); - - // Se não achamos pelo IP (sessões encerradas têm ip:''), tentar por janela de tempo - // Sessões ativas no horário: inicio <= horaRef <= termino (ou termino vazio) - let porHorario = []; - if (horaRef && comIp.length === 0) { - const [hh, mm] = horaRef.split(':').map(Number); - const refTotalMin = hh * 60 + mm; - - porHorario = todos.filter(a => { - if (!a.inicio) return false; - const inicioMatch = a.inicio.match(/(\d{2}):(\d{2})/); - if (!inicioMatch) return false; - const inicioMin = parseInt(inicioMatch[1]) * 60 + parseInt(inicioMatch[2]); - if (inicioMin > refTotalMin) return false; // ainda não havia iniciado - - if (!a.termino) return true; // sessão ainda ativa - - const terminoMatch = a.termino.match(/(\d{2}):(\d{2})/); - if (!terminoMatch) return true; - const terminoMin = parseInt(terminoMatch[1]) * 60 + parseInt(terminoMatch[2]); - return terminoMin >= refTotalMin; - }); - } - - const encontrados = comIp.length > 0 ? comIp : porHorario; - return { - success: true, - dados: encontrados, - total: encontrados.length, - totalGeral: todos.length, - modoFallback: comIp.length === 0 && porHorario.length > 0, - instance: result.instance - }; - } catch (error) { - return { success: false, error: error.message }; - } - } - -} - -module.exports = TopSappManager; +const TopSappAPI = require('./api'); +const SgpAPI = require('../sgp/api'); +const TopSappConfig = require('./config'); +const logger = require('../utils/logger'); +const fs = require('fs').promises; +const path = require('path'); + +class TopSappManager { + constructor() { + this.topsappApi = new TopSappAPI(); + this.sgpApi = new SgpAPI(); + this.config = new TopSappConfig(); + + logger.info('TopSappManager inicializado'); + + // Limpar sessões expiradas a cada 10 minutos + setInterval(() => { + this.topsappApi.clearExpiredSessions(); + this.sgpApi.clearExpiredSessions(); + }, 10 * 60 * 1000); + + console.log('🎯 TopSappManager inicializado'); + } + + /** + * Verificar se o TopSapp está configurado e pronto + */ + isReady(location) { + return this.config.isReady(location); + } + + /** + * Backend (TopSapp ou SGP) responsável pela localidade. + * Pedro Afonso usa o SGP; as demais localidades usam o TopSapp. + */ + _api(location = '') { + return this.config.usesSgp(location) ? this.sgpApi : this.topsappApi; + } + + /** + * Obter status de configuração + */ + getConfigStatus() { + return this.config.getStatus(); + } + + /** + * Buscar cliente por nome, CPF ou ID + */ + async buscarCliente(query, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado. Verifique as credenciais no arquivo .env' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + const result = await this._api(location).buscarCliente(query, location, credentials); + + if (result.success && result.clientes) { + return { + ...result, + total: result.clientes.length, + instancia: result.instance || location + }; + } + + return result; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Obter dados cadastrais completos do cliente + */ + async obterDadosCadastrais(idCliente, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + + console.log(`🔧 Obtendo dados cadastrais do cliente ${idCliente} (usando apenas serviços para evitar erro 400)`); + + // Buscar dados completos do cliente e serviços em paralelo + const [dadosCliente, servicos] = await Promise.all([ + this._api(location).obterDadosCompletosCliente(idCliente, location, credentials), + this._api(location).obterServicosCliente(idCliente, location, credentials) + ]); + + if (!dadosCliente.success) { + console.log(`❌ Falha ao obter dados do cliente: ${dadosCliente.error}`); + return dadosCliente; + } + + console.log(`✅ Dados do cliente obtidos com sucesso: ${dadosCliente.cliente.nome}`); + + const resultado = { + success: true, + cliente: dadosCliente.cliente, + servicos: [], + instancia: dadosCliente.instance + }; + + if (servicos.success && servicos.data) { + // Normalizar dados dos serviços + const servicosData = servicos.data.dados || servicos.data.servicos || []; + resultado.servicos = servicosData.map(servico => ({ + // Dados básicos do serviço + id_servico: servico.id_servico, + id_cliente: servico.id_cliente, + statusFormatado: this.getStatusServicoDetalhado(servico.id_status), + planoNome: servico.descricao || servico.servico_nome || servico.nome || 'Não informado', + + // Dados de acesso + login: servico.usuario || servico.login || servico.pppoe_login || '', + senha: servico.senha || servico.pppoe_senha || '', + ip: servico.ip || servico.ip_fixo || '', + + // Dados financeiros + vencimento: servico.vencimento || servico.dia_vencimento || '', + valor: servico.servico_valor || servico.valor || servico.preco || 0, + + // Dados técnicos + tecnologia: servico.tecnologia || 'FTTH', + velocidade_down: servico.servico_download || servico.velocidade_down || servico.down || '', + velocidade_up: servico.servico_upload || servico.velocidade_up || servico.up || '', + + // Dados FTTH específicos + base: servico.base || '', + id_ap: servico.id_ap || '', + pon: servico.pon || servico.porta_pon || '', + perfil: servico.perfil || '', + serial: servico.serial || servico.serial_onu || servico.mac || '', + serviceport: servico.serviceport || '', + + // Sinais ópticos + ultimo_rx_power: servico.ultimo_rx_power || servico.sinal_rx || servico.rx || '', + ultimo_tx_power: servico.ultimo_tx_power || servico.sinal_tx || servico.tx || '', + data_ultimo_sinal: servico.data_ultimo_sinal || '', + hora_ultimo_sinal: servico.hora_ultimo_sinal || '', + + // Status de conexão + conexao: servico.conexao || '', + ultimo_acesso: servico.ultimo_acesso || '', + ultimo_ip: servico.ultimo_ip || '', + + // Dados de instalação + instalacao_endereco: servico.instalacao_endereco || '', + instalacao_numero: servico.instalacao_numero || '', + instalacao_bairro: servico.instalacao_bairro || '', + instalacao_cidade: servico.instalacao_cidade || '', + instalacao_uf: servico.instalacao_uf || '', + instalacao_cep: servico.instalacao_cep || '', + instalacao_complemento: servico.instalacao_complemento || '', + + // Coordenadas + latitude: servico.latitude || '', + longitude: servico.longitude || '', + + // Dados DHCP / IPoE + dhcp_username: servico.dhcp_username || '', + dhcp_agent_circuit_id: servico.dhcp_agent_circuit_id || '', + dhcp_client_id: servico.dhcp_client_id || '', + dhcp_liberar: servico.dhcp_liberar, + + // Datas importantes + data_cadastro: servico.data_cadastro || '', + data_ativacao: servico.data_ativacao || '', + data_bloqueio: servico.data_bloqueio || '', + data_desativacao: servico.data_desativacao || '', + + // Status original + id_status: servico.id_status, + status: servico.status + })); + } + + return resultado; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Obter status detalhado do serviço + */ + getStatusServicoDetalhado(status) { + switch (parseInt(status)) { + case 1: return '🟢 Ativo'; + case 2: return '🔴 Bloqueado'; + case 3: return '⚫ Desativado'; + case 4: return '🟡 Suspenso'; + case 5: return '🔵 Aguardando Ativação'; + default: return `❓ Status ${status}`; + } + } + + /** + * Obter dados completos do cliente + */ + async obterDadosCompletos(idCliente, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + + // Buscar dados do cliente, serviços e faturas em paralelo + const [dadosCliente, servicos, faturas] = await Promise.all([ + this._api(location).obterDadosCliente(idCliente, location, credentials), + this._api(location).obterServicosCliente(idCliente, location, credentials), + this._api(location).obterFaturas(idCliente, location, credentials) + ]); + + const resultado = { + success: true, + cliente: null, + servicos: [], + faturas: [], + instancia: location + }; + + if (dadosCliente.success && dadosCliente.data.clientes) { + resultado.cliente = dadosCliente.data.clientes[0]; + } + + if (servicos.success && servicos.data.servicos) { + resultado.servicos = servicos.data.servicos; + } + + if (faturas.success && faturas.data.faturas) { + resultado.faturas = faturas.data.faturas; + } + + return resultado; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Obter faturas organizadas por serviço + */ + async obterFaturasDetalhadas(idCliente, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + + // Buscar serviços do cliente primeiro + const servicos = await this._api(location).obterServicosCliente(idCliente, location, credentials); + + if (!servicos.success) { + return servicos; + } + + const servicosData = servicos.data.dados || servicos.data.servicos || []; + + // Organizar faturas por serviço buscando cada serviço individualmente + const faturasPorServico = []; + + for (const servico of servicosData) { + const faturasDoServico = []; + + // Buscar faturas filtradas pelo id_servico diretamente na API + const faturasServico = await this._api(location).obterFaturas( + idCliente, location, credentials, servico.id_servico + ); + + const faturasData = (faturasServico.success && faturasServico.data) + ? (faturasServico.data.dados || []) + : []; + + for (const fatura of faturasData) { + // Obter dados detalhados da fatura (linha digitável e PIX) + const dadosFatura = await this._api(location).obterDadosFatura( + fatura.id, + fatura.codigo_cliente || idCliente, + location, + credentials + ); + + const faturaDetalhada = { + id: fatura.id, + valor: fatura.valor, + vencimento: fatura.vencimento, + status: fatura.pago === 'SIM' ? 'Pago' : 'Pendente', + descricao: fatura.historico || fatura.descricao || 'Mensalidade', + periodo: this.formatarPeriodo(fatura.periodo_inicio, fatura.periodo_fim), + id_servico: fatura.id_servico || servico.id_servico, + linhaDigitavel: null, + pixCopiaCola: null + }; + + // Adicionar dados de pagamento se disponíveis + if (dadosFatura.success && dadosFatura.data) { + const dadosReturn = dadosFatura.data.fatura || dadosFatura.data.dados || dadosFatura.data; + + if (dadosReturn.linha_digitavel) { + faturaDetalhada.linhaDigitavel = dadosReturn.linha_digitavel; + } + + faturaDetalhada.pixCopiaCola = dadosReturn.qrcode + || dadosReturn.pix_copia_cola + || dadosReturn.codigo_pix + || null; + } + + faturasDoServico.push(faturaDetalhada); + } + + // Se o serviço tem faturas, adicionar à lista + if (faturasDoServico.length > 0) { + faturasPorServico.push({ + servico: { + id: servico.id_servico, + nome: servico.descricao || servico.plano_nome || 'Serviço', + base: servico.base || 'Base', + tecnologia: servico.tecnologia || 'FTTH', + valor_mensal: servico.servico_valor || servico.valor || 0, + vencimento_dia: servico.vencimento || 'Não definido', + status: servico.status || 'Não definido', + login: servico.usuario || servico.login || servico.pppoe_login || '', + endereco: servico.instalacao_endereco || servico.end_rua || '' + }, + faturas: faturasDoServico + }); + } + } + + return { + success: true, + faturasPorServico, + totalServicos: faturasPorServico.length, + totalFaturas: faturasPorServico.reduce((total, item) => total + item.faturas.length, 0), + instancia: location + }; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Obter boletos do cliente + */ + async obterBoletos(idCliente, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + const faturas = await this._api(location).obterFaturas(idCliente, location, credentials); + + if (!faturas.success) { + return faturas; + } + + const boletos = []; + + for (const fatura of faturas.data.dados || []) { + // Obter dados da fatura (linha digitável) + const dadosFatura = await this._api(location).obterDadosFatura(fatura.id, fatura.codigo_cliente, location, credentials); + + const boleto = { + id: fatura.id, + valor: fatura.valor, + vencimento: fatura.vencimento, + status: fatura.pago === 'SIM' ? 'Pago' : 'Pendente', + descricao: fatura.historico, + periodo: this.formatarPeriodo(fatura.periodo_inicio, fatura.periodo_fim), + linhaDigitavel: null, + pixCopiaCola: null + }; + + // Adicionar linha digitável e PIX copia e cola se obtida com sucesso + if (dadosFatura.success && dadosFatura.data.fatura) { + if (dadosFatura.data.fatura.linha_digitavel) { + boleto.linhaDigitavel = dadosFatura.data.fatura.linha_digitavel; + } + if (dadosFatura.data.fatura.qrcode) { + boleto.pixCopiaCola = dadosFatura.data.fatura.qrcode; + } + } + + boletos.push(boleto); + } + + return { + success: true, + boletos, + total: boletos.length, + instancia: faturas.instance + }; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Gerar PDF do boleto + */ + async gerarBoletoPDF(idFatura, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + const boletoBase64 = await this._api(location).obterBoletoBase64(idFatura, location, credentials); + + if (!boletoBase64.success) { + return boletoBase64; + } + + // Converter Base64 para PDF + const pdfResult = this._api(location).convertBase64ToPDF( + boletoBase64.data.arquivo, + `boleto_${idFatura}.pdf` + ); + + if (pdfResult.success) { + return { + success: true, + filename: pdfResult.filename, + buffer: pdfResult.buffer, + mimeType: pdfResult.mimeType, + instancia: boletoBase64.instance + }; + } + + return pdfResult; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Salvar PDF do boleto em arquivo + */ + async salvarBoletoPDF(idFatura, location = '', outputDir = './boletos') { + try { + const resultado = await this.gerarBoletoPDF(idFatura, location); + + if (!resultado.success) { + return resultado; + } + + // Criar diretório se não existir + await fs.mkdir(outputDir, { recursive: true }); + + const filepath = path.join(outputDir, resultado.filename); + await fs.writeFile(filepath, resultado.buffer); + + return { + success: true, + filepath, + filename: resultado.filename, + instancia: resultado.instancia + }; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Bloquear cliente + */ + async bloquearCliente(idCliente, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + + // Obter serviços do cliente + const servicos = await this._api(location).obterServicosCliente(idCliente, location, credentials); + + if (!servicos.success) { + return servicos; + } + + const resultados = []; + + // Bloquear todos os serviços ativos + for (const servico of servicos.data.dados || []) { + if (servico.status === 1 || servico.estado === 'liberado') { // Apenas serviços ativos + const resultado = await this._api(location).bloquearServico(servico.id, idCliente, location, credentials); + resultados.push({ + servicoId: servico.id, + descricao: servico.descricao || servico.nome, + resultado + }); + } + } + + return { + success: true, + servicosBloqueados: resultados, + total: resultados.length, + instancia: servicos.instance + }; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Liberar cliente + */ + async liberarCliente(idCliente, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + + // Obter serviços do cliente + const servicos = await this._api(location).obterServicosCliente(idCliente, location, credentials); + + if (!servicos.success) { + return servicos; + } + + const resultados = []; + + // Liberar todos os serviços bloqueados + for (const servico of servicos.data.dados || []) { + if (servico.status === 2 || servico.estado === 'bloqueado') { // Apenas serviços bloqueados + const resultado = await this._api(location).liberarServico(servico.id, idCliente, location, credentials); + resultados.push({ + servicoId: servico.id, + descricao: servico.descricao || servico.nome, + resultado + }); + } + } + + return { + success: true, + servicosLiberados: resultados, + total: resultados.length, + instancia: servicos.instance + }; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Obter lista de serviços do cliente + */ + async obterServicosCliente(idCliente, location = '') { + logger.debug('obterServicosCliente chamado', { + idCliente, + location, + tipoIdCliente: typeof idCliente + }); + + if (!this.isReady(location)) { + logger.warn('TopSapp não está configurado'); + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + logger.debug('Credenciais obtidas', { location, hasCredentials: !!credentials }); + + const servicos = await this._api(location).obterServicosCliente(idCliente, location, credentials); + logger.debug('Resposta da API obterServicosCliente', { + success: servicos.success, + dataLength: servicos.data?.dados?.length, + error: servicos.error + }); + + if (!servicos.success) { + logger.warn('API retornou erro', servicos.error); + return servicos; + } + + // Converter idCliente para número para comparação + console.log(`🔍 Recebido idCliente:`, idCliente, `(tipo: ${typeof idCliente})`); + logger.debug('Processando idCliente', { + originalValue: idCliente, + originalType: typeof idCliente + }); + + const clienteId = parseInt(idCliente, 10); + logger.debug('idCliente convertido', { + clienteId, + isValid: !isNaN(clienteId) + }); + console.log(`🔍 Convertido para clienteId:`, clienteId, `(tipo: ${typeof clienteId})`); + + // Verificar se a conversão foi bem-sucedida + if (isNaN(clienteId)) { + console.log(`❌ ERRO: idCliente inválido. Valor original:`, idCliente); + return { + success: false, + error: `ID do cliente inválido: ${idCliente}` + }; + } + + console.log(`🔍 Manager - Filtrando serviços para cliente ID: ${clienteId}`); + + // Acessar os dados da resposta + let dadosServicos = servicos.data?.dados || servicos.data || []; + + // Se não é array, tentar converter + if (!Array.isArray(dadosServicos)) { + console.log(`⚠️ Dados retornados não são um array:`, typeof dadosServicos); + dadosServicos = []; + } + + console.log(`📊 Total de registros retornados pela API: ${dadosServicos.length}`); + + if (dadosServicos.length > 100) { + console.log(`⚠️ ALERTA: API retornou ${dadosServicos.length} registros - muito mais que o esperado para um cliente`); + + // Log dos primeiros registros para debug + const amostra = dadosServicos.slice(0, 5).map(item => ({ + id: item.id, + idCliente: item.idCliente || item.cliente_id || item.id_cliente, + nome: item.nome || item.descricao + })); + console.log(`🔍 Amostra dos primeiros registros:`, amostra); + } + + // Filtrar RIGOROSAMENTE apenas os serviços do cliente específico + const servicosDoCliente = dadosServicos.filter(servico => { + // Tentar diferentes campos para o ID do cliente + const possiveisIds = [ + servico.idCliente, + servico.cliente_id, + servico.id_cliente, + servico.clienteId, + servico.cod_cliente, + servico.codigo_cliente + ]; + + // Verificar se algum dos IDs corresponde ao cliente + for (const id of possiveisIds) { + if (id !== undefined && id !== null) { + const servicoClienteId = parseInt(id, 10); + if (servicoClienteId === clienteId) { + return true; + } + } + } + + return false; + }); + + console.log(`🎯 Serviços filtrados para o cliente ${clienteId}: ${servicosDoCliente.length}`); + + // Debug: mostrar um exemplo de serviço filtrado + if (servicosDoCliente.length > 0) { + console.log(`📋 Exemplo de serviço encontrado:`, { + id: servicosDoCliente[0].id || servicosDoCliente[0].id_servico, + descricao: servicosDoCliente[0].descricao, + id_cliente: servicosDoCliente[0].id_cliente, + status: servicosDoCliente[0].status + }); + } + + // Se ainda assim temos muitos resultados, algo está errado + if (servicosDoCliente.length > 50) { + console.log(`❌ ERRO: Ainda temos ${servicosDoCliente.length} serviços após filtro - possível problema na API`); + + // Log detalhado para investigação + const idsEncontrados = servicosDoCliente.map(s => ({ + id: s.id, + idCliente: s.idCliente || s.cliente_id || s.id_cliente, + nome: s.nome || s.descricao + })).slice(0, 10); + + console.log(`🔍 IDs encontrados (primeiros 10):`, idsEncontrados); + } + + const servicosFormatados = servicosDoCliente.map(servico => ({ + id: servico.id || servico.id_servico, + id_servico: servico.id_servico || servico.id, + nome: servico.nome || servico.descricao || 'Serviço sem nome', + descricao: servico.descricao || servico.nome || 'Serviço sem descrição', + status: servico.status || servico.estado || (servico.id_status === 1 ? 'liberado' : servico.id_status === 2 ? 'bloqueado' : 'desativado'), + id_status: servico.id_status, + statusNumerico: servico.id_status || servico.status, + valorMensal: servico.valor_mensal || servico.servico_valor || '0.00', + tipoServico: servico.tipo_servico || 'Não informado', + dataAtivacao: servico.data_ativacao, + observacoes: servico.observacoes, + clienteId: servico.idCliente || servico.cliente_id || servico.id_cliente, + id_cliente: servico.id_cliente || servico.idCliente || servico.cliente_id, + // Manter todos os campos originais também + ...servico + })); + + return { + success: true, + servicos: servicosFormatados, + total: servicosFormatados.length, + instancia: servicos.instance, + debug: { + totalRetornadoAPI: dadosServicos.length, + totalFiltrado: servicosFormatados.length, + clienteId: clienteId + } + }; + + } catch (error) { + console.error(`❌ Erro ao obter serviços do cliente ${idCliente}:`, error); + return { + success: false, + error: error.message + }; + } + } + + /** + * Bloquear serviço específico + */ + async bloquearServicoEspecifico(idCliente, idServico, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + const resultado = await this._api(location).bloquearServico(idServico, idCliente, location, credentials); + + return { + ...resultado, + instancia: location + }; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Liberar serviço específico + */ + async liberarServicoEspecifico(idCliente, idServico, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + const resultado = await this._api(location).liberarServico(idServico, idCliente, location, credentials); + + return { + ...resultado, + instancia: location + }; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Alterar status de um serviço específico + */ + async alterarStatusServico(idServico, acao, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + + // Determinar o status numérico baseado na ação + let status; + if (acao === 'bloquear') { + status = 2; // Bloqueado + } else if (acao === 'liberar') { + status = 1; // Ativo + } else { + return { + success: false, + error: 'Ação inválida. Use "bloquear" ou "liberar"' + }; + } + + // Primeiro, obter informações do serviço para saber o idCliente + const servicos = await this._api(location).obterServicosCliente(null, location, credentials); + + if (!servicos.success) { + return { + success: false, + error: 'Erro ao obter informações do serviço' + }; + } + + // Procurar o serviço específico + const servicoEncontrado = (servicos.data.dados || []).find(s => + s.id_servico == idServico || s.id == idServico + ); + + if (!servicoEncontrado) { + return { + success: false, + error: 'Serviço não encontrado' + }; + } + + const idCliente = servicoEncontrado.id_cliente; + + logger.debug('Alterando status do serviço', { + idServico, + idCliente, + status, + acao + }); + + const resultado = await this._api(location).alterarStatusServico(idServico, idCliente, status, location, credentials); + + return { + ...resultado, + instancia: location + }; + + } catch (error) { + logger.error('Erro ao alterar status do serviço', error); + return { + success: false, + error: error.message + }; + } + } + + /** + * Obter tipos de atendimento do suporte + */ + async obterSuporteTiposAtendimento(location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + const result = await this._api(location).obterSuporteTiposAtendimento(location, credentials); + + return result; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Obter técnicos disponíveis + */ + async obterTecnicos(location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + const result = await this._api(location).obterTecnicos(location, credentials); + + return result; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Obter operadores disponíveis + */ + async obterOperadores(location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + const result = await this._api(location).obterOperadores(location, credentials); + + return result; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Obter funcionários disponíveis + */ + async obterFuncionarios(location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + const result = await this._api(location).obterFuncionarios(location, credentials); + + return result; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Obter categorias de suporte + */ + async obterSuporteCategorias(location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + const result = await this._api(location).obterSuporteCategorias(location, credentials); + + return result; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Adicionar novo suporte + */ + async adicionarSuporte(dadosSuporte, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + const result = await this._api(location).adicionarSuporte(dadosSuporte, location, credentials); + + if (result.success) { + console.log(`✅ Suporte criado com sucesso. Protocolo: ${result.data.protocolo || 'N/A'}`); + } + + return result; + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Encaminhar suporte para um técnico + */ + async encaminharSuporte(idSuporte, idTecnico, descricao, location = '') { + if (!this.isReady(location)) { + return { success: false, error: 'TopSapp não está configurado' }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + return await this._api(location).encaminharSuporte(idSuporte, idTecnico, descricao, location, credentials); + } catch (error) { + return { success: false, error: error.message }; + } + } + + /** + * Obter instâncias disponíveis + */ + getInstances() { + return [...this.topsappApi.getInstances(), ...this.sgpApi.getInstances()]; + } + + /** + * Formatar período de datas para DD/MM/YYYY + */ + formatarPeriodo(dataInicio, dataFim) { + const formatarData = (data) => { + if (!data) return ''; + + // Se já está no formato DD/MM/YYYY, retornar como está + if (data.match(/^\d{2}\/\d{2}\/\d{4}$/)) { + return data; + } + + // Se está no formato YYYY-MM-DD, converter + if (data.match(/^\d{4}-\d{2}-\d{2}$/)) { + const [ano, mes, dia] = data.split('-'); + return `${dia}/${mes}/${ano}`; + } + + // Tentar converter outros formatos + try { + const dataObj = new Date(data); + if (!isNaN(dataObj.getTime())) { + const dia = String(dataObj.getDate()).padStart(2, '0'); + const mes = String(dataObj.getMonth() + 1).padStart(2, '0'); + const ano = dataObj.getFullYear(); + return `${dia}/${mes}/${ano}`; + } + } catch (error) { + console.warn('Erro ao formatar data:', data, error); + } + + return data; + }; + + const inicio = formatarData(dataInicio); + const fim = formatarData(dataFim); + + if (inicio && fim) { + return `${inicio} a ${fim}`; + } else if (inicio) { + return inicio; + } else if (fim) { + return fim; + } else { + return 'Período não informado'; + } + } + + /** + * Alias para obterServicosCliente + */ + async obterServicos(idCliente, location = '') { + return await this.obterServicosCliente(idCliente, location); + } + + /** + * Alias para bloquearServicoEspecifico + */ + async bloquearServico(idServico, idCliente, location = '') { + return await this.bloquearServicoEspecifico(idCliente, idServico, location); + } + + /** + * Alias para liberarServicoEspecifico + */ + async liberarServico(idServico, idCliente, location = '') { + return await this.liberarServicoEspecifico(idCliente, idServico, location); + } + + /** + * Obter faturas do cliente + */ + async obterFaturas(idCliente, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + return await this._api(location).obterFaturas(idCliente, location, credentials); + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + + /** + * Obter dados completos de uma fatura (linha digitável, PIX, etc) + */ + async obterDadosFatura(idFatura, location = '') { + if (!this.isReady(location)) { + return { + success: false, + error: 'TopSapp não está configurado' + }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + + // No SGP o título é consultado diretamente pelo id + if (this.config.usesSgp(location)) { + return await this.sgpApi.obterDadosFatura(idFatura, null, location, credentials); + } + + // Primeiro obter a lista de faturas para pegar o idCliente + const faturas = await this._api(location).obterFaturas(null, location, credentials); + + if (!faturas.success || !faturas.data || !faturas.data.dados) { + return { + success: false, + error: 'Erro ao buscar faturas' + }; + } + + // Encontrar a fatura específica + const fatura = faturas.data.dados.find(f => f.id == idFatura); + + if (!fatura) { + return { + success: false, + error: 'Fatura não encontrada' + }; + } + + const idCliente = fatura.codigo_cliente || fatura.id_cliente; + + // Obter dados completos da fatura + return await this._api(location).obterDadosFatura(idFatura, idCliente, location, credentials); + + } catch (error) { + return { + success: false, + error: error.message + }; + } + } + /** + * Obter suportes abertos de um cliente + */ + async obterSuportesAbertos(idCliente, location = '') { + if (!this.isReady(location)) return { success: false, error: 'TopSapp não configurado' }; + const credentials = this.config.getCredentialsByLocation(location); + return await this._api(location).obterSuportesAbertos(idCliente, location, credentials); + } + + /** + * Finalizar suporte + */ + async finalizarSuporte(idSuporte, descricao, location = '') { + if (!this.isReady(location)) return { success: false, error: 'TopSapp não configurado' }; + const credentials = this.config.getCredentialsByLocation(location); + return await this._api(location).finalizarSuporte(idSuporte, descricao, location, credentials); + } + + /** + * Obter lista de OLTs cadastradas no TopSapp + */ + async obterOlts(location = '') { + if (!this.isReady(location)) return { success: false, error: 'TopSapp não configurado' }; + const credentials = this.config.getCredentialsByLocation(location); + return await this._api(location).obterOlts(location, credentials); + } + + /** + * Atualizar dados FTTH de um serviço (vincular ONU a cliente/serviço) + */ + async atualizarFTTH(idCliente, idServico, ftthData, location = '') { + if (!this.isReady(location)) return { success: false, error: 'TopSapp não configurado' }; + const credentials = this.config.getCredentialsByLocation(location); + return await this._api(location).atualizarFTTH(idCliente, idServico, ftthData, location, credentials); + } + + /** + * Mapeamento de base/OLT para prefixo do Circuit-ID IPoE + */ + _resolverPrefixoIPoE(baseInfo) { + if (!baseInfo) return null; + const b = String(baseInfo).toUpperCase(); + if (/INTERNET\s+FIBRA\s+OPTICA/i.test(b)) return 'FH_GRI'; + if (/COLMEIA/i.test(b)) return 'CLM01'; + if (/DITO/i.test(b)) return 'DIT'; + if (/CANAA\s*2/i.test(b)) return 'CAN02'; + if (/CANAA/i.test(b)) return 'CAN01'; + if (/PEDRO\s*AFONSO\s*2|CANAVIEIRAS/i.test(b)) return 'PA02'; + if (/PEDRO\s*AFONSO/i.test(b)) return 'PA01'; + return null; + } + + /** + * Mudar serviço para IPoE (DHCP Option 82) + * Determina automaticamente o prefixo do Circuit-ID pela OLT/base + */ + async mudarParaIPoE(idCliente, idServico, location = '') { + if (!this.isReady(location)) { + return { success: false, error: 'TopSapp não está configurado' }; + } + + try { + const credentials = this.config.getCredentialsByLocation(location); + + // Obter serviços do cliente para descobrir serial e OLT/base + const servicosResult = await this._api(location).obterServicosCliente(idCliente, location, credentials); + if (!servicosResult.success) { + return { success: false, error: 'Não foi possível obter serviços do cliente' }; + } + + const dados = servicosResult.data?.dados || servicosResult.data || []; + const idServicoNum = parseInt(idServico, 10); + const servico = dados.find(s => parseInt(s.id_servico, 10) === idServicoNum); + + if (!servico) { + return { success: false, error: `Serviço #${idServico} não encontrado` }; + } + + const serial = servico.serial || servico.mac || ''; + if (!serial) { + return { success: false, error: 'Serviço não possui serial de ONU cadastrado' }; + } + + const baseInfo = servico.base || ''; + const prefixo = this._resolverPrefixoIPoE(baseInfo); + if (!prefixo) { + return { success: false, error: `Não foi possível identificar a OLT/base deste serviço (base: "${baseInfo}"). Configure o mapeamento.` }; + } + + const circuitId = `${prefixo}:${serial}`; + const login = servico.usuario || servico.pppoe_login || servico.login || ''; + + logger.info(`[IPoE] Dados gerados para serviço ${idServico} do cliente ${idCliente}: circuit_id=${circuitId}`); + + // A API AlterarServico não suporta campos DHCP. + // Retornamos os dados calculados para configuração manual no painel TopSapp. + return { + success: true, + circuitId, + prefixo, + serial, + baseInfo, + login, + servico: servico.descricao || servico.nome || 'N/A' + }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + /** + * Busca sessão ativa com determinado IP privado CGNAT. + * Usa status:'on' — somente sessões ativas têm o campo `ip` preenchido pelo RADIUS. + */ + async buscarConexaoAtivaPorIp(privateIP, location = '') { + if (!this.isReady(location)) { + return { success: false, error: 'TopSapp não está configurado' }; + } + try { + const credentials = this.config.getCredentialsByLocation(location); + const instance = this._api(location).getInstanceByLocation(location); + const result = await this._api(location).obterAcessos({ status: 'on' }, instance, credentials); + + if (!result.success) return result; + + const todos = result.data?.dados || []; + console.log(`🔍 CGNAT ativas: ${todos.length} sessões ativas retornadas`); + + const encontrados = todos.filter(s => s.ip === privateIP); + return { + success: true, + dados: encontrados, + total: encontrados.length, + totalGeral: todos.length, + instance: result.instance + }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + /** + * Busca qual sessão estava ativa num determinado horário (histórico). + * Como sessões encerradas têm ip:'', filtramos por janela de tempo: + * sessões que iniciaram antes do horário e encerraram depois (ou ainda ativas). + * @param {string} privateIP + * @param {string} dataInicial DD/MM/YYYY + * @param {string} dataFinal DD/MM/YYYY + * @param {string} horaRef HH:MM (opcional) — filtra sessões ativas naquele momento + * @param {string} location + */ + async buscarSessaoPorHorario(privateIP, dataInicial, dataFinal, horaRef = null, location = '') { + if (!this.isReady(location)) { + return { success: false, error: 'TopSapp não está configurado' }; + } + try { + const credentials = this.config.getCredentialsByLocation(location); + const instance = this._api(location).getInstanceByLocation(location); + const params = {}; + if (dataInicial) params.dataInicial = dataInicial; + if (dataFinal) params.dataFinal = dataFinal; + const result = await this._api(location).obterAcessos(params, instance, credentials); + + if (!result.success) return result; + + const todos = result.data?.dados || []; + console.log(`🔍 CGNAT histórico: ${todos.length} registros retornados para o período`); + + // Sessões com IP preenchido (ativas durante o período) + const comIp = todos.filter(a => a.ip === privateIP); + + // Se não achamos pelo IP (sessões encerradas têm ip:''), tentar por janela de tempo + // Sessões ativas no horário: inicio <= horaRef <= termino (ou termino vazio) + let porHorario = []; + if (horaRef && comIp.length === 0) { + const [hh, mm] = horaRef.split(':').map(Number); + const refTotalMin = hh * 60 + mm; + + porHorario = todos.filter(a => { + if (!a.inicio) return false; + const inicioMatch = a.inicio.match(/(\d{2}):(\d{2})/); + if (!inicioMatch) return false; + const inicioMin = parseInt(inicioMatch[1]) * 60 + parseInt(inicioMatch[2]); + if (inicioMin > refTotalMin) return false; // ainda não havia iniciado + + if (!a.termino) return true; // sessão ainda ativa + + const terminoMatch = a.termino.match(/(\d{2}):(\d{2})/); + if (!terminoMatch) return true; + const terminoMin = parseInt(terminoMatch[1]) * 60 + parseInt(terminoMatch[2]); + return terminoMin >= refTotalMin; + }); + } + + const encontrados = comIp.length > 0 ? comIp : porHorario; + return { + success: true, + dados: encontrados, + total: encontrados.length, + totalGeral: todos.length, + modoFallback: comIp.length === 0 && porHorario.length > 0, + instance: result.instance + }; + } catch (error) { + return { success: false, error: error.message }; + } + } + +} + +module.exports = TopSappManager;