From 938bf189f2dd8c7a64f3d131d05a4db2d2af760b Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Fri, 24 Oct 2025 13:13:08 -0300 Subject: [PATCH] TEST: Testando fluxo completo do projeto --- .env | 2 +- config/apiConfig.js | 3 +- src/app.js | 2 +- src/controllers/processStatusController.js | 2 +- src/controllers/processSyncController.js | 19 ++++--- src/models/ticketGlpiModel.js | 3 +- src/models/ticketSnModel.js | 64 ++++++++++++++++++---- src/services/servicenowService.js | 16 +++++- 8 files changed, 84 insertions(+), 27 deletions(-) diff --git a/.env b/.env index 8494053..771f1ff 100644 --- a/.env +++ b/.env @@ -6,7 +6,7 @@ SERVICENOW_INCIDENT_URL="https://caoadev.service-now.com/api/now/table/incident?sysparm_display_value=all&sysparm_limit=20&sysparm_query=ORDERBYDESCsys_created_on&sysparm_fields=number%2state%2Copened_by%2Copened_at%2Clocation%2Cshort_description%2Cdescription%2Cvariables.caller_id%2Cvariables.location%2Cvariables.telephone_favorecido_rh%2Cvariables.numero_ramal%2Copened_by.email%2Csys_id%5Eassignment_group%3D46d44f91c611227500713c12f0b7c78d" SERVICENOW_REQUEST_URL="https://caoadev.service-now.com/api/now/table/sc_req_item?sysparm_fields=number%2Cstate%2Crequest%2Copened_by%2Copened_at%2Clocation%2Cshort_description%2Cdescription%2Cassignment_group%2Csys_id&sysparm_display_value=all&sysparm_query=assignment_group%3D1132c9e01bfad410e77eb9121b4bcbab" SERVICENOW_TABLE_INCIDENT_URL="https://caoadev.service-now.com/api/now/table/incident/" -SERVICENOW_TABLE_REQUEST_URL="https://caoadev.service-now.com/api/now/table/sc_req_item/" +SERVICENOW_TABLE_REQUEST_URL="https://caoadev.service-now.com/api/now/table/sc_req_item" SERVICENOW_TABLE_JOURNAL_URL="https://caoadev.service-now.com/api/now/table/sys_journal_field" SERVICENOW_USERNAME=SOTHIS.CAOA SERVICENOW_PASSWORD=#Sothis20 diff --git a/config/apiConfig.js b/config/apiConfig.js index effa6a4..f0088d2 100644 --- a/config/apiConfig.js +++ b/config/apiConfig.js @@ -25,7 +25,8 @@ const snIncidentConfig = { } const snRequestConfig = { - baseUrl: process.env.SERVICENOW_REQUEST_URL, + // A URL base deve ser limpa, sem parâmetros. Os filtros serão adicionados no serviço. + baseUrl: process.env.SERVICENOW_REQUEST_URL, } const snTableIncidentConfig = { diff --git a/src/app.js b/src/app.js index 8580389..02b1506 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 075905b..8724a62 100644 --- a/src/controllers/processStatusController.js +++ b/src/controllers/processStatusController.js @@ -64,7 +64,7 @@ const processStatusAndClosureController = async () => { } else if (snCurrentStatus === 'Resolvido' && glpiCurrentStatus === 5) { // Ambos já estão resolvidos, nenhuma ação necessária. logInfo(`SN ${ticketSnDetails.ticket_number} e GLPI ${ticket.glpi_ticket_id} já estão no estado Resolvido/Solucionado. Nenhuma ação necessária.`); - } else if (snCurrentStatus === 'Encerrado - Omitido') { // SN foi para 'Closed' + } else if (snCurrentStatus === 'Encerrado - Omitido' || snCurrentStatus === 'Encerrado') { // SN foi para 'Closed' logInfo(`SN ${ticketSnDetails.ticket_number} foi Fechado. Sincronizando para GLPI...`); await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id); await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'closed'); diff --git a/src/controllers/processSyncController.js b/src/controllers/processSyncController.js index 77dcd20..ae41b1f 100644 --- a/src/controllers/processSyncController.js +++ b/src/controllers/processSyncController.js @@ -23,7 +23,12 @@ const processAndSaveTickets = async (tickets, type) => { const { id: idTableTicketSn, wasInserted } = await TicketSnModel.saveTicket(ticket, type); // 2. Prepara os dados para a tabela de sincronização 'ticket_sync' - const ticketStatus = ticket.state?.display_value; + // Para requisições, o status está diretamente em ticket.state + const ticketStatus = type === 'incidente' ? ticket.state?.display_value : ticket.state; + + + + console.log(ticketStatus) const dataSync = { sn_ticket_id: idTableTicketSn, last_sn_sync: new Date(), @@ -35,13 +40,12 @@ const processAndSaveTickets = async (tickets, type) => { // 3. Define o status inicial de sincronização if (ticketStatus === 'Encerrado' || ticketStatus === 'Encerrado - Omitido') { dataSync.sn_sync_status = 'closed'; - dataSync.glpi_sync_status = 'ignored'; - dataSync.source_last = 'SNOW'; + dataSync.glpi_sync_status = 'ignored'; // Ignora no GLPI se já veio fechado do SN } else { dataSync.sn_sync_status = 'collected'; // Status que indica que foi coletado do SN - dataSync.glpi_sync_status = 'pending_check'; // Status que indica que precisa ser checkado no GLPI - dataSync.source_last = 'SNOW'; + dataSync.glpi_sync_status = 'pending_check'; // Status que indica que precisa ser checkado no GLPI } + dataSync.source_last = 'SNOW'; // A última atualização veio do ServiceNow // 4. Cria o registro de sincronização se ele não existir const exists = await TicketSyncModel.getIdSyncBySnId(idTableTicketSn); @@ -81,13 +85,14 @@ const processSyncController = async () => { logInfo(`📡 Buscando tickets do ServiceNow atualizados desde: ${watermark}`); // Busca incidentes + const incidents = await fetchTicketsApi(watermark); const latestIncidentUpdate = await processAndSaveTickets(incidents, 'incidente'); // Busca requisições - //logInfo('📡 Buscando requisições do ServiceNow...'); + logInfo('📡 Buscando requisições do ServiceNow...'); const requests = await fetchRequestsApi(watermark); - const latestRequestUpdate = await processAndSaveTickets(requests, 'requisicao'); + const latestRequestUpdate = await processAndSaveTickets(requests, 'requisicao'); // 2. Determina a nova marca d'água (o timestamp mais recente entre todos os tickets processados) const newWatermark = latestIncidentUpdate > latestRequestUpdate ? latestIncidentUpdate : latestRequestUpdate; diff --git a/src/models/ticketGlpiModel.js b/src/models/ticketGlpiModel.js index f4f4dbe..c451cd3 100644 --- a/src/models/ticketGlpiModel.js +++ b/src/models/ticketGlpiModel.js @@ -12,7 +12,8 @@ class TicketGlpiModel { 'Em Atendimento': 2, 'Em Espera': 4, 'Resolvido': 5, - 'Encerrado - Omitido': 6 + 'Encerrado - Omitido': 6, + 'Encerrado': 6 // Mapeia para Fechado no GLPI }; return statusMap[snState] || 1; } diff --git a/src/models/ticketSnModel.js b/src/models/ticketSnModel.js index 348a260..a93deec 100644 --- a/src/models/ticketSnModel.js +++ b/src/models/ticketSnModel.js @@ -27,19 +27,59 @@ class TicketSnModel { tipo = EXCLUDED.tipo RETURNING id, (xmax = 0) AS "wasInserted"`; // xmax=0 é um truque do PostgreSQL para saber se foi INSERT + // Trata a diferença na estrutura do campo 'number' entre Incidentes e Requisições + const ticketNumber = typeof ticketData.number === 'object' ? ticketData.number?.value : ticketData.number; + + // 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; + locationName = ticketData.location?.display_value || null; + openedAt = ticketData.opened_at?.value || 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; + sysId = ticketData.sys_id || null; + shortDescription = ticketData.short_description || null; + state = ticketData.state || null; + description = ticketData.description || null; + // Para requisições, o sys_id da localização vem no final da URL do link + const locationLink = ticketData.location?.link; + locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null; + locationName = ticketData.location?.display_value || null; + openedAt = ticketData.opened_at || null; + } + const values = [ - ticketData.number?.value || null, - ticketData.sys_id?.value || null, - ticketData.short_description?.value || null, - ticketData.state?.display_value || null, - ticketData.description?.value || null, - ticketData.opened_by?.display_value || null, - ticketData.sys_created_by?.display_value || null, - ticketData.location?.value || null, - ticketData.location?.display_value || null, - ticketData['variables.numero_ramal']?.value || null, - ticketData['variables.telephone_favorecido_rh']?.value || null, - ticketData.opened_at?.value ? new Date(ticketData.opened_at.value) : null, + ticketNumber, + sysId, + shortDescription, + state, + description, + callerName, + callerEmail, + locationId, + locationName, + ramal, + telefone, + openedAt + ? (typeof openedAt === 'string' && openedAt.includes('/') + ? new Date(openedAt.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$2-$1')) + : new Date(openedAt)) + : null, typeTicket ]; diff --git a/src/services/servicenowService.js b/src/services/servicenowService.js index 3252063..7cbdfa8 100644 --- a/src/services/servicenowService.js +++ b/src/services/servicenowService.js @@ -9,8 +9,9 @@ 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`; + const url = `${apiConfig.snIncidentConfig.baseUrl}?sysparm_query=${query}&sysparm_display_value=true&sysparm_fields=${fields}`; const ticketsResponse = await axios.get(url, { auth: apiConfig.servicenowAuthentication.auth }); @@ -21,10 +22,19 @@ const fetchTicketsFromServiceNow = async (watermark) => { } } + + const fetchRequestsFromServiceNow = async (watermark) => { try { - const query = `active=true^sys_updated_on>=${watermark}`; - const url = `${apiConfig.snRequestConfig.baseUrl}?sysparm_query=${query}&sysparm_display_value=true`; + // A tabela de requisições (sc_req_item) não possui o campo 'active'. A query deve ser apenas pela data. + // 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'; + // 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 requestsResponse = await axios.get(url, { auth: apiConfig.servicenowAuthentication.auth });