const TicketSyncModel = require('../models/ticketSyncModel'); const TicketSnModel = require('../models/ticketSnModel'); const TicketGlpiModel = require('../models/ticketGlpiModel'); const TicketUpdateModel = require('../models/ticketUpdateModel'); const { logInfo, logError, logWarning } = require('../utils/logger'); const { stripHTML } = require('../utils/commentSanitizer'); const { closeTicketInServiceNow, updateStatusInServiceNow, addWorkNoteToServiceNow } = require('../services/servicenowService'); const { closeTicketInGlpi } = require('../services/glpiTicketService'); const processStatusAndClosureController = async () => { logInfo('Iniciando processamento de status e fechamento de tickets...'); try { // --- FASE 1: Sincronização de Status para Tickets Abertos (Bidirecional) --- logInfo('Iniciando verificação e sincronização de status...'); // Busca todos os tickets que não estão permanentemente fechados. const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor(); if (ticketsToMonitor.length === 0) { logInfo('Nenhum ticket sincronizado (aberto) encontrado para verificação de status.'); } else { logInfo(`Encontrados ${ticketsToMonitor.length} tickets para monitorar status (synced/solved).`); } for (const ticket of ticketsToMonitor) { try { // Obter detalhes completos do ticket SN para ter sys_id, tipo e status atual const ticketSnDetails = await TicketSnModel.findById(ticket.sn_ticket_id); if (!ticketSnDetails) { logWarning(`Detalhes do ticket SN ${ticket.sn_ticket_id} não encontrados. Pulando.`); continue; } const snCurrentStatus = ticketSnDetails.status; // ================= LÓGICA DE DETECÇÃO DE MUDANÇA NO GLPI ================= // Primeiro, verificamos se o GLPI mudou de estado por conta própria. const glpiLiveStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id); const glpiDbStatus = ticket.glpi_sync_status; // Converte o status numérico do GLPI para a string que usamos no nosso banco (ex: 'solved') const glpiLiveStatusAsString = TicketGlpiModel.mapGlpiStatusToString(glpiLiveStatus).toLowerCase().replace(/ /g, '_').replace('-', '_'); if (glpiLiveStatus !== null && glpiLiveStatusAsString !== glpiDbStatus) { logInfo(`Mudança de status detectada no GLPI para o ticket ${ticket.glpi_ticket_id}. De '${glpiDbStatus}' para '${glpiLiveStatusAsString}'.`); // Se uma mudança foi detectada no GLPI, forçamos o "bastão" para ele. ticket.source_last = 'GLPI'; } // ================= LÓGICA SNOW -> GLPI ================= // Se a última atualização veio do ServiceNow, atualiza o GLPI if (ticket.source_last === 'SNOW') { logInfo(`SN ${ticketSnDetails.ticket_number} (SNOW -> GLPI): Verificando status '${snCurrentStatus}'`); // **LÓGICA DE REABERTURA REFINADA** // Busca o status atual do GLPI para tomar uma decisão inteligente. const glpiCurrentStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id); // Se o ticket no GLPI está permanentemente Fechado (6), não fazemos nada. if (glpiCurrentStatus === 6) { logWarning(`Ticket GLPI ${ticket.glpi_ticket_id} está permanentemente Fechado (6). Ação de reabertura pelo SN ('${snCurrentStatus}') foi bloqueada.`); } else if (snCurrentStatus === 'Resolvido' && glpiCurrentStatus !== 5) { // SN foi para 'Resolved' e GLPI ainda não está 'Solucionado' logInfo(`SN ${ticketSnDetails.ticket_number} foi Resolvido. Sincronizando para GLPI...`); await closeTicketInGlpi(ticket); await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'solved', 'solved'); } else if (snCurrentStatus === 'Resolvido' && glpiCurrentStatus === 5) { // Ambos já estão resolvidos, nenhuma ação necessária. logInfo(`SN ${ticketSnDetails.ticket_number} e GLPI ${ticket.glpi_ticket_id} já estão no estado Resolvido/Solucionado. Nenhuma ação necessária.`); } else if (snCurrentStatus === 'Encerrado - Omitido') { // SN foi para 'Closed' logInfo(`SN ${ticketSnDetails.ticket_number} foi Fechado. Sincronizando para GLPI...`); await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id); await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'closed'); } else { // Se estiver Solucionado (5) ou em qualquer outro estado, permite a atualização (reabertura). await TicketGlpiModel.updateStatus(ticket.glpi_ticket_id, snCurrentStatus); logInfo(`Status do ticket GLPI ${ticket.glpi_ticket_id} atualizado para '${snCurrentStatus}'.`); await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'GLPI'); // Passa o bastão } } // ================= LÓGICA GLPI -> SNOW ================= // Se a última atualização veio do GLPI, atualiza o ServiceNow else if (ticket.source_last === 'GLPI') { logInfo(`SN ${ticketSnDetails.ticket_number} (GLPI -> SNOW): Verificando status no GLPI.`); const glpiCurrentStatus = glpiLiveStatus; // Reutiliza o status que já buscamos if (glpiCurrentStatus !== null) { if (glpiCurrentStatus === 5) { // GLPI foi para 'Solucionado' // Apenas processa se o SN não estiver já resolvido/fechado if (ticket.sn_sync_status !== 'solved' && ticket.sn_sync_status !== 'closed') { logInfo(`GLPI ${ticket.glpi_ticket_id} foi Solucionado. Sincronizando para SN...`); 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); } else { // FLUXO NORMAL DE RESOLUÇÃO const closeNotes = solution?.content ? stripHTML(solution.content) : 'Resolvido via integração GLPI.'; const resolvedAt = solution?.date_mod || new Date(); await closeTicketInServiceNow(ticket.sn_ticket_id, closeNotes, resolvedAt); await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'solved', 'solved'); } } } else if (glpiCurrentStatus === 6) { logInfo(`GLPI ${ticket.glpi_ticket_id} foi Fechado. Sincronizando para SN...`); // Apenas marca nosso banco como fechado, pois o SN já deve estar fechado ou será fechado por automação interna. if (ticket.glpi_sync_status !== 'closed') { await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', ticket.sn_sync_status); } } else { // Se o status for de um ticket aberto (reabertura, etc.), propaga a mudança. const glpiStatusAsString = TicketGlpiModel.mapGlpiStatusToString(glpiCurrentStatus); if (glpiStatusAsString !== snCurrentStatus) { logInfo(`Diferença de status detectada (GLPI: ${glpiStatusAsString}, SN: ${snCurrentStatus}). Sincronizando...`); await updateStatusInServiceNow(ticket.sn_ticket_id, glpiStatusAsString); } } // Passa o bastão de volta para o SN se a ação não for de finalização if (glpiCurrentStatus < 5) { await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW'); } } else { logWarning(`Não foi possível obter o status atual do ticket GLPI ${ticket.glpi_ticket_id}.`); } } // Se source_last não está definido ou é desconhecido, podemos definir um padrão ou logar um aviso. // Por enquanto, vamos apenas logar. else { logWarning(`source_last desconhecido para o ticket SN ${ticketSnDetails.ticket_number}. Pulando sincronização de status.`); } } catch (error) { logError(error, `Erro ao sincronizar status do ticket SN ${ticket.sn_ticket_id} (GLPI ID: ${ticket.glpi_ticket_id}).`); // Marcar o ticket como erro para revisão manual await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'error_sync', 'error_sync'); } } } catch (error) { logError(error, 'Erro geral no processamento de status e fechamento de tickets.'); } }; module.exports = { processStatusAndClosureController };