const TicketSnModel = require('../models/ticketSnModel'); const TicketGlpiModel = require('../models/ticketGlpiModel'); const TicketUpdateModel = require('../models/ticketUpdateModel'); const TicketSyncModel = require('../models/ticketSyncModel'); const { logInfo, logError } = require('../utils/logger'); const { addWorkNoteToServiceNow, updateExternalTicketInServiceNow, queueExternalTicketLinkRetry, reassignTicketInServiceNow } = require('./servicenowService'); const { stripHTML } = require('../utils/commentSanitizer'); const outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27); const caoaTiGroupId = process.env.SERVICENOW_CAOA_TI_GROUP_ID; const syncTicketsToGlpi = async () => { try { logInfo('š Iniciando sincronização para GLPI...'); const pendingTickets = await TicketSnModel.getPendingTickets(); if (pendingTickets.length === 0) { logInfo('ā Nenhum ticket pendente para sincronizar'); return; } logInfo(`š Encontrados ${pendingTickets.length} tickets para sincronizar`); 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; } }; const processSingleTicket = async (ticket) => { try { logInfo(`š Processando ticket: ${ticket.ticket_number}`); 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'); const linkedOk = await updateExternalTicketInServiceNow(ticket.id, ticket.glpi_ticket_id); await queueExternalTicketLinkRetry(ticket.ticket_sync_id, ticket.id, ticket.glpi_ticket_id, linkedOk); return; } const existingTicket = await TicketGlpiModel.ticketExists(ticket.ticket_number); await TicketSyncModel.updateLastSync(ticket.id, 'glpi'); if (existingTicket) { logInfo(`ā Ticket ${ticket.ticket_number} encontrado no GLPI. Atualizando registro local com o ID: ${existingTicket}`); const glpiLiveStatus = await TicketGlpiModel.getTicketStatus(existingTicket); if (glpiLiveStatus === 6) { 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; } await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', existingTicket); const existingLinkOk = await updateExternalTicketInServiceNow(ticket.id, existingTicket); await queueExternalTicketLinkRetry(ticket.ticket_sync_id, ticket.id, existingTicket, existingLinkOk); logInfo(`ā Ticket ${ticket.ticket_number} jĆ” existe no GLPI! GLPI ID: ${existingTicket}`); return; } const glpiData = { ticket_number: ticket.ticket_number, short_description: ticket.short_description, description: ticket.description, justificativa: ticket.justificativa, 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 }; const glpiTicketId = await TicketGlpiModel.createTicket(glpiData); await TicketSyncModel.updateLastSync(ticket.id, 'glpi'); await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', glpiTicketId); const externalLinkOk = await updateExternalTicketInServiceNow(ticket.id, glpiTicketId); await queueExternalTicketLinkRetry(ticket.ticket_sync_id, ticket.id, glpiTicketId, externalLinkOk); logInfo(`ā Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`); } catch (error) { logError(error, `ā Erro no ticket ${ticket.ticket_number}`); try { await TicketSyncModel.updateStatus(ticket.id, 'error', 'collected'); } catch (innerError) { logError(innerError, `ā Erro ao registrar falha de sincronização para ticket ${ticket.id}`); } } }; const isOutOfScopeSolution = (solution) => solution?.solutiontypes_id === outOfScopeSolutionTypeId; /** * Regra "fora de escopo": nota no SN + transferencia para fila CAOA + remove grupo do GLPI + fecha * o ticket no GLPI. Antes existia uma copia quase identica em processGlpiClosureController.js e * outra em processStatusController.js (uma delas nao removia o grupo do GLPI) - consolidado aqui. */ const handleOutOfScopeResolution = async (ticket, solution) => { const justification = solution?.content ? stripHTML(solution.content) : 'Ticket marcado como "Fora do escopo" no GLPI.'; await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket fora do escopo Sothis. Chamado transferido para a fila CAOA Redes com justificativa: ${justification}`); await reassignTicketInServiceNow(ticket.sn_ticket_id, caoaTiGroupId); await TicketGlpiModel.unassignGroupFromTicket(ticket.glpi_ticket_id); await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id); await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored'); logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} fora do escopo: grupo removido, transferido para CAOA e fechado.`); }; const addServiceNowClosureNoteToGlpi = async (ticket, collaboratorName, finalStatus = 'encerrado') => { const sourceId = `sn_closure_note:${ticket.glpi_ticket_id}:${finalStatus}`; try { const alreadySent = await TicketUpdateModel.getBySourceId(sourceId); if (alreadySent) { logInfo(`Nota de encerramento do ServiceNow ja registrada para o GLPI ${ticket.glpi_ticket_id} (status '${finalStatus}'). Ignorando reenvio.`); return { ok: true, isNew: false }; } const resolvedBy = collaboratorName && collaboratorName.trim() ? collaboratorName.trim() : 'colaborador CAOA'; const statusText = finalStatus === 'solucionado' ? 'Chamado solucionado no Service Now e removido do monitoramento.' : 'Chamado encerrado no Service Now e removido do monitoramento.'; const styledMessage = `