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}`);
+ }
+ }
}
};