diff --git a/.env b/.env index ed86ef2..efed84d 100644 --- a/.env +++ b/.env @@ -18,12 +18,16 @@ GLPI_DB_NAME=glpi_data GLPI_DB_CHARSET=utf8mb4 -# Database Configuration -DB_HOST=localhost -DB_PORT=3306 # or 5432 for PostgreSQL -DB_USER=your_database_user -DB_PASSWORD=your_database_password -DB_NAME=your_database_name +# Banco SNGLPI (PostgreSQL) +SNGLPI_DB_HOST=10.0.120.75 +SNGLPI_DB_PORT=5432 +SNGLPI_DB_NAME=snglpi +SNGLPI_DB_USER=desenvolvimento +SNGLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W + +# Caminho do CSV +LOCATION_MAPPING_CSV_PATH=\\10.0.121.40\Tecnica\Controle CAOA\ENTIDADE SERVICE NOW\LOCATION_MAPPING.csv + # Logging Configuration LOG_LEVEL=info diff --git a/.gitignore b/.gitignore index 307e13c..dcd0dce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ config/*.json .DS_Store *.tmp servicenow_requests.json -servicenow_tickets.json \ No newline at end of file +servicenow_tickets.json +location_mapping.log \ No newline at end of file diff --git a/src/models/locationMappingModel.js b/src/models/locationMappingModel.js new file mode 100644 index 0000000..7460ea1 --- /dev/null +++ b/src/models/locationMappingModel.js @@ -0,0 +1,52 @@ +// src/models/locationMappingModel.js +const pool = require('../data/database'); +const { logInfo, logError } = require('../utils/logger'); + +class LocationMappingModel { + + // Buscar entity_id do GLPI baseado no location_id do ServiceNow + static async getGlpiEntityId(snLocationId) { + try { + const query = ` + SELECT glpi_entity_id + FROM location_mapping + WHERE sn_location_id = $1 + `; + + const result = await pool.query(query, [snLocationId]); + + if (result.rows.length > 0) { + return result.rows[0].glpi_entity_id; + } + + return null; + } catch (error) { + logError(error, `Erro ao buscar entity_id para location_id: ${snLocationId}`); + return null; + } + } + + // Buscar entity_id do GLPI baseado no location_name do ServiceNow + static async getGlpiEntityIdByName(snLocationName) { + try { + const query = ` + SELECT glpi_entity_id + FROM location_mapping + WHERE sn_location_name = $1 + `; + + const result = await pool.query(query, [snLocationName]); + + if (result.rows.length > 0) { + return result.rows[0].glpi_entity_id; + } + + return null; + } catch (error) { + logError(error, `Erro ao buscar entity_id para location_name: ${snLocationName}`); + return null; + } + } +} + +module.exports = LocationMappingModel; \ No newline at end of file diff --git a/src/models/ticketGlpiModel.js b/src/models/ticketGlpiModel.js index 4eff0ae..852300b 100644 --- a/src/models/ticketGlpiModel.js +++ b/src/models/ticketGlpiModel.js @@ -1,5 +1,7 @@ +// src/models/ticketGlpiModel.js const glpiPool = require('../data/glpiDataBase'); const { logInfo, logError } = require('../utils/logger'); +const LocationMappingModel = require('./locationMappingModel'); class TicketGlpiModel { @@ -41,9 +43,10 @@ class TicketGlpiModel { // 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'; + 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; @@ -60,8 +63,6 @@ class TicketGlpiModel { return 0; // Default se nenhum critério for atendido } - - static formatDescription(ticketData) { // Função auxiliar para extrair informações usando regex const extractInfo = (regex, description) => { @@ -91,6 +92,10 @@ class TicketGlpiModel { Campo Valor + + Nome: + ${ticketData.caller_id} + Email: ${email} @@ -123,6 +128,15 @@ class TicketGlpiModel { try { await connection.beginTransaction(); + // Buscar entity_id com base no location_id ou location_name + let entityId = 1371; // Valor padrão (fallback) + + 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; + } + // Extrai o email da descrição, se existir e não houver email no ticketData let email = ticketData.caller_email; if (!email && ticketData.description) { @@ -142,7 +156,7 @@ class TicketGlpiModel { // Prepara os dados para o GLPI const glpiTicketData = { - entities_id: 1371, + entities_id: entityId, // Usar o entity_id mapeado name: ticketData.short_description || 'Ticket sem descrição', date: ticketData.opened_at || new Date(), date_mod: ticketData.updated_at || new Date(), diff --git a/src/models/ticketSnModel.js b/src/models/ticketSnModel.js index 5dfbf7d..984204d 100644 --- a/src/models/ticketSnModel.js +++ b/src/models/ticketSnModel.js @@ -75,25 +75,26 @@ class TicketSnModel { try { const query = ` SELECT - id, - ticket_number, - short_description, - description, - status, - caller_id, - caller_email, - location_id, - location_name, - ramal, - telefone, - opened_at, - tipo, - created_at, - updated_at - FROM tickets_sn - WHERE glpi_sync_status IS NULL - AND status NOT IN ('Encerrado', 'Encerrado - Omitido') - ORDER BY opened_at + 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.opened_at, + ts.tipo, + ts.created_at, + ts.updated_at + FROM tickets_sn ts + LEFT JOIN ticket_sync tsync ON ts.id = tsync.sn_ticket_id + WHERE tsync.id IS NULL + AND ts.status NOT IN ('Encerrado', 'Encerrado - Omitido') + ORDER BY ts.opened_at `; const result = await pool.query(query); @@ -106,53 +107,79 @@ class TicketSnModel { } } - // Atualiza o status de sincronização de um ticket específico // Inativo - static async updateSyncStatus(ticketId, status, glpiTicketId = null) { + // Busca um ticket pelo número + static async findByTicketNumber(ticketNumber) { try { const query = ` - UPDATE tickets_sn - SET glpi_sync_status = $1, - glpi_ticket_id = $2, - glpi_sync_date = NOW() - WHERE id = $3 - RETURNING id, ticket_number + SELECT * FROM tickets_sn + WHERE ticket_number = $1 `; - const result = await pool.query(query, [status, glpiTicketId, ticketId]); - - if (result.rows.length > 0) { - logInfo(`🔄 Status atualizado: ${status} para ticket ${result.rows[0].ticket_number}`, { - ticket_id: result.rows[0].id, - ticket_number: result.rows[0].ticket_number, - glpi_ticket_id: glpiTicketId, - status: status - }); - } + const result = await pool.query(query, [ticketNumber]); + return result.rows[0] || null; } catch (error) { - logError(error, `❌ Erro ao atualizar status de sync para ticket ${ticketId}`); + logError(error, `❌ Erro ao buscar ticket por número: ${ticketNumber}`); + return null; } } - - // Coleta o status de sincronização para todos os tickets (uso em testes) - static async getSyncStatus() { + // Busca um ticket pelo ID + static async findById(ticketId) { try { const query = ` - SELECT - glpi_sync_status, - COUNT(*) as total, - COUNT(glpi_ticket_id) as synchronized - FROM tickets_sn - GROUP BY glpi_sync_status + SELECT * FROM tickets_sn + WHERE id = $1 `; - const result = await pool.query(query); - return result.rows; + const result = await pool.query(query, [ticketId]); + return result.rows[0] || null; } catch (error) { - logError(error, '❌ Erro ao verificar status de sync'); - return []; + logError(error, `❌ Erro ao buscar ticket por ID: ${ticketId}`); + return null; + } + } + + // Atualiza os dados de um ticket + static async updateTicket(ticketId, updateData) { + try { + const fields = []; + const values = []; + let paramCount = 1; + + // Construir dinamicamente a query baseada nos campos fornecidos + for (const [key, value] of Object.entries(updateData)) { + fields.push(`${key} = $${paramCount}`); + values.push(value); + paramCount++; + } + + // Adicionar updated_at + fields.push(`updated_at = NOW()`); + + // Adicionar o ID no final dos valores + 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; } } } @@ -161,11 +188,10 @@ 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. + * e buscar/atualizar tickets específicos. * * 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. + * A função getPendingTickets busca tickets que ainda não foram sincronizados com o GLPI (não possuem registro em ticket_sync) + * e ignora os tickets com status 'Encerrado' ou 'Encerrado - Omitido'. */ module.exports = TicketSnModel; \ No newline at end of file diff --git a/src/models/ticketSyncModel.js b/src/models/ticketSyncModel.js new file mode 100644 index 0000000..6042ae8 --- /dev/null +++ b/src/models/ticketSyncModel.js @@ -0,0 +1,120 @@ +// src/models/ticketSyncModel.js +const pool = require('../data/database'); +const { logInfo, logError } = require('../utils/logger'); + +class TicketSyncModel { + + // Cria um registro de sincronização + static async createSyncRecord(snTicketId, glpiTicketId = null) { + try { + const query = ` + INSERT INTO ticket_sync (sn_ticket_id, glpi_ticket_id) + VALUES ($1, $2) + RETURNING * + `; + const result = await pool.query(query, [snTicketId, glpiTicketId]); + logInfo(`✅ Registro de sincronização criado para ticket SN: ${snTicketId}, GLPI: ${glpiTicketId || 'null'}`); + return result.rows[0]; + } catch (error) { + logError(error, `❌ Erro ao criar registro de sincronização para ticket ${snTicketId}`); + throw error; + } + } + + // Atualiza o status de sincronização com o GLPI + static async updateGlpiSyncStatus(syncId, status, glpiTicketId = null) { + try { + const query = ` + UPDATE ticket_sync + SET glpi_sync_status = $1, + glpi_ticket_id = COALESCE($2, glpi_ticket_id), + last_glpi_sync = NOW() + WHERE id = $3 + RETURNING * + `; + const result = await pool.query(query, [status, glpiTicketId, syncId]); + logInfo(`✅ Status GLPI atualizado para: ${status}, sync ID: ${syncId}`); + return result.rows[0]; + } catch (error) { + logError(error, `❌ Erro ao atualizar status GLPI para sync ${syncId}`); + throw error; + } + } + + // Atualiza o status de sincronização com o ServiceNow + static async updateSnSyncStatus(syncId, status) { + try { + const query = ` + UPDATE ticket_sync + SET sn_sync_status = $1, + last_sn_sync = NOW() + WHERE id = $2 + RETURNING * + `; + const result = await pool.query(query, [status, syncId]); + logInfo(`✅ Status SN atualizado para: ${status}, sync ID: ${syncId}`); + return result.rows[0]; + } catch (error) { + logError(error, `❌ Erro ao atualizar status SN para sync ${syncId}`); + throw error; + } + } + + // Busca registro de sincronização por ticket SN + static async findBySnTicketId(snTicketId) { + try { + const query = `SELECT * FROM ticket_sync WHERE sn_ticket_id = $1`; + const result = await pool.query(query, [snTicketId]); + return result.rows[0]; + } catch (error) { + logError(error, `❌ Erro ao buscar sync por SN ticket ID: ${snTicketId}`); + return null; + } + } + + // Busca registro de sincronização por ticket GLPI + static async findByGlpiTicketId(glpiTicketId) { + try { + const query = `SELECT * FROM ticket_sync WHERE glpi_ticket_id = $1`; + const result = await pool.query(query, [glpiTicketId]); + return result.rows[0]; + } catch (error) { + logError(error, `❌ Erro ao buscar sync por GLPI ticket ID: ${glpiTicketId}`); + return null; + } + } + + // Busca todos os registros de sincronização com status pendente para GLPI + static async findPendingGlpiSync() { + try { + const query = ` + SELECT * FROM ticket_sync + WHERE glpi_sync_status = 'pending' + ORDER BY created_at + `; + const result = await pool.query(query); + return result.rows; + } catch (error) { + logError(error, '❌ Erro ao buscar sincronizações pendentes para GLPI'); + return []; + } + } + + // Busca todos os registros de sincronização com status pendente para ServiceNow + static async findPendingSnSync() { + try { + const query = ` + SELECT * FROM ticket_sync + WHERE sn_sync_status = 'pending' + ORDER BY created_at + `; + const result = await pool.query(query); + return result.rows; + } catch (error) { + logError(error, '❌ Erro ao buscar sincronizações pendentes para ServiceNow'); + return []; + } + } +} + +module.exports = TicketSyncModel; \ No newline at end of file diff --git a/src/scripts/database/01_create_tables.sql b/src/scripts/database/01_create_tables.sql deleted file mode 100644 index 12ababf..0000000 --- a/src/scripts/database/01_create_tables.sql +++ /dev/null @@ -1,28 +0,0 @@ -CREATE TABLE tickets_sn ( - id SERIAL PRIMARY KEY, -- ID automático (1, 2, 3...) - ticket_number VARCHAR(32) NOT NULL UNIQUE, -- Número do ticket (INC0140378) - ÚNICO - sys_id VARCHAR(64) NOT NULL UNIQUE, -- Sys ID do ticket no ServiceNow - ÚNICO - short_description TEXT, -- Resumo do problema - tipo VARCHAR(10), -- Requisição ou incidente - status VARCHAR(55), -- Resumo do problema - description TEXT, -- Descrição completa - caller_id VARCHAR(255), -- Nome de quem abriu o ticket - caller_email VARCHAR(255), -- Email do solicitante - location_id VARCHAR(64), -- ID da localização - location_name VARCHAR(255), -- Nome da localização - ramal VARCHAR(100), -- Ramal do solicitante - telefone VARCHAR(100), -- Telefone do solicitante - opened_at TIMESTAMP, -- Quando o ticket foi aberto - created_at TIMESTAMP DEFAULT NOW(), -- Quando registramos no BD - updated_at TIMESTAMP DEFAULT NOW(), -- Quando atualizamos pela última vez - 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; - - diff --git a/src/scripts/database/scriptBD.sql b/src/scripts/database/scriptBD.sql new file mode 100644 index 0000000..db80e0a --- /dev/null +++ b/src/scripts/database/scriptBD.sql @@ -0,0 +1,249 @@ +-- ============================================= +-- BANCO DE DADOS: snglpi +-- DESCRIÇÃO: Sistema de integração entre ServiceNow e GLPI +-- OBS: Chamados sempre são abertos pelo ServiceNow +-- ============================================= + +-- ============================================= +-- TABELA: tickets_sn +-- DESCRIÇÃO: Armazena tickets importados do ServiceNow +-- ============================================= +CREATE TABLE tickets_sn ( + id SERIAL PRIMARY KEY, -- ID único do registro no banco snglpi + ticket_number VARCHAR(32) NOT NULL UNIQUE, -- Número do ticket no ServiceNow (ex: INC0140378) + sys_id VARCHAR(64) NOT NULL UNIQUE, -- Identificador único do ticket no ServiceNow + short_description TEXT, -- Descrição resumida do ticket + tipo VARCHAR(10), -- Tipo do ticket: 'incidente' ou 'requisicao' + status VARCHAR(55), -- Status atual do ticket no ServiceNow + description TEXT, -- Descrição completa do ticket + caller_id VARCHAR(255), -- Nome do solicitante do ticket + caller_email VARCHAR(255), -- Email do solicitante + location_id VARCHAR(64), -- ID da localização no ServiceNow + location_name VARCHAR(255), -- Nome da localização no ServiceNow + ramal VARCHAR(100), -- Ramal do solicitante + telefone VARCHAR(100), -- Telefone do solicitante + opened_at TIMESTAMP, -- Data e hora de abertura no ServiceNow + created_at TIMESTAMP DEFAULT NOW(), -- Data e hora de criação do registro + updated_at TIMESTAMP DEFAULT NOW() -- Data e hora da última atualização do registro +); + +-- Índices para melhor performance nas consultas frequentes +CREATE INDEX idx_tickets_sn_status ON tickets_sn(status); +CREATE INDEX idx_tickets_sn_tipo ON tickets_sn(tipo); +CREATE INDEX idx_tickets_sn_opened_at ON tickets_sn(opened_at); +CREATE INDEX idx_tickets_sn_location_id ON tickets_sn(location_id); +CREATE INDEX idx_tickets_sn_location_name ON tickets_sn(location_name); + +-- ============================================= +-- TABELA: ticket_sync +-- DESCRIÇÃO: Controla o status de sincronização entre ServiceNow e GLPI +-- OBS: Chamados sempre são criados no ServiceNow primeiro +-- ============================================= +CREATE TABLE ticket_sync ( + id SERIAL PRIMARY KEY, -- ID único do registro de sincronização + sn_ticket_id INTEGER NOT NULL, -- Referência ao ticket na tabela tickets_sn + glpi_ticket_id INTEGER, -- ID do ticket correspondente no GLPI + last_sn_sync TIMESTAMP, -- Data e hora da última sincronização com ServiceNow + last_glpi_sync TIMESTAMP, -- Data e hora da última sincronização com GLPI + sn_sync_status VARCHAR(20) DEFAULT 'pending',-- Status da sincronização com ServiceNow: 'pending', 'completed', 'error' + glpi_sync_status VARCHAR(20) DEFAULT 'pending', -- Status da sincronização com GLPI: 'pending', 'completed', 'error' + created_at TIMESTAMP DEFAULT NOW(), -- Data e hora de criação do registro + updated_at TIMESTAMP DEFAULT NOW(), -- Data e hora da última atualização do registro + + -- Chaves estrangeiras + CONSTRAINT fk_ticket_sync_sn_ticket + FOREIGN KEY (sn_ticket_id) + REFERENCES tickets_sn(id) + ON DELETE CASCADE +); + +-- Índices para melhor performance nas consultas frequentes +CREATE INDEX idx_ticket_sync_sn_ticket ON ticket_sync(sn_ticket_id); +CREATE INDEX idx_ticket_sync_glpi_ticket ON ticket_sync(glpi_ticket_id); +CREATE INDEX idx_ticket_sync_status ON ticket_sync(sn_sync_status, glpi_sync_status); + +-- ============================================= +-- TABELA: ticket_updates +-- DESCRIÇÃO: Armazena as atualizações e tarefas relacionadas aos tickets +-- OBS: Atualizações podem vir de ambos os sistemas (ServiceNow e GLPI) +-- ============================================= +CREATE TABLE ticket_updates ( + id SERIAL PRIMARY KEY, -- ID único da atualização + ticket_sync_id INTEGER NOT NULL, -- Referência ao registro de sincronização + update_type VARCHAR(20) NOT NULL, -- Tipo de atualização: 'task', 'note', 'status_change' + source_system VARCHAR(10) NOT NULL, -- Sistema de origem: 'sn' (ServiceNow) ou 'glpi' + content TEXT, -- Conteúdo da atualização/tarefa + author VARCHAR(255), -- Autor da atualização + created_at_source TIMESTAMP, -- Data e hora de criação no sistema de origem + sync_status VARCHAR(20) DEFAULT 'pending', -- Status da sincronização: 'pending', 'completed', 'error' + sync_date TIMESTAMP, -- Data e hora da sincronização + created_at TIMESTAMP DEFAULT NOW(), -- Data e hora de criação do registro + updated_at TIMESTAMP DEFAULT NOW(), -- Data e hora da última atualização do registro + + -- Chaves estrangeiras + CONSTRAINT fk_ticket_updates_sync + FOREIGN KEY (ticket_sync_id) + REFERENCES ticket_sync(id) + ON DELETE CASCADE +); + +-- Índices para melhor performance nas consultas frequentes +CREATE INDEX idx_ticket_updates_sync ON ticket_updates(ticket_sync_id); +CREATE INDEX idx_ticket_updates_status ON ticket_updates(sync_status); +CREATE INDEX idx_ticket_updates_source ON ticket_updates(source_system); +CREATE INDEX idx_ticket_updates_type ON ticket_updates(update_type); +CREATE INDEX idx_ticket_updates_created ON ticket_updates(created_at_source); + +-- ============================================= +-- TABELA: location_mapping +-- DESCRIÇÃO: Mapeamento entre localizações do ServiceNow e entidades do GLPI +-- ============================================= +CREATE TABLE location_mapping ( + id SERIAL PRIMARY KEY, -- ID único do mapeamento + sn_location_id VARCHAR(255), -- ID da localização no ServiceNow + sn_location_name VARCHAR(255) NOT NULL, -- Nome da localização no ServiceNow + glpi_entity_id INTEGER NOT NULL, -- ID da entidade correspondente no GLPI + glpi_entity_name VARCHAR(255) NOT NULL, -- Nome da entidade no GLPI + created_at TIMESTAMP DEFAULT NOW(), -- Data e hora de criação do registro + updated_at TIMESTAMP DEFAULT NOW() -- Data e hora da última atualização do registro +); + +-- Índices para melhor performance nas consultas frequentes +CREATE INDEX idx_location_mapping_sn_id ON location_mapping(sn_location_id); +CREATE INDEX idx_location_mapping_sn_name ON location_mapping(sn_location_name); +CREATE INDEX idx_location_mapping_glpi_id ON location_mapping(glpi_entity_id); +CREATE INDEX idx_location_mapping_glpi_name ON location_mapping(glpi_entity_name); + +-- ============================================= +-- TRIGGERS para atualizar automaticamente updated_at +-- ============================================= + +-- Função para atualizar o campo updated_at +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Trigger para tickets_sn +CREATE TRIGGER trigger_tickets_sn_updated_at + BEFORE UPDATE ON tickets_sn + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- Trigger para ticket_sync +CREATE TRIGGER trigger_ticket_sync_updated_at + BEFORE UPDATE ON ticket_sync + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- Trigger para ticket_updates +CREATE TRIGGER trigger_ticket_updates_updated_at + BEFORE UPDATE ON ticket_updates + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- Trigger para location_mapping +CREATE TRIGGER trigger_location_mapping_updated_at + BEFORE UPDATE ON location_mapping + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- ============================================= +-- VIEWS para consultas úteis +-- ============================================= + +-- View para tickets pendentes de sincronização com GLPI +CREATE VIEW view_tickets_pending_glpi AS +SELECT + ts.ticket_number, + ts.short_description, + ts.status, + ts.tipo, + ts.opened_at, + tsync.created_at as sync_created +FROM tickets_sn ts +JOIN ticket_sync tsync ON ts.id = tsync.sn_ticket_id +WHERE tsync.glpi_sync_status = 'pending'; + +-- View para tickets pendentes de sincronização com ServiceNow +CREATE VIEW view_tickets_pending_sn AS +SELECT + ts.ticket_number, + ts.short_description, + ts.status, + ts.tipo, + ts.opened_at, + tsync.created_at as sync_created +FROM tickets_sn ts +JOIN ticket_sync tsync ON ts.id = tsync.sn_ticket_id +WHERE tsync.sn_sync_status = 'pending'; + +-- View para mapeamento de localizações completo +CREATE VIEW view_location_mapping_complete AS +SELECT + sn_location_id, + sn_location_name, + glpi_entity_id, + glpi_entity_name, + created_at, + updated_at +FROM location_mapping +ORDER BY sn_location_name; + +-- ============================================= +-- COMENTÁRIOS NAS TABELAS E COLUNAS +-- ============================================= + +COMMENT ON TABLE tickets_sn IS 'Armazena tickets importados do ServiceNow'; +COMMENT ON COLUMN tickets_sn.id IS 'ID único do registro no banco snglpi'; +COMMENT ON COLUMN tickets_sn.ticket_number IS 'Número do ticket no ServiceNow (ex: INC0140378)'; +COMMENT ON COLUMN tickets_sn.sys_id IS 'Identificador único do ticket no ServiceNow'; +COMMENT ON COLUMN tickets_sn.short_description IS 'Descrição resumida do ticket'; +COMMENT ON COLUMN tickets_sn.tipo IS 'Tipo do ticket: incidente ou requisicao'; +COMMENT ON COLUMN tickets_sn.status IS 'Status atual do ticket no ServiceNow'; +COMMENT ON COLUMN tickets_sn.description IS 'Descrição completa do ticket'; +COMMENT ON COLUMN tickets_sn.caller_id IS 'Nome do solicitante do ticket'; +COMMENT ON COLUMN tickets_sn.caller_email IS 'Email do solicitante'; +COMMENT ON COLUMN tickets_sn.location_id IS 'ID da localização no ServiceNow'; +COMMENT ON COLUMN tickets_sn.location_name IS 'Nome da localização no ServiceNow'; +COMMENT ON COLUMN tickets_sn.ramal IS 'Ramal do solicitante'; +COMMENT ON COLUMN tickets_sn.telefone IS 'Telefone do solicitante'; +COMMENT ON COLUMN tickets_sn.opened_at IS 'Data e hora de abertura no ServiceNow'; +COMMENT ON COLUMN tickets_sn.created_at IS 'Data e hora de criação do registro'; +COMMENT ON COLUMN tickets_sn.updated_at IS 'Data e hora da última atualização do registro'; + +COMMENT ON TABLE ticket_sync IS 'Controla o status de sincronização entre ServiceNow e GLPI'; +COMMENT ON COLUMN ticket_sync.id IS 'ID único do registro de sincronização'; +COMMENT ON COLUMN ticket_sync.sn_ticket_id IS 'Referência ao ticket na tabela tickets_sn'; +COMMENT ON COLUMN ticket_sync.glpi_ticket_id IS 'ID do ticket correspondente no GLPI'; +COMMENT ON COLUMN ticket_sync.last_sn_sync IS 'Data e hora da última sincronização com ServiceNow'; +COMMENT ON COLUMN ticket_sync.last_glpi_sync IS 'Data e hora da última sincronização com GLPI'; +COMMENT ON COLUMN ticket_sync.sn_sync_status IS 'Status da sincronização com ServiceNow: pending, completed, error'; +COMMENT ON COLUMN ticket_sync.glpi_sync_status IS 'Status da sincronização com GLPI: pending, completed, error'; +COMMENT ON COLUMN ticket_sync.created_at IS 'Data e hora de criação do registro'; +COMMENT ON COLUMN ticket_sync.updated_at IS 'Data e hora da última atualização do registro'; + +COMMENT ON TABLE ticket_updates IS 'Armazena as atualizações e tarefas relacionadas aos tickets'; +COMMENT ON COLUMN ticket_updates.id IS 'ID único da atualização'; +COMMENT ON COLUMN ticket_updates.ticket_sync_id IS 'Referência ao registro de sincronização'; +COMMENT ON COLUMN ticket_updates.update_type IS 'Tipo de atualização: task, note, status_change'; +COMMENT ON COLUMN ticket_updates.source_system IS 'Sistema de origem: sn (ServiceNow) ou glpi'; +COMMENT ON COLUMN ticket_updates.content IS 'Conteúdo da atualização/tarefa'; +COMMENT ON COLUMN ticket_updates.author IS 'Autor da atualização'; +COMMENT ON COLUMN ticket_updates.created_at_source IS 'Data e hora de criação no sistema de origem'; +COMMENT ON COLUMN ticket_updates.sync_status IS 'Status da sincronização: pending, completed, error'; +COMMENT ON COLUMN ticket_updates.sync_date IS 'Data e hora da sincronização'; +COMMENT ON COLUMN ticket_updates.created_at IS 'Data e hora de criação do registro'; +COMMENT ON COLUMN ticket_updates.updated_at IS 'Data e hora da última atualização do registro'; + +COMMENT ON TABLE location_mapping IS 'Mapeamento entre localizações do ServiceNow e entidades do GLPI'; +COMMENT ON COLUMN location_mapping.id IS 'ID único do mapeamento'; +COMMENT ON COLUMN location_mapping.sn_location_id IS 'ID da localização no ServiceNow'; +COMMENT ON COLUMN location_mapping.sn_location_name IS 'Nome da localização no ServiceNow'; +COMMENT ON COLUMN location_mapping.glpi_entity_id IS 'ID da entidade correspondente no GLPI'; +COMMENT ON COLUMN location_mapping.glpi_entity_name IS 'Nome da entidade no GLPI'; +COMMENT ON COLUMN location_mapping.created_at IS 'Data e hora de criação do registro'; +COMMENT ON COLUMN location_mapping.updated_at IS 'Data e hora da última atualização do registro'; \ No newline at end of file diff --git a/src/scripts/python/update_location_mapping.py b/src/scripts/python/update_location_mapping.py new file mode 100644 index 0000000..28d150f --- /dev/null +++ b/src/scripts/python/update_location_mapping.py @@ -0,0 +1,184 @@ +# scripts/update_location_mapping.py +import csv +import psycopg2 +import mysql.connector +from mysql.connector import Error +import logging +from datetime import datetime +import os +from dotenv import load_dotenv +import pandas as pd + +# Carrega as variáveis do .env +load_dotenv() + +# Configuração de logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('location_mapping.log'), + logging.StreamHandler() + ] +) + +# Configurações de banco de dados a partir do .env +SNGLPI_DB_CONFIG = { + 'host': os.getenv('SNGLPI_DB_HOST'), + 'port': os.getenv('SNGLPI_DB_PORT'), + 'database': os.getenv('SNGLPI_DB_NAME'), + 'user': os.getenv('SNGLPI_DB_USER'), + 'password': os.getenv('SNGLPI_DB_PASSWORD') +} + +GLPI_DB_CONFIG = { + 'host': os.getenv('GLPI_DB_HOST'), + 'port': os.getenv('GLPI_DB_PORT'), + 'database': os.getenv('GLPI_DB_NAME'), + 'user': os.getenv('GLPI_DB_USER'), + 'password': os.getenv('GLPI_DB_PASSWORD'), + 'charset': os.getenv('GLPI_DB_CHARSET', 'utf8mb4') +} + +# Caminho para o arquivo CSV +CSV_PATH = os.getenv('LOCATION_MAPPING_CSV_PATH') + +# Função para conectar ao banco SNGLPI +def get_snglpi_connection(): + try: + conn = psycopg2.connect(**SNGLPI_DB_CONFIG) + return conn + except Exception as e: + logging.error(f"Erro ao conectar ao banco SNGLPI: {e}") + return None + +# Função para conectar ao banco GLPI +def get_glpi_connection(): + try: + conn = mysql.connector.connect(**GLPI_DB_CONFIG) + return conn + except Error as e: + logging.error(f"Erro ao conectar ao banco GLPI: {e}") + return None + +# Função para buscar location_id no SNGLPI +def find_sn_location_id(location_name, cursor): + try: + query = """ + SELECT DISTINCT location_id + FROM tickets_sn + WHERE location_name = %s + LIMIT 1 + """ + cursor.execute(query, (location_name,)) + result = cursor.fetchone() + return result[0] if result else None + except Exception as e: + logging.error(f"Erro ao buscar location_id para {location_name}: {e}") + return None + +# Função para buscar entity_id no GLPI +def find_glpi_entity_id(entity_name, cursor): + try: + query = """ + SELECT id + FROM glpi_entities + WHERE name = %s + LIMIT 1 + """ + cursor.execute(query, (entity_name,)) + result = cursor.fetchone() + return result[0] if result else None + except Exception as e: + logging.error(f"Erro ao buscar entity_id para {entity_name}: {e}") + return None + +# Função principal para atualizar a tabela de mapeamento +def update_mapping_table(): + try: + # Verificar se o arquivo CSV existe + if not os.path.exists(CSV_PATH): + logging.error(f"Arquivo CSV não encontrado: {CSV_PATH}") + return False + + # Conectar aos bancos + snglpi_conn = get_snglpi_connection() + glpi_conn = get_glpi_connection() + + if not snglpi_conn or not glpi_conn: + return False + + snglpi_cur = snglpi_conn.cursor() + glpi_cur = glpi_conn.cursor() + + # Ler CSV + df = pd.read_csv(CSV_PATH, encoding='latin-1', sep=';') + print(df.head()) + print(df.columns) + print(df) + # Adicionar colunas para IDs + df['sn_location_id'] = None + df['glpi_entity_id'] = None + + + # Processar cada linha do CSV + mappings = [] + for index, row in df.iterrows(): + sn_location_name = row['sn_location_name'] + glpi_entity_name = row['glpi_entity_name'] + + # Buscar IDs + sn_id = find_sn_location_id(sn_location_name, snglpi_cur) + glpi_id = find_glpi_entity_id(glpi_entity_name, glpi_cur) + + # Preencher o DataFrame + df.loc[index, 'sn_location_id'] = sn_id + df.loc[index, 'glpi_entity_id'] = glpi_id + + if glpi_id: + # Se o glpi_entity_id for encontrado, adiciona para inserção + mappings.append((sn_id, sn_location_name, glpi_id, glpi_entity_name)) + logging.debug(f"Mapeamento: {sn_location_name} -> {glpi_entity_name} (ID: {glpi_id})") + else: + logging.warning(f"Não foi possível encontrar entity_id para: {glpi_entity_name}") + + # Atualizar tabela de mapeamento + if mappings: + # Limpar tabela existente + snglpi_cur.execute("DELETE FROM location_mapping") + + # Inserir novos mapeamentos + insert_query = """ + INSERT INTO location_mapping + (sn_location_id, sn_location_name, glpi_entity_id, glpi_entity_name) + VALUES (%s, %s, %s, %s) + """ + snglpi_cur.executemany(insert_query, mappings) + snglpi_conn.commit() + + logging.info(f"Tabela atualizada com {len(mappings)} mapeamentos") + else: + logging.warning("Nenhum mapeamento válido encontrado para atualização") + + # Fechar conexões + snglpi_cur.close() + glpi_cur.close() + snglpi_conn.close() + glpi_conn.close() + + return True + + except Exception as e: + logging.error(f"Erro no processo de atualização: {e}") + # Rollback em caso de erro + if 'snglpi_conn' in locals(): + snglpi_conn.rollback() + return False + +if __name__ == "__main__": + logging.info("Iniciando atualização de mapeamento de localizações") + success = update_mapping_table() + if success: + logging.info("Atualização concluída com sucesso") + else: + logging.error("Falha na atualização") \ No newline at end of file diff --git a/src/services/glpiService.js b/src/services/glpiService.js index 92a90b7..1081fd3 100644 --- a/src/services/glpiService.js +++ b/src/services/glpiService.js @@ -1,6 +1,7 @@ // src/services/glpiService.js const TicketSnModel = require('../models/ticketSnModel'); const TicketGlpiModel = require('../models/ticketGlpiModel'); +const TicketSyncModel = require('../models/ticketSyncModel'); const { logInfo, logError } = require('../utils/logger'); // Método principal que o controller vai chamar @@ -8,10 +9,9 @@ const syncTicketsToGlpi = async () => { try { logInfo('🔄 Iniciando sincronização para GLPI...'); - // 1. Buscar tickets pendentes usando SEU MODEL + // 1. Buscar tickets pendentes const pendingTickets = await TicketSnModel.getPendingTickets(); - if (pendingTickets.length === 0) { logInfo('✅ Nenhum ticket pendente para sincronizar'); return; @@ -34,6 +34,8 @@ const syncTicketsToGlpi = async () => { // Processar um ticket individual const processSingleTicket = async (ticket) => { + let syncRecord = null; + try { logInfo(`🔍 Processando ticket: ${ticket.ticket_number}`); @@ -41,45 +43,54 @@ const processSingleTicket = async (ticket) => { const existingTicket = await TicketGlpiModel.ticketExists(ticket.ticket_number); if (existingTicket) { logInfo(`⏭️ Ticket já existe no GLPI: ${existingTicket}`); - await TicketSnModel.updateSyncStatus(ticket.id, 'duplicate', existingTicket); + // Criar registro de sincronização indicando que já existe + syncRecord = await TicketSyncModel.createSyncRecord(ticket.id, existingTicket); + await TicketSyncModel.updateGlpiSyncStatus(syncRecord.id, 'duplicate', existingTicket); return; } - // 2. Converter dados para formato GLPI (O MODEL vai fazer o mapeamento) + + // 2. Converter dados para formato GLPI const glpiData = { - // Dados básicos do PostgreSQL ticket_number: ticket.ticket_number, short_description: ticket.short_description, description: ticket.description, + caller_id: ticket.caller_id, 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 + type: ticket.tipo, + state: ticket.status, + priority: '3', + location_id: ticket.location_id, 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) + // 3. Inserir no GLPI const glpiTicketId = await TicketGlpiModel.createTicket(glpiData); - // 4. Atualizar status no PostgreSQL usando SEU MODEL - await TicketSnModel.updateSyncStatus(ticket.id, 'completed', glpiTicketId); + // 4. Criar registro de sincronização + syncRecord = await TicketSyncModel.createSyncRecord(ticket.id, glpiTicketId); + await TicketSyncModel.updateGlpiSyncStatus(syncRecord.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'); + + // Se temos um registro de sync, atualizar o status para error + if (syncRecord) { + await TicketSyncModel.updateGlpiSyncStatus(syncRecord.id, 'error'); + } else { + // Se não temos um registro, criar um com status error + try { + syncRecord = await TicketSyncModel.createSyncRecord(ticket.id); + await TicketSyncModel.updateGlpiSyncStatus(syncRecord.id, 'error'); + } catch (innerError) { + logError(innerError, `❌ Erro ao registrar falha de sincronização para ticket ${ticket.id}`); + } + } } };