2025-09-23 21:01:01 -03:00
// src/services/servicenowService.js
2025-08-30 19:04:35 -03:00
const axios = require ( 'axios' ) ;
2025-09-03 11:13:23 -03:00
const apiConfig = require ( '../../config/apiConfig' ) ;
2025-10-29 14:20:21 -03:00
const { stripHTML } = require ( '../utils/commentSanitizer' ) ;
2025-09-05 20:28:15 -03:00
const TicketSnModel = require ( '../models/ticketSnModel' ) ;
const { logInfo , logError , logSync } = require ( '../utils/logger' ) ;
2025-10-10 08:12:26 -03:00
const TicketUpdateModel = require ( '../models/ticketUpdateModel' ) ;
const TicketSyncModel = require ( '../models/ticketSyncModel' ) ;
2025-10-23 17:30:03 -03:00
const { log } = require ( 'winston' ) ;
2025-08-30 19:04:35 -03:00
2025-10-23 17:30:03 -03:00
const fetchTicketsFromServiceNow = async ( watermark ) => {
2025-08-30 19:04:35 -03:00
try {
2025-10-28 14:06:00 -03:00
const watermarkDate = new Date ( watermark ) ;
2025-10-29 14:20:21 -03:00
const watermarkBRT = new Date ( watermarkDate . getTime ( ) ) ;
2025-10-28 14:06:00 -03:00
const formattedWatermark = watermarkBRT . toISOString ( ) . replace ( 'T' , ' ' ) . substring ( 0 , 19 ) ;
const fields = 'number,sys_id,short_description,state,description,caller_id,caller_id.email,caller_id.phone,caller_id.mobile_phone,location,opened_at,sys_updated_on' ;
2025-10-29 14:20:21 -03:00
const baseUrl = apiConfig . snTableIncidentConfig . baseUrl . replace ( /\/$/ , "" ) ;
const query = ` assignment_group= ${ apiConfig . assignmentGroup . username } ^sys_updated_on>= ${ formattedWatermark } ` ;
const url = ` ${ baseUrl } ?sysparm_query= ${ encodeURIComponent ( query ) } &sysparm_display_value=true&sysparm_fields= ${ fields } ` ;
2025-10-23 17:30:03 -03:00
const ticketsResponse = await axios . get ( url , {
2025-09-03 16:11:30 -03:00
auth : apiConfig . servicenowAuthentication . auth
2025-09-03 11:13:23 -03:00
} ) ;
2025-10-23 17:30:03 -03:00
return ticketsResponse . data . result || [ ] ;
2025-09-03 16:11:30 -03:00
} catch ( error ) {
2025-09-05 20:28:15 -03:00
logError ( error , 'SERVICE NOW Tickets' ) ;
2025-10-29 14:20:21 -03:00
throw error ;
2025-09-03 16:11:30 -03:00
}
}
2025-10-23 17:30:03 -03:00
const fetchRequestsFromServiceNow = async ( watermark ) => {
2025-09-03 16:11:30 -03:00
try {
2025-10-29 14:20:21 -03:00
const query = ` assignment_group= ${ apiConfig . assignmentGroup . username } ^sys_updated_on>= ${ watermark } ` ;
const fields = 'number,request,opened_at,short_description,sys_id,assignment_group,opened_by,opened_by.display_value,opened_by.email,description,location,state,sys_updated_on,variables' ;
2025-10-24 13:13:08 -03:00
const baseUrl = apiConfig . snTableRequestConfig . baseUrl . replace ( /\/$/ , "" ) ;
2025-10-28 14:06:00 -03:00
const url = ` ${ baseUrl } ?sysparm_query= ${ encodeURIComponent ( query ) } &sysparm_display_value=true&sysparm_fields= ${ fields } ` ;
2025-10-23 17:30:03 -03:00
const requestsResponse = await axios . get ( url , {
2025-09-03 16:11:30 -03:00
auth : apiConfig . servicenowAuthentication . auth
} ) ;
2025-10-23 17:30:03 -03:00
return requestsResponse . data . result || [ ] ;
2025-08-30 19:04:35 -03:00
} catch ( error ) {
2025-09-05 20:28:15 -03:00
logError ( error , 'SERVICE NOW Requests' ) ;
2025-10-29 14:20:21 -03:00
throw error ;
2025-08-30 19:04:35 -03:00
}
2025-09-03 11:13:23 -03:00
}
2025-08-30 19:04:35 -03:00
2025-10-02 10:16:56 -03:00
const getTicketSysId = async ( ticketId ) => {
2025-09-25 15:59:52 -03:00
try {
2025-10-02 10:16:56 -03:00
const ticket = await TicketSnModel . findById ( ticketId ) ;
2025-10-23 17:30:03 -03:00
if ( ! ticket ) {
logError ( ` Ticket com ID local ${ ticketId } não encontrado. ` , 'getTicketSysId' ) ;
return null ;
}
2025-09-25 15:59:52 -03:00
return ticket . sys _id ;
} catch ( error ) {
logError ( error , '🚨Falha ao obter sys_id do ticket' ) ;
return null ;
}
}
2025-10-23 17:30:03 -03:00
2025-10-02 10:16:56 -03:00
const getTicketTypeById = async ( ticketId ) => {
try {
const ticket = await TicketSnModel . findById ( ticketId ) ;
2025-10-23 17:30:03 -03:00
if ( ! ticket ) {
logError ( ` Ticket com ID local ${ ticketId } não encontrado. ` , 'getTicketTypeById' ) ;
return null ;
}
2025-10-02 10:16:56 -03:00
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 ;
}
}
2025-09-25 15:59:52 -03:00
const getCommentId = async ( sysId , comment ) => {
try {
2025-10-02 10:16:56 -03:00
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 ` ;
2025-09-25 15:59:52 -03:00
const response = await axios . get ( url , {
auth : apiConfig . servicenowAuthentication . auth
} ) ;
2025-10-02 10:16:56 -03:00
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
2025-09-25 15:59:52 -03:00
}
}
} catch ( error ) {
logError ( error , '🚨Falha ao obter ID do comentário no ServiceNow' ) ;
return null ;
}
}
2025-10-29 14:40:21 -03:00
2025-10-23 17:30:03 -03:00
const createCommentInServiceNow = async ( snId , sysId , comment , ticketType ) => {
2025-09-25 15:59:52 -03:00
try {
2025-10-02 10:16:56 -03:00
let url = '' ;
let payload = { } ;
if ( ticketType == 'incidente' ) {
2025-10-28 14:06:00 -03:00
url = ` ${ apiConfig . snTableIncidentConfig . baseUrl } / ${ sysId } ` ;
2025-10-02 10:16:56 -03:00
payload = {
comments : comment
} ;
} else if ( ticketType == 'requisicao' ) {
2025-10-28 14:06:00 -03:00
url = ` ${ apiConfig . snTableRequestConfig . baseUrl } / ${ sysId } ` ;
2025-10-02 10:16:56 -03:00
payload = {
comments : comment
} ;
} else {
logError ( ` Tipo de ticket desconhecido: ${ ticketType } ` , { sysId , ticketType } ) ;
return null ;
}
2025-09-25 15:59:52 -03:00
2025-10-02 10:16:56 -03:00
const response = await axios . patch ( url , payload , {
2025-09-25 15:59:52 -03:00
auth : apiConfig . servicenowAuthentication . auth
} ) ;
2025-10-23 17:30:03 -03:00
await TicketSyncModel . updateLastSync ( snId , 'servicenow' ) ; // Atualiza o timestamp
2025-09-25 15:59:52 -03:00
2025-10-02 10:16:56 -03:00
// Verificar se retorna status 200 para sucesso
if ( response . status !== 200 ) {
logError ( ` ❌ Falha ao criar comentário. Status: ${ response . status } ` , { sysId , ticketType } ) ;
return null ;
}
2025-09-25 15:59:52 -03:00
// retornar id do comentário criado
const commentId = await getCommentId ( sysId , comment ) ;
2025-10-02 10:16:56 -03:00
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 } ) ;
2025-09-25 15:59:52 -03:00
return commentId ;
} catch ( error ) {
logError ( error , '🚨Falha ao criar comentário no ServiceNow' ) ;
}
}
2025-10-02 10:16:56 -03:00
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 ;
}
2025-10-23 17:30:03 -03:00
const snCommentId = await createCommentInServiceNow ( ticketId , snSysId , comment , ticketType ) ;
2025-10-02 10:16:56 -03:00
return snCommentId ;
}
2025-10-17 16:58:14 -03:00
const existingCommentInServiceNow = async ( ticketId , comment ) => {
2025-10-02 10:16:56 -03:00
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 ;
}
}
2025-10-17 16:58:14 -03:00
const fetchCommentsFromServiceNow = async ( ticket ) => {
2025-10-07 10:28:39 -03:00
try {
2025-10-17 16:58:14 -03:00
logInfo ( ` Iniciando busca de comentários do ServiceNow para o ticket SN Ticket: ${ ticket . glpi _ticket _id } ` ) ;
2025-10-10 08:12:26 -03:00
2025-10-17 16:58:14 -03:00
const sys _id = ticket . sys _id ;
2025-10-07 10:28:39 -03:00
const allComments = [ ] ;
const pageSize = 100 ;
let offset = 0 ;
let page = 0 ;
const internalMaxPages = 1000 ; // safeguard interno para evitar loop infinito
while ( true ) {
page ++ ;
const params = {
sysparm _query : ` element_id= ${ encodeURIComponent ( sys _id ) } ^element=comments ` ,
sysparm _fields : 'sys_id,element,value,sys_created_on,sys_created_by,element_id' ,
sysparm _limit : pageSize ,
sysparm _offset : offset
} ;
2025-10-17 16:58:14 -03:00
logInfo ( ` Buscando página ${ page } de comentários do ticket SN Ticket: ${ ticket . glpi _ticket _id } ` ) ;
2025-10-07 10:28:39 -03:00
const response = await axios . get ( apiConfig . snTableJournalConfig . baseUrl , {
auth : apiConfig . servicenowAuthentication . auth ,
params
} ) ;
2025-10-23 17:30:03 -03:00
await TicketSyncModel . updateLastSync ( ticket . sn _ticket _id , 'servicenow' ) ; // Atualiza o timestamp
2025-10-07 10:28:39 -03:00
const comments = response ? . data ? . result || [ ] ;
2025-10-13 06:06:52 -03:00
2025-10-07 10:28:39 -03:00
if ( ! Array . isArray ( comments ) || comments . length === 0 ) {
break ; // não há mais comentários
2025-10-17 16:58:14 -03:00
}
2025-10-07 10:28:39 -03:00
allComments . push ( ... comments ) ;
if ( comments . length < pageSize ) {
break ; // última página
}
// avançar para próxima página
offset += pageSize ;
// proteção extra para evitar loop infinito em caso de comportamento inesperado da API
if ( page >= internalMaxPages ) {
logError ( new Error ( 'Max pages reached while paginating comments' ) , { ticketId , page } ) ;
break ;
}
}
2025-10-23 17:30:03 -03:00
2025-10-17 16:58:14 -03:00
// 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 ) {
logInfo ( ` Nenhum comentário encontrado no Service Now para o ticket GLPI ID: ${ ticket . glpi _ticket _id } ` ) ;
return [ ] ;
}
2025-10-13 06:06:52 -03:00
2025-10-17 16:58:14 -03:00
logInfo ( ` Encontrado ${ filteredComments . length } comentários para o ticket SN Ticket: ${ ticket . glpi _ticket _id } ` )
const commentsInserted = [ ] ;
2025-10-10 08:12:26 -03:00
// Insere cada comment no banco intermediario
for ( const comment of filteredComments ) {
2025-10-23 17:30:03 -03:00
const existingComment = await TicketUpdateModel . getBySourceId ( comment . sys _id ) ;
2025-10-10 08:12:26 -03:00
if ( existingComment ) {
logInfo ( ` Comentário: ${ comment . sys _id } já existe no banco de dados ` , { step : 3 } ) ;
continue ;
} else {
const commentData = {
2025-10-17 16:58:14 -03:00
ticket _sync _id : ticket . id ,
2025-10-23 17:30:03 -03:00
update _type : 'comment' ,
2025-10-10 08:12:26 -03:00
source _system : 'servicenow' ,
content : comment . value ,
created _at : new Date ( comment . sys _created _on ) ,
author : "CAOA" ,
updated _at : new Date ( ) ,
source _id : comment . sys _id ,
destiny _id : null
} ;
2025-10-13 06:06:52 -03:00
const commentInserted = await TicketUpdateModel . insert ( commentData ) ;
2025-10-10 08:12:26 -03:00
2025-10-17 16:58:14 -03:00
2025-10-23 17:30:03 -03:00
if ( commentInserted . success ) {
commentsInserted . push ( commentData ) ;
logInfo ( ` ✅ Comentário ID ${ comment . sys _id } do ${ commentData . source _system } inserido no banco! ` , { step : 4 } ) ;
2025-10-10 08:12:26 -03:00
} else {
logError ( ` ❌ Erro ao inserir comentário ID ${ comment . sys _id } no banco: ${ commentInserted . error } ` , { step : 4 } ) ;
2025-10-17 16:58:14 -03:00
return [ ] ;
2025-10-10 08:12:26 -03:00
}
2025-10-23 17:30:03 -03:00
}
}
return commentsInserted ;
} catch ( error ) {
logError ( error , '🚨Falha ao buscar comentários do ServiceNow' ) ;
return [ ] ;
}
} ;
2025-10-29 14:20:21 -03:00
2025-10-23 17:30:03 -03:00
const fetchAndProcessClosedTicketsFromSN = async ( ) => {
try {
logInfo ( '📡 Verificando status de tickets sincronizados no ServiceNow...' ) ;
// 1. Busca apenas os tickets que estão abertos e sincronizados no nosso banco.
const syncedTickets = await TicketSyncModel . getTicketsSynced ( ) ;
if ( syncedTickets . length === 0 ) {
logInfo ( 'Nenhum ticket sincronizado para verificar status no ServiceNow.' ) ;
return ;
}
// 2. Para cada ticket, verifica seu status real no ServiceNow.
for ( const ticket of syncedTickets ) {
const ticketSnDetails = await TicketSnModel . findById ( ticket . sn _ticket _id ) ;
2025-10-10 08:12:26 -03:00
2025-10-23 17:30:03 -03:00
// 3. Se o status for de fechamento, inicia o processo.
if ( ticketSnDetails && ticketSnDetails . status === 'Encerrado' ) { // 'Encerrado' mapeia para 'Resolved' (6)
logInfo ( ` Ticket SN ${ ticketSnDetails . ticket _number } foi Resolvido. Marcando como 'solved'... ` ) ;
await TicketSyncModel . updateStatusAndSourceLast ( ticket . sn _ticket _id , 'synced' , 'solved' , 'SNOW' ) ;
2025-10-17 16:58:14 -03:00
2025-10-23 17:30:03 -03:00
} else if ( ticketSnDetails && ticketSnDetails . status === 'Encerrado - Omitido' ) { // 'Encerrado - Omitido' mapeia para 'Closed' (7)
logInfo ( ` Ticket SN ${ ticketSnDetails . ticket _number } foi Fechado permanentemente. Marcando para sincronização de fechamento... ` ) ;
2025-10-17 16:58:14 -03:00
2025-10-23 17:30:03 -03:00
// Sanitiza o conteúdo da nota de fechamento antes de salvar.
const sanitizedContent = stripHTML ( ticketSnDetails . close _notes || 'Chamado encerrado sem notas.' ) ;
// Salva a nota de fechamento no nosso banco intermediário.
await TicketUpdateModel . insert ( {
ticket _sync _id : ticket . id ,
source _system : 'servicenow' ,
update _type : 'close_message' ,
content : sanitizedContent ,
author : 'ServiceNow' ,
created _at : ticketSnDetails . updated _at ,
source _id : ` close- ${ ticketSnDetails . sys _id } ` ,
} ) ;
// Atualiza o status no nosso banco para 'pending_close' no GLPI.
await TicketSyncModel . updateStatusAndSourceLast ( ticket . sn _ticket _id , 'pending_close' , 'closed' , 'SNOW' ) ;
2025-10-10 08:12:26 -03:00
}
}
2025-10-23 17:30:03 -03:00
2025-10-07 10:28:39 -03:00
} catch ( error ) {
2025-10-23 17:30:03 -03:00
logError ( error , '🚨Falha ao buscar chamados encerrados do ServiceNow' ) ;
2025-10-07 10:28:39 -03:00
}
} ;
2025-10-23 17:30:03 -03:00
const updateStatusInServiceNow = async ( snTicketId , state ) => {
// Mapeamento de status de string para código numérico do ServiceNow
const stateMap = {
'Aguardando Atendimento' : '1' , // New
'Em Atendimento' : '2' , // In Progress
2025-10-28 17:57:39 -03:00
'Em Espera' : '-5' , // On Hold
2025-10-23 17:30:03 -03:00
'Encerrado' : '6' , // Resolved
'Encerrado - Omitido' : '7' // Closed
} ;
const stateCode = stateMap [ state ] || state ; // Usa o código mapeado ou o valor original se não encontrar
let sysId = null ; // Declarar sysId fora do try para estar disponível no catch
try {
const ticketSn = await TicketSnModel . findById ( snTicketId ) ;
if ( ! ticketSn ) {
logError ( ` Ticket SN com ID ${ snTicketId } não encontrado para atualização de status. ` , 'updateStatusInServiceNow' ) ;
return null ;
}
sysId = ticketSn . sys _id ; // Atribuir valor
const ticketType = ticketSn . tipo ;
let url = '' ;
let payload = { } ;
if ( ticketType === 'incidente' ) {
2025-10-28 14:06:00 -03:00
url = ` ${ apiConfig . snTableIncidentConfig . baseUrl } / ${ sysId } ` ;
2025-10-23 17:30:03 -03:00
payload = { state : stateCode , incident _state : stateCode } ; // Envia state e incident_state
} else if ( ticketType === 'requisicao' ) {
2025-10-28 14:06:00 -03:00
url = ` ${ apiConfig . snTableRequestConfig . baseUrl } / ${ sysId } ` ;
2025-10-23 17:30:03 -03:00
payload = { state : stateCode } ; // Requisições podem ter um fluxo diferente
} else {
logError ( ` Tipo de ticket desconhecido: ${ ticketType } ` , { sysId , ticketType } ) ;
throw new Error ( ` Tipo de ticket desconhecido: ${ ticketType } ` ) ;
}
const response = await axios . patch ( url , payload , {
auth : apiConfig . servicenowAuthentication . auth
} ) ;
await TicketSyncModel . updateLastSync ( snTicketId , 'servicenow' ) ; // Atualiza o timestamp
if ( response . status !== 200 ) {
logError ( ` ❌ Falha ao atualizar status. Status: ${ response . status } - ${ response . statusText } ` , { sysId , ticketType , state : stateCode } ) ;
throw new Error ( ` Falha ao atualizar status no ServiceNow: ${ response . status } - ${ response . statusText } ` ) ;
}
logInfo ( ` ✅ Status do ticket atualizado com sucesso no ServiceNow. ` , { sysId , ticketType , state : stateCode } ) ;
return true ;
} catch ( error ) {
logError ( error , ` 🚨 Falha ao atualizar status no ServiceNow para o ticket com sys_id: ${ sysId } ` ) ;
return null ;
}
} ;
const closeTicketInServiceNow = async ( snTicketId , closeNotes , resolvedAt ) => {
let sysId = null ; // Declarar sysId fora do try
try {
const ticketSn = await TicketSnModel . findById ( snTicketId ) ;
if ( ! ticketSn ) {
logError ( ` Ticket SN com ID ${ snTicketId } não encontrado para fechamento. ` , 'closeTicketInServiceNow' ) ;
return null ;
}
2025-10-28 14:06:00 -03:00
2025-10-23 17:30:03 -03:00
sysId = ticketSn . sys _id ;
const ticketType = ticketSn . tipo ;
let url = '' ; // Declarar url fora do if
let payload = { } ;
// O ID do usuário que está resolvendo o chamado. Pegue o sys_id do usuário da integração no SN.
// O nome do usuário que está resolvendo o chamado.
const resolvedBySysId = 'b131d6ed1b2ba510c5e163923b4bcb77' ; // sys_id do usuário que resolveu
if ( ticketType === 'incidente' ) {
2025-10-28 14:06:00 -03:00
url = ` ${ apiConfig . snTableIncidentConfig . baseUrl } / ${ sysId } ` ;
2025-10-23 17:30:03 -03:00
payload = {
state : '6' , // 6 = Resolved
incident _state : '6' , // 6 = Resolved
resolved _by : resolvedBySysId ,
resolved _at : new Date ( resolvedAt ) . toISOString ( ) . slice ( 0 , 19 ) . replace ( 'T' , ' ' ) ,
close _code : 'Closed/Resolved by caller' ,
close _notes : closeNotes ,
work _notes : ` Ticket Encerrado por Suporte Técnico Sothis: ${ closeNotes } `
} ;
2025-10-28 14:06:00 -03:00
2025-10-23 17:30:03 -03:00
} else if ( ticketType === 'requisicao' ) {
2025-10-28 14:06:00 -03:00
url = ` ${ apiConfig . snTableRequestConfig . baseUrl } / ${ sysId } ` ;
2025-10-23 17:30:03 -03:00
payload = {
2025-10-28 17:57:39 -03:00
state : '6' ,
resolved _by : resolvedBySysId ,
resolved _at : new Date ( resolvedAt ) . toISOString ( ) . slice ( 0 , 19 ) . replace ( 'T' , ' ' ) ,
closeNotes : closeNotes ,
work _notes : ` Ticket Encerrado por Suporte Técnico Sothis: ${ closeNotes } ` ,
2025-10-23 17:30:03 -03:00
} ;
} else {
logError ( ` Tipo de ticket desconhecido: ${ ticketType } ` , { sysId , ticketType } ) ;
throw new Error ( ` Tipo de ticket desconhecido: ${ ticketType } ` ) ;
}
const response = await axios . patch ( url , payload , {
auth : apiConfig . servicenowAuthentication . auth ,
headers : {
'Content-Type' : 'application/json' ,
'Accept' : 'application/json'
}
} ) ;
await TicketSyncModel . updateLastSync ( snTicketId , 'servicenow' ) ; // Corrigido para usar snTicketId
if ( response . status !== 200 ) {
logError ( ` ❌ Falha ao resolver ticket. Status: ${ response . status } - ${ response . statusText } ` , { sysId , ticketType } ) ;
throw new Error ( ` Falha ao fechar ticket no ServiceNow: ${ response . status } - ${ response . statusText } ` ) ;
}
logInfo ( ` ✅ Ticket resolvido com sucesso no ServiceNow. ` , { sysId , ticketType } ) ;
return true ;
} catch ( error ) {
logError ( error , ` 🚨 Falha ao fechar ticket no ServiceNow (sysId: ${ sysId } ) ` ) ;
return null ;
}
} ;
const addWorkNoteToServiceNow = async ( snTicketId , workNote ) => {
let sysId = null ;
try {
const ticketSn = await TicketSnModel . findById ( snTicketId ) ;
if ( ! ticketSn ) {
logError ( ` Ticket SN com ID ${ snTicketId } não encontrado para adicionar work note. ` , 'addWorkNoteToServiceNow' ) ;
return null ;
}
sysId = ticketSn . sys _id ;
const ticketType = ticketSn . tipo ;
let url = '' ;
if ( ticketType === 'incidente' ) {
2025-10-28 14:06:00 -03:00
url = ` ${ apiConfig . snTableIncidentConfig . baseUrl } / ${ sysId } ` ;
2025-10-23 17:30:03 -03:00
} else if ( ticketType === 'requisicao' ) {
2025-10-28 14:06:00 -03:00
url = ` ${ apiConfig . snTableRequestConfig . baseUrl } / ${ sysId } ` ;
2025-10-23 17:30:03 -03:00
} else {
throw new Error ( ` Tipo de ticket desconhecido: ${ ticketType } ` ) ;
}
const payload = { work _notes : workNote } ;
await axios . patch ( url , payload , {
auth : apiConfig . servicenowAuthentication . auth ,
headers : { 'Content-Type' : 'application/json' , 'Accept' : 'application/json' }
} ) ;
await TicketSyncModel . updateLastSync ( snTicketId , 'servicenow' ) ;
logInfo ( ` ✅ Work note adicionada com sucesso ao ticket SN ${ sysId } . ` ) ;
return true ;
} catch ( error ) {
logError ( error , ` 🚨 Falha ao adicionar work note ao ticket SN (sysId: ${ sysId } ) ` ) ;
return null ;
}
} ;
2025-10-28 14:06:00 -03:00
/ * *
* Busca o valor de uma variável de catálogo ( sc _item _option ) para um Request Item ( RITM ) específico .
* @ param { string } ritmSysId - O sys _id do sc _req _item ( RITM ) .
* @ param { string } variableName - O nome interno ( name ) da variável de catálogo .
* @ returns { Promise < string | null > } O valor da variável ou null se não encontrada / erro .
* /
const fetchScItemOptionValue = async ( ritmSysId , variableName ) => {
try {
const baseUrl = apiConfig . snScItemOptionConfig . baseUrl . replace ( /\/$/ , "" ) ;
const url = ` ${ baseUrl } ?sysparm_query=request_item= ${ ritmSysId } ^sc_item_option.item_option_new.question_textINPlease justify the need,Telephone,Select what you want&sysparm_fields=sc_item_option.item_option_new.question_text,sc_item_option.value ` ;
const response = await axios . get ( url , {
auth : apiConfig . servicenowAuthentication . auth
} ) ;
const result = response . data . result ;
return ( result && result . length > 0 ) ? result : null ;
} catch ( error ) {
logError ( error , ` 🚨 Falha ao buscar valor da variável ' ${ variableName } ' para RITM ${ ritmSysId } ` ) ;
return null ;
}
} ;
2025-08-30 19:04:35 -03:00
module . exports = {
2025-10-23 17:30:03 -03:00
fetchTicketsFromServiceNow ,
fetchRequestsFromServiceNow ,
getTicketSysId ,
2025-10-29 14:40:21 -03:00
getTicketTypeById ,
2025-10-23 17:30:03 -03:00
createCommentInServiceNow ,
getCommentId ,
existingCommentInServiceNow ,
syncCommentToServiceNow ,
fetchCommentsFromServiceNow ,
fetchAndProcessClosedTicketsFromSN ,
2025-10-29 14:40:21 -03:00
closeTicketInServiceNow ,
updateStatusInServiceNow ,
addWorkNoteToServiceNow ,
fetchScItemOptionValue
2025-10-23 17:30:03 -03:00
} ;
2025-10-29 14:40:21 -03:00
/ * *
* @ module servicenowService
* @ description Este serviço encapsula todas as interações com a API REST do ServiceNow .
* Ele é responsável por buscar , criar e atualizar dados na plataforma ServiceNow .
*
* Principais Funcionalidades :
* - * * Busca de Tickets * * : ` fetchTicketsFromServiceNow ` e ` fetchRequestsFromServiceNow ` buscam incidentes e requisições , respectivamente , utilizando uma marca d ' água para obter apenas os registros novos ou atualizados .
* - * * Busca de Comentários ( ` fetchCommentsFromServiceNow ` ) * * : Busca todos os comentários ( journal entries ) de um ticket , com lógica de paginação para lidar com grandes volumes de dados . Ele também filtra os comentários gerados pelo próprio sistema de integração para evitar loops .
* - * * Criação de Comentários * * : ` createCommentInServiceNow ` e a função wrapper ` syncCommentToServiceNow ` adicionam comentários ( work notes / comments ) a um ticket existente no ServiceNow .
* - * * Atualização de Status * * : ` updateStatusInServiceNow ` modifica o estado de um ticket ( ex : para 'Em Atendimento' ) .
* - * * Fechamento de Tickets ( ` closeTicketInServiceNow ` ) * * : Altera o estado de um ticket para "Resolvido" , preenchendo os campos obrigatórios como notas de fechamento e data de resolução .
* - * * Adição de Notas ( ` addWorkNoteToServiceNow ` ) * * : Adiciona uma nota de trabalho a um ticket sem alterar seu status , útil para registrar informações ou avisos .
* - * * Busca de Variáveis de Catálogo ( ` fetchScItemOptionValue ` ) * * : Função especializada para buscar valores de variáveis em itens de requisição ( RITMs ) , usada para enriquecer a descrição de tickets .
*
* Todas as funções de interação com a API são envolvidas em blocos try - catch com logging detalhado para facilitar o monitoramento e a depuração .
* /