// src/services/glpiService.js const TicketSnModel = require('../models/ticketSnModel'); const TicketGlpiModel = require('../models/ticketGlpiModel'); const TicketUpdateModel = require('../models/ticketUpdateModel'); const TicketSyncModel = require('../models/ticketSyncModel'); const { logInfo, logError, logWarning } = require('../utils/logger'); const { addWorkNoteToServiceNow } = require('./servicenowService'); // Método principal que o controller vai chamar const syncTicketsToGlpi = async () => { try { logInfo('🔄 Iniciando sincronização para GLPI...'); // 1. Buscar tickets pendentes const pendingTickets = await TicketSnModel.getPendingTickets(); if (pendingTickets.length === 0) { logInfo('✅ Nenhum ticket pendente para sincronizar'); return; } logInfo(`📋 Encontrados ${pendingTickets.length} tickets para sincronizar`); // 2. Processar cada ticket for (const ticket of pendingTickets) { await processSingleTicket(ticket); } logInfo('🎉 Sincronização com GLPI concluída!'); } catch (error) { logError(error, '❌ Erro na sincronização GLPI'); throw error; } }; // Processar um ticket individual const processSingleTicket = async (ticket) => { try { logInfo(`🔍 Processando ticket: ${ticket.ticket_number}`); // 1. Verifica se o ticket já possui um ID do GLPI no nosso banco de sincronização. if (ticket.glpi_ticket_id) { logInfo(`✅ Ticket ${ticket.ticket_number} já possui GLPI ID (${ticket.glpi_ticket_id}) registrado. Apenas atualizando status.`); await TicketSyncModel.updateStatus(ticket.id, 'synced', 'synced'); return; } // 2. Se não tem ID local, verifica se o ticket existe no GLPI (buscando pelo número). const existingTicket = await TicketGlpiModel.ticketExists(ticket.ticket_number); await TicketSyncModel.updateLastSync(ticket.id, 'glpi'); // Atualiza o timestamp if (existingTicket) { logInfo(`✅ Ticket ${ticket.ticket_number} encontrado no GLPI. Atualizando registro local com o ID: ${existingTicket}`); // --- NOVA VALIDAÇÃO: Verificar status do ticket existente no GLPI --- const glpiLiveStatus = await TicketGlpiModel.getTicketStatus(existingTicket); if (glpiLiveStatus === 6) { // Status 6 = Fechado no GLPI logInfo(`Ticket GLPI ${existingTicket} já está Fechado. Marcando ticket SN ${ticket.ticket_number} como 'closed'/'ignored' e não monitorado.`); await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'ignored', 'closed', existingTicket); return; // Não processa mais este ticket } // --- FIM DA NOVA VALIDAÇÃO --- // Atualiza nosso registro de sincronização com o ID do GLPI encontrado. await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', existingTicket); logInfo(`✅ Ticket ${ticket.ticket_number} já existe no GLPI! GLPI ID: ${existingTicket}`); return; // Adiciona este return para evitar a criação de um novo ticket } // 3. Converter dados para formato GLPI const glpiData = { ticket_number: ticket.ticket_number, short_description: ticket.short_description, description: ticket.description, caller_id: ticket.caller_id, caller_email: ticket.caller_email, caller_phone: ticket.telefone, caller_ramal: ticket.ramal, type: ticket.tipo, state: ticket.status, priority: '3', location_id: ticket.location_id, location_name: ticket.location_name, opened_at: ticket.opened_at, updated_at: ticket.updated_at }; // Lógica para buscar descrição de sc_item_option se a descrição atual estiver vazia // e for um ticket de requisição (apenas para criação de novos tickets) if (glpiData.type === 'requisicao' && (!glpiData.description || glpiData.description.trim() === '')) { logInfo(`Descrição vazia para requisição ${ticket.ticket_number}. Tentando buscar de sc_item_option.`); // Você precisará saber o nome interno da variável de catálogo que contém a descrição. // Por exemplo, se for 'justificativa', use 'justificativa'. const scItemOptionDescription = await require('./servicenowService').fetchScItemOptionValue(ticket.sys_id, 'justificativa'); if (scItemOptionDescription) { glpiData.description = scItemOptionDescription; logInfo(`Descrição atualizada para requisição ${ticket.ticket_number} a partir de sc_item_option.`); } else { logWarning(`Não foi possível obter descrição de sc_item_option para requisição ${ticket.ticket_number}.`); } } // 3. Inserir no GLPI const glpiTicketId = await TicketGlpiModel.createTicket(glpiData); await TicketSyncModel.updateLastSync(ticket.id, 'glpi'); // Atualiza o timestamp // 4. Atualizar registro de sincronização await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', glpiTicketId); logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`); } catch (error) { logError(error, `❌ Erro no ticket ${ticket.ticket_number}`); // Se temos um registro de sync, atualizar o status para error try { await TicketSyncModel.updateStatus(ticket.id, 'error', 'collected'); } catch (innerError) { logError(innerError, `❌ Erro ao registrar falha de sincronização para ticket ${ticket.id}`); } } }; const checkGlpiStatusChanges = async () => { try { logInfo('📡 Verificando mudanças de status nos tickets do GLPI...'); // 1. Busca todos os tickets que estão sendo monitorados const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor(); if (ticketsToMonitor.length === 0) { logInfo('Nenhum ticket ativo para verificar status no GLPI.'); return; } for (const ticket of ticketsToMonitor) { // 2. Para cada ticket, busca seu status real no GLPI const glpiStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id); // 3. Compara o status real com o que temos no nosso banco const currentDbStatus = ticket.glpi_sync_status; const glpiStatusAsString = TicketGlpiModel.mapGlpiStatusToString(glpiStatus).toLowerCase().replace(/ /g, '_'); // ex: 'em_atendimento' // Se o status no GLPI mudou para Solucionado (5) e nosso banco ainda não reflete isso if (glpiStatus === 5 && currentDbStatus !== 'solved' && currentDbStatus !== 'closed') { logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} foi Solucionado. Preparando para sincronização...`); const solution = await TicketGlpiModel.getTicketSolution(ticket.glpi_ticket_id); // REGRA "FORA DO ESCOPO" if (solution?.solutiontypes_id === 27) { logInfo(`REGRA DETECTADA: Ticket ${ticket.glpi_ticket_id} está fora do escopo (SolutionType ID: 27).`); const justification = solution?.content || 'Ticket marcado como "Fora do escopo" no GLPI.'; await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket fechado no GLPI com a justificativa: ${justification}`); await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored'); await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id); continue; // Pula para o próximo ticket } // FLUXO NORMAL DE RESOLUÇÃO const sanitizedContent = solution?.content ? stripHTML(solution.content) : 'Chamado solucionado sem notas.'; await TicketUpdateModel.insert({ ticket_sync_id: ticket.id, source_system: 'glpi', update_type: 'close_message', content: sanitizedContent, author: null, created_at: solution?.date_mod || new Date(), source_id: `solution-${ticket.glpi_ticket_id}`, }); // Marca o ticket para ser resolvido no SN await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'solved', 'pending_solve', 'GLPI'); } else if (glpiStatus === 6 && currentDbStatus !== 'closed') { logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} foi Fechado permanentemente. Marcando como 'closed'.`); await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', ticket.sn_sync_status); } else if (glpiStatus < 5 && glpiStatusAsString !== currentDbStatus && ticket.source_last !== 'GLPI') { // Para outros status (reabertura, etc.), apenas passa o bastão. logInfo(`Mudança de status (aberto) detectada no GLPI para o ticket ${ticket.glpi_ticket_id}. De '${currentDbStatus}' para '${glpiStatusAsString}'.`); await TicketSyncModel.updateSourceLast(ticket.sn_ticket_id, 'GLPI'); } } } catch (error) { logError(error, '🚨Falha ao verificar mudanças de status no GLPI'); } }; const closeTicketInGlpi = async (ticket) => { try { logInfo(`🔒 Iniciando fechamento do chamado GLPI ${ticket.glpi_ticket_id}...`); // Busca a mensagem de fechamento vinda do ServiceNow const closeMessageRecord = await TicketUpdateModel.getLatestUpdate(ticket.id, 'servicenow'); const solutionContent = closeMessageRecord?.content || 'Chamado encerrado via integração com o ServiceNow.'; // Chama o model para fechar o ticket no GLPI await TicketGlpiModel.closeTicket(ticket.glpi_ticket_id, solutionContent); await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi'); // Atualiza o timestamp logInfo(`✅ Chamado ${ticket.glpi_ticket_id} solucionado com sucesso no GLPI.`); } catch (error) { logError(error, `🚨Falha ao fechar chamado no GLPI: ${ticket.glpi_ticket_id}`); throw error; // Propaga o erro para o controller tratar } }; module.exports = { syncTicketsToGlpi, checkGlpiStatusChanges, closeTicketInGlpi };