diff --git a/.env b/.env index 771f1ff..a531a7a 100644 --- a/.env +++ b/.env @@ -3,11 +3,12 @@ # This is an example of the environment variables needed for the application. # ServiceNow API Configuration -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_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%3D1132c9e01bfad410e77eb9121b4bcbab" 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_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_SC_ITEM_OPTION_URL="https://caoadev.service-now.com/api/now/table/sc_item_option_mtom" SERVICENOW_USERNAME=SOTHIS.CAOA SERVICENOW_PASSWORD=#Sothis20 diff --git a/config/apiConfig.js b/config/apiConfig.js index f0088d2..8b59551 100644 --- a/config/apiConfig.js +++ b/config/apiConfig.js @@ -41,6 +41,10 @@ const snTableJournalConfig = { baseUrl: process.env.SERVICENOW_TABLE_JOURNAL_URL, } +const snScItemOptionConfig = { + baseUrl: process.env.SERVICENOW_SC_ITEM_OPTION_URL, +} + module.exports = { // configuração do ServiceNow e outras já existentes podem permanecer glpiDbConfig, @@ -49,5 +53,6 @@ module.exports = { snTableIncidentConfig, snTableJournalConfig, servicenowAuthentication, - snTableRequestConfig + snTableRequestConfig, + snScItemOptionConfig }; \ No newline at end of file diff --git a/src/app.js b/src/app.js index 02b1506..8580389 100644 --- a/src/app.js +++ b/src/app.js @@ -14,7 +14,7 @@ logInfo('Aplicação iniciada', { async function main() { try { await processErrorController(); // Reseta os erros - +await processTicketsController(); // Coleta tickets do SN e cria no GLPI + await processTicketsController(); // Coleta tickets do SN e cria no GLPI await processCommentsController(); // Sincroniza chamados Bidirecionalmente await processStatusAndClosureController(); // Fecha chamados Bidirecionalmente logInfo('🎉 Ciclo de sincronização concluído com sucesso! 🎉'); diff --git a/src/controllers/processStatusController.js b/src/controllers/processStatusController.js index 8724a62..626e2d8 100644 --- a/src/controllers/processStatusController.js +++ b/src/controllers/processStatusController.js @@ -35,13 +35,13 @@ const processStatusAndClosureController = async () => { // ================= LÓGICA DE DETECÇÃO DE MUDANÇA NO GLPI ================= // Primeiro, verificamos se o GLPI mudou de estado por conta própria. const glpiLiveStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id); - const glpiDbStatus = ticket.glpi_sync_status; - // Converte o status numérico do GLPI para a string que usamos no nosso banco (ex: 'solved') - const glpiLiveStatusAsString = TicketGlpiModel.mapGlpiStatusToString(glpiLiveStatus).toLowerCase().replace(/ /g, '_').replace('-', '_'); + // Converte o status numérico do GLPI para a string correspondente no ServiceNow (ex: 'Em Atendimento') + const glpiLiveStatusAsString = TicketGlpiModel.mapGlpiStatusToString(glpiLiveStatus); - if (glpiLiveStatus !== null && glpiLiveStatusAsString !== glpiDbStatus) { - logInfo(`Mudança de status detectada no GLPI para o ticket ${ticket.glpi_ticket_id}. De '${glpiDbStatus}' para '${glpiLiveStatusAsString}'.`); + // Compara o status real do GLPI com o status que temos do ServiceNow + if (glpiLiveStatus !== null && glpiLiveStatusAsString !== snCurrentStatus) { + logInfo(`Mudança de status detectada no GLPI para o ticket ${ticket.glpi_ticket_id}. GLPI está como '${glpiLiveStatusAsString}', SN está como '${snCurrentStatus}'.`); // Se uma mudança foi detectada no GLPI, forçamos o "bastão" para ele. ticket.source_last = 'GLPI'; } @@ -95,6 +95,17 @@ const processStatusAndClosureController = async () => { await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored'); await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id); } else { + // REGRA DE NEGÓCIO: Não pode fechar um ticket que está "Em Espera" ou "Aguardando Atendimento" no SN. + if (snCurrentStatus === 'Em Espera' || snCurrentStatus === 'Aguardando Atendimento') { + logWarning(`Tentativa de fechar o ticket SN ${ticketSnDetails.ticket_number} que está '${snCurrentStatus}'. Ação bloqueada.`); + // Não Adiciona uma nota no SN e reabre o ticket no GLPI para refletir o status do SN. + //await addWorkNoteToServiceNow(ticket.sn_ticket_id, `O ticket correspondente no GLPI (${ticket.glpi_ticket_id}) foi solucionado, mas o chamado no ServiceNow está '${snCurrentStatus}'. É necessário retomar o atendimento para prosseguir com o encerramento.`); + //Atualiza o status do GLPI + await TicketGlpiModel.updateStatus(ticket.glpi_ticket_id, snCurrentStatus); // Reabre o GLPI imediatamente + await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW'); // Passa o bastão para o SN + continue; // Pula para o próximo ticket + } + // FLUXO NORMAL DE RESOLUÇÃO const closeNotes = solution?.content ? stripHTML(solution.content) : 'Resolvido via integração GLPI.'; const resolvedAt = solution?.date_mod || new Date(); diff --git a/src/controllers/processSyncController.js b/src/controllers/processSyncController.js index 47c2a98..638ee48 100644 --- a/src/controllers/processSyncController.js +++ b/src/controllers/processSyncController.js @@ -1,8 +1,9 @@ const TicketSnModel = require('../models/ticketSnModel'); const TicketSyncModel = require('../models/ticketSyncModel'); const SyncControlModel = require('../models/syncControlModel'); -const { fetchTicketsFromServiceNow: fetchTicketsApi, fetchRequestsFromServiceNow: fetchRequestsApi } = require('../services/servicenowService'); +const { fetchTicketsFromServiceNow: fetchTicketsApi, fetchRequestsFromServiceNow: fetchRequestsApi, fetchScItemOptionValue } = require('../services/servicenowService'); const { logInfo, logError, logSync } = require('../utils/logger'); +const e = require('express'); /** * Processa e salva um lote de tickets (incidentes ou requisições). @@ -10,22 +11,63 @@ const { logInfo, logError, logSync } = require('../utils/logger'); * @param {('incidente'|'requisicao')} type - O tipo de ticket. */ const processAndSaveTickets = async (tickets, type) => { + // Helper para converter a data do formato 'DD/MM/YYYY HH:mm:ss' para um objeto Date. + const parseServiceNowDate = (dateString) => { + if (!dateString) return null; + // Verifica se já está em um formato reconhecível (como ISO 8601) + if (!dateString.includes('/')) { + return new Date(dateString); + } + const parts = dateString.match(/(\d{2})\/(\d{2})\/(\d{4}) (\d{2}):(\d{2}):(\d{2})/); + if (!parts) return null; // Retorna nulo se o formato for inesperado + // Formato: new Date(ano, mês-1, dia, hora, minuto, segundo) + return new Date(parts[3], parts[2] - 1, parts[1], parts[4], parts[5], parts[6]); + }; + if (!tickets || tickets.length === 0) { logInfo(`Nenhum ticket do tipo '${type}' para processar.`); return ''; // Retorna string vazia se não houver tickets } logInfo(`💾 Iniciando salvamento de ${tickets.length} ${type}s no banco...`); - let latestUpdateTimestamp = ''; + let latestUpdateTimestamp = new Date(0); // Inicia com a data mínima (epoch) for (const ticket of tickets) { try { + // --- LÓGICA DE ENRIQUECIMENTO DA DESCRIÇÃO --- + // Se for uma requisição e a descrição estiver vazia, busca na sc_item_option. + if (type === 'requisicao' && (!ticket.description || ticket.description.trim() === '')) { + logInfo(`Descrição vazia para requisição ${ticket.number}. Buscando em sc_item_option...`); + // O nome 'justificativa' é um exemplo, use o nome real da sua variável. + const extraData = await fetchScItemOptionValue(ticket.sys_id, 'justificativa'); + const vars = {}; + (extraData || []).forEach(item => { + const question = item['sc_item_option.item_option_new.question_text']; + const value = item['sc_item_option.value']; + + if (question === 'Select what you want') vars.whatYouWant = value; + if (question === 'Please justify the need') vars.justification = value; + if (question === 'Telephone') vars.telephone = value; + }); + + + if (vars.justification) { + ticket.description = vars.justification; // Atualiza o objeto do ticket em memória + logInfo(`Descrição da requisição ${ticket.number} atualizada com valor de sc_item_option.`); + } + if (vars.telephone){ + ticket.telefone = vars.telephone; + logInfo(`Telefone da requisição ${ticket.number} atualizado com valor de sc_item_option.`); + } + } + // --- FIM DA LÓGICA DE ENRIQUECIMENTO --- + // 1. Salva o ticket na tabela 'tickets_sn' const { id: idTableTicketSn, wasInserted } = await TicketSnModel.saveTicket(ticket, type); // 2. Prepara os dados para a tabela de sincronização 'ticket_sync' // Para requisições, o status está diretamente em ticket.state - const ticketStatus = type === 'incidente' ? ticket.state?.display_value : ticket.state; - + const ticketStatus = ticket.state; + const dataSync = { sn_ticket_id: idTableTicketSn, last_sn_sync: new Date(), @@ -52,17 +94,16 @@ const processAndSaveTickets = async (tickets, type) => { // Se o ticket foi atualizado (não inserido), significa que uma mudança veio do SN. // Então, definimos o SN como a última fonte da verdade. await TicketSyncModel.updateSourceLast(idTableTicketSn, 'SNOW'); - logInfo(`Ticket ${ticket.number?.value} atualizado pelo ServiceNow. 'source_last' definido como 'SNOW'.`); + logInfo(`Ticket ${ticket.number} atualizado pelo ServiceNow. 'source_last' definido como 'SNOW'.`); } else { logInfo(`Registro de sincronização para o ticket ${ticket.number?.value} já existe. Nenhuma ação necessária.`); } // Se já existe, não faz nada, pois a lógica de atualização de status será em outro processo. // Guarda o timestamp mais recente do lote para usar como a próxima marca d'água - let currentUpdate = type === 'incidente' ? ticket.sys_updated_on?.value : ticket.sys_updated_on; - currentUpdate = defaultDate(currentUpdate); - if (currentUpdate && currentUpdate > latestUpdateTimestamp) { - latestUpdateTimestamp = currentUpdate; + const currentUpdateDate = parseServiceNowDate(ticket.sys_updated_on); + if (currentUpdateDate && currentUpdateDate.getTime() > latestUpdateTimestamp.getTime()) { + latestUpdateTimestamp = currentUpdateDate; } } catch (error) { @@ -70,7 +111,11 @@ const processAndSaveTickets = async (tickets, type) => { } } logSync('ServiceNow', tickets.length, `${type}s`); - return latestUpdateTimestamp; + + // Se encontramos uma data válida, a formata para 'YYYY-MM-DD HH:MM:SS'. Senão, retorna string vazia. + return latestUpdateTimestamp.getTime() > 0 + ? latestUpdateTimestamp.toISOString().slice(0, 19).replace('T', ' ') + : ''; }; /** @@ -94,11 +139,12 @@ const processSyncController = async () => { // 2. Determina a nova marca d'água (o timestamp mais recente entre todos os tickets processados) const newWatermark = latestIncidentUpdate > latestRequestUpdate ? latestIncidentUpdate : latestRequestUpdate; - + // 3. Se processamos algum ticket, atualiza a marca d'água para a próxima execução if (newWatermark) { // Adiciona 1 segundo para evitar reprocessar o último ticket em caso de timestamps iguais - const nextWatermark = new Date(new Date(newWatermark).getTime() + 1000); + const sixHoursInMillis = 6 * 60 * 60 * 1000; + const nextWatermark = new Date(new Date(newWatermark).getTime() - sixHoursInMillis); await SyncControlModel.setWatermark('servicenow', nextWatermark); } else { logInfo("Nenhum ticket novo ou atualizado encontrado. A marca d'água não foi alterada."); @@ -109,29 +155,6 @@ const processSyncController = async () => { } }; - -//Função auxiliar para padronizar data - -function defaultDate(dataStr) { - if (!dataStr) return null; - const [data, hora] = dataStr.split(' '); - - let ano, mes, dia; - if (data.includes('/')) { - // Ex: 31/12/2024 → 2024-12-31 - [dia, mes, ano] = data.split('/'); - } else { - // Ex: 2025-10-24 → já está no formato - [ano, mes, dia] = data.split('-'); - } - - // Garante formato consistente (YYYY-MM-DD HH:mm:ss) - return `${ano.padStart(4, '0')}-${mes.padStart(2, '0')}-${dia.padStart(2, '0')} ${hora}`; -} - - - - module.exports = { processSyncController }; \ No newline at end of file diff --git a/src/data/glpiDataBase.js b/src/data/glpiDataBase.js index 1aea981..e5bd960 100644 --- a/src/data/glpiDataBase.js +++ b/src/data/glpiDataBase.js @@ -11,7 +11,9 @@ const glpiPool = mysql.createPool({ password: process.env.GLPI_DB_PASSWORD, waitForConnections: true, connectionLimit: 10, - queueLimit: 0 + queueLimit: 0, + // Fecha conexões ociosas após 60 segundos, evitando o erro ECONNRESET. + idleTimeout: 60000 }); // Testar conexão diff --git a/src/models/ticketGlpiModel.js b/src/models/ticketGlpiModel.js index c451cd3..429d2fe 100644 --- a/src/models/ticketGlpiModel.js +++ b/src/models/ticketGlpiModel.js @@ -295,8 +295,8 @@ class TicketGlpiModel { static async insertComment(comment, items_id) { const connection = await glpiPool.getConnection(); try { - const query = 'INSERT INTO glpi_itilfollowups(itemtype, items_id, date, users_id, content, date_creation, date_mod, timeline_position) VALUES(?, ?, ?, ?, ?, ?, ?, ?)'; - const values = ['Ticket', items_id, comment.created_at, 971, comment.content , comment.created_at, comment.created_at, 1]; + const query = 'INSERT INTO glpi_itilfollowups(itemtype, items_id, date, users_id, content, date_creation, date_mod, timeline_position) VALUES(?, ?, NOW(), ?, ?, NOW(), NOW(), ?)'; + const values = ['Ticket', items_id, 971, comment.content , 1]; const [result] = await connection.execute(query, values); return [{ id: result.insertId }]; // Retorna o ID inserido diff --git a/src/models/ticketSnModel.js b/src/models/ticketSnModel.js index a93deec..b0a7365 100644 --- a/src/models/ticketSnModel.js +++ b/src/models/ticketSnModel.js @@ -33,25 +33,28 @@ class TicketSnModel { // Mapeamento inteligente de campos baseado no tipo de ticket let callerName, callerEmail, ramal, telefone, sysId, shortDescription, state, description, locationId, locationName, openedAt; + + if (typeTicket === 'incidente') { callerName = ticketData.caller_id?.display_value; // O campo de email para incidentes vem como um objeto, precisamos extrair o valor. const emailObject = ticketData['caller_id.email'] || {}; callerEmail = (typeof emailObject === 'object' ? emailObject.value : emailObject) || null; ramal = null; // Incidentes não têm essa variável por padrão - telefone = null; // Incidentes não têm essa variável por padrão - sysId = ticketData.sys_id?.value || null; - shortDescription = ticketData.short_description?.value || null; - state = ticketData.state?.display_value || null; - description = ticketData.description?.value || null; - locationId = ticketData.location?.value || null; + telefone = ticketData['caller_id.phone'] || ticketData['caller_id.mobile_phone'] || null; // Incidentes não têm essa variável por padrão + sysId = ticketData.sys_id || null; + shortDescription = ticketData.short_description || null; + state = ticketData.state || null; + description = ticketData.description || null; + const locationLink = ticketData.location?.link; + locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null; locationName = ticketData.location?.display_value || null; - openedAt = ticketData.opened_at?.value || null; + openedAt = ticketData.opened_at || null; } else { // 'requisicao' callerName = ticketData.opened_by?.display_value || null; callerEmail = ticketData['opened_by.email'] || null; ramal = ticketData['variables.numero_ramal'] || null; - telefone = ticketData['variables.telephone_favorecido_rh'] || null; + telefone = ticketData['variables.telephone_favorecido_rh'] || ticketData.telefone || null; sysId = ticketData.sys_id || null; shortDescription = ticketData.short_description || null; state = ticketData.state || null; diff --git a/src/services/glpiTicketService.js b/src/services/glpiTicketService.js index be9045f..b2fa7b0 100644 --- a/src/services/glpiTicketService.js +++ b/src/services/glpiTicketService.js @@ -3,7 +3,7 @@ const TicketSnModel = require('../models/ticketSnModel'); const TicketGlpiModel = require('../models/ticketGlpiModel'); const TicketUpdateModel = require('../models/ticketUpdateModel'); const TicketSyncModel = require('../models/ticketSyncModel'); -const { logInfo, logError } = require('../utils/logger'); +const { logInfo, logError, logWarning } = require('../utils/logger'); const { addWorkNoteToServiceNow } = require('./servicenowService'); @@ -53,10 +53,20 @@ const processSingleTicket = async (ticket) => { if (existingTicket) { logInfo(`✅ Ticket ${ticket.ticket_number} encontrado no GLPI. Atualizando registro local com o ID: ${existingTicket}`); + + // --- NOVA VALIDAÇÃO: Verificar status do ticket existente no GLPI --- + const glpiLiveStatus = await TicketGlpiModel.getTicketStatus(existingTicket); + if (glpiLiveStatus === 6) { // Status 6 = Fechado no GLPI + logInfo(`Ticket GLPI ${existingTicket} já está Fechado. Marcando ticket SN ${ticket.ticket_number} como 'closed'/'ignored' e não monitorado.`); + await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'ignored', 'closed', existingTicket); + return; // Não processa mais este ticket + } + // --- FIM DA NOVA VALIDAÇÃO --- + // 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; + return; // Adiciona este return para evitar a criação de um novo ticket } // 3. Converter dados para formato GLPI @@ -77,6 +87,21 @@ const processSingleTicket = async (ticket) => { updated_at: ticket.updated_at }; + // Lógica para buscar descrição de sc_item_option se a descrição atual estiver vazia + // e for um ticket de requisição (apenas para criação de novos tickets) + if (glpiData.type === 'requisicao' && (!glpiData.description || glpiData.description.trim() === '')) { + logInfo(`Descrição vazia para requisição ${ticket.ticket_number}. Tentando buscar de sc_item_option.`); + // Você precisará saber o nome interno da variável de catálogo que contém a descrição. + // Por exemplo, se for 'justificativa', use 'justificativa'. + const scItemOptionDescription = await require('./servicenowService').fetchScItemOptionValue(ticket.sys_id, 'justificativa'); + if (scItemOptionDescription) { + glpiData.description = scItemOptionDescription; + logInfo(`Descrição atualizada para requisição ${ticket.ticket_number} a partir de sc_item_option.`); + } else { + logWarning(`Não foi possível obter descrição de sc_item_option para requisição ${ticket.ticket_number}.`); + } + } + // 3. Inserir no GLPI const glpiTicketId = await TicketGlpiModel.createTicket(glpiData); await TicketSyncModel.updateLastSync(ticket.id, 'glpi'); // Atualiza o timestamp diff --git a/src/services/servicenowService.js b/src/services/servicenowService.js index 7cbdfa8..929807b 100644 --- a/src/services/servicenowService.js +++ b/src/services/servicenowService.js @@ -9,9 +9,14 @@ const { log } = require('winston'); const fetchTicketsFromServiceNow = async (watermark) => { try { - const fields = 'number,sys_id,short_description,state,description,caller_id,caller_id.email,location,opened_at,sys_updated_on'; - const query = `active=true^sys_updated_on>=${watermark}`; - const url = `${apiConfig.snIncidentConfig.baseUrl}?sysparm_query=${query}&sysparm_display_value=true&sysparm_fields=${fields}`; + // A watermark vem como string 'YYYY-MM-DD HH:MM:SS'. Converte para Date. + const watermarkDate = new Date(watermark); + // Ajusta para o fuso horário correto se necessário e formata para a query do SN. + const watermarkBRT = new Date(watermarkDate.getTime()); // Se precisar de ajuste de fuso, faça aqui. + 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'; + const query = `assignment_group=1132c9e01bfad410e77eb9121b4bcbab^sys_updated_on>=${formattedWatermark}`; + const url = `${apiConfig.snTableIncidentConfig.baseUrl}?sysparm_query=${encodeURIComponent(query)}&sysparm_display_value=true&sysparm_fields=${fields}`; const ticketsResponse = await axios.get(url, { auth: apiConfig.servicenowAuthentication.auth }); @@ -30,10 +35,12 @@ const fetchRequestsFromServiceNow = async (watermark) => { // Construímos a query completa aqui, combinando o grupo de atribuição com a watermark. const assignmentGroupSysId = '1132c9e01bfad410e77eb9121b4bcbab'; // Sys ID do grupo 'Sothis' const query = `assignment_group=${assignmentGroupSysId}^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.numero_ramal,variables.telephone_favorecido_rh'; + 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'; // Garante que a URL base não tenha uma barra no final para evitar URL malformada. const baseUrl = apiConfig.snTableRequestConfig.baseUrl.replace(/\/$/, ""); - const url = `${baseUrl}?sysparm_query=${query}&sysparm_display_value=true&sysparm_fields=${fields}`; + //const url = `${baseUrl}?sysparm_query=${query}&sysparm_display_value=true&sysparm_fields=${fields}`; + const url = `${baseUrl}?sysparm_query=${encodeURIComponent(query)}&sysparm_display_value=true&sysparm_fields=${fields}`; + const requestsResponse = await axios.get(url, { auth: apiConfig.servicenowAuthentication.auth @@ -108,12 +115,12 @@ const createCommentInServiceNow = async (snId, sysId, comment, ticketType) => { let url = ''; let payload = {}; if (ticketType == 'incidente') { - url = `${apiConfig.snTableIncidentConfig.baseUrl}${sysId}`; + url = `${apiConfig.snTableIncidentConfig.baseUrl}/${sysId}`; payload = { comments: comment }; } else if (ticketType == 'requisicao') { - url = `${apiConfig.snTableRequestConfig.baseUrl}${sysId}`; + url = `${apiConfig.snTableRequestConfig.baseUrl}/${sysId}`; payload = { comments: comment }; @@ -364,10 +371,10 @@ const updateStatusInServiceNow = async (snTicketId, state) => { let payload = {}; if (ticketType === 'incidente') { - url = `${apiConfig.snTableIncidentConfig.baseUrl}${sysId}`; + url = `${apiConfig.snTableIncidentConfig.baseUrl}/${sysId}`; payload = { state: stateCode, incident_state: stateCode }; // Envia state e incident_state } else if (ticketType === 'requisicao') { - url = `${apiConfig.snTableRequestConfig.baseUrl}${sysId}`; + url = `${apiConfig.snTableRequestConfig.baseUrl}/${sysId}`; payload = { state: stateCode }; // Requisições podem ter um fluxo diferente } else { logError(`Tipo de ticket desconhecido: ${ticketType}`, { sysId, ticketType }); @@ -406,7 +413,7 @@ const closeTicketInServiceNow = async (snTicketId, closeNotes, resolvedAt) => { logError(`Ticket SN com ID ${snTicketId} não encontrado para fechamento.`, 'closeTicketInServiceNow'); return null; } - + sysId = ticketSn.sys_id; const ticketType = ticketSn.tipo; let url = ''; // Declarar url fora do if @@ -417,7 +424,7 @@ const closeTicketInServiceNow = async (snTicketId, closeNotes, resolvedAt) => { const resolvedBySysId = 'b131d6ed1b2ba510c5e163923b4bcb77'; // sys_id do usuário que resolveu if (ticketType === 'incidente') { - url = `${apiConfig.snTableIncidentConfig.baseUrl}${sysId}`; + url = `${apiConfig.snTableIncidentConfig.baseUrl}/${sysId}`; payload = { state: '6', // 6 = Resolved incident_state: '6', // 6 = Resolved @@ -427,13 +434,19 @@ const closeTicketInServiceNow = async (snTicketId, closeNotes, resolvedAt) => { close_notes: closeNotes, work_notes: `Ticket Encerrado por Suporte Técnico Sothis: ${closeNotes}` }; + + } else if (ticketType === 'requisicao') { - url = `${apiConfig.snTableRequestConfig.baseUrl}${sysId}`; + + url = `${apiConfig.snTableRequestConfig.baseUrl}/${sysId}`; payload = { - state: '6', // Assumindo que '6' também é um estado de resolução para requisições - // Requisições podem não ter os mesmos campos de resolução. Ajuste se necessário. + state: '6', work_notes: closeNotes || 'Requisição resolvida automaticamente via integração GLPI.' }; + + console.log(url); + console.log(payload); + } else { logError(`Tipo de ticket desconhecido: ${ticketType}`, { sysId, ticketType }); throw new Error(`Tipo de ticket desconhecido: ${ticketType}`); @@ -478,9 +491,9 @@ const addWorkNoteToServiceNow = async (snTicketId, workNote) => { let url = ''; if (ticketType === 'incidente') { - url = `${apiConfig.snTableIncidentConfig.baseUrl}${sysId}`; + url = `${apiConfig.snTableIncidentConfig.baseUrl}/${sysId}`; } else if (ticketType === 'requisicao') { - url = `${apiConfig.snTableRequestConfig.baseUrl}${sysId}`; + url = `${apiConfig.snTableRequestConfig.baseUrl}/${sysId}`; } else { throw new Error(`Tipo de ticket desconhecido: ${ticketType}`); } @@ -502,6 +515,30 @@ const addWorkNoteToServiceNow = async (snTicketId, workNote) => { } }; +/** + * 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} 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; + } +}; + module.exports = { fetchTicketsFromServiceNow, fetchRequestsFromServiceNow, @@ -512,7 +549,8 @@ module.exports = { syncCommentToServiceNow, fetchCommentsFromServiceNow, fetchAndProcessClosedTicketsFromSN, - closeTicketInServiceNow, - updateStatusInServiceNow, - addWorkNoteToServiceNow + closeTicketInServiceNow, // Adicionei a vírgula aqui + updateStatusInServiceNow, // Adicionei a vírgula aqui + addWorkNoteToServiceNow, // Adicionei a vírgula aqui + fetchScItemOptionValue // Nova função };