TEST: Testando fluxo completo do projeto
This commit is contained in:
parent
0cc27df2cf
commit
938bf189f2
2
.env
2
.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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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! 🎉');
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
];
|
||||
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user