const pool = require('../data/database'); const { logInfo, logError, logDebug } = require('../utils/logger'); class TicketSnModel { static async saveTicket(ticketData, typeTicket) { const query = ` INSERT INTO tickets_sn (ticket_number, sys_id, short_description, status, description, caller_id, caller_email, location_id, location_name, ramal, telefone, justificativa, opened_at, tipo) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT (ticket_number) DO UPDATE SET sys_id = EXCLUDED.sys_id, short_description = EXCLUDED.short_description, status = EXCLUDED.status, description = EXCLUDED.description, caller_id = EXCLUDED.caller_id, caller_email = EXCLUDED.caller_email, location_id = EXCLUDED.location_id, location_name = EXCLUDED.location_name, ramal = EXCLUDED.ramal, telefone = EXCLUDED.telefone, justificativa = EXCLUDED.justificativa, opened_at = EXCLUDED.opened_at, tipo = EXCLUDED.tipo RETURNING id, (xmax = 0) AS "wasInserted"`; const ticketNumber = typeof ticketData.number === 'object' ? ticketData.number?.value : ticketData.number; let callerName, callerEmail, ramal, telefone, justificativa, sysId, shortDescription, state, description, locationId, locationName, openedAt; if (typeTicket === 'incidente') { callerName = ticketData.caller_id?.display_value; 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 = ticketData['caller_id.phone'] || ticketData['caller_id.mobile_phone'] || null; // Incidentes não têm essa variável por padrão sysId = ticketData.sys_id || null; shortDescription = ticketData.short_description || null; state = ticketData.state || null; description = ticketData.description || null; justificativa = null; const locationLink = ticketData.location?.link; locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null; locationName = ticketData.location?.display_value || null; openedAt = ticketData.opened_at || 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'] || ticketData['variables.telephone_favorecido_ti'] || ticketData.telefone || null; sysId = ticketData.sys_id || null; shortDescription = ticketData.short_description || null; state = ticketData.state || null; description = ticketData.description || null; justificativa = ticketData.justificativa || null; 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 = [ ticketNumber, sysId, shortDescription, state, description, callerName, callerEmail, locationId, locationName, ramal, telefone, justificativa, openedAt ? (typeof openedAt === 'string' && openedAt.includes('/') ? new Date(openedAt.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$2-$1')) : new Date(openedAt)) : null, typeTicket ]; try { const result = await pool.query(query, values); const wasInserted = result.rows[0].wasInserted; if (wasInserted) { logInfo(`✅ NOVO ticket salvo! ID: ${result.rows[0].id}`, { ticket_id: result.rows[0].id, ticket_number: values[0], action: 'insert' }); } else { // Fica em debug: a margem de seguranca de 6h do watermark reenvia o mesmo ticket // toda vez que ele ainda esta "quente", mesmo sem mudanca real de campo. O sinal // de mudanca de status de verdade ja e logado em processSyncController.js. logDebug(`🔄 Ticket atualizado! ID: ${result.rows[0].id}`, { ticket_id: result.rows[0].id, ticket_number: values[0], action: 'update' }); } return { id: result.rows[0].id, wasInserted }; } catch (error) { logError(error, `❌ Erro ao salvar ticket ${values[0]}`); throw error; } } static async getPendingTickets() { try { const query = ` SELECT tsync.id AS ticket_sync_id, ts.id, ts.ticket_number, ts.short_description, ts.description, ts.status, ts.caller_id, ts.caller_email, ts.location_id, ts.location_name, ts.ramal, ts.telefone, ts.justificativa, ts.opened_at, ts.tipo, ts.created_at, ts.updated_at, tsync.glpi_ticket_id FROM tickets_sn ts LEFT JOIN ticket_sync tsync ON ts.id = tsync.sn_ticket_id WHERE tsync.glpi_sync_status = 'pending_check' ORDER BY ts.opened_at `; const result = await pool.query(query); logDebug(`📋 Tickets pendentes encontrados: ${result.rows.length}`); return result.rows; } catch (error) { logError(error, '❌ Erro ao buscar tickets pendentes'); return []; } } static async findByTicketNumber(ticketNumber) { try { const query = ` SELECT * FROM tickets_sn WHERE ticket_number = $1 `; const result = await pool.query(query, [ticketNumber]); return result.rows[0] || null; } catch (error) { logError(error, `❌ Erro ao buscar ticket por número: ${ticketNumber}`); return null; } } static async getStatus(ticketId) { try { const query = ` SELECT status,tipo FROM tickets_sn WHERE id = $1`; const result = await pool.query(query, [ticketId]); return result.rows || null; } catch (error) { logError(error, `❌ Erro ao buscar status do ticket: ${ticketId}`); return null; } } static async findById(ticketId) { try { const query = ` SELECT * FROM tickets_sn WHERE id = $1 `; const result = await pool.query(query, [ticketId]); return result.rows[0] || null; } catch (error) { logError(error, `❌ Erro ao buscar ticket por ID: ${ticketId}`); return null; } } static async updateTicket(ticketId, updateData) { try { const fields = []; const values = []; let paramCount = 1; for (const [key, value] of Object.entries(updateData)) { fields.push(`${key} = $${paramCount}`); values.push(value); paramCount++; } fields.push(`updated_at = NOW()`); values.push(ticketId); const query = ` UPDATE tickets_sn SET ${fields.join(', ')} WHERE id = $${paramCount} RETURNING * `; const result = await pool.query(query, values); if (result.rows.length > 0) { logInfo(`✅ Ticket atualizado: ${ticketId}`); return result.rows[0]; } return null; } catch (error) { logError(error, `❌ Erro ao atualizar ticket ${ticketId}`); throw error; } } } /** * @module TicketSnModel * @description Este módulo serve como a camada de acesso a dados para a tabela `tickets_sn`, que armazena uma cópia local dos dados dos tickets do ServiceNow. * Manter uma cópia local otimiza as consultas e reduz a dependência de chamadas de API repetitivas ao ServiceNow. * * Principais Funcionalidades: * - `saveTicket(ticketData, typeTicket)`: Insere um novo ticket ou atualiza um existente (UPSERT) na tabela `tickets_sn`. Ele mapeia de forma inteligente os campos de incidentes e requisições, que possuem estruturas diferentes na API do ServiceNow. Retorna se o registro foi inserido ou atualizado. * - `getPendingTickets()`: Busca todos os tickets que estão no estado 'pending_check', indicando que foram coletados do ServiceNow mas ainda não foram processados para criação no GLPI. * - Funções de Busca (`findByTicketNumber`, `findById`, `getStatus`): Métodos para recuperar registros de tickets ou seus status com base em diferentes identificadores. * - `updateTicket(ticketId, updateData)`: Atualiza dinamicamente os campos de um registro de ticket existente. */ module.exports = TicketSnModel;