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;