WIP: Model do GLPI em construção

Adicionados dados do banco ao .ENV
Erros de sintaxe persistem
This commit is contained in:
Rafael Alves Lopes 2025-09-11 20:57:43 -03:00
parent 9ca4a27a81
commit 40d0b2f4c6
10 changed files with 459 additions and 161 deletions

15
.env
View File

@ -8,20 +8,15 @@ SERVICENOW_REQUEST_URL="https://caoadev.service-now.com/api/now/table/sc_req_ite
SERVICENOW_USERNAME=SOTHIS.CAOA SERVICENOW_USERNAME=SOTHIS.CAOA
SERVICENOW_PASSWORD=#Sothis20 SERVICENOW_PASSWORD=#Sothis20
# GLPI API Configuration
GLPI_URL=https://your-glpi-instance/apirest.php/
GLPI_APP_TOKEN=your_glpi_app_token
GLPI_USER_TOKEN=your_glpi_user_token
# GLPI access via DB (use DB connection instead of API) # GLPI access via DB (use DB connection instead of API)
GLPI_DB_TYPE=mysql # ou postgresql GLPI_DB_TYPE=mysql # ou postgresql
GLPI_DB_HOST=localhost GLPI_DB_HOST=177.73.177.32
GLPI_DB_PORT=3306 GLPI_DB_PORT=3306
GLPI_DB_USER=glpi_user GLPI_DB_USER=snglpi
GLPI_DB_PASSWORD=your_glpi_db_password GLPI_DB_PASSWORD=j2633669
GLPI_DB_NAME=glpi_database GLPI_DB_NAME=glpi_data
GLPI_DB_CHARSET=utf8mb4 GLPI_DB_CHARSET=utf8mb4
GLPI_TABLE_PREFIX=glpi_ # deixe em branco se não houver prefixo
# Database Configuration # Database Configuration
DB_HOST=localhost DB_HOST=localhost

126
package-lock.json generated
View File

@ -12,6 +12,7 @@
"axios": "^0.21.4", "axios": "^0.21.4",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"express": "^5.1.0", "express": "^5.1.0",
"mysql2": "^3.14.5",
"pg": "^8.7.1", "pg": "^8.7.1",
"winston": "^3.17.0" "winston": "^3.17.0"
} }
@ -61,6 +62,15 @@
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/axios": { "node_modules/axios": {
"version": "0.21.4", "version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
@ -229,6 +239,15 @@
} }
} }
}, },
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -445,6 +464,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"license": "MIT",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -582,6 +610,12 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/is-stream": { "node_modules/is-stream": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@ -617,6 +651,36 @@
"node": ">= 12.0.0" "node": ">= 12.0.0"
} }
}, },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/lru.min": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz",
"integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -674,6 +738,54 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mysql2": {
"version": "3.14.5",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.5.tgz",
"integrity": "sha512-40hDf8LPUsuuJ2hFq+UgOuPwt2IFLIRDvMv6ez9hKbXeYuZPxDDwiJW7KdknvOsQqKznaKczOT1kELgFkhDvFg==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.7.0",
"long": "^5.2.1",
"lru.min": "^1.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/named-placeholders": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
"integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
"license": "MIT",
"dependencies": {
"lru-cache": "^7.14.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@ -1011,6 +1123,11 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/seq-queue": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
@ -1122,6 +1239,15 @@
"node": ">= 10.x" "node": ">= 10.x"
} }
}, },
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/stack-trace": { "node_modules/stack-trace": {
"version": "0.0.10", "version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",

View File

@ -12,6 +12,7 @@
"axios": "^0.21.4", "axios": "^0.21.4",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"express": "^5.1.0", "express": "^5.1.0",
"mysql2": "^3.14.5",
"pg": "^8.7.1", "pg": "^8.7.1",
"winston": "^3.17.0" "winston": "^3.17.0"
}, },

View File

@ -1,4 +1,6 @@
// src/app.js
const { snServices } = require('./controllers/servicenowController'); const { snServices } = require('./controllers/servicenowController');
const { glpiServices } = require('./controllers/glpiController'); // NOVO
const { logInfo } = require('./utils/logger'); const { logInfo } = require('./utils/logger');
logInfo('Aplicação iniciada', { logInfo('Aplicação iniciada', {
@ -6,5 +8,14 @@ logInfo('Aplicação iniciada', {
environment: process.env.NODE_ENV || 'development' environment: process.env.NODE_ENV || 'development'
}); });
async function main() {
try {
await snServices();
//await glpiServices();
snServices(); } catch (error) {
logInfo('Erro na aplicação:', error);
}
}
main();

View File

@ -0,0 +1,10 @@
// src/controllers/glpiController.js
const { syncTicketsToGlpi } = require('../services/glpiService');
const glpiServices = async () => {
await syncTicketsToGlpi();
}
module.exports = {
glpiServices
}

View File

@ -4,14 +4,11 @@ const mysql = require('mysql2/promise');
const { logInfo, logError, logWarning } = require('../utils/logger'); const { logInfo, logError, logWarning } = require('../utils/logger');
const glpiPool = mysql.createPool({ const glpiPool = mysql.createPool({
host: '177.73.177.32', host: process.env.GLPI_DB_HOST,
port: 3306, port: process.env.GLPI_DB_PORT,
database: process.env.GLPI_DB_NAME, database: process.env.GLPI_DB_NAME,
user: 'snglpi', user: process.env.GLPI_DB_USER,
password: 'j2633669', password: process.env.GLPI_DB_PASSWORD,
connectionLimit: 10,
acquireTimeout: 60000,
timeout: 60000
}); });
// Testar conexão // Testar conexão

View File

@ -3,6 +3,7 @@ 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 ServiceNow → GLPI
static mapStatus(snState) { static mapStatus(snState) {
const statusMap = { const statusMap = {
@ -38,69 +39,71 @@ class TicketGlpiModel {
return categoryMap[shortDescription] || 5717; return categoryMap[shortDescription] || 5717;
} }
// Criar ticket no GLPI // Criar ticket no GLPI
static async createTicket(snTicket, mapping) { // src/models/ticketGlpiModel.js
static async createTicket(ticketData) {
const connection = await glpiPool.getConnection(); const connection = await glpiPool.getConnection();
try { try {
await connection.beginTransaction(); await connection.beginTransaction();
// 1. Preparar dados para o GLPI // 1. Preparar dados para o GLPI (usando valores fixos para teste)
const glpiTicketData = { const glpiTicketData = {
entities_id: mapping.entityId || 1371, entities_id: 1371, // Valor fixo para teste
name: snTicket.short_description?.value || 'Sem título', name: ticketData.short_description || 'Sem título',
date: snTicket.sys_created_on?.value ? new Date(snTicket.sys_created_on.value) : new Date(), date: ticketData.opened_at || new Date(),
date_mod: snTicket.sys_updated_on?.value ? new Date(snTicket.sys_updated_on.value) : new Date(), date_mod: ticketData.updated_at || new Date(),
status: this.mapStatus(snTicket.state?.value), status: TicketGlpiModel.mapStatus(ticketData.state || '1'),
users_id_recipient: mapping.userId || 917, users_id_recipient: 917, // Valor fixo para teste
requesttypes_id: 1, requesttypes_id: 1,
content: snTicket.description?.value || '', content: ticketData.description || '',
urgency: this.mapUrgency(snTicket.urgency?.value), urgency: 3, // Valor fixo
impact: this.mapImpact(snTicket.impact?.value), impact: 3, // Valor fixo
priority: this.mapPriority(snTicket.priority?.value), priority: TicketGlpiModel.mapPriority(ticketData.priority || '3'),
type: 1, // Incidente type: 1, // Incidente
itilcategories_id: this.mapCategory(snTicket.short_description?.value), itilcategories_id: TicketGlpiModel.mapCategory(ticketData.short_description),
global_validation: 1, global_validation: 1,
date_creation: snTicket.sys_created_on?.value ? new Date(snTicket.sys_created_on.value) : new Date() date_creation: ticketData.opened_at || new Date()
}; };
// 2. Inserir ticket principal await connection.execute(requesterQuery, [ticketId, 917]); // Default user
const ticketQuery = ` // Construir a query manualmente
INSERT INTO glpi_tickets SET ? const fields = Object.keys(glpiTicketData).join(', ');
`; const placeholders = Object.keys(glpiTicketData).map(() => '?').join(', ');
const values = Object.values(glpiTicketData);
const [ticketResult] = await connection.execute(ticketQuery, [glpiTicketData]); const ticketQuery = `INSERT INTO glpi_tickets (${fields}) VALUES (${placeholders})`;
// Inserir ticket principal
const [ticketResult] = await connection.execute(ticketQuery, values);
const ticketId = ticketResult.insertId; const ticketId = ticketResult.insertId;
// 3. Adicionar requerente // Adicionar requerente
const requesterQuery = ` await connection.execute(
INSERT INTO glpi_tickets_users `INSERT INTO glpi_tickets_users
(tickets_id, users_id, type, use_notification, alternative_email) (tickets_id, users_id, type, use_notification, alternative_email)
VALUES (?, ?, 1, 1, '') VALUES (?, ?, 1, 1, '')`,
`; [ticketId, 917]
await connection.execute(requesterQuery, [ticketId, mapping.userId || 917]); );
// 4. Adicionar grupo // Adicionar grupo
const groupQuery = ` await connection.execute(
INSERT INTO glpi_groups_tickets `INSERT INTO glpi_groups_tickets
(tickets_id, groups_id, type) (tickets_id, groups_id, type)
VALUES (?, ?, 2) VALUES (?, ?, 2)`,
`; [ticketId, 30]
await connection.execute(groupQuery, [ticketId, mapping.groupId || 30]); );
await connection.commit(); await connection.commit();
logInfo(`✅ Ticket GLPI criado: ${ticketId}`, { logInfo(`✅ Ticket GLPI criado: ${ticketId}`);
glpi_ticket_id: ticketId,
sn_ticket_number: snTicket.number?.value,
location: snTicket.location?.display_value
});
return ticketId; return ticketId;
} catch (error) { } catch (error) {
await connection.rollback(); await connection.rollback();
logError(error, `❌ Erro ao criar ticket GLPI para ${snTicket.number?.value}`); logError(error, `❌ Erro ao criar ticket GLPI`);
throw error; throw error;
} finally { } finally {
connection.release(); connection.release();

View File

@ -1,3 +1,4 @@
// src/models/ticketSnModel.js
const pool = require('../data/database'); const pool = require('../data/database');
const { logInfo, logError, logWarning } = require('../utils/logger'); const { logInfo, logError, logWarning } = require('../utils/logger');
@ -6,37 +7,40 @@ class TicketSnModel {
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, description, caller_id, caller_email, (ticket_number, 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) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (ticket_number) ON CONFLICT (ticket_number)
DO UPDATE SET DO UPDATE SET
short_description = EXCLUDED.short_description, short_description = EXCLUDED.short_description,
status = EXCLUDED.status,
description = EXCLUDED.description, description = EXCLUDED.description,
location_id = EXCLUDED.location_id,
location_name = EXCLUDED.location_name,
caller_id = EXCLUDED.caller_id, caller_id = EXCLUDED.caller_id,
caller_email = EXCLUDED.caller_email, caller_email = EXCLUDED.caller_email,
location_id = EXCLUDED.location_id,
location_name = EXCLUDED.location_name,
ramal = EXCLUDED.ramal, ramal = EXCLUDED.ramal,
telefone = EXCLUDED.telefone, telefone = EXCLUDED.telefone,
opened_at = EXCLUDED.opened_at,
tipo = EXCLUDED.tipo,
updated_at = NOW() updated_at = NOW()
RETURNING id, xmax = 0 as inserted`; RETURNING id, xmax = 0 as inserted`;
const values = [ const values = [
ticketData.number?.value || null, ticketData.number?.value || null, // $1 - ticket_number
ticketData.short_description?.value || null, ticketData.short_description?.value || null, // $2 - short_description
ticketData.description?.value || null, ticketData.state?.display_value || null, // $3 - status
ticketData.opened_by?.display_value || null, ticketData.description?.value || null, // $4 - description
ticketData['opened_by.email']?.value || null, ticketData.opened_by?.display_value || null, // $5 - caller_id
ticketData.location?.value || null, // location_id (value) ticketData['opened_by.email']?.value || null, // $6 - caller_email
ticketData.location?.display_value || null, // location_name (display_value) ticketData.location?.value || null, // $7 - location_id
ticketData['variables.numero_ramal']?.value || null, ticketData.location?.display_value || null, // $8 - location_name
ticketData['variables.telephone_favorecido_rh']?.value || null, ticketData['variables.numero_ramal']?.value || null, // $9 - ramal
ticketData.opened_at?.value ? new Date(ticketData.opened_at.value) : null, ticketData['variables.telephone_favorecido_rh']?.value || null, // $10 - telefone
typeTicket ticketData.opened_at?.value ? new Date(ticketData.opened_at.value) : null, // $11 - opened_at
typeTicket // $12 - tipo
]; ];
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; // true = INSERT, false = UPDATE
@ -62,6 +66,116 @@ class TicketSnModel {
} }
} }
// NOVO: Buscar tickets pendentes para sincronização com GLPI
static async getPendingTickets(limit = 3) {
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
ORDER BY opened_at ASC
LIMIT $1
`;
const result = await pool.query(query, [limit]);
logInfo(`📋 Tickets pendentes encontrados: ${result.rows.length}`);
return result.rows;
} catch (error) {
logError(error, '❌ Erro ao buscar tickets pendentes');
return [];
} }
}
// NOVO: Atualizar status de sincronização com GLPI
static async updateSyncStatus(ticketId, status, glpiTicketId = null) {
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
`;
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
});
}
} catch (error) {
logError(error, `❌ Erro ao atualizar status de sync para ticket ${ticketId}`);
}
}
// 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]);
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() {
try {
const query = `
SELECT
glpi_sync_status,
COUNT(*) as total,
COUNT(glpi_ticket_id) as synchronized
FROM tickets_sn
GROUP BY glpi_sync_status
`;
const result = await pool.query(query);
return result.rows;
} catch (error) {
logError(error, '❌ Erro ao verificar status de sync');
return [];
}
}
}
module.exports = TicketSnModel; module.exports = TicketSnModel;

View File

@ -3,6 +3,7 @@ CREATE TABLE tickets_sn (
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 short_description TEXT, -- Resumo do problema
tipo VARCHAR(10), -- Requisição ou incidente tipo VARCHAR(10), -- Requisição ou incidente
status VARCHAR(55), -- Resumo do problema
description TEXT, -- Descrição completa description TEXT, -- Descrição completa
caller_id VARCHAR(255), -- Nome de quem abriu o ticket caller_id VARCHAR(255), -- Nome de quem abriu o ticket
caller_email VARCHAR(255), -- Email do solicitante caller_email VARCHAR(255), -- Email do solicitante
@ -12,9 +13,14 @@ CREATE TABLE tickets_sn (
telefone VARCHAR(100), -- Telefone do solicitante telefone VARCHAR(100), -- Telefone do solicitante
opened_at TIMESTAMP, -- Quando o ticket foi aberto opened_at TIMESTAMP, -- Quando o ticket foi aberto
created_at TIMESTAMP DEFAULT NOW(), -- Quando registramos no BD created_at TIMESTAMP DEFAULT NOW(), -- Quando registramos no BD
updated_at TIMESTAMP DEFAULT NOW() -- Quando atualizamos pela última vez updated_at TIMESTAMP DEFAULT NOW(), -- Quando atualizamos pela última vez
glpi_sync_status VARCHAR(20),
glpi_ticket_id INT
); );
SELECT * FROM tickets_sn; SELECT * FROM tickets_sn;
SELECT * FROM tickets_sn LIMIT 3;

View File

@ -1,57 +1,92 @@
// src/services/glpiService.js // src/services/glpiService.js
const TicketSnModel = require('../models/ticketSnModel'); // SEU MODEL do PostgreSQL
const TicketGlpiModel = require('../models/ticketGlpiModel'); // Model do GLPI
const { logInfo, logError } = require('../utils/logger');
const axios = require('axios'); class GlpiService {
const { GLPI_URL, GLPI_APP_TOKEN, GLPI_USER_TOKEN } = process.env;
// Function to create a ticket in GLPI // Método principal que o controller vai chamar
const createTicketInGLPI = async (ticketData) => { static async syncTicketsToGlpi() {
try { try {
const response = await axios.post(`${GLPI_URL}/ticket`, ticketData, { logInfo('🔄 Iniciando sincronização para GLPI...');
headers: {
'Content-Type': 'application/json', // 1. Buscar tickets pendentes usando SEU MODEL
'App-Token': GLPI_APP_TOKEN, const pendingTickets = await TicketSnModel.getPendingTickets();
'Authorization': `Bearer ${GLPI_USER_TOKEN}`
if (pendingTickets.length === 0) {
logInfo('✅ Nenhum ticket pendente para sincronizar');
return;
} }
});
return response.data; 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) { } catch (error) {
throw new Error(`Error creating ticket in GLPI: ${error.message}`); 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
}; };
// Function to update a ticket in GLPI // 3. Inserir no GLPI (O MODEL faz INSERT e mapeamento)
const updateTicketInGLPI = async (ticketId, ticketData) => { const glpiTicketId = await TicketGlpiModel.createTicket(glpiData);
try {
const response = await axios.put(`${GLPI_URL}/ticket/${ticketId}`, ticketData, { // 4. Atualizar status no PostgreSQL usando SEU MODEL
headers: { await TicketSnModel.updateSyncStatus(ticket.id, 'completed', glpiTicketId);
'Content-Type': 'application/json',
'App-Token': GLPI_APP_TOKEN, logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`);
'Authorization': `Bearer ${GLPI_USER_TOKEN}`
}
});
return response.data;
} catch (error) {
throw new Error(`Error updating ticket in GLPI: ${error.message}`);
}
};
// Function to fetch ticket details from GLPI
const getTicketFromGLPI = async (ticketId) => {
try {
const response = await axios.get(`${GLPI_URL}/ticket/${ticketId}`, {
headers: {
'App-Token': GLPI_APP_TOKEN,
'Authorization': `Bearer ${GLPI_USER_TOKEN}`
}
});
return response.data;
} catch (error) { } catch (error) {
throw new Error(`Error fetching ticket from GLPI: ${error.message}`); logError(error, `❌ Erro no ticket ${ticket.ticket_number}`);
await TicketSnModel.updateSyncStatus(ticket.id, 'error');
} }
}
}
const syncTicketsToGlpi = async () => {
await GlpiService.syncTicketsToGlpi();
}; };
module.exports = { module.exports = {
createTicketInGLPI, syncTicketsToGlpi,
updateTicketInGLPI, GlpiService
getTicketFromGLPI
}; };