FEAT: RN atualizada. Chamados que estão fechados fora do escopo saem da fila sothis e entram na fila CAOA no ServiceNow

This commit is contained in:
Rafael Alves Lopes 2026-06-26 11:39:57 -03:00
parent 00b573d60d
commit ed19cd76a7
6 changed files with 91 additions and 9 deletions

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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
};