FEAT: Sistema de sincroanização de tickets refeito para melhor sincronia e garantia dos dados
This commit is contained in:
parent
4f7a699f92
commit
f7eaf34e7d
@ -1,4 +1,5 @@
|
||||
// src/app.js
|
||||
require('dotenv').config();
|
||||
const { processTicketsController } = require('./controllers/processTicketsController');
|
||||
const { processCommentsController } = require('./controllers/processCommentsController');
|
||||
const { logInfo } = require('./utils/logger');
|
||||
@ -10,7 +11,7 @@ logInfo('Aplicação iniciada', {
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await processTicketsController();
|
||||
//await processTicketsController();
|
||||
await processCommentsController();
|
||||
} catch (error) {
|
||||
logInfo('Erro na aplicação:', error);
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
const {fetchCommentsFromServiceNow } = require('../services/servicenowService');
|
||||
const { syncCommentsGlpitoSN } = require('../services/glpiCommentService');
|
||||
const { syncCommentsGlpitoSN, syncCommentsSNtoGlpi } = require('../services/glpiCommentService');
|
||||
const TicketSyncModel = require('../models/ticketSyncModel');
|
||||
|
||||
|
||||
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 = {
|
||||
processCommentsController
|
||||
}
|
||||
@ -249,27 +249,27 @@ class TicketGlpiModel {
|
||||
static async ticketExists(snTicketNumber) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT id FROM glpi_tickets
|
||||
WHERE name LIKE ? OR content LIKE ?
|
||||
LIMIT 1
|
||||
|
||||
SELECT id FROM glpi_tickets WHERE name LIKE ? OR content LIKE ? LIMIT 1
|
||||
`;
|
||||
|
||||
const [rows] = await glpiPool.execute(query, [
|
||||
const values = [
|
||||
`%${snTicketNumber}%`,
|
||||
`%${snTicketNumber}%`
|
||||
]);
|
||||
];
|
||||
|
||||
|
||||
const [rows] = await glpiPool.execute(query,values);
|
||||
|
||||
return rows.length > 0 ? rows[0].id : null;
|
||||
} catch (error) {
|
||||
logError(error, 'Erro ao verificar ticket existente');
|
||||
logError(`Erro ao verificar ticket existente: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Buscar followups (notas) de um ticket no GLPI
|
||||
static async getFollowupsByItemId(itemId) {
|
||||
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]);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
@ -278,8 +278,19 @@ 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
|
||||
|
||||
@ -88,7 +88,8 @@ class TicketSnModel {
|
||||
ts.opened_at,
|
||||
ts.tipo,
|
||||
ts.created_at,
|
||||
ts.updated_at
|
||||
ts.updated_at,
|
||||
tsync.glpi_ticket_id
|
||||
FROM tickets_sn ts
|
||||
LEFT JOIN ticket_sync tsync ON ts.id = tsync.sn_ticket_id
|
||||
WHERE tsync.glpi_sync_status = 'pending_check'
|
||||
|
||||
@ -8,6 +8,8 @@ class TicketSyncModel {
|
||||
// Cria um registro de sincronização para um ticket
|
||||
static async createSyncRecord(data) {
|
||||
try {
|
||||
|
||||
|
||||
const query = `INSERT INTO ticket_sync (
|
||||
sn_ticket_id,
|
||||
glpi_ticket_id,
|
||||
@ -19,7 +21,20 @@ class TicketSyncModel {
|
||||
glpi_sync_status
|
||||
) 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) {
|
||||
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
|
||||
static async getTicketsToSync() {
|
||||
static async getTicketsToSync(glpi_sync_status, sn_sync_status) {
|
||||
try {
|
||||
const query = 'SELECT id, glpi_ticket_id, sn_ticket_id FROM ticket_sync';
|
||||
const { rows } = await pool.query(query);
|
||||
const query = 'SELECT id, glpi_ticket_id, sn_ticket_id FROM ticket_sync WHERE glpi_sync_status = $1 AND sn_sync_status = $2';
|
||||
const values =[glpi_sync_status , sn_sync_status];
|
||||
const { rows } = await pool.query(query, values);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
logError(error, '❌ Erro ao buscar tickets para sincronizar');
|
||||
@ -43,15 +59,24 @@ class TicketSyncModel {
|
||||
// Atualiza o status de sincronização de um ticket
|
||||
static async updateStatus(ticketSyncId, glpiStatus, snStatus) {
|
||||
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]);
|
||||
} 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) {
|
||||
logError(error, `❌ Erro ao atualizar status de sincronização para o ID: ${ticketSyncId}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Buscar o ID de sincronização baseado no glpi_ticket_id
|
||||
static async getIdSyncByGlpiId(glpiTicketId) {
|
||||
try {
|
||||
@ -77,14 +102,38 @@ class TicketSyncModel {
|
||||
}
|
||||
static async getByTicketNumber(ticketNumber) {
|
||||
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) {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
/**
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
// src/models/ticketUpdateModel.js
|
||||
const ticketSyncModel = require('./ticketSyncModel');
|
||||
const pool = require('../data/database');
|
||||
const { logError } = require('../utils/logger');
|
||||
|
||||
|
||||
class TicketUpdateModel {
|
||||
|
||||
static async insert(updateData) {
|
||||
@ -17,7 +19,7 @@ class TicketUpdateModel {
|
||||
created_at = $6,
|
||||
updated_at = $7,
|
||||
destiny_id = $9
|
||||
RETURNING id;
|
||||
RETURNING *;
|
||||
`;
|
||||
const values = [
|
||||
updateData.ticket_sync_id,
|
||||
@ -42,7 +44,7 @@ class TicketUpdateModel {
|
||||
//Buscar o ID de sincronização baseado no source_id
|
||||
static async getIdSyncBySourceId(sourceId) {
|
||||
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]);
|
||||
return rows[0] ? rows[0].id : null;
|
||||
} catch (error) {
|
||||
@ -65,10 +67,9 @@ class TicketUpdateModel {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
static async getCommentSync(glpiTicketId, sourceId) {
|
||||
const ticketSyncModel = require('./ticketSyncModel');
|
||||
const ticketSyncId = await TicketUpdateModel.getIdSyncBySourceId(sourceId);
|
||||
static async getCommentSync(sourceId) {
|
||||
|
||||
const ticketSyncId = await TicketUpdateModel.getIdSyncBySourceId(sourceId);
|
||||
if (!ticketSyncId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -6,123 +6,110 @@ const TicketUpdateModel = require('../models/ticketUpdateModel');
|
||||
const { logInfo, logError } = require('../utils/logger');
|
||||
const { existingCommentInServiceNow, syncCommentToServiceNow } = require('./servicenowService');
|
||||
const { sanitizeGLPIComment } = require('../utils/commentSanitizer');
|
||||
const { log } = require('winston');
|
||||
|
||||
// Sincroniza comentários do GLPI para o Banco de Dados local
|
||||
const syncCommentsGlpitoSN = async () => {
|
||||
const syncCommentsGlpitoSN = async (ticket) => {
|
||||
try {
|
||||
|
||||
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);
|
||||
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) {
|
||||
|
||||
// DEBUG: Mostra antes e depois
|
||||
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)}...`);
|
||||
|
||||
try {
|
||||
const sanitized = sanitizeGLPIComment(comment);
|
||||
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(
|
||||
ticket.sn_ticket_id,
|
||||
sanitizedContent // ✅ Usa o conteúdo sanitizado
|
||||
);
|
||||
|
||||
if (existingComment && commentExistsInSN) {
|
||||
logInfo(`Comentário ID ${comment.id} já sincronizado no banco de dados e no ServiceNow`, { step: 3 });
|
||||
// Caso 1: já sincronizado
|
||||
if (existingComment && existsInSN) {
|
||||
logInfo(`🔁 Comentário ${comment.id} já sincronizado.`);
|
||||
await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'synced', 'synced');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existingComment && commentExistsInSN) {
|
||||
// Só insere no banco, pois já está no ServiceNow
|
||||
const sync_id = await TicketSyncModel.getIdSyncByGlpiId(ticket.glpi_ticket_id);
|
||||
const commentData = {
|
||||
ticket_sync_id: sync_id,
|
||||
// Caso 2: está no SN mas não no banco
|
||||
if (!existingComment && existsInSN) {
|
||||
await TicketUpdateModel.insert({
|
||||
ticket_sync_id: syncId,
|
||||
source_system: 'glpi',
|
||||
content: sanitizedContent, // ✅ Usa o conteúdo sanitizado
|
||||
content: sanitized,
|
||||
created_at: new Date(comment.date_creation),
|
||||
author: 'Sothis',
|
||||
updated_at: new Date(comment.date_mod),
|
||||
source_id: comment.id,
|
||||
destiny_id: commentExistsInSN.destiny_id
|
||||
};
|
||||
const insertResult = await TicketUpdateModel.insert(commentData);
|
||||
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 });
|
||||
}
|
||||
destiny_id: existsInSN.destiny_id
|
||||
});
|
||||
await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'synced', 'synced');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingComment && !commentExistsInSN) {
|
||||
logInfo(`Comentário ID ${comment.id} já existe no banco, mas não no ServiceNow. Sincronizando...`, { step: 3 });
|
||||
// Só sincroniza no ServiceNow
|
||||
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitizedContent); // ✅ Usa o conteúdo sanitizado
|
||||
logInfo(`✅ Comentário ID ${comment.id} sincronizado no ServiceNow!`, { step: 4 });
|
||||
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 });
|
||||
}
|
||||
// Caso 3: existe no banco mas falta no SN
|
||||
if (existingComment && !existsInSN) {
|
||||
await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'synced', 'pending_cmt_sync');
|
||||
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitized);
|
||||
await TicketUpdateModel.updateDestinyId(comment.id, snCommentId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Se não existe em nenhum dos dois, sincroniza nos dois normalmente
|
||||
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitizedContent); // ✅ Usa o conteúdo sanitizado
|
||||
const sync_id = await TicketSyncModel.getIdSyncByGlpiId(ticket.glpi_ticket_id);
|
||||
const commentData = {
|
||||
ticket_sync_id: sync_id,
|
||||
// Caso 4: não existe em nenhum lugar → sincroniza ambos
|
||||
await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'pending_cmt_sync', 'pending_cmt_sync');
|
||||
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitized);
|
||||
await TicketUpdateModel.insert({
|
||||
ticket_sync_id: syncId,
|
||||
source_system: 'glpi',
|
||||
content: sanitizedContent, // ✅ Usa o conteúdo sanitizado
|
||||
content: sanitized,
|
||||
created_at: new Date(comment.date_creation),
|
||||
updated_at: new Date(comment.date_mod),
|
||||
source_id: comment.id,
|
||||
destiny_id: snCommentId
|
||||
};
|
||||
const insertResult = await TicketUpdateModel.insert(commentData);
|
||||
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 });
|
||||
}
|
||||
});
|
||||
await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'synced', 'synced');
|
||||
|
||||
const hasError = !insertResult.success;
|
||||
const snStatus = 'pending';
|
||||
const glpiStatus = hasError ? 'Error' : 'updated';
|
||||
await TicketSyncModel.updateStatus(ticket.id, glpiStatus, snStatus);
|
||||
logInfo(`Status de sincronização para o ticket ID ${ticket.id} atualizado para GLPI: '${glpiStatus}', SN: '${snStatus}'.`);
|
||||
} catch (err) {
|
||||
logError(`Erro ao sincronizar comentário ${comment.id}: ${err}`);
|
||||
await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'error', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
logInfo('✅ Sincronização de comentários do GLPI -> ServiceNow concluída!');
|
||||
logInfo('✅ Sincronização de comentários GLPI → SN concluída!');
|
||||
} catch (error) {
|
||||
logError(error, 'GLPI Tickets');
|
||||
}
|
||||
logError(`Erro geral na sincronização: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
const syncCommentsSNtoGlpi = async () => {
|
||||
try {
|
||||
|
||||
const syncCommentsSNtoGlpi = async (comments, ticket) => {
|
||||
logInfo('🔄 Iniciando sincronização de comments do ServiceNow para o GLPI...');
|
||||
|
||||
for (const comment of comments) {
|
||||
try {
|
||||
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(error, 'ServiceNow Comments');
|
||||
} 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -37,29 +37,25 @@ const processSingleTicket = async (ticket) => {
|
||||
try {
|
||||
logInfo(`🔍 Processando ticket: ${ticket.ticket_number}`);
|
||||
|
||||
// 1. Verificar se já existe no GLPI
|
||||
const existingTicket = await TicketGlpiModel.ticketExists(ticket.ticket_number);
|
||||
const idSyncRecord = await TicketSyncModel.getByTicketNumber(ticket.ticket_number);
|
||||
|
||||
|
||||
if (existingTicket && idSyncRecord) {
|
||||
await TicketSyncModel.updateGlpiSyncStatus(idSyncRecord.id, 'synced', existingTicket);
|
||||
await TicketSyncModel.updateSnSyncStatus(idSyncRecord.id, 'synced');
|
||||
// 1. Verifica se o ticket já possui um ID do GLPI no nosso banco de sincronização.
|
||||
if (ticket.glpi_ticket_id) {
|
||||
logInfo(`✅ Ticket ${ticket.ticket_number} já possui GLPI ID (${ticket.glpi_ticket_id}) registrado. Apenas atualizando status.`);
|
||||
await TicketSyncModel.updateStatus(ticket.id, 'synced', 'synced');
|
||||
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 = {
|
||||
ticket_number: ticket.ticket_number,
|
||||
short_description: ticket.short_description,
|
||||
@ -80,8 +76,8 @@ const processSingleTicket = async (ticket) => {
|
||||
// 3. Inserir no GLPI
|
||||
const glpiTicketId = await TicketGlpiModel.createTicket(glpiData);
|
||||
|
||||
// 4. Criar registro de sincronização
|
||||
await TicketSyncModel.updateGlpiSyncStatus(idSyncRecord.id, 'synced', glpiTicketId);
|
||||
// 4. Atualizar registro de sincronização
|
||||
await TicketSyncModel.updateStatus(ticket.id, 'synced', 'synced');
|
||||
|
||||
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}`);
|
||||
|
||||
// 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 {
|
||||
syncRecord = await TicketSyncModel.createSyncRecord(ticket.id);
|
||||
await TicketSyncModel.updateGlpiSyncStatus(idSyncRecord.id, 'error');
|
||||
await TicketSyncModel.updateStatus(idSyncRecord.id, 'error', 'synced');
|
||||
} catch (innerError) {
|
||||
logError(innerError, `❌ Erro ao registrar falha de sincronização para ticket ${ticket.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
@ -18,8 +18,6 @@ const fetchTicketsFromServiceNow = async () => {
|
||||
logInfo('💾 Iniciando salvamento de incidentes no banco...');
|
||||
for (const ticket of ticketsResponse.data.result) {
|
||||
|
||||
|
||||
|
||||
//Insere no banco de dados como Incidente
|
||||
const idTableTicketSn = await TicketSnModel.saveTicket(ticket, 'incidente');
|
||||
|
||||
@ -42,12 +40,10 @@ const fetchTicketsFromServiceNow = async () => {
|
||||
dataSync.sn_sync_status ='synced'
|
||||
dataSync.glpi_sync_status = 'pending_check'
|
||||
}
|
||||
|
||||
await TicketSyncModel.createSyncRecord(dataSync);
|
||||
|
||||
|
||||
|
||||
|
||||
//Cria apenas se não existir
|
||||
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');
|
||||
@ -82,7 +78,7 @@ const fetchRequestsFromServiceNow = async () => {
|
||||
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.glpi_sync_status = 'ignored'
|
||||
} 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
|
||||
|
||||
const getTicketSysId = async (ticketId) => {
|
||||
try {
|
||||
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) => {
|
||||
try {
|
||||
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
|
||||
const getCommentId = async (sysId, comment) => {
|
||||
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`;
|
||||
|
||||
@ -166,15 +147,9 @@ const getCommentId = async (sysId, comment) => {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Função para criar um comentário em um ticket no ServiceNow
|
||||
const createCommentInServiceNow = async (sysId, comment, ticketType) => {
|
||||
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 payload = {};
|
||||
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) => {
|
||||
logInfo(`Iniciando sincronização do comentário para o ticket SN Ticket: ${ticketId}`);
|
||||
const snSysId = await getTicketSysId(ticketId);
|
||||
@ -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
|
||||
// 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
|
||||
|
||||
const fetchCommentsFromServiceNow = async () => {
|
||||
const fetchCommentsFromServiceNow = async (ticket) => {
|
||||
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 pageSize = 100;
|
||||
let offset = 0;
|
||||
@ -283,8 +248,7 @@ const fetchCommentsFromServiceNow = async () => {
|
||||
sysparm_limit: pageSize,
|
||||
sysparm_offset: offset
|
||||
};
|
||||
|
||||
logInfo(`Buscando página ${page} de comentários do ticket SN Ticket: ${ticketId}`, { ticketId, page });
|
||||
logInfo(`Buscando página ${page} de comentários do ticket SN Ticket: ${ticket.glpi_ticket_id}`);
|
||||
const response = await axios.get(apiConfig.snTableJournalConfig.baseUrl, {
|
||||
auth: apiConfig.servicenowAuthentication.auth,
|
||||
params
|
||||
@ -312,39 +276,35 @@ const fetchCommentsFromServiceNow = async () => {
|
||||
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
|
||||
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"
|
||||
// Traz apenas os tickets que não são do 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
|
||||
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) {
|
||||
TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'synced');
|
||||
logInfo(`Comentário: ${comment.sys_id} já existe no banco de dados`, { step: 3 });
|
||||
continue;
|
||||
} else {
|
||||
|
||||
console.log(comment);
|
||||
|
||||
const ticketSnId = await TicketSyncModel.getIdSyncBySnId(ticketId);
|
||||
const ticketSyncId = await TicketSyncModel.getIdSyncBySnId(ticketId);
|
||||
|
||||
|
||||
const commentData = {
|
||||
ticket_sync_id: parseInt(ticketSnId),
|
||||
ticket_sync_id: ticketSyncId,
|
||||
ticket_sync_id: ticket.id,
|
||||
source_system: 'servicenow',
|
||||
content: comment.value,
|
||||
created_at: new Date(comment.sys_created_on),
|
||||
@ -354,38 +314,22 @@ const fetchCommentsFromServiceNow = async () => {
|
||||
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);
|
||||
|
||||
if (commentInserted.success) {
|
||||
// Insere Glpi status como
|
||||
// ding e SN status como synced
|
||||
await TicketSyncModel.updateStatus(ticketGlpiId.id, '
|
||||
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 });
|
||||
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'pendind_cmt_sync', 'synced');
|
||||
commentsInserted.push(commentInserted);
|
||||
logInfo(`✅ Comentário ID ${comment.sys_id} do ${comment.source_system} inserido no banco!`, { step: 4 });
|
||||
|
||||
} else {
|
||||
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) {
|
||||
logError(error, '🚨Falha ao buscar comentários do ServiceNow');
|
||||
return [];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user