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_TABLE_JOURNAL_URL=
|
||||||
SERVICENOW_SC_ITEM_OPTION_URL=
|
SERVICENOW_SC_ITEM_OPTION_URL=
|
||||||
SERVICENOW_EXTERNAL_TICKET_FIELD=u_external_tickets
|
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) ---
|
# --- Configuracao do Banco de Dados do GLPI (MySQL) ---
|
||||||
GLPI_DB_TYPE=mysql
|
GLPI_DB_TYPE=mysql
|
||||||
|
|||||||
26
README.md
26
README.md
@ -72,9 +72,33 @@ npm run dev
|
|||||||
|
|
||||||
## Producao
|
## Producao
|
||||||
|
|
||||||
1. PM2:
|
### Deploy manual na VM
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
git pull origin master
|
||||||
|
npm ci --omit=dev
|
||||||
pm2 start ecosystem.config.js --env production
|
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
|
## Observacoes
|
||||||
|
|||||||
@ -2,12 +2,13 @@ const TicketSyncModel = require('../models/ticketSyncModel');
|
|||||||
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
||||||
const TicketUpdateModel = require('../models/ticketUpdateModel');
|
const TicketUpdateModel = require('../models/ticketUpdateModel');
|
||||||
const TicketSnModel = require('../models/ticketSnModel');
|
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 { 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 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 outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27);
|
||||||
|
const caoaTiGroupId = process.env.SERVICENOW_CAOA_TI_GROUP_ID;
|
||||||
|
|
||||||
const processGlpiClosureController = async () => {
|
const processGlpiClosureController = async () => {
|
||||||
try {
|
try {
|
||||||
@ -35,10 +36,17 @@ const processGlpiClosureController = async () => {
|
|||||||
continue;
|
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);
|
const solution = await TicketGlpiModel.getTicketSolution(ticket.glpi_ticket_id);
|
||||||
if (solution?.solutiontypes_id === outOfScopeSolutionTypeId) {
|
if (solution?.solutiontypes_id === outOfScopeSolutionTypeId) {
|
||||||
const justification = solution?.content ? stripHTML(solution.content) : 'Ticket marcado como fora do escopo.';
|
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 TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored');
|
||||||
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
||||||
continue;
|
continue;
|
||||||
@ -50,7 +58,12 @@ const processGlpiClosureController = async () => {
|
|||||||
|
|
||||||
const closeNotes = solution?.content ? stripHTML(solution.content) : 'Resolvido via integracao GLPI.';
|
const closeNotes = solution?.content ? stripHTML(solution.content) : 'Resolvido via integracao GLPI.';
|
||||||
const resolvedAt = solution?.date_mod || new Date();
|
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');
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'solved', 'solved');
|
||||||
logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} solucionado e refletido no SN.`);
|
logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} solucionado e refletido no SN.`);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -3,10 +3,11 @@ const TicketSnModel = require('../models/ticketSnModel');
|
|||||||
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
||||||
const { logInfo, logError, logWarning } = require('../utils/logger');
|
const { logInfo, logError, logWarning } = require('../utils/logger');
|
||||||
const { stripHTML } = require('../utils/commentSanitizer');
|
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 { addServiceNowClosureNoteToGlpi } = require('../services/glpiTicketService');
|
||||||
|
|
||||||
const outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27);
|
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 snCloseRetryMax = Number(process.env.SN_CLOSE_RETRY_MAX || 2);
|
||||||
const snCloseRetryDelayMs = Number(process.env.SN_CLOSE_RETRY_DELAY_MS || 1500);
|
const snCloseRetryDelayMs = Number(process.env.SN_CLOSE_RETRY_DELAY_MS || 1500);
|
||||||
|
|
||||||
@ -142,7 +143,8 @@ const processStatusAndClosureController = async () => {
|
|||||||
if (solution?.solutiontypes_id === outOfScopeSolutionTypeId) {
|
if (solution?.solutiontypes_id === outOfScopeSolutionTypeId) {
|
||||||
logInfo(`REGRA DETECTADA: Ticket ${ticket.glpi_ticket_id} está fora do escopo (SolutionType 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.';
|
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 TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored');
|
||||||
await TicketGlpiModel.unassignGroupFromTicket(ticket.glpi_ticket_id);
|
await TicketGlpiModel.unassignGroupFromTicket(ticket.glpi_ticket_id);
|
||||||
await TicketGlpiModel.forceCloseTicket(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 TicketUpdateModel = require('../models/ticketUpdateModel');
|
||||||
const TicketSyncModel = require('../models/ticketSyncModel');
|
const TicketSyncModel = require('../models/ticketSyncModel');
|
||||||
const { logInfo, logError } = require('../utils/logger');
|
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 { stripHTML } = require('../utils/commentSanitizer');
|
||||||
|
|
||||||
const outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27);
|
const outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27);
|
||||||
|
const caoaTiGroupId = process.env.SERVICENOW_CAOA_TI_GROUP_ID;
|
||||||
|
|
||||||
const syncTicketsToGlpi = async () => {
|
const syncTicketsToGlpi = async () => {
|
||||||
try {
|
try {
|
||||||
@ -124,7 +125,8 @@ const checkGlpiStatusChanges = async () => {
|
|||||||
if (solution?.solutiontypes_id === outOfScopeSolutionTypeId) {
|
if (solution?.solutiontypes_id === outOfScopeSolutionTypeId) {
|
||||||
logInfo(`REGRA DETECTADA: Ticket ${ticket.glpi_ticket_id} está fora do escopo (SolutionType 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.';
|
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 TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored');
|
||||||
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
||||||
continue;
|
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 queueExternalTicketLinkRetry = async (ticketSyncId, snTicketId, glpiTicketId, shouldMarkDone = false) => {
|
||||||
const sourceId = `external_link:${snTicketId}:${glpiTicketId}`;
|
const sourceId = `external_link:${snTicketId}:${glpiTicketId}`;
|
||||||
const payload = JSON.stringify({ sn_ticket_id: snTicketId, glpi_ticket_id: glpiTicketId, field: externalTicketField });
|
const payload = JSON.stringify({ sn_ticket_id: snTicketId, glpi_ticket_id: glpiTicketId, field: externalTicketField });
|
||||||
@ -786,6 +824,7 @@ module.exports = {
|
|||||||
addCommentToServiceNow,
|
addCommentToServiceNow,
|
||||||
fetchScItemOptionValue,
|
fetchScItemOptionValue,
|
||||||
updateExternalTicketInServiceNow,
|
updateExternalTicketInServiceNow,
|
||||||
|
reassignTicketInServiceNow,
|
||||||
queueExternalTicketLinkRetry,
|
queueExternalTicketLinkRetry,
|
||||||
processPendingExternalTicketLinks
|
processPendingExternalTicketLinks
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user