Motor de regras único, mandante configurável e deploy via secret do Gitea #1
@ -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
|
||||
|
||||
26
README.md
26
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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user