TEST: Testando fluxo completo do projeto

This commit is contained in:
Rafael Alves Lopes 2025-10-24 13:13:08 -03:00
parent 0cc27df2cf
commit 938bf189f2
8 changed files with 84 additions and 27 deletions

2
.env
View File

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

View File

@ -25,6 +25,7 @@ const snIncidentConfig = {
}
const snRequestConfig = {
// A URL base deve ser limpa, sem parâmetros. Os filtros serão adicionados no serviço.
baseUrl: process.env.SERVICENOW_REQUEST_URL,
}

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

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

View File

@ -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.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,11 +85,12 @@ 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');

View File

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

View File

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

View File

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