snglpi/src/controllers/processSyncController.js

160 lines
7.7 KiB
JavaScript
Raw Normal View History

const TicketSnModel = require('../models/ticketSnModel');
const TicketSyncModel = require('../models/ticketSyncModel');
const SyncControlModel = require('../models/syncControlModel');
const { fetchTicketsFromServiceNow: fetchTicketsApi, fetchRequestsFromServiceNow: fetchRequestsApi, fetchScItemOptionValue } = require('../services/servicenowService');
const { logInfo, logError, logSync, logDebug } = require('../utils/logger');
/**
* Processa e salva um lote de tickets (incidentes ou requisicoes).
* @param {Array} tickets - A lista de tickets da API do ServiceNow.
* @param {('incidente'|'requisicao')} type - O tipo de ticket.
*/
const processAndSaveTickets = async (tickets, type) => {
const parseServiceNowDate = (dateString) => {
if (!dateString) return null;
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, mes-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) {
logDebug(`Nenhum ticket do tipo '${type}' para processar.`);
return '';
}
logInfo(`INFO: Iniciando salvamento de ${tickets.length} ${type}s no banco...`);
let latestUpdateTimestamp = new Date(0);
for (const ticket of tickets) {
try {
if (type === 'requisicao') {
const ticketNumber = typeof ticket.number === 'object' ? ticket.number?.value : ticket.number;
logInfo(`Buscando variaveis de catalogo para requisicao ${ticketNumber}.`);
const extraData = await fetchScItemOptionValue(ticket.sys_id, 'justificativa');
const vars = {};
(extraData || []).forEach((item) => {
const question = item.question_text;
const name = item.variable_name;
const value = item.value;
if (name === 'select_what_you_want' || question === 'Select what you want') vars.whatYouWant = value;
if (name === 'please_justify_the_need' || question === 'Please justify the need') vars.justification = value;
if (name === 'telephone_favorecido_ti' || question === 'Telephone') vars.telephone = value;
});
ticket.justificativa = vars.justification || null;
if (vars.telephone) {
ticket.telefone = vars.telephone;
}
logInfo(`Variaveis da requisicao ${ticketNumber}: justificativa=${ticket.justificativa ? 'ok' : 'vazia'}, telefone=${ticket.telefone ? 'ok' : 'vazio'}.`);
}
FEAT: Motor de regras único de status/fechamento + mandante configurável Consolida os dois controladores legados que decidiam status/fechamento de forma concorrente (processGlpiClosureController + processStatusController) em um único processTicketLifecycleController, eliminando a dependência do campo source_last (mantido só como rastro de auditoria) e a flag ENABLE_STATUS_SYNC (removida). Isso corrige o bug de reabertura: chamados reabertos no SN voltavam a ser fechados no GLPI porque a regra de precedência de finalização do fluxo legado forçava o status do GLPI de volta pro SN. Correções de bugs encontrados em teste (Frente 1): - Constraint UNIQUE em ticket_sync.sn_ticket_id + ON CONFLICT DO NOTHING na criação do registro, prevenindo duplicação de tickets. - Fechamento no SN agora é confirmado por leitura ao vivo do status antes de marcar o ticket como solved/closed, em vez de confiar só no HTTP 200 (a API do SN pode retornar sucesso e silenciosamente ignorar um valor de estado inválido — descoberto e corrigido também no mandante). - Nota de encerramento do SN pro GLPI agora é idempotente e criada uma única vez por ticket. - Erros de sincronização passam a ter um limite de tentativas (MAX_ERROR_RETRIES) antes de marcar o ticket como error_permanent, exigindo intervenção manual em vez de tentar para sempre. - Chamados criados no GLPI passam a gravar o número do ticket do SN no campo externalid. Nova funcionalidade (Frente 2): quando o SN resolve/encerra um chamado por conta própria antes do GLPI, além da nota informativa no GLPI (agora com o nome de quem encerrou, buscado ao vivo via resolved_by/closed_by/ sys_updated_by), o próprio SN recebe um aviso de que a Sothis não vai mais atender aquele chamado e ele é transferido pra fila de TI da CAOA. Nova funcionalidade (Mandante, regras 19-22): espelhamento opcional e configurável de status intermediário (Aguardando Atendimento/Em Atendimento/Em Espera) entre os dois sistemas, controlado pela env MANDANTE=GLPI|SNOW. Sem essa env configurada, mantém o comportamento atual (nenhum espelhamento de status intermediário). Nunca afeta os status finais (resolvido/encerrado/fora de escopo), que continuam regidos pelas regras fixas de fechamento. Corrigido também o workflow de deploy (.gitea/workflows/deploy-production.yml), que ainda validava os controladores antigos (já removidos) e exigia ENABLE_STATUS_SYNC=true, o que quebraria o próximo deploy em produção. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 09:45:34 -03:00
const ticketNumber = typeof ticket.number === 'object' ? ticket.number?.value : ticket.number;
const existingTicketSn = await TicketSnModel.findByTicketNumber(ticketNumber);
const oldStatus = existingTicketSn?.status || null;
const { id: idTableTicketSn, wasInserted } = await TicketSnModel.saveTicket(ticket, type);
const ticketStatus = ticket.state;
const dataSync = {
sn_ticket_id: idTableTicketSn,
last_sn_sync: new Date(),
glpi_ticket_id: null,
created_at: new Date(),
updated_at: new Date()
};
if (ticketStatus === 'Encerrado' || ticketStatus === 'Encerrado - Omitido') {
dataSync.sn_sync_status = 'closed';
dataSync.glpi_sync_status = 'ignored';
} else {
dataSync.sn_sync_status = 'collected';
dataSync.glpi_sync_status = 'pending_check';
}
dataSync.source_last = 'SNOW';
const exists = await TicketSyncModel.getIdSyncBySnId(idTableTicketSn);
if (!exists) {
await TicketSyncModel.createSyncRecord(dataSync);
} else if (wasInserted === false) {
FEAT: Motor de regras único de status/fechamento + mandante configurável Consolida os dois controladores legados que decidiam status/fechamento de forma concorrente (processGlpiClosureController + processStatusController) em um único processTicketLifecycleController, eliminando a dependência do campo source_last (mantido só como rastro de auditoria) e a flag ENABLE_STATUS_SYNC (removida). Isso corrige o bug de reabertura: chamados reabertos no SN voltavam a ser fechados no GLPI porque a regra de precedência de finalização do fluxo legado forçava o status do GLPI de volta pro SN. Correções de bugs encontrados em teste (Frente 1): - Constraint UNIQUE em ticket_sync.sn_ticket_id + ON CONFLICT DO NOTHING na criação do registro, prevenindo duplicação de tickets. - Fechamento no SN agora é confirmado por leitura ao vivo do status antes de marcar o ticket como solved/closed, em vez de confiar só no HTTP 200 (a API do SN pode retornar sucesso e silenciosamente ignorar um valor de estado inválido — descoberto e corrigido também no mandante). - Nota de encerramento do SN pro GLPI agora é idempotente e criada uma única vez por ticket. - Erros de sincronização passam a ter um limite de tentativas (MAX_ERROR_RETRIES) antes de marcar o ticket como error_permanent, exigindo intervenção manual em vez de tentar para sempre. - Chamados criados no GLPI passam a gravar o número do ticket do SN no campo externalid. Nova funcionalidade (Frente 2): quando o SN resolve/encerra um chamado por conta própria antes do GLPI, além da nota informativa no GLPI (agora com o nome de quem encerrou, buscado ao vivo via resolved_by/closed_by/ sys_updated_by), o próprio SN recebe um aviso de que a Sothis não vai mais atender aquele chamado e ele é transferido pra fila de TI da CAOA. Nova funcionalidade (Mandante, regras 19-22): espelhamento opcional e configurável de status intermediário (Aguardando Atendimento/Em Atendimento/Em Espera) entre os dois sistemas, controlado pela env MANDANTE=GLPI|SNOW. Sem essa env configurada, mantém o comportamento atual (nenhum espelhamento de status intermediário). Nunca afeta os status finais (resolvido/encerrado/fora de escopo), que continuam regidos pelas regras fixas de fechamento. Corrigido também o workflow de deploy (.gitea/workflows/deploy-production.yml), que ainda validava os controladores antigos (já removidos) e exigia ENABLE_STATUS_SYNC=true, o que quebraria o próximo deploy em produção. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 09:45:34 -03:00
if (oldStatus !== ticketStatus) {
await TicketSyncModel.updateSourceLast(idTableTicketSn, 'SNOW');
logInfo(`Ticket ${ticket.number} atualizado pelo ServiceNow (status '${oldStatus}' -> '${ticketStatus}'). 'source_last' definido como 'SNOW'.`);
} else {
logInfo(`Ticket ${ticket.number} recebido de novo pela margem de seguranca do watermark, sem mudanca de status ('${ticketStatus}'). Ignorando reset de 'source_last'.`);
}
} else {
logDebug(`Registro de sincronizacao para o ticket ${ticket.number?.value} ja existe. Nenhuma acao necessaria.`);
}
const currentUpdateDate = parseServiceNowDate(ticket.sys_updated_on);
if (currentUpdateDate && currentUpdateDate.getTime() > latestUpdateTimestamp.getTime()) {
latestUpdateTimestamp = currentUpdateDate;
}
} catch (error) {
logError(error, `Erro ao processar o ticket ${ticket.number?.value} do tipo ${type}.`);
}
}
logSync('ServiceNow', tickets.length, `${type}s`);
return latestUpdateTimestamp.getTime() > 0
? latestUpdateTimestamp.toISOString().slice(0, 19).replace('T', ' ')
: '';
};
/**
* Controller para buscar tickets e requisicoes do ServiceNow e salva-los localmente.
*/
const processSyncController = async () => {
try {
const watermark = await SyncControlModel.getWatermark('servicenow');
logDebug(`Buscando tickets do ServiceNow atualizados desde: ${watermark}`);
const incidents = await fetchTicketsApi(watermark);
const latestIncidentUpdate = await processAndSaveTickets(incidents, 'incidente');
logDebug('Buscando requisicoes do ServiceNow...');
const requests = await fetchRequestsApi(watermark);
const latestRequestUpdate = await processAndSaveTickets(requests, 'requisicao');
const newWatermark = latestIncidentUpdate > latestRequestUpdate ? latestIncidentUpdate : latestRequestUpdate;
if (newWatermark) {
// Mantem margem de seguranca de 6 horas para evitar perda de eventos por atraso de replicacao, timezone ou clock skew entre sistemas.
const sixHoursInMillis = 6 * 60 * 60 * 1000;
const nextWatermark = new Date(new Date(newWatermark).getTime() - sixHoursInMillis);
await SyncControlModel.setWatermark('servicenow', nextWatermark);
} else {
logDebug("Nenhum ticket novo ou atualizado encontrado. A marca d'agua nao foi alterada.");
}
} catch (error) {
logError(error, 'Erro no processo de sincronizacao de tickets do ServiceNow.');
}
};
module.exports = {
processSyncController
};
/**
* @file processSyncController.js
* @description
* Este controlador e responsavel por buscar novos tickets e atualizacoes do ServiceNow.
* Ele utiliza uma "marca d'agua" (watermark) para buscar apenas os registros modificados desde a ultima execucao,
* otimizando o processo.
*
* Funcionalidades:
* - Busca incidentes e requisicoes do ServiceNow com base na data da ultima atualizacao.
* - Para cada ticket, salva ou atualiza o registro no banco de dados intermediario (`tickets_sn`).
* - Cria ou atualiza o registro de controle na tabela `ticket_sync`, definindo o status inicial e a origem da atualizacao (`source_last`).
* - Implementa uma logica de enriquecimento de dados para requisicoes com descricao vazia, buscando informacoes de variaveis de catalogo (`sc_item_option`).
* - Ao final, atualiza a marca d'agua para a proxima execucao, apenas se houverem tickets atualizados na execucao vigente.
*/