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
2026-02-19 11:21:40 -03:00
const resolvedBySysId = process . env . SERVICENOW _RESOLVED _BY _SYSID || 'b131d6ed1b2ba510c5e163923b4bcb77' ;
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 ) {
2026-02-19 11:21:40 -03:00
logError ( ` Ticket com ID local ${ ticketId } nao encontrado. ` , 'getTicketSysId' ) ;
2025-10-23 17:30:03 -03:00
return null ;
}
2025-09-25 15:59:52 -03:00
return ticket . sys _id ;
} catch ( error ) {
2026-02-19 11:21:40 -03:00
logError ( error , 'ERRO: Falha ao obter sys_id do ticket' ) ;
2025-09-25 15:59:52 -03:00
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 ) {
2026-02-19 11:21:40 -03:00
logError ( ` Ticket com ID local ${ ticketId } nao encontrado. ` , 'getTicketTypeById' ) ;
2025-10-23 17:30:03 -03:00
return null ;
}
2025-10-02 10:16:56 -03:00
const ticketType = ticket . tipo ;
2026-02-19 11:21:40 -03:00
logInfo ( ` INFO: Tipo do ticket ${ ticketId } - Tipo: ${ ticketType } ` , { ticketId , ticketType } ) ;
2025-10-02 10:16:56 -03:00
return ticketType ;
} catch ( error ) {
2026-02-19 11:21:40 -03:00
logError ( error , 'ERRO: Falha ao obter tipo do ticket' ) ;
2025-10-02 10:16:56 -03:00
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 ;
2026-02-19 11:21:40 -03:00
// Procurar o comentario exato na lista retornada
2025-10-02 10:16:56 -03:00
for ( const item of comments ) {
if ( item . value && item . value . includes ( comment ) ) {
2026-02-19 11:21:40 -03:00
return item . sys _id ; // Retorna o sys_id do comentario encontrado
2025-09-25 15:59:52 -03:00
}
}
} catch ( error ) {
2026-02-19 11:21:40 -03:00
logError ( error , 'ERRO: Falha ao obter ID do comentario no ServiceNow' ) ;
2025-09-25 15:59:52 -03:00
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 ) {
2026-02-19 11:21:40 -03:00
logError ( ` ERRO: Falha ao criar comentario. Status: ${ response . status } ` , { sysId , ticketType } ) ;
2025-10-02 10:16:56 -03:00
return null ;
}
2025-09-25 15:59:52 -03:00
2026-02-19 11:21:40 -03:00
// retornar id do comentario criado
2025-09-25 15:59:52 -03:00
const commentId = await getCommentId ( sysId , comment ) ;
2025-10-02 10:16:56 -03:00
if ( ! commentId ) {
2026-02-19 11:21:40 -03:00
logError ( 'ERRO: Comentario criado, mas nao foi possivel obter o ID do comentario' , { sysId } ) ;
2025-10-02 10:16:56 -03:00
return null ;
}
2026-02-19 11:21:40 -03:00
logInfo ( ` OK: Comentario criado com sucesso no ServiceNow. Comment ID: ${ commentId } ` , { sysId , ticketType } ) ;
2025-10-02 10:16:56 -03:00
2025-09-25 15:59:52 -03:00
return commentId ;
} catch ( error ) {
2026-02-19 11:21:40 -03:00
logError ( error , 'ERRO: Falha ao criar comentario no ServiceNow' ) ;
2025-09-25 15:59:52 -03:00
}
}
2025-10-02 10:16:56 -03:00
const syncCommentToServiceNow = async ( ticketId , comment ) => {
2026-02-19 11:21:40 -03:00
logInfo ( ` Iniciando sincronizacao do comentario para o ticket SN Ticket: ${ ticketId } ` ) ;
2025-10-02 10:16:56 -03:00
const snSysId = await getTicketSysId ( ticketId ) ;
const ticketType = await getTicketTypeById ( ticketId ) ;
if ( ! snSysId ) {
2026-02-19 11:21:40 -03:00
logError ( ` ERRO: sys_id do ServiceNow nao encontrado para o ticket SN Ticket: ${ ticketId } ` ) ;
2025-10-02 10:16:56 -03:00
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 ) {
2026-02-19 11:21:40 -03:00
logError ( error , 'ERRO: Falha ao verificar existencia do comentario no ServiceNow' ) ;
2025-10-02 10:16:56 -03:00
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
2026-02-19 11:21:40 -03:00
logInfo ( ` Iniciando busca de comentarios 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
} ;
2026-02-19 11:21:40 -03:00
logInfo ( ` Buscando pagina ${ page } de comentarios 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 ) {
2026-02-19 11:21:40 -03:00
break ; // nao ha mais comentarios
2025-10-17 16:58:14 -03:00
}
2025-10-07 10:28:39 -03:00
allComments . push ( ... comments ) ;
if ( comments . length < pageSize ) {
2026-02-19 11:21:40 -03:00
break ; // ultima pagina
2025-10-07 10:28:39 -03:00
}
2026-02-19 11:21:40 -03:00
// avancar para proxima pagina
2025-10-07 10:28:39 -03:00
offset += pageSize ;
2026-02-19 11:21:40 -03:00
// protecao extra para evitar loop infinito em caso de comportamento inesperado da API
2025-10-07 10:28:39 -03:00
if ( page >= internalMaxPages ) {
logError ( new Error ( 'Max pages reached while paginating comments' ) , { ticketId , page } ) ;
break ;
}
}
2025-10-23 17:30:03 -03:00
2026-02-19 11:21:40 -03:00
// Traz apenas os tickets que nao sao do sys_created_by: "SOTHIS.CAOA"
2025-10-17 16:58:14 -03:00
const filteredComments = allComments . filter ( comment => comment . sys _created _by !== 'SOTHIS.CAOA' ) ;
if ( filteredComments . length === 0 ) {
2026-02-19 11:21:40 -03:00
logInfo ( ` Nenhum comentario encontrado no Service Now para o ticket GLPI ID: ${ ticket . glpi _ticket _id } ` ) ;
2025-10-17 16:58:14 -03:00
return [ ] ;
}
2025-10-13 06:06:52 -03:00
2026-02-19 11:21:40 -03:00
logInfo ( ` Encontrado ${ filteredComments . length } comentarios para o ticket SN Ticket: ${ ticket . glpi _ticket _id } ` )
2025-10-17 16:58:14 -03:00
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 ) {
2026-02-19 11:21:40 -03:00
logInfo ( ` Comentario: ${ comment . sys _id } ja existe no banco de dados ` , { step : 3 } ) ;
2025-10-10 08:12:26 -03:00
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 ) ;
2026-02-19 11:21:40 -03:00
logInfo ( ` OK: Comentario ID ${ comment . sys _id } do ${ commentData . source _system } inserido no banco! ` , { step : 4 } ) ;
2025-10-10 08:12:26 -03:00
} else {
2026-02-19 11:21:40 -03:00
logError ( ` ERRO: Erro ao inserir comentario 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 ) {
2026-02-19 11:21:40 -03:00
logError ( error , 'ERRO: Falha ao buscar comentarios do ServiceNow' ) ;
2025-10-23 17:30:03 -03:00
return [ ] ;
}
} ;
2025-10-29 14:20:21 -03:00
2025-10-23 17:30:03 -03:00
const fetchAndProcessClosedTicketsFromSN = async ( ) => {
try {
2026-02-19 11:21:40 -03:00
logInfo ( 'INFO: Verificando status de tickets sincronizados no ServiceNow...' ) ;
2025-10-23 17:30:03 -03:00
2026-02-19 11:21:40 -03:00
// 1. Busca apenas os tickets que estao abertos e sincronizados no nosso banco.
2025-10-23 17:30:03 -03:00
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)
2026-02-19 11:21:40 -03:00
logInfo ( ` Ticket SN ${ ticketSnDetails . ticket _number } foi Fechado permanentemente. Marcando para sincronizacao de fechamento... ` ) ;
2025-10-17 16:58:14 -03:00
2026-02-19 11:21:40 -03:00
// Sanitiza o conteudo da nota de fechamento antes de salvar.
2025-10-23 17:30:03 -03:00
const sanitizedContent = stripHTML ( ticketSnDetails . close _notes || 'Chamado encerrado sem notas.' ) ;
2026-02-19 11:21:40 -03:00
// Salva a nota de fechamento no nosso banco intermediario.
2025-10-23 17:30:03 -03:00
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 ) {
2026-02-19 11:21:40 -03:00
logError ( error , 'ERRO: 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 ) => {
2026-02-19 11:21:40 -03:00
// Mapeamento de status de string para codigo numerico do ServiceNow
2025-10-23 17:30:03 -03:00
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
} ;
2026-02-19 11:21:40 -03:00
const stateCode = stateMap [ state ] || state ; // Usa o codigo mapeado ou o valor original se nao encontrar
2025-10-23 17:30:03 -03:00
2026-02-19 11:21:40 -03:00
let sysId = null ; // Declarar sysId fora do try para estar disponivel no catch
2025-10-23 17:30:03 -03:00
try {
const ticketSn = await TicketSnModel . findById ( snTicketId ) ;
if ( ! ticketSn ) {
2026-02-19 11:21:40 -03:00
logError ( ` Ticket SN com ID ${ snTicketId } nao encontrado para atualizacao de status. ` , 'updateStatusInServiceNow' ) ;
2025-10-23 17:30:03 -03:00
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 } ` ;
2026-02-19 11:21:40 -03:00
payload = { state : stateCode } ; // Requisicoes podem ter um fluxo diferente
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
} ) ;
await TicketSyncModel . updateLastSync ( snTicketId , 'servicenow' ) ; // Atualiza o timestamp
if ( response . status !== 200 ) {
2026-02-19 11:21:40 -03:00
logError ( ` ERRO: Falha ao atualizar status. Status: ${ response . status } - ${ response . statusText } ` , { sysId , ticketType , state : stateCode } ) ;
2025-10-23 17:30:03 -03:00
throw new Error ( ` Falha ao atualizar status no ServiceNow: ${ response . status } - ${ response . statusText } ` ) ;
}
2026-02-19 11:21:40 -03:00
logInfo ( ` OK: Status do ticket atualizado com sucesso no ServiceNow. ` , { sysId , ticketType , state : stateCode } ) ;
2025-10-23 17:30:03 -03:00
return true ;
} catch ( error ) {
2026-02-19 11:21:40 -03:00
logError ( error , ` ERRO: Falha ao atualizar status no ServiceNow para o ticket com sys_id: ${ sysId } ` ) ;
2025-10-23 17:30:03 -03:00
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 ) {
2026-02-19 11:21:40 -03:00
logError ( ` Ticket SN com ID ${ snTicketId } nao encontrado para fechamento. ` , 'closeTicketInServiceNow' ) ;
2025-10-23 17:30:03 -03:00
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 = { } ;
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 ,
2026-02-19 11:21:40 -03:00
work _notes : ` Ticket Encerrado por Suporte Tecnico Sothis: ${ closeNotes } `
2025-10-23 17:30:03 -03:00
} ;
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 ,
2026-02-19 11:21:40 -03:00
work _notes : ` Ticket Encerrado por Suporte Tecnico Sothis: ${ closeNotes } ` ,
2025-10-28 17:57:39 -03:00
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 ) {
2026-02-19 11:21:40 -03:00
logError ( ` ERRO: Falha ao resolver ticket. Status: ${ response . status } - ${ response . statusText } ` , { sysId , ticketType } ) ;
2025-10-23 17:30:03 -03:00
throw new Error ( ` Falha ao fechar ticket no ServiceNow: ${ response . status } - ${ response . statusText } ` ) ;
}
2026-02-19 11:21:40 -03:00
logInfo ( ` OK: Ticket resolvido com sucesso no ServiceNow. ` , { sysId , ticketType } ) ;
2025-10-23 17:30:03 -03:00
return true ;
} catch ( error ) {
2026-02-19 11:21:40 -03:00
logError ( error , ` ERRO: Falha ao fechar ticket no ServiceNow (sysId: ${ sysId } ) ` ) ;
2025-10-23 17:30:03 -03:00
return null ;
}
} ;
const addWorkNoteToServiceNow = async ( snTicketId , workNote ) => {
let sysId = null ;
try {
const ticketSn = await TicketSnModel . findById ( snTicketId ) ;
if ( ! ticketSn ) {
2026-02-19 11:21:40 -03:00
logError ( ` Ticket SN com ID ${ snTicketId } nao encontrado para adicionar work note. ` , 'addWorkNoteToServiceNow' ) ;
2025-10-23 17:30:03 -03:00
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' ) ;
2026-02-19 11:21:40 -03:00
logInfo ( ` OK: Work note adicionada com sucesso ao ticket SN ${ sysId } . ` ) ;
2025-10-23 17:30:03 -03:00
return true ;
} catch ( error ) {
2026-02-19 11:21:40 -03:00
logError ( error , ` ERRO: Falha ao adicionar work note ao ticket SN (sysId: ${ sysId } ) ` ) ;
2025-10-23 17:30:03 -03:00
return null ;
}
} ;
2025-10-28 14:06:00 -03:00
/ * *
2026-02-19 11:21:40 -03:00
* Busca o valor de uma variavel de catalogo ( sc _item _option ) para um Request Item ( RITM ) especifico .
2025-10-28 14:06:00 -03:00
* @ param { string } ritmSysId - O sys _id do sc _req _item ( RITM ) .
2026-02-19 11:21:40 -03:00
* @ param { string } variableName - O nome interno ( name ) da variavel de catalogo .
* @ returns { Promise < string | null > } O valor da variavel ou null se nao encontrada / erro .
2025-10-28 14:06:00 -03:00
* /
const fetchScItemOptionValue = async ( ritmSysId , variableName ) => {
try {
const baseUrl = apiConfig . snScItemOptionConfig . baseUrl . replace ( /\/$/ , "" ) ;
2026-02-19 18:22:38 -03:00
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.item_option_new.name,sc_item_option.value ` ;
2025-10-28 14:06:00 -03:00
const response = await axios . get ( url , {
auth : apiConfig . servicenowAuthentication . auth
} ) ;
2026-02-19 18:22:38 -03:00
const result = response . data . result || [ ] ;
if ( result . length === 0 ) return null ;
const normalize = ( value ) => {
if ( value === null || value === undefined ) return '' ;
if ( typeof value === 'string' ) return value ;
if ( typeof value === 'object' ) {
return value . value || value . display _value || '' ;
}
return String ( value ) ;
} ;
return result . map ( ( item ) => ( {
question _text : normalize ( item [ 'sc_item_option.item_option_new.question_text' ] ) ,
variable _name : normalize ( item [ 'sc_item_option.item_option_new.name' ] ) ,
value : normalize ( item [ 'sc_item_option.value' ] )
} ) ) ;
2025-10-28 14:06:00 -03:00
} catch ( error ) {
2026-02-19 11:21:40 -03:00
logError ( error , ` ERRO: Falha ao buscar valor da variavel ' ${ variableName } ' para RITM ${ ritmSysId } ` ) ;
2025-10-28 14:06:00 -03:00
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
2026-02-19 11:21:40 -03:00
* @ description Este servico encapsula todas as interacoes com a API REST do ServiceNow .
* Ele e responsavel por buscar , criar e atualizar dados na plataforma ServiceNow .
2025-10-29 14:40:21 -03:00
*
* Principais Funcionalidades :
2026-02-19 11:21:40 -03:00
* - * * Busca de Tickets * * : ` fetchTicketsFromServiceNow ` e ` fetchRequestsFromServiceNow ` buscam incidentes e requisicoes , respectivamente , utilizando uma marca d ' agua para obter apenas os registros novos ou atualizados .
* - * * Busca de Comentarios ( ` fetchCommentsFromServiceNow ` ) * * : Busca todos os comentarios ( journal entries ) de um ticket , com logica de paginacao para lidar com grandes volumes de dados . Ele tambem filtra os comentarios gerados pelo proprio sistema de integracao para evitar loops .
* - * * Criacao de Comentarios * * : ` createCommentInServiceNow ` e a funcao wrapper ` syncCommentToServiceNow ` adicionam comentarios ( work notes / comments ) a um ticket existente no ServiceNow .
* - * * Atualizacao 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 obrigatorios como notas de fechamento e data de resolucao .
* - * * Adicao de Notas ( ` addWorkNoteToServiceNow ` ) * * : Adiciona uma nota de trabalho a um ticket sem alterar seu status , util para registrar informacoes ou avisos .
* - * * Busca de Variaveis de Catalogo ( ` fetchScItemOptionValue ` ) * * : Funcao especializada para buscar valores de variaveis em itens de requisicao ( RITMs ) , usada para enriquecer a descricao de tickets .
2025-10-29 14:40:21 -03:00
*
2026-02-19 11:21:40 -03:00
* Todas as funcoes de interacao com a API sao envolvidas em blocos try - catch com logging detalhado para facilitar o monitoramento e a depuracao .
2025-10-29 14:40:21 -03:00
* /