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 } = 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) { logInfo(`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' && (!ticket.description || ticket.description.trim() === '')) { logInfo(`Descricao vazia para requisicao ${ticket.number}. Buscando em sc_item_option...`); const extraData = await fetchScItemOptionValue(ticket.sys_id, 'justificativa'); const vars = {}; (extraData || []).forEach(item => { const question = item['sc_item_option.item_option_new.question_text']; const value = item['sc_item_option.value']; if (question === 'Select what you want') vars.whatYouWant = value; if (question === 'Please justify the need') vars.justification = value; if (question === 'Telephone') vars.telephone = value; }); if (vars.justification) { ticket.description = vars.justification; logInfo(`Descricao da requisicao ${ticket.number} atualizada com valor de sc_item_option.`); } if (vars.telephone){ ticket.telefone = vars.telephone; logInfo(`Telefone da requisicao ${ticket.number} atualizado com valor de sc_item_option.`); } } 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) { await TicketSyncModel.updateSourceLast(idTableTicketSn, 'SNOW'); logInfo(`Ticket ${ticket.number} atualizado pelo ServiceNow. 'source_last' definido como 'SNOW'.`); } else { logInfo(`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'); logInfo(`INFO: Buscando tickets do ServiceNow atualizados desde: ${watermark}`); const incidents = await fetchTicketsApi(watermark); const latestIncidentUpdate = await processAndSaveTickets(incidents, 'incidente'); logInfo('INFO: 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 { logInfo("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. */