From c964bcf18ae048c7b20964eda076bb8b935ed84b Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Wed, 4 Mar 2026 18:15:53 -0300 Subject: [PATCH] =?UTF-8?q?FEATURE:=20Chamados=20reabertos=20possuem=20uma?= =?UTF-8?q?=20nova=20colora=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/controllers/processCommentsController.js | 33 ++++++++++++-------- src/services/glpiCommentService.js | 22 +++++++++++-- src/services/servicenowService.js | 22 ++++++------- 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index c4c84b1..99f8087 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ servicenow_tickets.json location_mapping.log .env* !.env.example +docs/checklist.md \ No newline at end of file diff --git a/src/controllers/processCommentsController.js b/src/controllers/processCommentsController.js index ab780f4..425246c 100644 --- a/src/controllers/processCommentsController.js +++ b/src/controllers/processCommentsController.js @@ -6,27 +6,27 @@ const TicketGlpiModel = require('../models/ticketGlpiModel'); const { logInfo } = require('../utils/logger'); const ensureReopenFromSNWhenGlpiSolved = async (ticket) => { - if (ticket.glpi_sync_status !== 'solved') { - return; + const glpiStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id); + const glpiIsSolved = glpiStatus === 5; + if (!glpiIsSolved) { + return { reopened: false, glpiSolved: false }; } const ticketSn = await TicketSnModel.findById(ticket.sn_ticket_id); if (!ticketSn) { - return; + return { reopened: false, glpiSolved: true }; } const liveSnStatus = await fetchTicketLiveStatusFromServiceNow(ticketSn.sys_id, ticketSn.tipo); const snIsFinal = liveSnStatus === 'Resolvido' || liveSnStatus === 'Encerrado' || liveSnStatus === 'Encerrado - Omitido'; if (snIsFinal) { - return; + return { reopened: false, glpiSolved: true }; } - const glpiStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id); - if (glpiStatus === 5) { - await TicketGlpiModel.updateStatus(ticket.glpi_ticket_id, 'Em Atendimento'); - await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'synced'); - logInfo(`Reabertura detectada no SN para ticket ${ticket.sn_ticket_id}. GLPI ${ticket.glpi_ticket_id} retornou para Em Atendimento.`); - } + await TicketGlpiModel.updateStatus(ticket.glpi_ticket_id, 'Em Atendimento'); + await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'synced'); + logInfo(`Reabertura detectada no SN para ticket ${ticket.sn_ticket_id}. GLPI ${ticket.glpi_ticket_id} retornou para Em Atendimento.`); + return { reopened: true, glpiSolved: true }; }; const processCommentsController = async () => { @@ -34,11 +34,18 @@ const processCommentsController = async () => { logInfo(`Tickets em monitoramento: ${ticketsToMonitor.length}`); for (const ticket of ticketsToMonitor) { - await ensureReopenFromSNWhenGlpiSolved(ticket); + const reopenState = await ensureReopenFromSNWhenGlpiSolved(ticket); + const reopenedThisCycle = reopenState.reopened; + const glpiWasSolved = reopenState.glpiSolved; + if (glpiWasSolved && !reopenedThisCycle) { + logInfo(`Ticket ${ticket.glpi_ticket_id} permanece Solucionado no GLPI. Comentarios SN -> GLPI bloqueados ate reabertura.`); + await syncCommentsGlpitoSN(ticket); + continue; + } const comments = await fetchCommentsFromServiceNow(ticket); if (comments.length !== 0) { - await syncCommentsSNtoGlpi(comments, ticket); + await syncCommentsSNtoGlpi(comments, ticket, { highlightReopenContext: reopenedThisCycle }); } await syncCommentsGlpitoSN(ticket); @@ -47,4 +54,4 @@ const processCommentsController = async () => { module.exports = { processCommentsController -}; \ No newline at end of file +}; diff --git a/src/services/glpiCommentService.js b/src/services/glpiCommentService.js index aac446e..f5aaf6f 100644 --- a/src/services/glpiCommentService.js +++ b/src/services/glpiCommentService.js @@ -28,7 +28,7 @@ const normalizeAuthorName = (rawAuthor) => { return toTitleCase(normalizedRaw); }; -const formatSNUpdateForGlpi = (update) => { +const formatSNUpdateForGlpi = (update, options = {}) => { const escapeHtml = (value) => String(value || '') .replace(/&/g, '&') .replace(/ { const content = escapeHtml(update.content || ''); const isAutomaticTask = update.update_type === 'task' && /registro alterado por:/i.test(String(update.content || '')); const isSnClosureMessage = update.update_type === 'comment' && /chamado resolvido|chamado encerrado/i.test(String(update.content || '')); + const isSnReopenMessageByText = update.update_type === 'comment' && /reabert|reopen/i.test(String(update.content || '')); + const isSnReopenMessageByContext = Boolean(options.isReopenContextComment); + const isSnReopenMessage = isSnReopenMessageByText || isSnReopenMessageByContext; if (isAutomaticTask) { return ` @@ -59,6 +62,14 @@ const formatSNUpdateForGlpi = (update) => { `; } + if (isSnReopenMessage) { + return ` +
+
Reabertura de chamado no ServiceNow
+
${content}
+
`; + } + return `
${author}
@@ -153,7 +164,7 @@ const syncCommentsGlpitoSN = async (ticket) => { }; -const syncCommentsSNtoGlpi = async (comments, ticket) => { +const syncCommentsSNtoGlpi = async (comments, ticket, options = {}) => { logInfo('INFO: Iniciando sincronizacao de comments do ServiceNow para o GLPI...'); const orderedComments = [...comments].sort((a, b) => { @@ -161,17 +172,22 @@ const syncCommentsSNtoGlpi = async (comments, ticket) => { const bTime = new Date(b.created_at).getTime(); return aTime - bTime; }); + let reopenHighlightPending = Boolean(options.highlightReopenContext); for (const comment of orderedComments) { try { + const isReopenContextComment = reopenHighlightPending && comment.update_type === 'comment'; const formattedPayload = { ...comment, - content: formatSNUpdateForGlpi(comment) + content: formatSNUpdateForGlpi(comment, { isReopenContextComment }) }; const isTask = comment.update_type === 'task'; const insertedId = isTask ? await TicketGlpiModel.insertTask(formattedPayload, ticket.glpi_ticket_id) : await TicketGlpiModel.insertComment(formattedPayload, ticket.glpi_ticket_id); + if (isReopenContextComment) { + reopenHighlightPending = false; + } await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi'); if (ticket.sn_sync_status === 'synced' && ticket.glpi_sync_status === 'synced') { await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW'); diff --git a/src/services/servicenowService.js b/src/services/servicenowService.js index 74c10f6..7394bbd 100644 --- a/src/services/servicenowService.js +++ b/src/services/servicenowService.js @@ -43,6 +43,12 @@ const normalizeServiceNowDateTime = (value) => { return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())} ${pad(parsed.getHours())}:${pad(parsed.getMinutes())}:${pad(parsed.getSeconds())}`; }; +const isAutomaticServiceNowTask = (entry) => { + if (!entry || entry.element !== 'work_notes') return false; + const content = String(entry.value || ''); + return /registro alterado por:/i.test(content); +}; + const fetchTicketsFromServiceNow = async (watermark) => { try { const watermarkDate = new Date(watermark); @@ -285,10 +291,13 @@ const fetchCommentsFromServiceNow = async (ticket) => { // Em ambiente de teste, a regra pode ser desligada por env. const filteredComments = allComments.filter((entry) => { if (!shouldIgnoreDefaultUserJournal) { - return true; + return !isAutomaticServiceNowTask(entry); } const createdBy = String(entry.sys_created_by || '').trim().toLowerCase(); const ignoredUser = ignoredJournalUsers.includes(createdBy); + if (isAutomaticServiceNowTask(entry)) { + return false; + } if (entry.element === 'comments') { return !ignoredUser; } @@ -322,20 +331,11 @@ const fetchCommentsFromServiceNow = async (ticket) => { continue; } else { - let normalizedContent = comment.value; - if ( - comment.element === 'work_notes' && - typeof normalizedContent === 'string' && - normalizedContent.includes('Registro alterado por:') - ) { - normalizedContent = `Tarefa automatica originada no ServiceNow (registro automatico).\n\nMensagem original:\n${normalizedContent}`; - } - const commentData = { ticket_sync_id: ticket.id, update_type: comment.element === 'work_notes' ? 'task' : 'comment', source_system: 'servicenow', - content: normalizedContent, + content: comment.value, created_at: normalizeServiceNowDateTime(comment.sys_created_on), author: comment.sys_created_by || 'ServiceNow', updated_at: normalizeServiceNowDateTime(comment.sys_created_on),