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'); const e = require('express'); /** * Processa e salva um lote de tickets (incidentes ou requisições). * @param {Array} tickets - A lista de tickets da API do ServiceNow. * @param {('incidente'|'requisicao')} type - O tipo de ticket. */ const processAndSaveTickets = async (tickets, type) => { // Helper para converter a data do formato 'DD/MM/YYYY HH:mm:ss' para um objeto Date. const parseServiceNowDate = (dateString) => { if (!dateString) return null; // Verifica se já está em um formato reconhecível (como ISO 8601) 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, mês-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 ''; // Retorna string vazia se não houver tickets } logInfo(`💾 Iniciando salvamento de ${tickets.length} ${type}s no banco...`); let latestUpdateTimestamp = new Date(0); // Inicia com a data mínima (epoch) for (const ticket of tickets) { try { // --- LÓGICA DE ENRIQUECIMENTO DA DESCRIÇÃO --- // Se for uma requisição e a descrição estiver vazia, busca na sc_item_option. if (type === 'requisicao' && (!ticket.description || ticket.description.trim() === '')) { logInfo(`Descrição vazia para requisição ${ticket.number}. Buscando em sc_item_option...`); // O nome 'justificativa' é um exemplo, use o nome real da sua variável. 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; // Atualiza o objeto do ticket em memória logInfo(`Descrição da requisição ${ticket.number} atualizada com valor de sc_item_option.`); } if (vars.telephone){ ticket.telefone = vars.telephone; logInfo(`Telefone da requisição ${ticket.number} atualizado com valor de sc_item_option.`); } } // --- FIM DA LÓGICA DE ENRIQUECIMENTO --- // 1. Salva o ticket na tabela 'tickets_sn' const { id: idTableTicketSn, wasInserted } = await TicketSnModel.saveTicket(ticket, type); // 2. Prepara os dados para a tabela de sincronização 'ticket_sync' // Para requisições, o status está diretamente em ticket.state 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() }; // 3. Define o status inicial de sincronização if (ticketStatus === 'Encerrado' || ticketStatus === 'Encerrado - Omitido') { dataSync.sn_sync_status = 'closed'; 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'; // 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); if (!exists) { await TicketSyncModel.createSyncRecord(dataSync); } else if (wasInserted === false) { // Se o ticket foi atualizado (não inserido), significa que uma mudança veio do SN. // Então, definimos o SN como a última fonte da verdade. await TicketSyncModel.updateSourceLast(idTableTicketSn, 'SNOW'); logInfo(`Ticket ${ticket.number} atualizado pelo ServiceNow. 'source_last' definido como 'SNOW'.`); } else { logInfo(`Registro de sincronização para o ticket ${ticket.number?.value} já existe. Nenhuma ação necessária.`); } // Se já existe, não faz nada, pois a lógica de atualização de status será em outro processo. // Guarda o timestamp mais recente do lote para usar como a próxima marca d'água 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`); // Se encontramos uma data válida, a formata para 'YYYY-MM-DD HH:MM:SS'. Senão, retorna string vazia. return latestUpdateTimestamp.getTime() > 0 ? latestUpdateTimestamp.toISOString().slice(0, 19).replace('T', ' ') : ''; }; /** * Controller para buscar tickets e requisições do ServiceNow e salvá-los localmente. */ const processSyncController = async () => { try { // 1. Pega a marca d'água da última execução bem-sucedida const watermark = await SyncControlModel.getWatermark('servicenow'); 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...'); const requests = await fetchRequestsApi(watermark); 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; // 3. Se processamos algum ticket, atualiza a marca d'água para a próxima execução if (newWatermark) { // Adiciona 1 segundo para evitar reprocessar o último ticket em caso de timestamps iguais 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'água não foi alterada."); } } catch (error) { logError(error, 'Erro no processo de sincronização de tickets do ServiceNow.'); } }; module.exports = { processSyncController };