FEATURE: Chamados reabertos possuem uma nova coloração
This commit is contained in:
parent
ce294d4220
commit
c964bcf18a
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,3 +8,4 @@ servicenow_tickets.json
|
|||||||
location_mapping.log
|
location_mapping.log
|
||||||
.env*
|
.env*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
docs/checklist.md
|
||||||
@ -6,27 +6,27 @@ const TicketGlpiModel = require('../models/ticketGlpiModel');
|
|||||||
const { logInfo } = require('../utils/logger');
|
const { logInfo } = require('../utils/logger');
|
||||||
|
|
||||||
const ensureReopenFromSNWhenGlpiSolved = async (ticket) => {
|
const ensureReopenFromSNWhenGlpiSolved = async (ticket) => {
|
||||||
if (ticket.glpi_sync_status !== 'solved') {
|
const glpiStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id);
|
||||||
return;
|
const glpiIsSolved = glpiStatus === 5;
|
||||||
|
if (!glpiIsSolved) {
|
||||||
|
return { reopened: false, glpiSolved: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticketSn = await TicketSnModel.findById(ticket.sn_ticket_id);
|
const ticketSn = await TicketSnModel.findById(ticket.sn_ticket_id);
|
||||||
if (!ticketSn) {
|
if (!ticketSn) {
|
||||||
return;
|
return { reopened: false, glpiSolved: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const liveSnStatus = await fetchTicketLiveStatusFromServiceNow(ticketSn.sys_id, ticketSn.tipo);
|
const liveSnStatus = await fetchTicketLiveStatusFromServiceNow(ticketSn.sys_id, ticketSn.tipo);
|
||||||
const snIsFinal = liveSnStatus === 'Resolvido' || liveSnStatus === 'Encerrado' || liveSnStatus === 'Encerrado - Omitido';
|
const snIsFinal = liveSnStatus === 'Resolvido' || liveSnStatus === 'Encerrado' || liveSnStatus === 'Encerrado - Omitido';
|
||||||
if (snIsFinal) {
|
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 TicketGlpiModel.updateStatus(ticket.glpi_ticket_id, 'Em Atendimento');
|
||||||
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'synced');
|
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.`);
|
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 () => {
|
const processCommentsController = async () => {
|
||||||
@ -34,11 +34,18 @@ const processCommentsController = async () => {
|
|||||||
logInfo(`Tickets em monitoramento: ${ticketsToMonitor.length}`);
|
logInfo(`Tickets em monitoramento: ${ticketsToMonitor.length}`);
|
||||||
|
|
||||||
for (const ticket of ticketsToMonitor) {
|
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);
|
const comments = await fetchCommentsFromServiceNow(ticket);
|
||||||
if (comments.length !== 0) {
|
if (comments.length !== 0) {
|
||||||
await syncCommentsSNtoGlpi(comments, ticket);
|
await syncCommentsSNtoGlpi(comments, ticket, { highlightReopenContext: reopenedThisCycle });
|
||||||
}
|
}
|
||||||
|
|
||||||
await syncCommentsGlpitoSN(ticket);
|
await syncCommentsGlpitoSN(ticket);
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const normalizeAuthorName = (rawAuthor) => {
|
|||||||
return toTitleCase(normalizedRaw);
|
return toTitleCase(normalizedRaw);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatSNUpdateForGlpi = (update) => {
|
const formatSNUpdateForGlpi = (update, options = {}) => {
|
||||||
const escapeHtml = (value) => String(value || '')
|
const escapeHtml = (value) => String(value || '')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
@ -39,6 +39,9 @@ const formatSNUpdateForGlpi = (update) => {
|
|||||||
const content = escapeHtml(update.content || '');
|
const content = escapeHtml(update.content || '');
|
||||||
const isAutomaticTask = update.update_type === 'task' && /registro alterado por:/i.test(String(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 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) {
|
if (isAutomaticTask) {
|
||||||
return `
|
return `
|
||||||
@ -59,6 +62,14 @@ const formatSNUpdateForGlpi = (update) => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSnReopenMessage) {
|
||||||
|
return `
|
||||||
|
<div style="border:1px solid #9ae6b4; border-left:4px solid #2f855a; background:#f0fff4; padding:12px; border-radius:4px;">
|
||||||
|
<div style="font-weight:700; color:#22543d; margin-bottom:6px;">Reabertura de chamado no ServiceNow</div>
|
||||||
|
<div style="color:#276749; font-size:12px; white-space: pre-wrap;">${content}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div style="border:1px solid #d6e9ff; border-left:4px solid #2b6cb0; background:#f7fbff; padding:12px; border-radius:4px;">
|
<div style="border:1px solid #d6e9ff; border-left:4px solid #2b6cb0; background:#f7fbff; padding:12px; border-radius:4px;">
|
||||||
<div style="font-weight:700; color:#1a365d; margin-bottom:6px;">${author}</div>
|
<div style="font-weight:700; color:#1a365d; margin-bottom:6px;">${author}</div>
|
||||||
@ -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...');
|
logInfo('INFO: Iniciando sincronizacao de comments do ServiceNow para o GLPI...');
|
||||||
|
|
||||||
const orderedComments = [...comments].sort((a, b) => {
|
const orderedComments = [...comments].sort((a, b) => {
|
||||||
@ -161,17 +172,22 @@ const syncCommentsSNtoGlpi = async (comments, ticket) => {
|
|||||||
const bTime = new Date(b.created_at).getTime();
|
const bTime = new Date(b.created_at).getTime();
|
||||||
return aTime - bTime;
|
return aTime - bTime;
|
||||||
});
|
});
|
||||||
|
let reopenHighlightPending = Boolean(options.highlightReopenContext);
|
||||||
|
|
||||||
for (const comment of orderedComments) {
|
for (const comment of orderedComments) {
|
||||||
try {
|
try {
|
||||||
|
const isReopenContextComment = reopenHighlightPending && comment.update_type === 'comment';
|
||||||
const formattedPayload = {
|
const formattedPayload = {
|
||||||
...comment,
|
...comment,
|
||||||
content: formatSNUpdateForGlpi(comment)
|
content: formatSNUpdateForGlpi(comment, { isReopenContextComment })
|
||||||
};
|
};
|
||||||
const isTask = comment.update_type === 'task';
|
const isTask = comment.update_type === 'task';
|
||||||
const insertedId = isTask
|
const insertedId = isTask
|
||||||
? await TicketGlpiModel.insertTask(formattedPayload, ticket.glpi_ticket_id)
|
? await TicketGlpiModel.insertTask(formattedPayload, ticket.glpi_ticket_id)
|
||||||
: await TicketGlpiModel.insertComment(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');
|
await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi');
|
||||||
if (ticket.sn_sync_status === 'synced' && ticket.glpi_sync_status === 'synced') {
|
if (ticket.sn_sync_status === 'synced' && ticket.glpi_sync_status === 'synced') {
|
||||||
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW');
|
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW');
|
||||||
|
|||||||
@ -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())}`;
|
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) => {
|
const fetchTicketsFromServiceNow = async (watermark) => {
|
||||||
try {
|
try {
|
||||||
const watermarkDate = new Date(watermark);
|
const watermarkDate = new Date(watermark);
|
||||||
@ -285,10 +291,13 @@ const fetchCommentsFromServiceNow = async (ticket) => {
|
|||||||
// Em ambiente de teste, a regra pode ser desligada por env.
|
// Em ambiente de teste, a regra pode ser desligada por env.
|
||||||
const filteredComments = allComments.filter((entry) => {
|
const filteredComments = allComments.filter((entry) => {
|
||||||
if (!shouldIgnoreDefaultUserJournal) {
|
if (!shouldIgnoreDefaultUserJournal) {
|
||||||
return true;
|
return !isAutomaticServiceNowTask(entry);
|
||||||
}
|
}
|
||||||
const createdBy = String(entry.sys_created_by || '').trim().toLowerCase();
|
const createdBy = String(entry.sys_created_by || '').trim().toLowerCase();
|
||||||
const ignoredUser = ignoredJournalUsers.includes(createdBy);
|
const ignoredUser = ignoredJournalUsers.includes(createdBy);
|
||||||
|
if (isAutomaticServiceNowTask(entry)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (entry.element === 'comments') {
|
if (entry.element === 'comments') {
|
||||||
return !ignoredUser;
|
return !ignoredUser;
|
||||||
}
|
}
|
||||||
@ -322,20 +331,11 @@ const fetchCommentsFromServiceNow = async (ticket) => {
|
|||||||
continue;
|
continue;
|
||||||
} else {
|
} 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 = {
|
const commentData = {
|
||||||
ticket_sync_id: ticket.id,
|
ticket_sync_id: ticket.id,
|
||||||
update_type: comment.element === 'work_notes' ? 'task' : 'comment',
|
update_type: comment.element === 'work_notes' ? 'task' : 'comment',
|
||||||
source_system: 'servicenow',
|
source_system: 'servicenow',
|
||||||
content: normalizedContent,
|
content: comment.value,
|
||||||
created_at: normalizeServiceNowDateTime(comment.sys_created_on),
|
created_at: normalizeServiceNowDateTime(comment.sys_created_on),
|
||||||
author: comment.sys_created_by || 'ServiceNow',
|
author: comment.sys_created_by || 'ServiceNow',
|
||||||
updated_at: normalizeServiceNowDateTime(comment.sys_created_on),
|
updated_at: normalizeServiceNowDateTime(comment.sys_created_on),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user