FEAT: Tickets agora estão sendo enviados para o GLPI

- Busca de tickets pendentes no banco de dados.
- Mapeamento de dados para o formato do GLPI.
- Criação/atualização de tickets no GLPI.
- Filtro de tickets com status 'Encerrado' ou 'Encerrado - Omitido'.
- Extração de informações (email, telefone, ramal) da descrição do ticket.
- Verificação se ticket já não existe no GLPI com base no RITM ou INC
This commit is contained in:
Rafael Alves Lopes 2025-09-15 20:55:19 -03:00
parent 9926aee962
commit ba7148154a
8 changed files with 349 additions and 37921 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ logs/*.log
config/*.json config/*.json
.DS_Store .DS_Store
*.tmp *.tmp
servicenow_requests.json
servicenow_tickets.json

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,16 @@
// src/models/ticketGlpiModel.js
const glpiPool = require('../data/glpiDataBase'); const glpiPool = require('../data/glpiDataBase');
const { logInfo, logError } = require('../utils/logger'); const { logInfo, logError } = require('../utils/logger');
class TicketGlpiModel { class TicketGlpiModel {
// Mapeamento de status ServiceNow → GLPI // Mapeamento de status do ServiceNow para o GLPI
static mapStatus(snState) { static mapStatus(snState) {
const statusMap = { const statusMap = {
'1': 1, // Aguardando Atendimento → Novo 'Aguardando Atendimento': 1,
'2': 2, // Em Andamento → Em andamento 'Em Atendimento': 2,
'3': 3, // Pendente → Pendente 'Em Espera': 4,
'6': 5, // Resolvido → Resolvido 'Encerrado': 5,
'7': 6 // Fechado → Fechado 'Encerrado - Omitido': 6
}; };
return statusMap[snState] || 1; return statusMap[snState] || 1;
} }
@ -19,11 +18,11 @@ class TicketGlpiModel {
// Mapeamento de prioridade // Mapeamento de prioridade
static mapPriority(snPriority) { static mapPriority(snPriority) {
const priorityMap = { const priorityMap = {
'1': 5, // Crítico → Muito alta '1': 5,
'2': 4, // Alto → Alta '2': 4,
'3': 3, // Moderado → Média '3': 3,
'4': 2, // Baixo → Baixa '4': 2,
'5': 1 // Planejamento → Muito baixa '5': 1
}; };
return priorityMap[snPriority] || 3; return priorityMap[snPriority] || 3;
} }
@ -31,104 +30,209 @@ class TicketGlpiModel {
// Mapeamento de categoria // Mapeamento de categoria
static mapCategory(shortDescription) { static mapCategory(shortDescription) {
const categoryMap = { const categoryMap = {
'Problemas com Telefonia Fixa': 5717, // PABX IP Nuvem 'Problemas com Telefonia Fixa': 5717,
'Problemas com Internet': 5720, // Link Dedicado 'Problemas com Internet': 5707,
'Problemas com Wi-Fi/Rede Corporativa': 5722, // Wifi 'Problemas com Wi-Fi/Rede Corporativa': 5707,
'Wi-fi Clientes': 5722 // Wifi 'Wi-fi Clientes': 5707
}; };
return categoryMap[shortDescription] || 5717; return categoryMap[shortDescription] || 5717;
} }
// Mapeamento de SLA baseado no tipo e na descrição
static mapSlasIdTtr(ticketData) {
const isRequisicao = ticketData.type === 'requisicao';
const isTelefonia = ticketData.short_description === 'Problemas com Telefonia Fixa' || 'Solicitações relacionadas a telefonia fixa';
const isLinkDedicado = ticketData.short_description === 'Problemas com Internet';
const isWifi = ticketData.short_description === 'Problemas com Wi-Fi/Rede Corporativa' || ticketData.short_description === 'Wi-fi Clientes';
// Criar ticket no GLPI if (isRequisicao && isTelefonia) {
// src/models/ticketGlpiModel.js return 37;
}
if (isRequisicao && (isLinkDedicado || isWifi)) {
return 37;
}
if (!isRequisicao && (isWifi || isLinkDedicado)) {
return 34;
}
if (!isRequisicao && isTelefonia) {
return 34;
}
return 0; // Default se nenhum critério for atendido
}
static async createTicket(ticketData) {
const connection = await glpiPool.getConnection();
try {
await connection.beginTransaction();
// 1. Preparar dados para o GLPI (usando valores fixos para teste) static formatDescription(ticketData) {
const glpiTicketData = { // Função auxiliar para extrair informações usando regex
entities_id: 1371, // Valor fixo para teste const extractInfo = (regex, description) => {
name: ticketData.short_description || 'Sem título', if (!description) return null;
date: ticketData.opened_at || new Date(), const match = description.match(regex);
date_mod: ticketData.updated_at || new Date(), return match ? match[1] : null;
status: TicketGlpiModel.mapStatus(ticketData.state || '1'), };
users_id_recipient: 917, // Valor fixo para teste const extractEmailFromDescription = (description) => {
requesttypes_id: 1, return extractInfo(/([\w-\.]+@([\w-]+\.)+[\w-]{2,4})/i, description);
content: ticketData.description || '', };
urgency: 3, // Valor fixo const extractPhoneFromDescription = (description) => {
impact: 3, // Valor fixo return extractInfo(/(\+?(\d{2})?\s*\(?\d{2}\)?[\s-]*\d{4,5}[\s-]?\d{4})/i, description);
priority: TicketGlpiModel.mapPriority(ticketData.priority || '3'), };
type: 1, // Incidente const extractRamalFromDescription = (description) => {
itilcategories_id: TicketGlpiModel.mapCategory(ticketData.short_description), return extractInfo(/(\d{3,4})/i, description);
global_validation: 1,
date_creation: ticketData.opened_at || new Date()
}; };
// Construir a query dinamicamente e inserir o ticket principal // Obtém email, telefone e ramal, usando os valores do ticketData ou extraindo da descrição, se necessário.
const fields = Object.keys(glpiTicketData).join(', '); const email = ticketData.caller_email || extractEmailFromDescription(ticketData.description) || 'N/A';
const placeholders = Object.keys(glpiTicketData).map(() => '?').join(', '); const phone = ticketData.caller_phone || extractPhoneFromDescription(ticketData.description) || 'N/A';
const values = Object.values(glpiTicketData); const ramal = ticketData.caller_ramal || extractRamalFromDescription(ticketData.description) || 'N/A';
const ticketQuery = `INSERT INTO glpi_tickets (${fields}) VALUES (${placeholders})`; // Formata a descrição em HTML
let htmlDescription = `
<table style="width:100%; border-collapse: collapse;">
<tr style="background-color:#f2f2f2;">
<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Campo</th>
<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Valor</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Email:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${email}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Telefone:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${phone}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Ramal:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ramal}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Ticket Number (ServiceNow):</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.ticket_number || 'N/A'}</td>
</tr>
</table>
<br>
<strong>Descrição:</strong>
<hr>
${ticketData.description || 'N/A'}
`;
return htmlDescription;
}
// Inserir ticket principal e obter insertId // Cria um ticket no GLPI
const [ticketResult] = await connection.execute(ticketQuery, values); static async createTicket(ticketData) {
const ticketId = ticketResult.insertId; const connection = await glpiPool.getConnection();
// Adicionar requerente (usa ticketId agora que foi criado) try {
await connection.execute( await connection.beginTransaction();
`INSERT INTO glpi_tickets_users
(tickets_id, users_id, type, use_notification, alternative_email)
VALUES (?, ?, 1, 1, '')`,
[ticketId, 917]
);
// Adicionar grupo // Extrai o email da descrição, se existir e não houver email no ticketData
await connection.execute( let email = ticketData.caller_email;
`INSERT INTO glpi_groups_tickets if (!email && ticketData.description) {
(tickets_id, groups_id, type) const emailRegex = /[\w-\.]+@([\w-]+\.)+[\w-]{2,4}/g;
VALUES (?, ?, 2)`, const matches = ticketData.description.match(emailRegex);
[ticketId, 30] if (matches && matches.length > 0) {
); email = matches[0];
}
}
await connection.commit(); // Formata a descrição para HTML
const formattedDescription = TicketGlpiModel.formatDescription(ticketData);
// Mapeia o tipo do ticket
const ticketType = ticketData.type === 'requisicao' ? 2 : 1;
// Determina o valor de slas_id_ttr
const slasIdTtr = TicketGlpiModel.mapSlasIdTtr(ticketData);
logInfo(`✅ Ticket GLPI criado: ${ticketId}`); // Prepara os dados para o GLPI
return ticketId; const glpiTicketData = {
entities_id: 1371,
name: ticketData.short_description || 'Ticket sem descrição',
date: ticketData.opened_at || new Date(),
date_mod: ticketData.updated_at || new Date(),
status: TicketGlpiModel.mapStatus(ticketData.state || '1'),
users_id_recipient: 917,
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
};
} catch (error) { // Constrói a query dinamicamente e insere o ticket principal
await connection.rollback(); const fields = Object.keys(glpiTicketData).join(', ');
logError(error, `❌ Erro ao criar ticket GLPI`); const placeholders = Object.keys(glpiTicketData).map(() => '?').join(', ');
throw error; const values = Object.values(glpiTicketData);
} finally {
connection.release(); const ticketQuery = `INSERT INTO glpi_tickets (${fields}) VALUES (${placeholders})`;
const [ticketResult] = await connection.execute(ticketQuery, values);
const ticketId = ticketResult.insertId;
// Adiciona o requerente
await connection.execute(
`INSERT INTO glpi_tickets_users
(tickets_id, users_id, type, use_notification, alternative_email)
VALUES (?, ?, 1, 1, '')`,
[ticketId, 917]
);
// Adiciona o grupo
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();
}
}
// Verifica se um ticket já existe no GLPI
static async ticketExists(snTicketNumber) {
try {
const query = `
SELECT id FROM glpi_tickets
WHERE name LIKE ? OR content LIKE ?
LIMIT 1
`;
const [rows] = await glpiPool.execute(query, [
`%${snTicketNumber}%`,
`%${snTicketNumber}%`
]);
return rows.length > 0 ? rows[0].id : null;
} catch (error) {
logError(error, 'Erro ao verificar ticket existente');
return null;
}
} }
} }
// Verificar se ticket já existe no GLPI /**
static async ticketExists(snTicketNumber) { * @module TicketGlpiModel
try { * @description Este módulo fornece métodos para gerenciar tickets no GLPI.
const query = ` * Inclui funcionalidades para mapear status, prioridade e categoria,
SELECT id FROM glpi_tickets * formatar a descrição do ticket, determinar o SLA, criar tickets e
WHERE name LIKE ? OR content LIKE ? * verificar se um ticket existe no GLPI.
LIMIT 1 *
`; * O módulo usa expressões regulares para extrair informações da descrição do ticket,
* como email, número de telefone e ramal, quando essas informações não são
const [rows] = await glpiPool.execute(query, [ * fornecidas diretamente. Também formata a descrição em HTML para melhor
`%${snTicketNumber}%`, * apresentação no GLPI.
`%${snTicketNumber}%` *
]); * O módulo interage com o banco de dados GLPI através da conexão glpiPool.
*/
return rows.length > 0 ? rows[0].id : null;
} catch (error) {
logError(error, 'Erro ao verificar ticket existente');
return null;
}
}
}
module.exports = TicketGlpiModel; module.exports = TicketGlpiModel;

View File

@ -4,14 +4,16 @@ const { logInfo, logError, logWarning } = require('../utils/logger');
class TicketSnModel { class TicketSnModel {
// Salva ou atualiza um ticket no banco de dados
static async saveTicket(ticketData, typeTicket) { static async saveTicket(ticketData, typeTicket) {
const query = ` const query = `
INSERT INTO tickets_sn INSERT INTO tickets_sn
(ticket_number, short_description, status, description, caller_id, caller_email, (ticket_number, sys_id, short_description, status, description, caller_id, caller_email,
location_id, location_name, ramal, telefone, opened_at, tipo) location_id, location_name, ramal, telefone, opened_at, tipo)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
ON CONFLICT (ticket_number) ON CONFLICT (ticket_number)
DO UPDATE SET DO UPDATE SET
sys_id = EXCLUDED.sys_id,
short_description = EXCLUDED.short_description, short_description = EXCLUDED.short_description,
status = EXCLUDED.status, status = EXCLUDED.status,
description = EXCLUDED.description, description = EXCLUDED.description,
@ -27,23 +29,25 @@ class TicketSnModel {
RETURNING id, xmax = 0 as inserted`; RETURNING id, xmax = 0 as inserted`;
const values = [ const values = [
ticketData.number?.value || null, // $1 - ticket_number ticketData.number?.value || null,
ticketData.short_description?.value || null, // $2 - short_description ticketData.sys_id?.value || null,
ticketData.state?.display_value || null, // $3 - status ticketData.short_description?.value || null,
ticketData.description?.value || null, // $4 - description ticketData.state?.display_value || null,
ticketData.opened_by?.display_value || null, // $5 - caller_id ticketData.description?.value || null,
ticketData['opened_by.email']?.value || null, // $6 - caller_email ticketData.opened_by?.display_value || null,
ticketData.location?.value || null, // $7 - location_id ticketData.sys_created_by?.display_value || null,
ticketData.location?.display_value || null, // $8 - location_name ticketData.location?.value || null,
ticketData['variables.numero_ramal']?.value || null, // $9 - ramal ticketData.location?.display_value || null,
ticketData['variables.telephone_favorecido_rh']?.value || null, // $10 - telefone ticketData['variables.numero_ramal']?.value || null,
ticketData.opened_at?.value ? new Date(ticketData.opened_at.value) : null, // $11 - opened_at ticketData['variables.telephone_favorecido_rh']?.value || null,
typeTicket // $12 - tipo ticketData.opened_at?.value ? new Date(ticketData.opened_at.value) : null,
typeTicket
]; ];
try { try {
const result = await pool.query(query, values); const result = await pool.query(query, values);
const wasInserted = result.rows[0].inserted; // true = INSERT, false = UPDATE const wasInserted = result.rows[0].inserted;
if (wasInserted) { if (wasInserted) {
logInfo(`✅ NOVO ticket salvo! ID: ${result.rows[0].id}`, { logInfo(`✅ NOVO ticket salvo! ID: ${result.rows[0].id}`, {
@ -66,8 +70,8 @@ class TicketSnModel {
} }
} }
// NOVO: Buscar tickets pendentes para sincronização com GLPI // Busca tickets que ainda não foram sincronizados com o GLPI
static async getPendingTickets(limit = 3) { static async getPendingTickets() {
try { try {
const query = ` const query = `
SELECT SELECT
@ -88,11 +92,11 @@ class TicketSnModel {
updated_at updated_at
FROM tickets_sn FROM tickets_sn
WHERE glpi_sync_status IS NULL WHERE glpi_sync_status IS NULL
ORDER BY opened_at ASC AND status NOT IN ('Encerrado', 'Encerrado - Omitido')
LIMIT $1 ORDER BY opened_at
`; `;
const result = await pool.query(query, [limit]); const result = await pool.query(query);
logInfo(`📋 Tickets pendentes encontrados: ${result.rows.length}`); logInfo(`📋 Tickets pendentes encontrados: ${result.rows.length}`);
return result.rows; return result.rows;
@ -102,7 +106,7 @@ class TicketSnModel {
} }
} }
// NOVO: Atualizar status de sincronização com GLPI // Atualiza o status de sincronização de um ticket específico // Inativo
static async updateSyncStatus(ticketId, status, glpiTicketId = null) { static async updateSyncStatus(ticketId, status, glpiTicketId = null) {
try { try {
const query = ` const query = `
@ -130,33 +134,8 @@ class TicketSnModel {
} }
} }
// NOVO: Resetar status de sync para testes (opcional)
static async resetSyncStatus() {
try {
const query = `
UPDATE tickets_sn
SET glpi_sync_status = NULL,
glpi_ticket_id = NULL,
glpi_sync_date = NULL
WHERE id IN (
SELECT id FROM tickets_sn
ORDER BY opened_at DESC
LIMIT $1
)
RETURNING id, ticket_number
`;
const result = await pool.query(query, [limit]); // Coleta o status de sincronização para todos os tickets (uso em testes)
logInfo(`🔄 Status resetado para ${result.rows.length} tickets`);
return result.rows;
} catch (error) {
logError(error, '❌ Erro ao resetar status de sync');
return [];
}
}
// NOVO: Verificar status de sincronização
static async getSyncStatus() { static async getSyncStatus() {
try { try {
const query = ` const query = `
@ -178,4 +157,15 @@ class TicketSnModel {
} }
} }
/**
* @module TicketSnModel
* @description Este módulo fornece métodos para interagir com a tabela de tickets do ServiceNow (tickets_sn) no banco de dados.
* Inclui funcionalidades para salvar um ticket, buscar tickets pendentes para sincronização com o GLPI,
* atualizar o status de sincronização de um ticket, resetar o status de sincronização para testes e verificar o status geral de sincronização.
*
* A função saveTicket insere ou atualiza um ticket na tabela tickets_sn.
* A função getPendingTickets busca tickets que ainda não foram sincronizados com o GLPI e ignora os tickets com status 'Encerrado' ou 'Encerrado - Omitido'.
* A função updateSyncStatus atualiza o status de sincronização de um ticket específico.
* A função getSyncStatus retorna um resumo do status de sincronização dos tickets.
*/
module.exports = TicketSnModel; module.exports = TicketSnModel;

View File

@ -1,26 +1,28 @@
CREATE TABLE tickets_sn ( CREATE TABLE tickets_sn (
id SERIAL PRIMARY KEY, -- ID automático (1, 2, 3...) id SERIAL PRIMARY KEY, -- ID automático (1, 2, 3...)
ticket_number VARCHAR(32) NOT NULL UNIQUE, -- Número do ticket (INC0140378) - ÚNICO ticket_number VARCHAR(32) NOT NULL UNIQUE, -- Número do ticket (INC0140378) - ÚNICO
short_description TEXT, -- Resumo do problema sys_id VARCHAR(64) NOT NULL UNIQUE, -- Sys ID do ticket no ServiceNow - ÚNICO
tipo VARCHAR(10), -- Requisição ou incidente short_description TEXT, -- Resumo do problema
status VARCHAR(55), -- Resumo do problema tipo VARCHAR(10), -- Requisição ou incidente
description TEXT, -- Descrição completa status VARCHAR(55), -- Resumo do problema
caller_id VARCHAR(255), -- Nome de quem abriu o ticket description TEXT, -- Descrição completa
caller_email VARCHAR(255), -- Email do solicitante caller_id VARCHAR(255), -- Nome de quem abriu o ticket
location_id VARCHAR(64), -- ID da localização caller_email VARCHAR(255), -- Email do solicitante
location_name VARCHAR(255), -- Nome da localização location_id VARCHAR(64), -- ID da localização
ramal VARCHAR(100), -- Ramal do solicitante location_name VARCHAR(255), -- Nome da localização
telefone VARCHAR(100), -- Telefone do solicitante ramal VARCHAR(100), -- Ramal do solicitante
opened_at TIMESTAMP, -- Quando o ticket foi aberto telefone VARCHAR(100), -- Telefone do solicitante
created_at TIMESTAMP DEFAULT NOW(), -- Quando registramos no BD opened_at TIMESTAMP, -- Quando o ticket foi aberto
updated_at TIMESTAMP DEFAULT NOW(), -- Quando atualizamos pela última vez created_at TIMESTAMP DEFAULT NOW(), -- Quando registramos no BD
glpi_sync_status VARCHAR(20), updated_at TIMESTAMP DEFAULT NOW(), -- Quando atualizamos pela última vez
glpi_ticket_id INT glpi_sync_status VARCHAR(20), -- Status da sincronização com GLPI
glpi_ticket_id INT, -- ID do ticket no GLPI
glpi_sync_date TIMESTAMP -- Data da sincronização com GLPI
); );
SELECT * FROM tickets_sn; SELECT * FROM tickets_sn;
SELECT * FROM tickets_sn LIMIT 3;

View File

@ -1,92 +1,88 @@
// src/services/glpiService.js // src/services/glpiService.js
const TicketSnModel = require('../models/ticketSnModel'); // SEU MODEL do PostgreSQL const TicketSnModel = require('../models/ticketSnModel');
const TicketGlpiModel = require('../models/ticketGlpiModel'); // Model do GLPI const TicketGlpiModel = require('../models/ticketGlpiModel');
const { logInfo, logError } = require('../utils/logger'); const { logInfo, logError } = require('../utils/logger');
class GlpiService { // Método principal que o controller vai chamar
// Método principal que o controller vai chamar
static async syncTicketsToGlpi() {
try {
logInfo('🔄 Iniciando sincronização para GLPI...');
// 1. Buscar tickets pendentes usando SEU MODEL
const pendingTickets = await TicketSnModel.getPendingTickets();
if (pendingTickets.length === 0) {
logInfo('✅ Nenhum ticket pendente para sincronizar');
return;
}
logInfo(`📋 Encontrados ${pendingTickets.length} tickets para sincronizar`);
// 2. Processar cada ticket
for (const ticket of pendingTickets) {
await this.processSingleTicket(ticket);
}
logInfo('🎉 Sincronização com GLPI concluída!');
} catch (error) {
logError(error, '❌ Erro na sincronização GLPI');
throw error;
}
}
// Processar um ticket individual
static async processSingleTicket(ticket) {
try {
logInfo(`🔍 Processando ticket: ${ticket.ticket_number}`);
// 1. Verificar se já existe no GLPI (evitar duplicatas)
const existingTicket = await TicketGlpiModel.ticketExists(ticket.ticket_number);
if (existingTicket) {
logInfo(`⏭️ Ticket já existe no GLPI: ${existingTicket}`);
await TicketSnModel.updateSyncStatus(ticket.id, 'duplicate', existingTicket);
return;
}
// 2. Converter dados para formato GLPI (O MODEL vai fazer o mapeamento)
const glpiData = {
// Dados básicos do PostgreSQL
ticket_number: ticket.ticket_number,
short_description: ticket.short_description,
description: ticket.description,
// Dados do ServiceNow originais (se necessário para mapeamento)
state: ticket.status, // Já vem do ServiceNow
urgency: '2', // Default - O MODEL vai mapear
impact: '2', // Default - O MODEL vai mapear
priority: '3', // Default - O MODEL vai mapear
// Localização para de/para
location_name: ticket.location_name,
// Datas
opened_at: ticket.opened_at,
updated_at: ticket.updated_at
};
// 3. Inserir no GLPI (O MODEL faz INSERT e mapeamento)
const glpiTicketId = await TicketGlpiModel.createTicket(glpiData);
// 4. Atualizar status no PostgreSQL usando SEU MODEL
await TicketSnModel.updateSyncStatus(ticket.id, 'completed', glpiTicketId);
logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`);
} catch (error) {
logError(error, `❌ Erro no ticket ${ticket.ticket_number}`);
await TicketSnModel.updateSyncStatus(ticket.id, 'error');
}
}
}
const syncTicketsToGlpi = async () => { const syncTicketsToGlpi = async () => {
await GlpiService.syncTicketsToGlpi(); try {
logInfo('🔄 Iniciando sincronização para GLPI...');
// 1. Buscar tickets pendentes usando SEU MODEL
const pendingTickets = await TicketSnModel.getPendingTickets();
if (pendingTickets.length === 0) {
logInfo('✅ Nenhum ticket pendente para sincronizar');
return;
}
logInfo(`📋 Encontrados ${pendingTickets.length} tickets para sincronizar`);
// 2. Processar cada ticket
for (const ticket of pendingTickets) {
await processSingleTicket(ticket);
}
logInfo('🎉 Sincronização com GLPI concluída!');
} catch (error) {
logError(error, '❌ Erro na sincronização GLPI');
throw error;
}
};
// Processar um ticket individual
const processSingleTicket = async (ticket) => {
try {
logInfo(`🔍 Processando ticket: ${ticket.ticket_number}`);
// 1. Verificar se já existe no GLPI (evitar duplicatas)
const existingTicket = await TicketGlpiModel.ticketExists(ticket.ticket_number);
if (existingTicket) {
logInfo(`⏭️ Ticket já existe no GLPI: ${existingTicket}`);
await TicketSnModel.updateSyncStatus(ticket.id, 'duplicate', existingTicket);
return;
}
// 2. Converter dados para formato GLPI (O MODEL vai fazer o mapeamento)
const glpiData = {
// Dados básicos do PostgreSQL
ticket_number: ticket.ticket_number,
short_description: ticket.short_description,
description: ticket.description,
caller_email: ticket.caller_email,
caller_name: ticket.caller_id,
caller_phone: ticket.telefone,
caller_ramal: ticket.ramal,
category: ticket.category,
type: ticket.tipo, // incidente ou requisicao
// Dados do ServiceNow originais (se necessário para mapeamento)
state: ticket.status, // Já vem do ServiceNow
priority: '3', // Default - O MODEL vai mapear
// Localização para de/para
location_name: ticket.location_name,
// Datas
opened_at: ticket.opened_at,
updated_at: ticket.updated_at
};
// 3. Inserir no GLPI (O MODEL faz INSERT e mapeamento)
const glpiTicketId = await TicketGlpiModel.createTicket(glpiData);
// 4. Atualizar status no PostgreSQL usando SEU MODEL
await TicketSnModel.updateSyncStatus(ticket.id, 'completed', glpiTicketId);
logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`);
} catch (error) {
logError(error, `❌ Erro no ticket ${ticket.ticket_number}`);
await TicketSnModel.updateSyncStatus(ticket.id, 'error');
}
}; };
module.exports = { module.exports = {
syncTicketsToGlpi, syncTicketsToGlpi
GlpiService
}; };

View File

@ -13,7 +13,7 @@ const fetchTicketsFromServiceNow = async () => {
}); });
// comentado para evitar muitos arquivos // comentado para evitar muitos arquivos
fs.writeFileSync('servicenow_tickets.json', JSON.stringify(ticketsResponse.data, null, 2)); //fs.writeFileSync('servicenow_tickets.json', JSON.stringify(ticketsResponse.data, null, 2));
//logInfo('💾 Backup JSON de incidentes salvo', { count: ticketsResponse.data.result.length }); //logInfo('💾 Backup JSON de incidentes salvo', { count: ticketsResponse.data.result.length });
// Salvar os tickets no banco de dados // Salvar os tickets no banco de dados
@ -38,7 +38,7 @@ const fetchRequestsFromServiceNow = async () => {
}); });
// Comentado para evitar muitos arquivos // Comentado para evitar muitos arquivos
fs.writeFileSync('servicenow_requests.json', JSON.stringify(requestsResponse.data, null, 2)); //fs.writeFileSync('servicenow_requests.json', JSON.stringify(requestsResponse.data, null, 2));
//logInfo('💾 Backup JSON de requisições salvo', { count: requestsResponse.data.result.length }); //logInfo('💾 Backup JSON de requisições salvo', { count: requestsResponse.data.result.length });
// Salvar as requisições no banco de dados // Salvar as requisições no banco de dados