FEAT: Coletado mais dados de incidentes e requisições.

*   **Enriquecimento de Dados e Criação de Tickets:**
    *   Requisições do ServiceNow com descrição vazia agora são enriquecidas buscando dados de variáveis de catálogo (`sc_item_option`), como justificativas e telefones, melhorando a qualidade dos tickets criados no GLPI.
    *   O `ticketGlpiModel` agora formata descrições em tabelas HTML para melhor legibilidade no GLPI.

*   **Refatoração e Novas Funções:**
    *   O `servicenowService` foi expandido com funções para fechar, atualizar status e adicionar notas de trabalho (`work_notes`) diretamente nos tickets do ServiceNow.

*   **Coleta de dados de telefone e ajustes para coletar o Assigment correro
    *   Water Mark Ajustado para que processo o chamado apenas uma vez.
This commit is contained in:
Rafael Alves Lopes 2025-10-28 14:06:00 -03:00
parent e436fe17af
commit a082bad82c
10 changed files with 184 additions and 76 deletions

5
.env
View File

@ -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

View File

@ -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
};

View File

@ -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! 🎉');

View File

@ -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();

View File

@ -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
};

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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<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;
}
};
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
};