diff --git a/src/bot/handler.js b/src/bot/handler.js index dd8ab59..90f0d41 100644 --- a/src/bot/handler.js +++ b/src/bot/handler.js @@ -276,6 +276,11 @@ class BotHandler { await this.iniciarProcessoFecharSuporte(chatId, user.id, target); break; + case 'mudar_ipoe': + console.log(`🔗 Executando handleMudarIPoE com target: ${target}`); + await this.handleMudarIPoE(chatId, target); + break; + case 'conectar': await this.handleConnect(chatId, target); break; @@ -2013,6 +2018,15 @@ Digite /help para ver todos os comandos disponíveis! } else if (data.startsWith('confirm_unblock_')) { console.log(`🔍 Chamando handleConfirmUnblockCallback`); await this.handleConfirmUnblockCallback(callbackQuery, data); + } else if (data.startsWith('ipoe_cliente_')) { + console.log(`🔍 Condição ipoe_cliente_ atendida`); + await this.handleIPoEClienteCallback(callbackQuery, data); + } else if (data.startsWith('ipoe_servico_')) { + console.log(`🔍 Condição ipoe_servico_ atendida`); + await this.handleIPoEServicoCallback(callbackQuery, data); + } else if (data.startsWith('confirm_ipoe_')) { + console.log(`🔍 Condição confirm_ipoe_ atendida`); + await this.handleConfirmIPoECallback(callbackQuery, data); } else if (data.startsWith('block_service_')) { console.log(`🔍 Chamando handleBlockServiceSelection`); await this.handleBlockServiceSelection(callbackQuery, data); @@ -8124,6 +8138,263 @@ Digite /help para ver todos os comandos disponíveis! await this._enviarMenuConfigurarRoteador(chatId, session.device, session.instance); } + + // ========== IPoE ========== + + /** + * Handler para comando "mudar para ipoe" + * Busca o cliente e inicia fluxo de seleção de serviço + */ + async handleMudarIPoE(chatId, target) { + if (!target) { + await this.bot.sendMessage(chatId, + `🔗 *Mudar para IPoE*\n\n` + + `💡 *Como usar:*\n` + + `• "mudar para ipoe João da Silva"\n` + + `• "ipoe 123.456.789-10"\n` + + `• "mudar ipoe 12345" (ID do cliente)\n\n` + + `O comando configura DHCP Option 82 (Agent Circuit ID) e habilita o DHCP no serviço do cliente no TopSapp.`, + { parse_mode: 'Markdown' } + ); + return; + } + + try { + await this.bot.sendMessage(chatId, '🔍 Buscando cliente...'); + + let location = ''; + let query = target; + + if (/guarai|colmeia/i.test(target)) { + location = 'Guarai/Colmeia'; + query = target.replace(/(guarai|colmeia)/gi, '').trim(); + } else if (/pedro afonso/i.test(target)) { + location = 'Pedro Afonso'; + query = target.replace(/pedro afonso/gi, '').trim(); + } + + const resultado = await this.topsapp.buscarCliente(query, location); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, `❌ Erro na busca: ${resultado.error}`); + return; + } + + if (!resultado.clientes || resultado.clientes.length === 0) { + await this.bot.sendMessage(chatId, `🔍 Nenhum cliente encontrado para: "${query}"`); + return; + } + + const instancia = resultado.instancia || location; + + if (resultado.clientes.length === 1) { + await this._mostrarServicosParaIPoE(chatId, resultado.clientes[0].id, instancia); + return; + } + + // Múltiplos clientes → teclado de seleção + let message = `🔗 *Mudar para IPoE — Selecione o cliente:*\n\n`; + const keyboard = []; + + resultado.clientes.slice(0, 8).forEach((cliente, index) => { + message += `*${index + 1}.* ${cliente.nome}`; + if (cliente.cpf) message += ` — CPF: \`${cliente.cpf}\``; + message += `\n`; + keyboard.push([{ + text: `🔗 ${cliente.nome.substring(0, 30)}`, + callback_data: `ipoe_cliente_${cliente.id}_${instancia}` + }]); + }); + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + + } catch (error) { + console.error('❌ Erro em handleMudarIPoE:', error); + await this.bot.sendMessage(chatId, `❌ Erro: ${error.message}`); + } + } + + /** + * Callback quando o usuário seleciona um cliente para IPoE + */ + async handleIPoEClienteCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('ipoe_cliente_', '').split('_'); + const idCliente = parts[0]; + const location = parts.slice(1).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this._mostrarServicosParaIPoE(chatId, idCliente, location); + } + + /** + * Exibe serviços ativos do cliente para seleção (IPoE) + */ + async _mostrarServicosParaIPoE(chatId, idCliente, location) { + const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); + + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviços: ${servicosResult.error}`); + return; + } + + const servicos = (servicosResult.servicos || []).filter(s => { + const st = s.id_status || s.statusNumerico; + return st === 1 || st === '1' || String(s.status).toLowerCase() === 'liberado'; + }); + + if (servicos.length === 0) { + await this.bot.sendMessage(chatId, `⚠️ Cliente sem serviços ativos para configurar IPoE.`); + return; + } + + if (servicos.length === 1) { + await this._confirmarMudarIPoE(chatId, idCliente, servicos[0], location); + return; + } + + let message = `🔗 *Mudar para IPoE — Selecione o serviço:*\n\n`; + const keyboard = []; + + servicos.forEach((s, i) => { + const nome = s.descricao || s.nome || `Serviço ${i + 1}`; + const serial = s.serial || s.mac || 'sem serial'; + message += `*${i + 1}.* ${nome} — Serial: \`${serial}\`\n`; + keyboard.push([{ + text: `🔗 ${nome.substring(0, 35)}`, + callback_data: `ipoe_servico_${idCliente}_${s.id_servico || s.id}_${location}` + }]); + }); + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + } + + /** + * Callback quando o usuário seleciona um serviço para IPoE + */ + async handleIPoEServicoCallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('ipoe_servico_', '').split('_'); + const idCliente = parts[0]; + const idServico = parts[1]; + const location = parts.slice(2).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + + // Recarregar serviço para ter dados atualizados + const servicosResult = await this.topsapp.obterServicosCliente(idCliente, location); + if (!servicosResult.success) { + await this.bot.sendMessage(chatId, `❌ Erro ao buscar serviço: ${servicosResult.error}`); + return; + } + + const servico = (servicosResult.servicos || []).find(s => + String(s.id_servico || s.id) === String(idServico) + ); + + if (!servico) { + await this.bot.sendMessage(chatId, `❌ Serviço #${idServico} não encontrado.`); + return; + } + + await this._confirmarMudarIPoE(chatId, idCliente, servico, location); + } + + /** + * Mostra preview do que será alterado e pede confirmação + */ + async _confirmarMudarIPoE(chatId, idCliente, servico, location) { + const serial = servico.serial || servico.mac || ''; + const baseInfo = servico.base || ''; + const prefixo = this.topsapp._resolverPrefixoIPoE + ? this.topsapp._resolverPrefixoIPoE(baseInfo) + : null; + + if (!serial) { + await this.bot.sendMessage(chatId, + `❌ *Serviço sem serial de ONU cadastrado.*\n\n` + + `Não é possível gerar o Circuit-ID sem o serial.\n` + + `Verifique o cadastro do serviço no TopSapp.`, + { parse_mode: 'Markdown' } + ); + return; + } + + if (!prefixo) { + await this.bot.sendMessage(chatId, + `❌ *Não foi possível identificar a OLT/base deste serviço.*\n\n` + + `Base cadastrada: "${baseInfo || 'vazio'}"\n` + + `Verifique o cadastro do serviço no TopSapp.`, + { parse_mode: 'Markdown' } + ); + return; + } + + const circuitId = `${prefixo}:${serial}`; + const nome = servico.descricao || servico.nome || 'N/A'; + const idServico = servico.id_servico || servico.id; + + let message = `🔗 *Confirmar mudança para IPoE*\n\n`; + message += `📦 *Serviço:* ${nome}\n`; + message += `🆔 *ID Serviço:* ${idServico}\n`; + message += `📟 *Serial ONU:* \`${serial}\`\n`; + message += `🏢 *Base/OLT:* ${baseInfo}\n`; + message += `🏷️ *Prefixo:* ${prefixo}\n\n`; + message += `*Será configurado:*\n`; + message += `🔗 Agent Circuit ID: \`${circuitId}\`\n`; + message += `✅ DHCP Liberar: *Sim*\n\n`; + message += `Deseja confirmar?`; + + const keyboard = [ + [ + { text: '✅ Confirmar', callback_data: `confirm_ipoe_${idCliente}_${idServico}_${location}` }, + { text: '❌ Cancelar', callback_data: 'cancel_action' } + ] + ]; + + await this.bot.sendMessage(chatId, message, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + } + + /** + * Callback de confirmação: executa a mudança para IPoE + */ + async handleConfirmIPoECallback(callbackQuery, data) { + const chatId = callbackQuery.message.chat.id; + const parts = data.replace('confirm_ipoe_', '').split('_'); + const idCliente = parts[0]; + const idServico = parts[1]; + const location = parts.slice(2).join('_'); + + await this.bot.answerCallbackQuery(callbackQuery.id); + await this.bot.sendMessage(chatId, '⏳ Configurando IPoE no TopSapp...'); + + const resultado = await this.topsapp.mudarParaIPoE(idCliente, idServico, location); + + if (resultado.success) { + let msg = `✅ *IPoE configurado com sucesso!*\n\n`; + msg += `📦 Serviço: ${resultado.servico}\n`; + msg += `📟 Serial: \`${resultado.serial}\`\n`; + msg += `🏢 OLT: ${resultado.baseInfo} (${resultado.prefixo})\n`; + msg += `🔗 Agent Circuit ID: \`${resultado.circuitId}\`\n`; + msg += `✅ DHCP Liberar: *Ativado*`; + + await this.bot.sendMessage(chatId, msg, { parse_mode: 'Markdown' }); + } else { + await this.bot.sendMessage(chatId, + `❌ *Erro ao configurar IPoE*\n\n⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + } + } } module.exports = BotHandler; diff --git a/src/nlp/processor.js b/src/nlp/processor.js index 70a6495..39dca08 100644 --- a/src/nlp/processor.js +++ b/src/nlp/processor.js @@ -36,11 +36,15 @@ class NLPProcessor { 'consultar_usuario': ['consultar usuario', 'consultar user', 'ver usuario', 'status usuario', 'status user', 'info usuario', 'info user', 'checar usuario', 'verificar usuario', 'pesquisar usuario', 'localizar usuario', 'buscar usuario', 'usuario conectado', 'user conectado', 'usuario online', 'user online', 'usuario ativo', 'user ativo'], 'diagnosticar': ['diagnosticar', 'diagnostico', 'diagnose', 'diagnosticar cliente', 'diagnostico cliente', 'checar cliente completo', 'verificar cliente completo', 'relatorio cliente', 'relatorio completo', 'status completo cliente', 'analisar cliente', 'inspecionar cliente', 'situacao do cliente', 'situação do cliente'], 'ver_roteador': ['ver roteador', 'info roteador', 'status roteador', 'roteador', 'cpe', 'ver cpe', 'info cpe', 'dados roteador', 'informações roteador', 'informacoes roteador', 'genieacs', 'ver device', 'info device', 'dispositivo cpe'], + 'mudar_ipoe': ['mudar para ipoe', 'mudar ipoe', 'ipoe', 'converter ipoe', 'ativar ipoe', 'habilitar ipoe', 'migrar ipoe', 'migrar para ipoe', 'converter para ipoe', 'passar para ipoe', 'trocar para ipoe', 'mudar pra ipoe'], 'configurar_roteador': ['configurar roteador', 'mudar roteador', 'alterar roteador', 'editar roteador', 'config roteador', 'configurar wifi', 'mudar wifi', 'alterar wifi', 'mudar ssid', 'alterar ssid', 'configurar cpe', 'mudar cpe', 'alterar cpe', 'configurar dns', 'mudar dns', 'configurar dhcp', 'mudar dhcp', 'alterar dhcp', 'mudar configuracoes roteador', 'mudar configurações roteador'] }; // Padrões de comandos this.commandPatterns = [ + // Padrão específico para IPoE (deve vir ANTES de configurar roteador) + /^(mudar|converter|ativar|habilitar|migrar|passar|trocar)\s+(?:para\s+)?ipoe(?:\s+(.+))?$/i, + /^ipoe(?:\s+(.+))?$/i, // Padrões específicos para GenieACS – configurar roteador /^(configurar|mudar|alterar|editar|config)\s+(roteador|cpe|wifi|ssid|dns|dhcp)(?:\s+(.+))?$/i, // Padrões específicos para GenieACS – ver roteador @@ -171,6 +175,18 @@ class NLPProcessor { const fullMatch = match[0]; const action = match[1]; + // Detectar comando de mudar para IPoE (deve vir ANTES de configurar roteador) + if (/ipoe/i.test(fullMatch)) { + // "mudar para ipoe João" ou "ipoe João" + const query = match[match.length - 1] || null; + return { + action: 'mudar_ipoe', + target: query ? query.trim() : null, + confidence: 0.95, + type: 'mudar_ipoe_pattern' + }; + } + // Detectar comando de configurar roteador / CPE via GenieACS if (/configurar|mudar|alterar|editar|config/i.test(action) && /roteador|cpe|wifi|ssid|dns|dhcp/i.test(fullMatch)) { const termo = match[3] || match[2] || null; diff --git a/src/topsapp/api.js b/src/topsapp/api.js index 4d41f96..897b36b 100644 --- a/src/topsapp/api.js +++ b/src/topsapp/api.js @@ -916,6 +916,26 @@ class TopSappAPI { } } + /** + * Configurar serviço para IPoE (DHCP Option 82) + * Define dhcp_agent_circuit_id e dhcp_liberar no serviço + */ + async configurarIPoE(idCliente, idServico, dhcpAgentCircuitId, 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), + dhcp_agent_circuit_id: dhcpAgentCircuitId, + dhcp_liberar: true + }, instance, credentials); + } catch (error) { + return { success: false, error: error.message }; + } + } + /** * Listar todas as instâncias disponíveis */ diff --git a/src/topsapp/manager.js b/src/topsapp/manager.js index e12db83..25fcf91 100644 --- a/src/topsapp/manager.js +++ b/src/topsapp/manager.js @@ -1255,6 +1255,82 @@ class TopSappManager { 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}`; + + logger.info(`[IPoE] Configurando serviço ${idServico} do cliente ${idCliente}: circuit_id=${circuitId}`); + + const resultado = await this.api.configurarIPoE(idCliente, idServico, circuitId, location, credentials); + + if (resultado.success) { + return { + success: true, + circuitId, + prefixo, + serial, + baseInfo, + servico: servico.descricao || servico.nome || 'N/A' + }; + } + + return resultado; + } catch (error) { + return { success: false, error: error.message }; + } + } + } module.exports = TopSappManager;