const glpiPool = require('../data/glpiDataBase'); const { logInfo, logError } = require('../utils/logger'); const LocationMappingModel = require('./locationMappingModel'); // Carrega o ID do usuário padrão do GLPI a partir das variáveis de ambiente const GLPI_DEFAULT_USER_ID = parseInt(process.env.GLPI_DEFAULT_USER_ID, 10) || 1118; const GLPI_REQ_DEFAULT_JUSTIFICATIVA = process.env.GLPI_REQ_DEFAULT_JUSTIFICATIVA || 'Chamado aberto por colaborador CAOA sem Justificativa'; const GLPI_REQ_DEFAULT_DESCRIPTION = process.env.GLPI_REQ_DEFAULT_DESCRIPTION || 'Chamado aberto por colaborador CAOA sem Descricao'; class TicketGlpiModel { static mapStatus(snState) { const statusMap = { 'Aguardando Atendimento': 1, 'Em Atendimento': 2, 'Em Espera': 4, 'Resolvido': 5, 'Encerrado - Omitido': 6, 'Encerrado': 6 }; return statusMap[snState] || 1; } static mapGlpiStatusToString(glpiStatus) { const statusMap = { 1: 'Aguardando Atendimento', // Novo 2: 'Em Atendimento', // Em atendimento (atribuído) 4: 'Em Espera', // Pendente 5: 'Resolvido', // Solucionado 6: 'Encerrado - Omitido' // Fechado }; return statusMap[glpiStatus] || 'Desconhecido'; } static mapPriority(snPriority) { const priorityMap = { '1': 5, '2': 4, '3': 3, '4': 2, '5': 1 }; return priorityMap[snPriority] || 3; } static mapCategory(shortDescription) { const categoryMap = { 'Problemas com Telefonia Fixa': 5717, 'Problemas com Internet': 5707, 'Problemas com Wi-Fi/Rede Corporativa': 5707, 'Wi-fi Clientes': 5707 }; return categoryMap[shortDescription] || 5717; } static mapSlasIdTtr(ticketData) { const isRequisicao = ticketData.type === 'requisicao'; const shortDesc = ticketData.short_description; const isTelefonia = shortDesc === 'Problemas com Telefonia Fixa' || shortDesc === 'Solicitações relacionadas a telefonia fixa'; const isLinkDedicado = shortDesc === 'Problemas com Internet'; const isWifi = shortDesc === 'Problemas com Wi-Fi/Rede Corporativa' || shortDesc === 'Wi-fi Clientes'; if (isRequisicao && isTelefonia) { return 37; } if (isRequisicao && (isLinkDedicado || isWifi)) { return 37; } if (!isRequisicao && (isWifi || isLinkDedicado)) { return 34; } if (!isRequisicao && isTelefonia) { return 34; } return 0; } static async formatTitle(ticketData) { try { let entityData = null; if (ticketData.location_id) { entityData = await LocationMappingModel.getGlpiEntity(ticketData.location_id); } else if (ticketData.location_name) { entityData = await LocationMappingModel.getGlpiEntityByName(ticketData.location_name); } const entityName = entityData?.name || 'Sem Entidade'; const tipo = ticketData.type === 'requisicao' ? 'REQUISIÇÃO' : 'INCIDENTE'; const tituloSn = ticketData.short_description || 'Sem título'; return { title: `${tipo} - ${entityName} - ${tituloSn}`, // Título formatado entityId: entityData?.id || 127 }; } catch (error) { logError(error, 'Erro ao formatar título do ticket'); const tipo = ticketData.type === 'requisicao' ? 'REQUISIÇÃO' : 'INCIDENTE'; return { title: `${tipo} - ${ticketData.short_description || 'Sem título'}`, entityId: 127 }; } } static formatDescription(ticketData) { const extractInfo = (regex, description) => { if (!description) return null; const match = description.match(regex); return match ? match[1] : null; }; const extractEmailFromDescription = (description) => { return extractInfo(/([\w-\.]+@([\w-]+\.)+[\w-]{2,4})/i, description); }; const extractPhoneFromDescription = (description) => { return extractInfo(/(\+?(\d{2})?\s*\(?\d{2}\)?[\s-]*\d{4,5}[\s-]?\d{4})/i, description); }; const extractRamalFromDescription = (description) => { return extractInfo(/(\d{3,4})/i, description); }; const rawDescription = ticketData.description; const isRequest = ticketData.type === 'requisicao'; const justificationForDisplay = isRequest ? (ticketData.justificativa && ticketData.justificativa.trim() ? ticketData.justificativa : GLPI_REQ_DEFAULT_JUSTIFICATIVA) : null; const descriptionForDisplay = isRequest ? (rawDescription && rawDescription.trim() ? rawDescription : GLPI_REQ_DEFAULT_DESCRIPTION) : (rawDescription || 'N/A'); const email = ticketData.caller_email || extractEmailFromDescription(rawDescription) || 'N/A'; const phone = ticketData.caller_phone || extractPhoneFromDescription(rawDescription) || 'N/A'; const ramal = ticketData.caller_ramal || extractRamalFromDescription(rawDescription) || 'N/A'; const requestExtraHtml = isRequest ? `
Justificativa
${justificationForDisplay}
` : ''; let htmlDescription = `
Campo Valor
Nome: ${ticketData.caller_id}
Email: ${email}
Telefone: ${phone}
Ramal: ${ramal}
Ticket Number (ServiceNow): ${ticketData.ticket_number || 'N/A'}
${requestExtraHtml}
Descricao
${descriptionForDisplay}
`; return htmlDescription; } static async createTicket(ticketData) { const connection = await glpiPool.getConnection(); try { await connection.beginTransaction(); let entityId = 127; if (ticketData.location_id) { entityId = await LocationMappingModel.getGlpiEntityId(ticketData.location_id) || entityId; } else if (ticketData.location_name) { entityId = await LocationMappingModel.getGlpiEntityIdByName(ticketData.location_name) || entityId; } let email = ticketData.caller_email; if (!email && ticketData.description) { const emailRegex = /[\w-\.]+@([\w-]+\.)+[\w-]{2,4}/g; const matches = ticketData.description.match(emailRegex); if (matches && matches.length > 0) { email = matches[0]; } } const { title: formattedTitle } = await TicketGlpiModel.formatTitle(ticketData); const formattedDescription = TicketGlpiModel.formatDescription(ticketData); const ticketType = ticketData.type === 'requisicao' ? 2 : 1; const slasIdTtr = TicketGlpiModel.mapSlasIdTtr(ticketData); const glpiTicketData = { entities_id: entityId, name: formattedTitle, date: ticketData.opened_at || new Date(), date_mod: ticketData.updated_at || new Date(), status: TicketGlpiModel.mapStatus(ticketData.state || '1'), // O status é mapeado users_id_recipient: GLPI_DEFAULT_USER_ID, requesttypes_id: 1, content: formattedDescription, urgency: 3, impact: 3, priority: TicketGlpiModel.mapPriority(ticketData.priority || '3'), type: ticketType, itilcategories_id: TicketGlpiModel.mapCategory(ticketData.short_description), global_validation: 1, date_creation: ticketData.opened_at || new Date(), slas_id_ttr: slasIdTtr }; const fields = Object.keys(glpiTicketData).join(', '); const placeholders = Object.keys(glpiTicketData).map(() => '?').join(', '); const values = Object.values(glpiTicketData); const ticketQuery = `INSERT INTO glpi_tickets (${fields}) VALUES (${placeholders})`; const [ticketResult] = await connection.execute(ticketQuery, values); const ticketId = ticketResult.insertId; await connection.execute( `INSERT INTO glpi_tickets_users (tickets_id, users_id, type, use_notification, alternative_email) VALUES (?, ?, 1, 1, '')`, // 1 = Requerente [ticketId, GLPI_DEFAULT_USER_ID] ); await connection.execute( `INSERT INTO glpi_groups_tickets (tickets_id, groups_id, type) VALUES (?, ?, 2)`, [ticketId, 30] ); await connection.commit(); logInfo(`✅ Ticket GLPI criado: ${ticketId}`); return ticketId; } catch (error) { await connection.rollback(); logError(error, `❌ Erro ao criar ticket GLPI`); throw error; } finally { connection.release(); } } static async ticketExists(snTicketNumber) { try { const query = ` SELECT id FROM glpi_tickets WHERE name LIKE ? OR content LIKE ? LIMIT 1 `; const values = [ `%${snTicketNumber}%`, `%${snTicketNumber}%` ]; const [rows] = await glpiPool.execute(query,values); return rows.length > 0 ? rows[0].id : null; } catch (error) { logError(`Erro ao verificar ticket existente: ${error}`); return null; } } static async getFollowupsByItemId(itemId) { const connection = await glpiPool.getConnection(); try { const query = 'SELECT id, content, date, date_creation, date_mod FROM glpi_itilfollowups WHERE items_id = ? AND users_id != ?'; const [rows] = await connection.execute(query,[itemId,GLPI_DEFAULT_USER_ID]); return rows; } catch (error) { logError(error, 'Erro ao buscar followups do ticket'); return null; } } static async insertComment(comment, items_id) { const connection = await glpiPool.getConnection(); try { const query = 'INSERT INTO glpi_itilfollowups(itemtype, items_id, date, users_id, content, date_creation, date_mod, timeline_position) VALUES(?, ?, NOW(), ?, ?, NOW(), NOW(), ?)'; const values = ['Ticket', items_id, GLPI_DEFAULT_USER_ID, comment.content , 1]; const [result] = await connection.execute(query, values); return [{ id: result.insertId }]; }catch(error){ logError(`Erro ao inserir o comentário no GLPI: ${error}`); } finally { if (connection) connection.release(); } } static async getClosedTickets(watermark, limit = 100) { const connection = await glpiPool.getConnection(); try { const query = ` SELECT t.id, t.solvedate, t.closedate, s.content as solution FROM glpi_tickets t LEFT JOIN glpi_itilsolutions s ON t.id = s.items_id AND s.itemtype = 'Ticket' WHERE t.status IN (5, 6) AND t.date_mod >= ? LIMIT ? `; const [rows] = await connection.execute(query, [watermark, limit]); return rows; } catch (error) { logError(error, '❌ Erro ao buscar tickets fechados no GLPI'); throw error; } finally { connection.release(); } } static async closeTicket(ticketId, solutionContent) { const connection = await glpiPool.getConnection(); try { await connection.beginTransaction(); const solutionQuery = `INSERT INTO glpi_itilsolutions (itemtype, items_id, content, users_id_editor, date_creation, date_mod) VALUES ('Ticket', ?, ?, ?, NOW(), NOW())`; await connection.execute(solutionQuery, [ticketId, solutionContent, GLPI_DEFAULT_USER_ID]); const updateTicketQuery = `UPDATE glpi_tickets SET status = 5, solvedate = NOW() WHERE id = ?`; await connection.execute(updateTicketQuery, [ticketId]); await connection.commit(); } catch (error) { await connection.rollback(); logError(error, `❌ Erro ao fechar ticket ${ticketId} no GLPI`); throw error; } finally { connection.release(); } } static async getTicketSolution(glpiTicketId) { const connection = await glpiPool.getConnection(); try { const query = ` SELECT content, date_mod, solutiontypes_id FROM glpi_itilsolutions WHERE items_id = ? AND itemtype = 'Ticket' ORDER BY date_mod DESC LIMIT 1`; const [rows] = await connection.execute(query, [glpiTicketId]); return rows[0] || null; } catch (error) { logError(error, `❌ Erro ao buscar solução do ticket GLPI ${glpiTicketId}`); throw error; } finally { connection.release(); } } static async forceCloseTicket(ticketId) { const connection = await glpiPool.getConnection(); try { const updateTicketQuery = `UPDATE glpi_tickets SET status = 6, closedate = NOW() WHERE id = ?`; await connection.execute(updateTicketQuery, [ticketId]); logInfo(`✅ Ticket GLPI ${ticketId} forçado para o estado Fechado (6).`); } catch (error) { logError(error, `❌ Erro ao forçar fechamento do ticket ${ticketId} no GLPI`); } finally { if (connection) connection.release(); } } static async updateStatus(ticketId, status) { const connection = await glpiPool.getConnection(); try { await connection.beginTransaction(); status = this.mapStatus(status); const updateQuery = `UPDATE glpi_tickets SET status = ? WHERE id = ?`; await connection.execute(updateQuery, [status, ticketId]); await connection.commit(); } catch (error) { await connection.rollback(); logError(error, `❌ Erro ao atualiza ticket ${ticketId} no GLPI`); throw error; } finally { connection.release(); } } static async getTicketStatus(glpiTicketId) { const connection = await glpiPool.getConnection(); try { const query = `SELECT status FROM glpi_tickets WHERE id = ?`; const [rows] = await connection.execute(query, [glpiTicketId]); if (rows.length > 0) { return rows[0].status; } return null; } catch (error) { logError(error, `❌ Erro ao buscar status do ticket GLPI ${glpiTicketId}`); throw error; } finally { connection.release(); } } } /** * @module TicketGlpiModel * @description Este módulo é a camada de acesso a dados para o banco de dados do GLPI. * Ele encapsula todas as interações diretas com o banco de dados MySQL do GLPI, * oferecendo uma interface clara para criar, atualizar e consultar tickets e seus componentes. * * Principais Funcionalidades: * - **Mapeamento de Dados**: Contém funções (`mapStatus`, `mapPriority`, `mapCategory`, etc.) para traduzir os dados do formato do ServiceNow para o formato esperado pelo GLPI. * - **Formatação de Conteúdo**: `formatTitle` e `formatDescription` preparam o título e a descrição do ticket, enriquecendo-os com informações da entidade e formatando a descrição em uma tabela HTML para melhor legibilidade. * - **Criação de Tickets (`createTicket`)**: Orquestra a criação de um novo ticket no GLPI, incluindo a inserção na tabela principal (`glpi_tickets`), a associação de usuários e grupos. * - **Consulta de Dados**: Oferece métodos para verificar a existência de um ticket (`ticketExists`), buscar seu status (`getTicketStatus`), soluções (`getTicketSolution`) e comentários/follow-ups (`getFollowupsByItemId`). * - **Atualização de Tickets**: Inclui métodos para atualizar o status (`updateStatus`), adicionar comentários (`insertComment`), e fechar tickets, seja de forma padrão com uma solução (`closeTicket`) ou forçando o estado para "Fechado" (`forceCloseTicket`). * * Este modelo é fundamental para abstrair a complexidade do esquema de banco de dados do GLPI e garantir que as operações sejam realizadas de forma transacional e segura. */ module.exports = TicketGlpiModel;