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_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_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_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_TABLE_JOURNAL_URL="https://caoadev.service-now.com/api/now/table/sys_journal_field"
|
||||||
SERVICENOW_USERNAME=SOTHIS.CAOA
|
SERVICENOW_USERNAME=SOTHIS.CAOA
|
||||||
SERVICENOW_PASSWORD=#Sothis20
|
SERVICENOW_PASSWORD=#Sothis20
|
||||||
|
|||||||
@ -25,7 +25,8 @@ const snIncidentConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const snRequestConfig = {
|
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 = {
|
const snTableIncidentConfig = {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ logInfo('Aplicação iniciada', {
|
|||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
await processErrorController(); // Reseta os erros
|
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 processCommentsController(); // Sincroniza chamados Bidirecionalmente
|
||||||
await processStatusAndClosureController(); // Fecha chamados Bidirecionalmente
|
await processStatusAndClosureController(); // Fecha chamados Bidirecionalmente
|
||||||
logInfo('🎉 Ciclo de sincronização concluído com sucesso! 🎉');
|
logInfo('🎉 Ciclo de sincronização concluído com sucesso! 🎉');
|
||||||
|
|||||||
@ -64,7 +64,7 @@ const processStatusAndClosureController = async () => {
|
|||||||
} else if (snCurrentStatus === 'Resolvido' && glpiCurrentStatus === 5) {
|
} else if (snCurrentStatus === 'Resolvido' && glpiCurrentStatus === 5) {
|
||||||
// Ambos já estão resolvidos, nenhuma ação necessária.
|
// 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.`);
|
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...`);
|
logInfo(`SN ${ticketSnDetails.ticket_number} foi Fechado. Sincronizando para GLPI...`);
|
||||||
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
||||||
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'closed');
|
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);
|
const { id: idTableTicketSn, wasInserted } = await TicketSnModel.saveTicket(ticket, type);
|
||||||
|
|
||||||
// 2. Prepara os dados para a tabela de sincronização 'ticket_sync'
|
// 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 = {
|
const dataSync = {
|
||||||
sn_ticket_id: idTableTicketSn,
|
sn_ticket_id: idTableTicketSn,
|
||||||
last_sn_sync: new Date(),
|
last_sn_sync: new Date(),
|
||||||
@ -35,13 +40,12 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
// 3. Define o status inicial de sincronização
|
// 3. Define o status inicial de sincronização
|
||||||
if (ticketStatus === 'Encerrado' || ticketStatus === 'Encerrado - Omitido') {
|
if (ticketStatus === 'Encerrado' || ticketStatus === 'Encerrado - Omitido') {
|
||||||
dataSync.sn_sync_status = 'closed';
|
dataSync.sn_sync_status = 'closed';
|
||||||
dataSync.glpi_sync_status = 'ignored';
|
dataSync.glpi_sync_status = 'ignored'; // Ignora no GLPI se já veio fechado do SN
|
||||||
dataSync.source_last = 'SNOW';
|
|
||||||
} else {
|
} else {
|
||||||
dataSync.sn_sync_status = 'collected'; // Status que indica que foi coletado do SN
|
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.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
|
// 4. Cria o registro de sincronização se ele não existir
|
||||||
const exists = await TicketSyncModel.getIdSyncBySnId(idTableTicketSn);
|
const exists = await TicketSyncModel.getIdSyncBySnId(idTableTicketSn);
|
||||||
@ -81,13 +85,14 @@ const processSyncController = async () => {
|
|||||||
logInfo(`📡 Buscando tickets do ServiceNow atualizados desde: ${watermark}`);
|
logInfo(`📡 Buscando tickets do ServiceNow atualizados desde: ${watermark}`);
|
||||||
|
|
||||||
// Busca incidentes
|
// Busca incidentes
|
||||||
|
|
||||||
const incidents = await fetchTicketsApi(watermark);
|
const incidents = await fetchTicketsApi(watermark);
|
||||||
const latestIncidentUpdate = await processAndSaveTickets(incidents, 'incidente');
|
const latestIncidentUpdate = await processAndSaveTickets(incidents, 'incidente');
|
||||||
|
|
||||||
// Busca requisições
|
// Busca requisições
|
||||||
//logInfo('📡 Buscando requisições do ServiceNow...');
|
logInfo('📡 Buscando requisições do ServiceNow...');
|
||||||
const requests = await fetchRequestsApi(watermark);
|
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)
|
// 2. Determina a nova marca d'água (o timestamp mais recente entre todos os tickets processados)
|
||||||
const newWatermark = latestIncidentUpdate > latestRequestUpdate ? latestIncidentUpdate : latestRequestUpdate;
|
const newWatermark = latestIncidentUpdate > latestRequestUpdate ? latestIncidentUpdate : latestRequestUpdate;
|
||||||
|
|||||||
@ -12,7 +12,8 @@ class TicketGlpiModel {
|
|||||||
'Em Atendimento': 2,
|
'Em Atendimento': 2,
|
||||||
'Em Espera': 4,
|
'Em Espera': 4,
|
||||||
'Resolvido': 5,
|
'Resolvido': 5,
|
||||||
'Encerrado - Omitido': 6
|
'Encerrado - Omitido': 6,
|
||||||
|
'Encerrado': 6 // Mapeia para Fechado no GLPI
|
||||||
};
|
};
|
||||||
return statusMap[snState] || 1;
|
return statusMap[snState] || 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,19 +27,59 @@ class TicketSnModel {
|
|||||||
tipo = EXCLUDED.tipo
|
tipo = EXCLUDED.tipo
|
||||||
RETURNING id, (xmax = 0) AS "wasInserted"`; // xmax=0 é um truque do PostgreSQL para saber se foi INSERT
|
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 = [
|
const values = [
|
||||||
ticketData.number?.value || null,
|
ticketNumber,
|
||||||
ticketData.sys_id?.value || null,
|
sysId,
|
||||||
ticketData.short_description?.value || null,
|
shortDescription,
|
||||||
ticketData.state?.display_value || null,
|
state,
|
||||||
ticketData.description?.value || null,
|
description,
|
||||||
ticketData.opened_by?.display_value || null,
|
callerName,
|
||||||
ticketData.sys_created_by?.display_value || null,
|
callerEmail,
|
||||||
ticketData.location?.value || null,
|
locationId,
|
||||||
ticketData.location?.display_value || null,
|
locationName,
|
||||||
ticketData['variables.numero_ramal']?.value || null,
|
ramal,
|
||||||
ticketData['variables.telephone_favorecido_rh']?.value || null,
|
telefone,
|
||||||
ticketData.opened_at?.value ? new Date(ticketData.opened_at.value) : null,
|
openedAt
|
||||||
|
? (typeof openedAt === 'string' && openedAt.includes('/')
|
||||||
|
? new Date(openedAt.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$2-$1'))
|
||||||
|
: new Date(openedAt))
|
||||||
|
: null,
|
||||||
typeTicket
|
typeTicket
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,9 @@ const { log } = require('winston');
|
|||||||
|
|
||||||
const fetchTicketsFromServiceNow = async (watermark) => {
|
const fetchTicketsFromServiceNow = async (watermark) => {
|
||||||
try {
|
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 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, {
|
const ticketsResponse = await axios.get(url, {
|
||||||
auth: apiConfig.servicenowAuthentication.auth
|
auth: apiConfig.servicenowAuthentication.auth
|
||||||
});
|
});
|
||||||
@ -21,10 +22,19 @@ const fetchTicketsFromServiceNow = async (watermark) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const fetchRequestsFromServiceNow = async (watermark) => {
|
const fetchRequestsFromServiceNow = async (watermark) => {
|
||||||
try {
|
try {
|
||||||
const query = `active=true^sys_updated_on>=${watermark}`;
|
// A tabela de requisições (sc_req_item) não possui o campo 'active'. A query deve ser apenas pela data.
|
||||||
const url = `${apiConfig.snRequestConfig.baseUrl}?sysparm_query=${query}&sysparm_display_value=true`;
|
// 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, {
|
const requestsResponse = await axios.get(url, {
|
||||||
auth: apiConfig.servicenowAuthentication.auth
|
auth: apiConfig.servicenowAuthentication.auth
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user