diff --git a/.env.example b/.env.example index 313e573..21096b5 100644 --- a/.env.example +++ b/.env.example @@ -38,6 +38,8 @@ SERVICENOW_TABLE_REQUEST_URL= SERVICENOW_TABLE_JOURNAL_URL= SERVICENOW_SC_ITEM_OPTION_URL= SERVICENOW_EXTERNAL_TICKET_FIELD=u_external_tickets +# Sys_id do grupo de TI da CAOA (destino de tickets fora do escopo Sothis) +SERVICENOW_CAOA_TI_GROUP_ID= # --- Configuracao do Banco de Dados do GLPI (MySQL) --- GLPI_DB_TYPE=mysql diff --git a/README.md b/README.md index 97a256d..9303101 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,33 @@ npm run dev ## Producao -1. PM2: +### Deploy manual na VM + ```bash +git pull origin master +npm ci --omit=dev pm2 start ecosystem.config.js --env production +pm2 save +``` + +Se o processo ja estiver rodando: + +```bash +pm2 restart sn-glpi-sync-cron --update-env +``` + +### Deploy via Gitea Actions + +O workflow `.gitea/workflows/deploy-production.yml` roda no runner com rotulo `vm-prod`. + +1. Configure a variavel do repositorio `PROD_DEPLOY_PATH` com o caminho da aplicacao na VM de producao. Se nao configurar, o workflow usa `/opt/sn-glpi-sync-new`. +2. Garanta que o arquivo `.env.production` exista nesse caminho na VM e contenha `ENABLE_STATUS_SYNC=true`. +3. Faça push na branch `master` ou execute o workflow manualmente em Actions. +4. Acompanhe pelo Gitea Actions ou na VM: + +```bash +pm2 list +pm2 logs sn-glpi-sync-cron ``` ## Observacoes diff --git a/src/controllers/processGlpiClosureController.js b/src/controllers/processGlpiClosureController.js index 465990e..dbe3542 100644 --- a/src/controllers/processGlpiClosureController.js +++ b/src/controllers/processGlpiClosureController.js @@ -2,12 +2,13 @@ const TicketSyncModel = require('../models/ticketSyncModel'); const TicketGlpiModel = require('../models/ticketGlpiModel'); const TicketUpdateModel = require('../models/ticketUpdateModel'); const TicketSnModel = require('../models/ticketSnModel'); -const { addCommentToServiceNow, addWorkNoteToServiceNow, updateStatusInServiceNow, fetchTicketLiveStatusFromServiceNow, closeTicketInServiceNow } = require('../services/servicenowService'); +const { addCommentToServiceNow, addWorkNoteToServiceNow, updateStatusInServiceNow, fetchTicketLiveStatusFromServiceNow, closeTicketInServiceNow, reassignTicketInServiceNow } = require('../services/servicenowService'); const { stripHTML } = require('../utils/commentSanitizer'); -const { logInfo, logError } = require('../utils/logger'); +const { logInfo, logError, logWarning } = require('../utils/logger'); const DEFINITIVE_CLOSE_NOTICE = 'Chamado encerrado definitivamente. Tempo para reabertura encerrado. Caso necessario, por favor abra um novo Ticket.'; const outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27); +const caoaTiGroupId = process.env.SERVICENOW_CAOA_TI_GROUP_ID; const processGlpiClosureController = async () => { try { @@ -35,10 +36,17 @@ const processGlpiClosureController = async () => { continue; } + if (liveSnStatus === 'Resolvido' || liveSnStatus === 'Encerrado' || liveSnStatus === 'Encerrado - Omitido') { + await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'solved', 'solved'); + logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} ja estava refletido no SN como '${liveSnStatus}'.`); + continue; + } + const solution = await TicketGlpiModel.getTicketSolution(ticket.glpi_ticket_id); if (solution?.solutiontypes_id === outOfScopeSolutionTypeId) { const justification = solution?.content ? stripHTML(solution.content) : 'Ticket marcado como fora do escopo.'; - await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket nao sera mais exibido para equipe Sothis, com a justificativa: ${justification}`); + 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 TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored'); await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id); continue; @@ -50,7 +58,12 @@ const processGlpiClosureController = async () => { const closeNotes = solution?.content ? stripHTML(solution.content) : 'Resolvido via integracao GLPI.'; const resolvedAt = solution?.date_mod || new Date(); - await closeTicketInServiceNow(ticket.sn_ticket_id, closeNotes, resolvedAt); + const closeOk = await closeTicketInServiceNow(ticket.sn_ticket_id, closeNotes, resolvedAt); + if (!closeOk) { + logWarning(`Fechamento do ticket SN ${ticketSn.ticket_number} falhou. Mantendo GLPI ${ticket.glpi_ticket_id} para nova tentativa.`); + continue; + } + await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'solved', 'solved'); logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} solucionado e refletido no SN.`); continue; diff --git a/src/controllers/processStatusController.js b/src/controllers/processStatusController.js index dc682cb..5f6501d 100644 --- a/src/controllers/processStatusController.js +++ b/src/controllers/processStatusController.js @@ -3,10 +3,11 @@ const TicketSnModel = require('../models/ticketSnModel'); const TicketGlpiModel = require('../models/ticketGlpiModel'); const { logInfo, logError, logWarning } = require('../utils/logger'); const { stripHTML } = require('../utils/commentSanitizer'); -const { closeTicketInServiceNow, updateStatusInServiceNow, addWorkNoteToServiceNow, fetchTicketLiveStatusFromServiceNow } = require('../services/servicenowService'); +const { closeTicketInServiceNow, updateStatusInServiceNow, addWorkNoteToServiceNow, fetchTicketLiveStatusFromServiceNow, reassignTicketInServiceNow } = require('../services/servicenowService'); const { addServiceNowClosureNoteToGlpi } = require('../services/glpiTicketService'); const outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27); +const caoaTiGroupId = process.env.SERVICENOW_CAOA_TI_GROUP_ID; const snCloseRetryMax = Number(process.env.SN_CLOSE_RETRY_MAX || 2); const snCloseRetryDelayMs = Number(process.env.SN_CLOSE_RETRY_DELAY_MS || 1500); @@ -142,7 +143,8 @@ const processStatusAndClosureController = async () => { if (solution?.solutiontypes_id === outOfScopeSolutionTypeId) { logInfo(`REGRA DETECTADA: Ticket ${ticket.glpi_ticket_id} está fora do escopo (SolutionType ID: ${outOfScopeSolutionTypeId}).`); const justification = solution?.content ? stripHTML(solution.content) : 'Ticket marcado como "Fora do escopo" no GLPI.'; - await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket não será mais exibido para equipe Sothis, com a justificativa: ${justification}`); + 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 TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored'); await TicketGlpiModel.unassignGroupFromTicket(ticket.glpi_ticket_id); await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id); diff --git a/src/services/glpiTicketService.js b/src/services/glpiTicketService.js index a231928..432c4c9 100644 --- a/src/services/glpiTicketService.js +++ b/src/services/glpiTicketService.js @@ -3,10 +3,11 @@ 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 } = require('./servicenowService'); +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 { @@ -124,7 +125,8 @@ const checkGlpiStatusChanges = async () => { if (solution?.solutiontypes_id === outOfScopeSolutionTypeId) { logInfo(`REGRA DETECTADA: Ticket ${ticket.glpi_ticket_id} está fora do escopo (SolutionType ID: ${outOfScopeSolutionTypeId}).`); const justification = solution?.content ? stripHTML(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 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 TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored'); await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id); continue; diff --git a/src/services/servicenowService.js b/src/services/servicenowService.js index 9ecde28..e6b32df 100644 --- a/src/services/servicenowService.js +++ b/src/services/servicenowService.js @@ -685,6 +685,44 @@ const updateExternalTicketInServiceNow = async (snTicketId, glpiTicketId) => { } }; +const reassignTicketInServiceNow = async (snTicketId, groupSysId) => { + let sysId = null; + try { + const ticketSn = await TicketSnModel.findById(snTicketId); + if (!ticketSn) { + logError(`Ticket SN com ID ${snTicketId} nao encontrado para reatribuicao de grupo.`, 'reassignTicketInServiceNow'); + return false; + } + + sysId = ticketSn.sys_id; + const ticketType = ticketSn.tipo; + let url = ''; + if (ticketType === 'incidente') { + url = `${apiConfig.snTableIncidentConfig.baseUrl}/${sysId}`; + } else if (ticketType === 'requisicao') { + url = `${apiConfig.snTableRequestConfig.baseUrl}/${sysId}`; + } else { + throw new Error(`Tipo de ticket desconhecido: ${ticketType}`); + } + + const response = await axios.patch(url, { assignment_group: groupSysId }, { + auth: apiConfig.servicenowAuthentication.auth, + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } + }); + + if (response.status !== 200) { + logError(`Falha ao reatribuir grupo no SN. Status: ${response.status}`, { snTicketId, groupSysId }); + return false; + } + + logInfo(`OK: Ticket SN ${snTicketId} reatribuido para grupo ${groupSysId}.`, { snTicketId, sysId }); + return true; + } catch (error) { + logError(error, `ERRO: Falha ao reatribuir grupo no SN (sysId: ${sysId})`); + return false; + } +}; + const queueExternalTicketLinkRetry = async (ticketSyncId, snTicketId, glpiTicketId, shouldMarkDone = false) => { const sourceId = `external_link:${snTicketId}:${glpiTicketId}`; const payload = JSON.stringify({ sn_ticket_id: snTicketId, glpi_ticket_id: glpiTicketId, field: externalTicketField }); @@ -786,6 +824,7 @@ module.exports = { addCommentToServiceNow, fetchScItemOptionValue, updateExternalTicketInServiceNow, + reassignTicketInServiceNow, queueExternalTicketLinkRetry, processPendingExternalTicketLinks };