FEAT: Sistema de sincroanização de tickets refeito para melhor sincronia e garantia dos dados

This commit is contained in:
Rafael Alves Lopes 2025-10-17 16:58:14 -03:00
parent 4f7a699f92
commit f7eaf34e7d
9 changed files with 249 additions and 258 deletions

View File

@ -1,4 +1,5 @@
// src/app.js // src/app.js
require('dotenv').config();
const { processTicketsController } = require('./controllers/processTicketsController'); const { processTicketsController } = require('./controllers/processTicketsController');
const { processCommentsController } = require('./controllers/processCommentsController'); const { processCommentsController } = require('./controllers/processCommentsController');
const { logInfo } = require('./utils/logger'); const { logInfo } = require('./utils/logger');
@ -10,7 +11,7 @@ logInfo('Aplicação iniciada', {
async function main() { async function main() {
try { try {
await processTicketsController(); //await processTicketsController();
await processCommentsController(); await processCommentsController();
} catch (error) { } catch (error) {
logInfo('Erro na aplicação:', error); logInfo('Erro na aplicação:', error);

View File

@ -1,12 +1,19 @@
const {fetchCommentsFromServiceNow } = require('../services/servicenowService'); const {fetchCommentsFromServiceNow } = require('../services/servicenowService');
const { syncCommentsGlpitoSN } = require('../services/glpiCommentService'); const { syncCommentsGlpitoSN, syncCommentsSNtoGlpi } = require('../services/glpiCommentService');
const TicketSyncModel = require('../models/ticketSyncModel');
const processCommentsController = async () => { const processCommentsController = async () => {
await fetchCommentsFromServiceNow();
await syncCommentsGlpitoSN();
}
const ticketsSynced = await TicketSyncModel.getTicketsSynced();
for (ticket of ticketsSynced){
const comments = await fetchCommentsFromServiceNow(ticket);
if(comments.length > 0){await syncCommentsSNtoGlpi(comments, ticket);}
await syncCommentsGlpitoSN(ticket);
}
}
module.exports = { module.exports = {
processCommentsController processCommentsController
} }

View File

@ -249,27 +249,27 @@ class TicketGlpiModel {
static async ticketExists(snTicketNumber) { static async ticketExists(snTicketNumber) {
try { try {
const query = ` const query = `
SELECT id FROM glpi_tickets SELECT id FROM glpi_tickets WHERE name LIKE ? OR content LIKE ? LIMIT 1
WHERE name LIKE ? OR content LIKE ?
LIMIT 1
`; `;
const [rows] = await glpiPool.execute(query, [ const values = [
`%${snTicketNumber}%`, `%${snTicketNumber}%`,
`%${snTicketNumber}%` `%${snTicketNumber}%`
]); ];
const [rows] = await glpiPool.execute(query,values);
return rows.length > 0 ? rows[0].id : null; return rows.length > 0 ? rows[0].id : null;
} catch (error) { } catch (error) {
logError(error, 'Erro ao verificar ticket existente'); logError(`Erro ao verificar ticket existente: ${error}`);
return null; return null;
} }
} }
// Buscar followups (notas) de um ticket no GLPI // Buscar followups (notas) de um ticket no GLPI
static async getFollowupsByItemId(itemId) { static async getFollowupsByItemId(itemId) {
try { try {
const query = 'SELECT id, content, date, date_creation, date_mod FROM glpi_itilfollowups WHERE items_id = ?'; const query = 'SELECT id, content, date, date_creation, date_mod FROM glpi_itilfollowups WHERE items_id = ? AND users_id != 971';
const [rows] = await glpiPool.execute(query, [itemId]); const [rows] = await glpiPool.execute(query, [itemId]);
return rows; return rows;
} catch (error) { } catch (error) {
@ -278,9 +278,20 @@ class TicketGlpiModel {
} }
} }
static async insertComment(comment, items_id) {
try {
const query = 'INSERT INTO glpi_itilfollowups(itemtype, items_id, date, users_id, content, date_creation, date_mod, timeline_position) VALUES(?, ?, ?, ?, ?, ?, ?, ?) RETURNING id';
const values = ['Ticket', items_id, comment.created_at, 971, comment.content , comment.created_at, comment.created_at, 1];
const [rows] = await glpiPool.execute(query, values);
return rows;
}catch(error){
logError(`Erro ao inserir o ticket: ${error}`);
}
}
} }
/** /**
* @module TicketGlpiModel * @module TicketGlpiModel
* @description Este módulo fornece métodos para gerenciar tickets no GLPI. * @description Este módulo fornece métodos para gerenciar tickets no GLPI.

View File

@ -88,7 +88,8 @@ class TicketSnModel {
ts.opened_at, ts.opened_at,
ts.tipo, ts.tipo,
ts.created_at, ts.created_at,
ts.updated_at ts.updated_at,
tsync.glpi_ticket_id
FROM tickets_sn ts FROM tickets_sn ts
LEFT JOIN ticket_sync tsync ON ts.id = tsync.sn_ticket_id LEFT JOIN ticket_sync tsync ON ts.id = tsync.sn_ticket_id
WHERE tsync.glpi_sync_status = 'pending_check' WHERE tsync.glpi_sync_status = 'pending_check'

View File

@ -8,6 +8,8 @@ class TicketSyncModel {
// Cria um registro de sincronização para um ticket // Cria um registro de sincronização para um ticket
static async createSyncRecord(data) { static async createSyncRecord(data) {
try { try {
const query = `INSERT INTO ticket_sync ( const query = `INSERT INTO ticket_sync (
sn_ticket_id, sn_ticket_id,
glpi_ticket_id, glpi_ticket_id,
@ -19,7 +21,20 @@ class TicketSyncModel {
glpi_sync_status glpi_sync_status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`; ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`;
await pool.query(query, data); const values = [
data.sn_ticket_id,
data.glpi_ticket,
data.last_sn_sync,
data.last_glpi_sync,
data.created_at,
data.updated_at,
data.sn_sync_status,
data.glpi_sync_status
]
await pool.query(query, values);
} catch (error) { } catch (error) {
logError(error, '❌ Erro ao criar registro de sincronização'); logError(error, '❌ Erro ao criar registro de sincronização');
@ -29,10 +44,11 @@ class TicketSyncModel {
// Buscar tickets que foram criados com sucesso para sincronizar comentários // Buscar tickets que foram criados com sucesso para sincronizar comentários
static async getTicketsToSync() { static async getTicketsToSync(glpi_sync_status, sn_sync_status) {
try { try {
const query = 'SELECT id, glpi_ticket_id, sn_ticket_id FROM ticket_sync'; const query = 'SELECT id, glpi_ticket_id, sn_ticket_id FROM ticket_sync WHERE glpi_sync_status = $1 AND sn_sync_status = $2';
const { rows } = await pool.query(query); const values =[glpi_sync_status , sn_sync_status];
const { rows } = await pool.query(query, values);
return rows; return rows;
} catch (error) { } catch (error) {
logError(error, '❌ Erro ao buscar tickets para sincronizar'); logError(error, '❌ Erro ao buscar tickets para sincronizar');
@ -43,15 +59,24 @@ class TicketSyncModel {
// Atualiza o status de sincronização de um ticket // Atualiza o status de sincronização de um ticket
static async updateStatus(ticketSyncId, glpiStatus, snStatus) { static async updateStatus(ticketSyncId, glpiStatus, snStatus) {
try { try {
const query = 'UPDATE ticket_sync SET glpi_sync_status = $1, sn_sync_status = $2 WHERE id = $3'; const query = 'UPDATE ticket_sync SET glpi_sync_status = $1, sn_sync_status = $2 WHERE sn_ticket_id = $3';
await pool.query(query, [glpiStatus, snStatus, ticketSyncId]); await pool.query(query, [glpiStatus, snStatus, ticketSyncId]);
} catch (error) {
logError(`${error} ❌ Erro ao atualizar status de sincronização para o ID: ${ticketSyncId}`);
throw error;
}
}
static async updateStatusAndGlpiTicket(ticketSyncId, glpiStatus, snStatus, glpiTicketId) {
try {
const query = 'UPDATE ticket_sync SET glpi_sync_status = $1, sn_sync_status = $2, glpi_ticket_id = $4 WHERE sn_ticket_id = $3';
await pool.query(query, [glpiStatus, snStatus, ticketSyncId, glpiTicketId]);
} catch (error) { } catch (error) {
logError(error, `❌ Erro ao atualizar status de sincronização para o ID: ${ticketSyncId}`); logError(error, `❌ Erro ao atualizar status de sincronização para o ID: ${ticketSyncId}`);
throw error; throw error;
} }
} }
// Buscar o ID de sincronização baseado no glpi_ticket_id // Buscar o ID de sincronização baseado no glpi_ticket_id
static async getIdSyncByGlpiId(glpiTicketId) { static async getIdSyncByGlpiId(glpiTicketId) {
try { try {
@ -77,14 +102,38 @@ class TicketSyncModel {
} }
static async getByTicketNumber(ticketNumber) { static async getByTicketNumber(ticketNumber) {
try { try {
const query = 'SELECT * FROM ticket_sync WHERE ticket_number = $1'; console.log(ticketNumber)
const query = 'SELECT * FROM ticket_sync WHERE sn_ticket_id = $1';
const { rows } = await pool.query(query, [ticketNumber]);
return rows[0] || null;
} catch (error) { } catch (error) {
logError(`${error}, ❌ Erro ao buscar ticket ${ticketNumber}}` )
} }
} }
// Buscar tickets que estão sincronizados
static async getTicketsSynced() {
try {
const query = `
SELECT
tsync.id,
tsync.glpi_ticket_id,
tsync.sn_ticket_id,
tsn.sys_id
FROM ticket_sync tsync
JOIN tickets_sn tsn ON tsync.sn_ticket_id = tsn.id
WHERE tsync.glpi_sync_status = 'synced' AND tsync.sn_sync_status = 'synced'`;
const { rows } = await pool.query(query);
return rows;
} catch (error) {
logError(error, '❌ Erro ao buscar tickets para sincronizar');
throw error;
}
}
} }
/** /**

View File

@ -1,7 +1,9 @@
// src/models/ticketUpdateModel.js // src/models/ticketUpdateModel.js
const ticketSyncModel = require('./ticketSyncModel');
const pool = require('../data/database'); const pool = require('../data/database');
const { logError } = require('../utils/logger'); const { logError } = require('../utils/logger');
class TicketUpdateModel { class TicketUpdateModel {
static async insert(updateData) { static async insert(updateData) {
@ -17,7 +19,7 @@ class TicketUpdateModel {
created_at = $6, created_at = $6,
updated_at = $7, updated_at = $7,
destiny_id = $9 destiny_id = $9
RETURNING id; RETURNING *;
`; `;
const values = [ const values = [
updateData.ticket_sync_id, updateData.ticket_sync_id,
@ -42,7 +44,7 @@ class TicketUpdateModel {
//Buscar o ID de sincronização baseado no source_id //Buscar o ID de sincronização baseado no source_id
static async getIdSyncBySourceId(sourceId) { static async getIdSyncBySourceId(sourceId) {
try { try {
const query = 'SELECT id FROM ticket_updates WHERE source_id = $1'; const query = 'SELECT tsync.id FROM ticket_updates tupd JOIN ticket_sync tsync ON tupd.ticket_sync_id = tsync.id WHERE source_id = $1';
const { rows } = await pool.query(query, [sourceId]); const { rows } = await pool.query(query, [sourceId]);
return rows[0] ? rows[0].id : null; return rows[0] ? rows[0].id : null;
} catch (error) { } catch (error) {
@ -65,10 +67,9 @@ class TicketUpdateModel {
throw error; throw error;
} }
} }
static async getCommentSync(glpiTicketId, sourceId) { static async getCommentSync(sourceId) {
const ticketSyncModel = require('./ticketSyncModel');
const ticketSyncId = await TicketUpdateModel.getIdSyncBySourceId(sourceId);
const ticketSyncId = await TicketUpdateModel.getIdSyncBySourceId(sourceId);
if (!ticketSyncId) { if (!ticketSyncId) {
return null; return null;
} }

View File

@ -6,123 +6,110 @@ const TicketUpdateModel = require('../models/ticketUpdateModel');
const { logInfo, logError } = require('../utils/logger'); const { logInfo, logError } = require('../utils/logger');
const { existingCommentInServiceNow, syncCommentToServiceNow } = require('./servicenowService'); const { existingCommentInServiceNow, syncCommentToServiceNow } = require('./servicenowService');
const { sanitizeGLPIComment } = require('../utils/commentSanitizer'); const { sanitizeGLPIComment } = require('../utils/commentSanitizer');
const { log } = require('winston');
// Sincroniza comentários do GLPI para o Banco de Dados local // Sincroniza comentários do GLPI para o Banco de Dados local
const syncCommentsGlpitoSN = async () => { const syncCommentsGlpitoSN = async (ticket) => {
try { try {
logInfo('🔄 Iniciando sincronização de comments do GLPI para o Service Now...'); logInfo('🔄 Iniciando sincronização de comments do GLPI para o Service Now...');
const effectiveTickets = await TicketSyncModel.getTicketsToSync();
logInfo(`🔍 ${effectiveTickets.length} tickets encontrados para sincronização de comentários`, { step: 1 });
for (const ticket of effectiveTickets) {
logInfo(`Buscando comentários para o ticket GLPI ID: ${ticket.glpi_ticket_id}`);
const comments = await TicketGlpiModel.getFollowupsByItemId(ticket.glpi_ticket_id); const comments = await TicketGlpiModel.getFollowupsByItemId(ticket.glpi_ticket_id);
logInfo(`📝 ${comments.length} comentários encontrados para o ticket GLPI ID: ${ticket.glpi_ticket_id}`, { step: 2 }); if (comments.length === 0) {
logInfo(`Nenhum comentário encontrado no GLPI para o ticket GLPI ID: ${ticket.glpi_ticket_id}`);
return;
}
logInfo(`📝 ${comments.length} comentários encontrados (GLPI ID: ${ticket.glpi_ticket_id})`);
await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'comment_collected', 'pending_cmt_check');
for (const comment of comments) { for (const comment of comments) {
try {
// DEBUG: Mostra antes e depois const sanitized = sanitizeGLPIComment(comment);
logInfo(`🔍 Processando comentário GLPI ID: ${comment.id}`);
logInfo(`📝 CONTEÚDO ORIGINAL: ${comment.content.substring(0, 200)}...`);
// Sanitiza o comentário
const sanitizedContent = sanitizeGLPIComment(comment);
logInfo(`✨ CONTEÚDO SANITIZADO: ${sanitizedContent.substring(0, 200)}...`);
const existingComment = await TicketUpdateModel.getCommentSync(ticket.glpi_ticket_id, comment.id); const existingComment = await TicketUpdateModel.getCommentSync(ticket.glpi_ticket_id, comment.id);
const existsInSN = await existingCommentInServiceNow(ticket.sn_ticket_id, sanitized);
const syncId = await TicketSyncModel.getIdSyncByGlpiId(ticket.glpi_ticket_id);
const commentExistsInSN = await existingCommentInServiceNow( // Caso 1: já sincronizado
ticket.sn_ticket_id, if (existingComment && existsInSN) {
sanitizedContent // ✅ Usa o conteúdo sanitizado logInfo(`🔁 Comentário ${comment.id} já sincronizado.`);
); await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'synced', 'synced');
if (existingComment && commentExistsInSN) {
logInfo(`Comentário ID ${comment.id} já sincronizado no banco de dados e no ServiceNow`, { step: 3 });
continue; continue;
} }
if (!existingComment && commentExistsInSN) { // Caso 2: está no SN mas não no banco
// Só insere no banco, pois já está no ServiceNow if (!existingComment && existsInSN) {
const sync_id = await TicketSyncModel.getIdSyncByGlpiId(ticket.glpi_ticket_id); await TicketUpdateModel.insert({
const commentData = { ticket_sync_id: syncId,
ticket_sync_id: sync_id,
source_system: 'glpi', source_system: 'glpi',
content: sanitizedContent, // ✅ Usa o conteúdo sanitizado content: sanitized,
created_at: new Date(comment.date_creation), created_at: new Date(comment.date_creation),
author: 'Sothis',
updated_at: new Date(comment.date_mod), updated_at: new Date(comment.date_mod),
source_id: comment.id, source_id: comment.id,
destiny_id: commentExistsInSN.destiny_id destiny_id: existsInSN.destiny_id
}; });
const insertResult = await TicketUpdateModel.insert(commentData); await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'synced', 'synced');
if (insertResult.success) {
logInfo(`✅ Comentário ID ${comment.id} inserido no banco! Novo ID: ${insertResult.newId}`, { step: 4 });
} else {
logError(`❌ Erro ao inserir comentário ID ${comment.id} no banco: ${insertResult.error}`, { step: 4 });
}
continue; continue;
} }
if (existingComment && !commentExistsInSN) { // Caso 3: existe no banco mas falta no SN
logInfo(`Comentário ID ${comment.id} já existe no banco, mas não no ServiceNow. Sincronizando...`, { step: 3 }); if (existingComment && !existsInSN) {
// Só sincroniza no ServiceNow await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'synced', 'pending_cmt_sync');
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitizedContent); // ✅ Usa o conteúdo sanitizado const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitized);
logInfo(`✅ Comentário ID ${comment.id} sincronizado no ServiceNow!`, { step: 4 }); await TicketUpdateModel.updateDestinyId(comment.id, snCommentId);
const updateData = await TicketUpdateModel.updateDestinyId(comment.id, snCommentId);
if (updateData && updateData.success) {
logInfo(`✅ ID do comentário de destino atualizado no banco: ${snCommentId}`, { step: 4 });
}
continue; continue;
} }
// Se não existe em nenhum dos dois, sincroniza nos dois normalmente // Caso 4: não existe em nenhum lugar → sincroniza ambos
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitizedContent); // ✅ Usa o conteúdo sanitizado await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'pending_cmt_sync', 'pending_cmt_sync');
const sync_id = await TicketSyncModel.getIdSyncByGlpiId(ticket.glpi_ticket_id); const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitized);
const commentData = { await TicketUpdateModel.insert({
ticket_sync_id: sync_id, ticket_sync_id: syncId,
source_system: 'glpi', source_system: 'glpi',
content: sanitizedContent, // ✅ Usa o conteúdo sanitizado content: sanitized,
created_at: new Date(comment.date_creation), created_at: new Date(comment.date_creation),
updated_at: new Date(comment.date_mod), updated_at: new Date(comment.date_mod),
source_id: comment.id, source_id: comment.id,
destiny_id: snCommentId destiny_id: snCommentId
}; });
const insertResult = await TicketUpdateModel.insert(commentData); await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'synced', 'synced');
if (insertResult.success) {
logInfo(`✅ Comentário ID ${comment.id} sincronizado no banco e no ServiceNow! Novo ID: ${insertResult.newId}`, { step: 4 });
} else {
logError(`❌ Erro ao sincronizar comentário ID ${comment.id}: ${insertResult.error}`, { step: 4 });
}
const hasError = !insertResult.success; } catch (err) {
const snStatus = 'pending'; logError(`Erro ao sincronizar comentário ${comment.id}: ${err}`);
const glpiStatus = hasError ? 'Error' : 'updated'; await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'error', 'error');
await TicketSyncModel.updateStatus(ticket.id, glpiStatus, snStatus);
logInfo(`Status de sincronização para o ticket ID ${ticket.id} atualizado para GLPI: '${glpiStatus}', SN: '${snStatus}'.`);
} }
} }
logInfo('✅ Sincronização de comentários do GLPI -> ServiceNow concluída!'); logInfo('✅ Sincronização de comentários GLPI → SN concluída!');
} catch (error) { } catch (error) {
logError(error, 'GLPI Tickets'); logError(`Erro geral na sincronização: ${error}`);
} }
} };
const syncCommentsSNtoGlpi = async () => {
const syncCommentsSNtoGlpi = async (comments, ticket) => {
logInfo('🔄 Iniciando sincronização de comments do ServiceNow para o GLPI...');
for (const comment of comments) {
try { try {
logInfo('🔄 Iniciando sincronização de comments do Service Now para o GLPI...'); const commentId = await TicketGlpiModel.insertComment(comment, ticket.glpi_ticket_id);
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'synced');
await TicketUpdateModel.updateDestinyId(comment.sys_id, commentId);
logInfo(`✅ Comentário inserido com sucesso no GLPI para o chamado: ${ticket.glpi_ticket_id}`);
} catch (error) {
logError(`⚠️ Erro ao processar comentário do ticket ${ticket.sn_ticket_id}: ${error}`);
try {
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'error');
} catch (err2) {
logError(`Falha ao atualizar status de erro do ticket ${ticket.sn_ticket_id}: ${err2}`);
} }
catch (error) {
logError(error, 'ServiceNow Comments');
} }
} }
};

View File

@ -37,29 +37,25 @@ const processSingleTicket = async (ticket) => {
try { try {
logInfo(`🔍 Processando ticket: ${ticket.ticket_number}`); logInfo(`🔍 Processando ticket: ${ticket.ticket_number}`);
// 1. Verificar se já existe no GLPI // 1. Verifica se o ticket já possui um ID do GLPI no nosso banco de sincronização.
const existingTicket = await TicketGlpiModel.ticketExists(ticket.ticket_number); if (ticket.glpi_ticket_id) {
const idSyncRecord = await TicketSyncModel.getByTicketNumber(ticket.ticket_number); logInfo(`✅ Ticket ${ticket.ticket_number} já possui GLPI ID (${ticket.glpi_ticket_id}) registrado. Apenas atualizando status.`);
await TicketSyncModel.updateStatus(ticket.id, 'synced', 'synced');
if (existingTicket && idSyncRecord) {
await TicketSyncModel.updateGlpiSyncStatus(idSyncRecord.id, 'synced', existingTicket);
await TicketSyncModel.updateSnSyncStatus(idSyncRecord.id, 'synced');
return; return;
}else if (existingTicket && !idSyncRecord) {
await TicketSyncModel.createSyncRecord({
sn_ticket_id: ticket.id,
glpi_ticket_id: existingTicket,
sn_sync_status: 'synced',
glpi_sync_status: 'synced',
last_glpi_sync: new Date()
});
return;
} }
// 2. Se não tem ID local, verifica se o ticket existe no GLPI (buscando pelo número).
const existingTicket = await TicketGlpiModel.ticketExists(ticket.ticket_number);
// 2. Converter dados para formato GLPI if (existingTicket) {
logInfo(`✅ Ticket ${ticket.ticket_number} encontrado no GLPI. Atualizando registro local com o ID: ${existingTicket}`);
// Atualiza nosso registro de sincronização com o ID do GLPI encontrado.
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', existingTicket);
logInfo(`✅ Ticket ${ticket.ticket_number} já existe no GLPI! GLPI ID: ${existingTicket}`);
return;
}
// 3. Converter dados para formato GLPI
const glpiData = { const glpiData = {
ticket_number: ticket.ticket_number, ticket_number: ticket.ticket_number,
short_description: ticket.short_description, short_description: ticket.short_description,
@ -80,8 +76,8 @@ const processSingleTicket = async (ticket) => {
// 3. Inserir no GLPI // 3. Inserir no GLPI
const glpiTicketId = await TicketGlpiModel.createTicket(glpiData); const glpiTicketId = await TicketGlpiModel.createTicket(glpiData);
// 4. Criar registro de sincronização // 4. Atualizar registro de sincronização
await TicketSyncModel.updateGlpiSyncStatus(idSyncRecord.id, 'synced', glpiTicketId); await TicketSyncModel.updateStatus(ticket.id, 'synced', 'synced');
logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`); logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`);
@ -89,18 +85,12 @@ const processSingleTicket = async (ticket) => {
logError(error, `❌ Erro no ticket ${ticket.ticket_number}`); logError(error, `❌ Erro no ticket ${ticket.ticket_number}`);
// Se temos um registro de sync, atualizar o status para error // Se temos um registro de sync, atualizar o status para error
if (syncRecord) {
await TicketSyncModel.updateGlpiSyncStatus(idSyncRecord.id, 'error');
} else {
// Se não temos um registro, criar um com status error
try { try {
syncRecord = await TicketSyncModel.createSyncRecord(ticket.id); await TicketSyncModel.updateStatus(idSyncRecord.id, 'error', 'synced');
await TicketSyncModel.updateGlpiSyncStatus(idSyncRecord.id, 'error');
} catch (innerError) { } catch (innerError) {
logError(innerError, `❌ Erro ao registrar falha de sincronização para ticket ${ticket.id}`); logError(innerError, `❌ Erro ao registrar falha de sincronização para ticket ${ticket.id}`);
} }
} }
}
}; };
module.exports = { module.exports = {

View File

@ -18,10 +18,8 @@ const fetchTicketsFromServiceNow = async () => {
logInfo('💾 Iniciando salvamento de incidentes no banco...'); logInfo('💾 Iniciando salvamento de incidentes no banco...');
for (const ticket of ticketsResponse.data.result) { for (const ticket of ticketsResponse.data.result) {
//Insere no banco de dados como Incidente //Insere no banco de dados como Incidente
const idTableTicketSn =await TicketSnModel.saveTicket(ticket, 'incidente'); const idTableTicketSn = await TicketSnModel.saveTicket(ticket, 'incidente');
//Salvando atualização no banco intermediario e defininfo sync status //Salvando atualização no banco intermediario e defininfo sync status
const ticketStatus = ticket.state?.display_value; const ticketStatus = ticket.state?.display_value;
@ -42,12 +40,10 @@ const fetchTicketsFromServiceNow = async () => {
dataSync.sn_sync_status ='synced' dataSync.sn_sync_status ='synced'
dataSync.glpi_sync_status = 'pending_check' dataSync.glpi_sync_status = 'pending_check'
} }
//Cria apenas se não existir
await TicketSyncModel.createSyncRecord(dataSync); const exists = await TicketSyncModel.getIdSyncBySnId(idTableTicketSn);
if (!exists) await TicketSyncModel.createSyncRecord(dataSync);
else await TicketSyncModel.updateStatus(exists, 'synced', 'pending_check');
} }
logSync('ServiceNow', ticketsResponse.data.result.length, 'incidentes'); logSync('ServiceNow', ticketsResponse.data.result.length, 'incidentes');
@ -82,7 +78,7 @@ const fetchRequestsFromServiceNow = async () => {
updated_at: null updated_at: null
} }
if (ticketStatus === 'Encerrado' || ticketStatus === 'Encerrado - Omitido') { if (request.state?.display_value === 'Encerrado' || request.state?.display_value === 'Encerrado - Omitido') {
dataSync.sn_sync_status ='closed' dataSync.sn_sync_status ='closed'
dataSync.glpi_sync_status = 'ignored' dataSync.glpi_sync_status = 'ignored'
} else { } else {
@ -103,7 +99,6 @@ const fetchRequestsFromServiceNow = async () => {
} }
// Função para obter o sys_id de um ticket pela tabela de sincronização do banco de dados local // Função para obter o sys_id de um ticket pela tabela de sincronização do banco de dados local
const getTicketSysId = async (ticketId) => { const getTicketSysId = async (ticketId) => {
try { try {
const ticket = await TicketSnModel.findById(ticketId); const ticket = await TicketSnModel.findById(ticketId);
@ -114,6 +109,7 @@ const getTicketSysId = async (ticketId) => {
} }
} }
// Função para obter o tipo de um ticket pela tabela de sincronização do banco de dados local
const getTicketTypeById = async (ticketId) => { const getTicketTypeById = async (ticketId) => {
try { try {
const ticket = await TicketSnModel.findById(ticketId); const ticket = await TicketSnModel.findById(ticketId);
@ -129,21 +125,6 @@ const getTicketTypeById = async (ticketId) => {
// Função para obter o código de um comentário dentro de um ticket do ServiceNow // Função para obter o código de um comentário dentro de um ticket do ServiceNow
const getCommentId = async (sysId, comment) => { const getCommentId = async (sysId, comment) => {
try { try {
// Exemplo de requisição GET para buscar comentários:
// GET https://caoadev.service-now.com/api/now/table/sys_journal_field?sysparm_query=element_id=<SYS_ID_DO_INCIDENTE>^element=comments^ORDERBYDESCsys_created_on&sysparm_fields=sys_id,element,value,sys_created_on,sys_created_by,element_id&sysparm_limit=1
// exemplo de resposta da API:
/*
"result": [
{
"sys_id": "SYS_ID_DO_COMENTARIO",
"sys_created_on": "2025-10-02 12:24:19",
"element_id": "SYS_ID_DO_INCIDENTE",
"value": "Teste fora do código",
"sys_created_by": "SOTHIS.CAOA",
"element": "comments"
}*/
const url = `${apiConfig.snTableJournalConfig.baseUrl}?sysparm_query=element_id=${sysId}^element=comments^ORDERBYDESCsys_created_on&sysparm_fields=sys_id,element,value,sys_created_on,sys_created_by,element_id`; const url = `${apiConfig.snTableJournalConfig.baseUrl}?sysparm_query=element_id=${sysId}^element=comments^ORDERBYDESCsys_created_on&sysparm_fields=sys_id,element,value,sys_created_on,sys_created_by,element_id`;
@ -166,15 +147,9 @@ const getCommentId = async (sysId, comment) => {
return null; return null;
} }
} }
// Função para criar um comentário em um ticket no ServiceNow // Função para criar um comentário em um ticket no ServiceNow
const createCommentInServiceNow = async (sysId, comment, ticketType) => { const createCommentInServiceNow = async (sysId, comment, ticketType) => {
try { try {
// Exemplo de requisição PATCH para atualizar um ticket:
// curl -u user:pass \
// -H "Content-Type: application/json" \
// -X PATCH "https://SEU_INSTANCE.service-now.com/api/now/table/incident/{SYS_ID_DO_INCIDENTE}" \
// -d '{"comments":"Estamos analisando seu chamado. Retorno em breve."}'
let url = ''; let url = '';
let payload = {}; let payload = {};
if (ticketType == 'incidente') { if (ticketType == 'incidente') {
@ -219,6 +194,7 @@ const createCommentInServiceNow = async (sysId, comment, ticketType) => {
} }
} }
// Função para criar um comentário
const syncCommentToServiceNow = async (ticketId, comment) => { const syncCommentToServiceNow = async (ticketId, comment) => {
logInfo(`Iniciando sincronização do comentário para o ticket SN Ticket: ${ticketId}`); logInfo(`Iniciando sincronização do comentário para o ticket SN Ticket: ${ticketId}`);
const snSysId = await getTicketSysId(ticketId); const snSysId = await getTicketSysId(ticketId);
@ -233,7 +209,7 @@ const syncCommentToServiceNow = async (ticketId, comment) => {
} }
// Função que verifica se o comentário já existe no ServiceNow antes de criar // Função que verifica se o comentário já existe no ServiceNow antes de criar
const existingCommentInServiceNow = async (ticketId, comment) => { const existingCommentInServiceNow = async (ticketId, comment) => {
try { try {
const sys_id = await getTicketSysId(ticketId); const sys_id = await getTicketSysId(ticketId);
const commentExists = await getCommentId(sys_id, comment); const commentExists = await getCommentId(sys_id, comment);
@ -248,27 +224,16 @@ const syncCommentToServiceNow = async (ticketId, comment) => {
// Função para buscar todos os comentários de tickets no ServiceNow a partir do ticket id no banco local // Função para buscar todos os comentários de tickets no ServiceNow a partir do ticket id no banco local
// Deve-se atentar para a paginação da API do ServiceNow // Deve-se atentar para a paginação da API do ServiceNow
// para fins de teste, gerar um arquivo JSON com os comentários de um ticket específico // para fins de teste, gerar um arquivo JSON com os comentários de um ticket específico
const fetchCommentsFromServiceNow = async (ticket) => {
const fetchCommentsFromServiceNow = async () => {
try { try {
const ticketsSyncList = await TicketSyncModel.getTicketsToSync(); logInfo(`Iniciando busca de comentários do ServiceNow para o ticket SN Ticket: ${ticket.glpi_ticket_id}`);
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'pending_cmt_check');
const sys_id = ticket.sys_id;
console.log(`Iniciando busca de comentários do ServiceNow para o ticket SN Ticket: ${ticketId}`);
//Coleta o sys id no bando intermediario para busca no service now
const sys_id = await getTicketSysId(ticketId);
//Se não encontrar é impossível atualizar
if (!sys_id) {
logError('sys_id não encontrado para ticket', { ticketId });
return [];
}
const allComments = []; const allComments = [];
const pageSize = 100; const pageSize = 100;
let offset = 0; let offset = 0;
@ -283,8 +248,7 @@ const fetchCommentsFromServiceNow = async () => {
sysparm_limit: pageSize, sysparm_limit: pageSize,
sysparm_offset: offset sysparm_offset: offset
}; };
logInfo(`Buscando página ${page} de comentários do ticket SN Ticket: ${ticket.glpi_ticket_id}`);
logInfo(`Buscando página ${page} de comentários do ticket SN Ticket: ${ticketId}`, { ticketId, page });
const response = await axios.get(apiConfig.snTableJournalConfig.baseUrl, { const response = await axios.get(apiConfig.snTableJournalConfig.baseUrl, {
auth: apiConfig.servicenowAuthentication.auth, auth: apiConfig.servicenowAuthentication.auth,
params params
@ -312,39 +276,35 @@ const fetchCommentsFromServiceNow = async () => {
break; break;
} }
} }
logInfo(`Total de comentários encontrados para o ticket SN Ticket ${ticketId}: ${allComments.length} comentários`, { ticketId, count: allComments.length });
// salvar em arquivo para análise // Traz apenas os tickets que não são do sys_created_by: "SOTHIS.CAOA"
const filename = `servicenow_comments_ticket_${ticketId}.json`;
// Salva no arquivo
fs.writeFileSync(filename, JSON.stringify(allComments, null, 2));
//Trata os dados para inserir no banco intermediario a
// as os tickets que não são do sys_created_by: "SOTHIS.CAOA"
const filteredComments = allComments.filter(comment => comment.sys_created_by !== 'SOTHIS.CAOA'); const filteredComments = allComments.filter(comment => comment.sys_created_by !== 'SOTHIS.CAOA');
if (filteredComments.length === 0) {
TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced ', 'synced');
logInfo(`Nenhum comentário encontrado no Service Now para o ticket GLPI ID: ${ticket.glpi_ticket_id}`);
return [];
}
logInfo(`Encontrado ${filteredComments.length} comentários para o ticket SN Ticket: ${ticket.glpi_ticket_id}`)
const commentsInserted = [];
// Insere cada comment no banco intermediario // Insere cada comment no banco intermediario
for (const comment of filteredComments) { for (const comment of filteredComments) {
const existingComment = await TicketUpdateModel.getCommentSync(ticketId, comment.sys_id); await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'pendind_cmt_sync');
const existingComment = await TicketUpdateModel.getCommentSync(comment.sys_id);
if (existingComment) { if (existingComment) {
TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'synced');
logInfo(`Comentário: ${comment.sys_id} já existe no banco de dados`, { step: 3 }); logInfo(`Comentário: ${comment.sys_id} já existe no banco de dados`, { step: 3 });
continue; continue;
} else { } else {
console.log(comment);
const ticketSnId = await TicketSyncModel.getIdSyncBySnId(ticketId);
const ticketSyncId = await TicketSyncModel.getIdSyncBySnId(ticketId);
const commentData = { const commentData = {
ticket_sync_id: parseInt(ticketSnId), ticket_sync_id: ticket.id,
ticket_sync_id: ticketSyncId,
source_system: 'servicenow', source_system: 'servicenow',
content: comment.value, content: comment.value,
created_at: new Date(comment.sys_created_on), created_at: new Date(comment.sys_created_on),
@ -354,38 +314,22 @@ const fetchCommentsFromServiceNow = async () => {
destiny_id: null destiny_id: null
}; };
commentInserted = await TicketUpdateModel.insert(commentData);
if (!ticketSyncId) {
logError(`❌ TicketSyncId não encontrado para o ticket SN ID: ${ticketId}`, { step: 4 });
continue;
}
const commentInserted = await TicketUpdateModel.insert(commentData); const commentInserted = await TicketUpdateModel.insert(commentData);
if (commentInserted.success) { if (commentInserted.success) {
// Insere Glpi status como await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'pendind_cmt_sync', 'synced');
// ding e SN status como synced commentsInserted.push(commentInserted);
await TicketSyncModel.updateStatus(ticketGlpiId.id, ' logInfo(`✅ Comentário ID ${comment.sys_id} do ${comment.source_system} inserido no banco!`, { step: 4 });
ding', 'synced');
// Atualiza o status para indicar que o SN está sincronizado e o GLPI está
// dente de atualização
await TicketSyncModel.updateStatus(ticketSyncId, '
ding', 'synced');
logInfo(`✅ Comentário ID ${comment.sys_id} inserido no banco! Novo ID: ${commentInserted.newId}`, { step: 4 });
} else { } else {
logError(`❌ Erro ao inserir comentário ID ${comment.sys_id} no banco: ${commentInserted.error}`, { step: 4 }); logError(`❌ Erro ao inserir comentário ID ${comment.sys_id} no banco: ${commentInserted.error}`, { step: 4 });
return [];
} }
if (!ticketSyncId) {
logError(`❌ TicketSyncId não encontrado para o ticket GLPI ID: ${ticketId}`, { step: 4 });
continue;
} else {
continue;
} }
} }
}
logInfo(`💾 Backup JSON de comentários salvo: ${filename}`, { count: allComments.length });
} catch (error) { } catch (error) {
logError(error, '🚨Falha ao buscar comentários do ServiceNow'); logError(error, '🚨Falha ao buscar comentários do ServiceNow');
return []; return [];