From 3c6765d7d745dde27c0679c439b35c2b36930fb9 Mon Sep 17 00:00:00 2001 From: Tulio Castro Date: Thu, 2 Oct 2025 10:16:56 -0300 Subject: [PATCH] =?UTF-8?q?FEAT:=20Adiciona=20nova=20URL=20de=20requisi?= =?UTF-8?q?=C3=A7=C3=A3o=20no=20.env,=20corrige=20e=20implementa=20fun?= =?UTF-8?q?=C3=A7=C3=B5es=20para=20sincroniza=C3=A7=C3=A3o=20de=20coment?= =?UTF-8?q?=C3=A1rios=20no=20ServiceNow.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 + config/apiConfig.js | 7 +- src/app.js | 1 - src/models/ticketUpdateModel.js | 31 +++++++- src/services/glpiCommentService.js | 65 +++++++++++------ src/services/servicenowService.js | 111 ++++++++++++++++++++++++----- 6 files changed, 177 insertions(+), 39 deletions(-) diff --git a/.env b/.env index 1e926fa..8494053 100644 --- a/.env +++ b/.env @@ -6,6 +6,7 @@ SERVICENOW_INCIDENT_URL="https://caoadev.service-now.com/api/now/table/incident?sysparm_display_value=all&sysparm_limit=20&sysparm_query=ORDERBYDESCsys_created_on&sysparm_fields=number%2state%2Copened_by%2Copened_at%2Clocation%2Cshort_description%2Cdescription%2Cvariables.caller_id%2Cvariables.location%2Cvariables.telephone_favorecido_rh%2Cvariables.numero_ramal%2Copened_by.email%2Csys_id%5Eassignment_group%3D46d44f91c611227500713c12f0b7c78d" SERVICENOW_REQUEST_URL="https://caoadev.service-now.com/api/now/table/sc_req_item?sysparm_fields=number%2Cstate%2Crequest%2Copened_by%2Copened_at%2Clocation%2Cshort_description%2Cdescription%2Cassignment_group%2Csys_id&sysparm_display_value=all&sysparm_query=assignment_group%3D1132c9e01bfad410e77eb9121b4bcbab" SERVICENOW_TABLE_INCIDENT_URL="https://caoadev.service-now.com/api/now/table/incident/" +SERVICENOW_TABLE_REQUEST_URL="https://caoadev.service-now.com/api/now/table/sc_req_item/" SERVICENOW_TABLE_JOURNAL_URL="https://caoadev.service-now.com/api/now/table/sys_journal_field" SERVICENOW_USERNAME=SOTHIS.CAOA SERVICENOW_PASSWORD=#Sothis20 diff --git a/config/apiConfig.js b/config/apiConfig.js index f112240..effa6a4 100644 --- a/config/apiConfig.js +++ b/config/apiConfig.js @@ -32,6 +32,10 @@ const snTableIncidentConfig = { baseUrl: process.env.SERVICENOW_TABLE_INCIDENT_URL, } +const snTableRequestConfig = { + baseUrl: process.env.SERVICENOW_TABLE_REQUEST_URL, +} + const snTableJournalConfig = { baseUrl: process.env.SERVICENOW_TABLE_JOURNAL_URL, } @@ -43,5 +47,6 @@ module.exports = { snRequestConfig, snTableIncidentConfig, snTableJournalConfig, - servicenowAuthentication + servicenowAuthentication, + snTableRequestConfig }; \ No newline at end of file diff --git a/src/app.js b/src/app.js index dfc9fc3..cd4a3fe 100644 --- a/src/app.js +++ b/src/app.js @@ -12,7 +12,6 @@ async function main() { try { //await snServices(); await glpiServices(); - logInfo('Aplicação finalizada com sucesso', { timestamp: new Date().toISOString() }); } catch (error) { logInfo('Erro na aplicação:', error); } diff --git a/src/models/ticketUpdateModel.js b/src/models/ticketUpdateModel.js index 17fcd02..6a68fe9 100644 --- a/src/models/ticketUpdateModel.js +++ b/src/models/ticketUpdateModel.js @@ -55,6 +55,35 @@ class TicketUpdateModel { return await this.getByTicketSyncIdAndSourceId(ticketSyncId, sourceId); } -} + static async getDestinyIdBySourceId(sourceId) { + try { + const query = ` + SELECT destiny_id FROM ticket_updates + WHERE source_id = $1 + `; + const { rows } = await pool.query(query, [sourceId]); + return rows[0] ? rows[0].destiny_id : null; + } catch (error) { + logError(error, '❌ Erro ao buscar destiny_id por source_id'); + throw error; + } + } + + static async updateDestinyId(sourceId, destinyId) { + try { + const query = ` + UPDATE ticket_updates + SET destiny_id = $1 + WHERE source_id = $2 + `; + await pool.query(query, [destinyId, sourceId]); + return { success: true, destiny_id: destinyId }; + } catch (error) { + logError(error, '❌ Erro ao atualizar destiny_id por source_id'); + throw error; + } + } + +} module.exports = TicketUpdateModel; \ No newline at end of file diff --git a/src/services/glpiCommentService.js b/src/services/glpiCommentService.js index e87050f..b33824d 100644 --- a/src/services/glpiCommentService.js +++ b/src/services/glpiCommentService.js @@ -4,23 +4,12 @@ const TicketGlpiModel = require('../models/ticketGlpiModel'); const TicketSyncModel = require('../models/ticketSyncModel'); const TicketUpdateModel = require('../models/ticketUpdateModel'); const { logInfo, logError } = require('../utils/logger'); -const { getTicketSysId, createCommentInServiceNow } = require('./servicenowService'); +const { existingCommentInServiceNow, syncCommentToServiceNow } = require('./servicenowService'); // Sincroniza comentários do GLPI para o Banco de Dados local const syncCommentsGlpi = async () => { try { - - // Função interna para sincronizar comentário no ServiceNow - const syncCommentToServiceNow = async (ticketNumber, comment) => { - logInfo(`Iniciando sincronização do comentário para o ticket SN Ticket: ${ticketNumber}`); - const snSysId = await getTicketSysId(ticketNumber); - if (!snSysId) { - logError(`❌ sys_id do ServiceNow não encontrado para o ticket SN Ticket: ${ticketNumber}`); - return null; - } - return await createCommentInServiceNow(snSysId, comment.content); - }; logInfo('🔄 Iniciando sincronização de comments do GLPI para o Service Now...'); const effectiveTickets = await TicketSyncModel.getTicketsToSync(); @@ -36,15 +25,53 @@ const syncCommentsGlpi = async () => { const existingComment = await TicketUpdateModel.getCommentSync(ticket.glpi_ticket_id, comment.id); - if (existingComment) { - logInfo(`Comentário ID ${comment.id} já sincronizado, pulando...`, { step: 3 }); + const commentExistsInSN = await existingCommentInServiceNow( + ticket.sn_ticket_id, + comment.content + ) + + if (existingComment && commentExistsInSN) { + logInfo(`Comentário ID ${comment.id} já sincronizado no banco de dados e no ServiceNow`, { step: 3 }); 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, + source_system: 'glpi', + content: comment.content, + created_at: new Date(comment.date_creation), + 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 }); + } + 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, comment.content); + // Se quiser, pode atualizar o registro no banco com o id do ServiceNow aqui + 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 }); + } + continue; + } + + // Se não existe em nenhum dos dois, sincroniza nos dois normalmente const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, comment); - const sync_id = await TicketSyncModel.getIdSyncByGlpiId(ticket.glpi_ticket_id); - const commentData = { ticket_sync_id: sync_id, source_system: 'glpi', @@ -54,11 +81,9 @@ const syncCommentsGlpi = async () => { source_id: comment.id, destiny_id: snCommentId }; - - const insertResult = await TicketUpdateModel.insert(commentData); if (insertResult.success) { - logInfo(`✅ Comentário ID ${comment.id} sincronizado com sucesso! Novo ID: ${insertResult.newId}`, { step: 4 }); + 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 }); } @@ -71,7 +96,7 @@ const syncCommentsGlpi = async () => { } } - logInfo('✅ Sincronização de comentários do GLPI concluída!'); + logInfo('✅ Sincronização de comentários do GLPI -> ServiceNow concluída!'); }catch (error) { logError(error, 'GLPI Tickets'); } diff --git a/src/services/servicenowService.js b/src/services/servicenowService.js index f29a039..639f489 100644 --- a/src/services/servicenowService.js +++ b/src/services/servicenowService.js @@ -58,9 +58,9 @@ 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 (ticketNumber) => { +const getTicketSysId = async (ticketId) => { try { - const ticket = await TicketSnModel.findByTicketNumber(ticketNumber); + const ticket = await TicketSnModel.findById(ticketId); return ticket.sys_id; } catch (error) { logError(error, '🚨Falha ao obter sys_id do ticket'); @@ -68,23 +68,51 @@ const getTicketSysId = async (ticketNumber) => { } } +const getTicketTypeById = async (ticketId) => { + try { + const ticket = await TicketSnModel.findById(ticketId); + const ticketType = ticket.tipo; + logInfo(`⚠️ Tipo do ticket ${ticketId} - Tipo: ${ticketType}`, { ticketId, ticketType }); + return ticketType; + } catch (error) { + logError(error, '🚨Falha ao obter tipo do ticket'); + return null; + } +} + // 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://SEU_INSTANCE.service-now.com/api/now/table/sys_journal_field?sysparm_query=name=incident^element=comments^element_id={SYS_ID_DO_INCIDENTE}&sysparm_fields=sys_id,sys_created_on,sys_created_by,value&sysparm_orderby_desc=sys_created_on&sysparm_limit=1&sysparm_orderby_desc=sys_created_on&sysparm_limit=1 - const url = `${apiConfig.snTableJournalConfig.baseUrl}?sysparm_query=name=incident^element=comments^element_id=${sysId}&sysparm_fields=sys_id,sys_created_on,sys_created_by,value&sysparm_orderby_desc=sys_created_on&sysparm_limit=1`; + //https://caoadev.service-now.com/api/now/table/sys_journal_field?sysparm_query=element_id=493386721b2c9a90c5e163923b4bcb4a^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": "5070ca12fb107290ef1ffe316eefdc28", + "sys_created_on": "2025-10-02 12:24:19", + "element_id": "493386721b2c9a90c5e163923b4bcb4a", + "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 response = await axios.get(url, { auth: apiConfig.servicenowAuthentication.auth }); - for (const entry of response.data.result) { - if (entry.value.includes(comment)) { - return entry.sys_id; - } else { - logInfo('Comentário não encontrado no ticket', { sysId }); + const data = response.data; + const comments = data.result; + + // Procurar o comentário exato na lista retornada + for (const item of comments) { + if (item.value && item.value.includes(comment)) { + return item.sys_id; // Retorna o sys_id do comentário encontrado } } @@ -95,27 +123,50 @@ const getCommentId = async (sysId, comment) => { } // Função para criar um comentário em um ticket no ServiceNow -const createCommentInServiceNow = async (sysId, comment) => { +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') { + url = `${apiConfig.snTableIncidentConfig.baseUrl}${sysId}`; + payload = { + comments: comment + }; + } else if (ticketType == 'requisicao') { + url = `${apiConfig.snTableRequestConfig.baseUrl}${sysId}`; + payload = { + comments: comment + }; + } else { + logError(`Tipo de ticket desconhecido: ${ticketType}`, { sysId, ticketType }); + return null; + } - const url = `${apiConfig.snTableIncidentConfig.baseUrl}${sysId}`; - const payload = { - comments: comment - }; - - await axios.patch(url, payload, { + const response = await axios.patch(url, payload, { auth: apiConfig.servicenowAuthentication.auth }); - logInfo('💬 Comentário criado com sucesso no ServiceNow', { sysId, comment }); + // Verificar se retorna status 200 para sucesso + if (response.status !== 200) { + logError(`❌ Falha ao criar comentário. Status: ${response.status}`, { sysId, ticketType }); + return null; + } // retornar id do comentário criado const commentId = await getCommentId(sysId, comment); + + if (!commentId) { + logError('❌ Comentário criado, mas não foi possível obter o ID do comentário', { sysId }); + return null; + } + + logInfo(`✅ Comentário criado com sucesso no ServiceNow. Comment ID: ${commentId}`, { sysId, ticketType }); + return commentId; } catch (error) { @@ -123,10 +174,38 @@ const createCommentInServiceNow = async (sysId, comment) => { } } +const syncCommentToServiceNow = async (ticketId, comment) => { + logInfo(`Iniciando sincronização do comentário para o ticket SN Ticket: ${ticketId}`); + const snSysId = await getTicketSysId(ticketId); + const ticketType = await getTicketTypeById(ticketId); + + if (!snSysId) { + logError(`❌ sys_id do ServiceNow não encontrado para o ticket SN Ticket: ${ticketId}`); + return null; + } + const snCommentId = await createCommentInServiceNow(snSysId, comment, ticketType); + return snCommentId; +} + +// Função que verifica se o comentário já existe no ServiceNow antes de criar + const existingCommentInServiceNow = async (ticketId, comment) => { + try { + const sys_id = await getTicketSysId(ticketId); + const commentExists = await getCommentId(sys_id, comment); + return commentExists; + + } catch (error) { + logError(error, '🚨Falha ao verificar existência do comentário no ServiceNow'); + return false; + } +} + module.exports = { fetchTicketsFromServiceNow, fetchRequestsFromServiceNow, getTicketSysId, createCommentInServiceNow, getCommentId, + existingCommentInServiceNow, + syncCommentToServiceNow } \ No newline at end of file