diff --git a/.env b/.env index c82f5b2..ed86ef2 100644 --- a/.env +++ b/.env @@ -8,20 +8,15 @@ SERVICENOW_REQUEST_URL="https://caoadev.service-now.com/api/now/table/sc_req_ite SERVICENOW_USERNAME=SOTHIS.CAOA 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_DB_TYPE=mysql # ou postgresql -GLPI_DB_HOST=localhost +GLPI_DB_HOST=177.73.177.32 GLPI_DB_PORT=3306 -GLPI_DB_USER=glpi_user -GLPI_DB_PASSWORD=your_glpi_db_password -GLPI_DB_NAME=glpi_database +GLPI_DB_USER=snglpi +GLPI_DB_PASSWORD=j2633669 +GLPI_DB_NAME=glpi_data GLPI_DB_CHARSET=utf8mb4 -GLPI_TABLE_PREFIX=glpi_ # deixe em branco se não houver prefixo + # Database Configuration DB_HOST=localhost diff --git a/package-lock.json b/package-lock.json index 3acf301..480db6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "axios": "^0.21.4", "dotenv": "^10.0.0", "express": "^5.1.0", + "mysql2": "^3.14.5", "pg": "^8.7.1", "winston": "^3.17.0" } @@ -61,6 +62,15 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "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": { "version": "0.21.4", "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -445,6 +464,15 @@ "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": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -582,6 +610,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -617,6 +651,36 @@ "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -674,6 +738,54 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1011,6 +1123,11 @@ "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": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -1122,6 +1239,15 @@ "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": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", diff --git a/package.json b/package.json index 5b9b9cf..c635be4 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "axios": "^0.21.4", "dotenv": "^10.0.0", "express": "^5.1.0", + "mysql2": "^3.14.5", "pg": "^8.7.1", "winston": "^3.17.0" }, diff --git a/src/app.js b/src/app.js index 0b7b93d..df49403 100644 --- a/src/app.js +++ b/src/app.js @@ -1,10 +1,21 @@ +// src/app.js const { snServices } = require('./controllers/servicenowController'); +const { glpiServices } = require('./controllers/glpiController'); // NOVO const { logInfo } = require('./utils/logger'); -logInfo('Aplicação iniciada', { +logInfo('Aplicação iniciada', { timestamp: new Date().toISOString(), environment: process.env.NODE_ENV || 'development' }); +async function main() { + try { + await snServices(); + //await glpiServices(); + + } catch (error) { + logInfo('Erro na aplicação:', error); + } +} -snServices(); \ No newline at end of file +main(); \ No newline at end of file diff --git a/src/controllers/glpiController.js b/src/controllers/glpiController.js index e69de29..546d0e5 100644 --- a/src/controllers/glpiController.js +++ b/src/controllers/glpiController.js @@ -0,0 +1,10 @@ +// src/controllers/glpiController.js +const { syncTicketsToGlpi } = require('../services/glpiService'); + +const glpiServices = async () => { + await syncTicketsToGlpi(); +} + +module.exports = { + glpiServices +} diff --git a/src/data/glpiDataBase.js b/src/data/glpiDataBase.js index 644769c..0f86927 100644 --- a/src/data/glpiDataBase.js +++ b/src/data/glpiDataBase.js @@ -4,14 +4,11 @@ const mysql = require('mysql2/promise'); const { logInfo, logError, logWarning } = require('../utils/logger'); const glpiPool = mysql.createPool({ - host: '177.73.177.32', - port: 3306, + host: process.env.GLPI_DB_HOST, + port: process.env.GLPI_DB_PORT, database: process.env.GLPI_DB_NAME, - user: 'snglpi', - password: 'j2633669', - connectionLimit: 10, - acquireTimeout: 60000, - timeout: 60000 + user: process.env.GLPI_DB_USER, + password: process.env.GLPI_DB_PASSWORD, }); // Testar conexão diff --git a/src/models/ticketGlpiModel.js b/src/models/ticketGlpiModel.js index b300602..1c1c468 100644 --- a/src/models/ticketGlpiModel.js +++ b/src/models/ticketGlpiModel.js @@ -3,6 +3,7 @@ const glpiPool = require('../data/glpiDataBase'); const { logInfo, logError } = require('../utils/logger'); class TicketGlpiModel { + // Mapeamento de status ServiceNow → GLPI static mapStatus(snState) { const statusMap = { @@ -38,74 +39,76 @@ class TicketGlpiModel { return categoryMap[shortDescription] || 5717; } + // Criar ticket no GLPI - static async createTicket(snTicket, mapping) { - const connection = await glpiPool.getConnection(); - - try { - await connection.beginTransaction(); +// src/models/ticketGlpiModel.js - // 1. Preparar dados para o GLPI - const glpiTicketData = { - entities_id: mapping.entityId || 1371, - name: snTicket.short_description?.value || 'Sem título', - date: snTicket.sys_created_on?.value ? new Date(snTicket.sys_created_on.value) : new Date(), - date_mod: snTicket.sys_updated_on?.value ? new Date(snTicket.sys_updated_on.value) : new Date(), - status: this.mapStatus(snTicket.state?.value), - users_id_recipient: mapping.userId || 917, - requesttypes_id: 1, - content: snTicket.description?.value || '', - urgency: this.mapUrgency(snTicket.urgency?.value), - impact: this.mapImpact(snTicket.impact?.value), - priority: this.mapPriority(snTicket.priority?.value), - type: 1, // Incidente - itilcategories_id: this.mapCategory(snTicket.short_description?.value), - global_validation: 1, - date_creation: snTicket.sys_created_on?.value ? new Date(snTicket.sys_created_on.value) : new Date() - }; +static async createTicket(ticketData) { +const connection = await glpiPool.getConnection(); - // 2. Inserir ticket principal - const ticketQuery = ` - INSERT INTO glpi_tickets SET ? - `; +try { + await connection.beginTransaction(); - const [ticketResult] = await connection.execute(ticketQuery, [glpiTicketData]); - const ticketId = ticketResult.insertId; + // 1. Preparar dados para o GLPI (usando valores fixos para teste) + const glpiTicketData = { + entities_id: 1371, // Valor fixo para teste + name: ticketData.short_description || 'Sem título', + date: ticketData.opened_at || new Date(), + date_mod: ticketData.updated_at || new Date(), + status: TicketGlpiModel.mapStatus(ticketData.state || '1'), + users_id_recipient: 917, // Valor fixo para teste + requesttypes_id: 1, + content: ticketData.description || '', + urgency: 3, // Valor fixo + impact: 3, // Valor fixo + priority: TicketGlpiModel.mapPriority(ticketData.priority || '3'), + type: 1, // Incidente + itilcategories_id: TicketGlpiModel.mapCategory(ticketData.short_description), + global_validation: 1, + date_creation: ticketData.opened_at || new Date() + }; - // 3. Adicionar requerente - const requesterQuery = ` - INSERT INTO glpi_tickets_users + await connection.execute(requesterQuery, [ticketId, 917]); // Default user + // Construir a query manualmente + const fields = Object.keys(glpiTicketData).join(', '); + const placeholders = Object.keys(glpiTicketData).map(() => '?').join(', '); + const values = Object.values(glpiTicketData); + + const ticketQuery = `INSERT INTO glpi_tickets (${fields}) VALUES (${placeholders})`; + + // Inserir ticket principal + const [ticketResult] = await connection.execute(ticketQuery, values); + const ticketId = ticketResult.insertId; + + // Adicionar requerente + await connection.execute( + `INSERT INTO glpi_tickets_users (tickets_id, users_id, type, use_notification, alternative_email) - VALUES (?, ?, 1, 1, '') - `; - await connection.execute(requesterQuery, [ticketId, mapping.userId || 917]); + VALUES (?, ?, 1, 1, '')`, + [ticketId, 917] + ); - // 4. Adicionar grupo - const groupQuery = ` - INSERT INTO glpi_groups_tickets + // Adicionar grupo + await connection.execute( + `INSERT INTO glpi_groups_tickets (tickets_id, groups_id, type) - VALUES (?, ?, 2) - `; - await connection.execute(groupQuery, [ticketId, mapping.groupId || 30]); + VALUES (?, ?, 2)`, + [ticketId, 30] + ); - await connection.commit(); + await connection.commit(); - logInfo(`✅ Ticket GLPI criado: ${ticketId}`, { - glpi_ticket_id: ticketId, - sn_ticket_number: snTicket.number?.value, - location: snTicket.location?.display_value - }); + logInfo(`✅ Ticket GLPI criado: ${ticketId}`); + return ticketId; - return ticketId; - - } catch (error) { - await connection.rollback(); - logError(error, `❌ Erro ao criar ticket GLPI para ${snTicket.number?.value}`); - throw error; - } finally { - connection.release(); - } + } catch (error) { + await connection.rollback(); + logError(error, `❌ Erro ao criar ticket GLPI`); + throw error; + } finally { + connection.release(); } +} // Verificar se ticket já existe no GLPI static async ticketExists(snTicketNumber) { diff --git a/src/models/ticketSnModel.js b/src/models/ticketSnModel.js index 99da7cc..6fb183e 100644 --- a/src/models/ticketSnModel.js +++ b/src/models/ticketSnModel.js @@ -1,3 +1,4 @@ +// src/models/ticketSnModel.js const pool = require('../data/database'); const { logInfo, logError, logWarning } = require('../utils/logger'); @@ -6,62 +7,175 @@ class TicketSnModel { static async saveTicket(ticketData, typeTicket) { const query = ` 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) - 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) DO UPDATE SET short_description = EXCLUDED.short_description, + status = EXCLUDED.status, description = EXCLUDED.description, - location_id = EXCLUDED.location_id, - location_name = EXCLUDED.location_name, caller_id = EXCLUDED.caller_id, caller_email = EXCLUDED.caller_email, + location_id = EXCLUDED.location_id, + location_name = EXCLUDED.location_name, ramal = EXCLUDED.ramal, telefone = EXCLUDED.telefone, + opened_at = EXCLUDED.opened_at, + tipo = EXCLUDED.tipo, updated_at = NOW() RETURNING id, xmax = 0 as inserted`; const values = [ - ticketData.number?.value || null, - ticketData.short_description?.value || null, - ticketData.description?.value || null, - ticketData.opened_by?.display_value || null, - ticketData['opened_by.email']?.value || null, - ticketData.location?.value || null, // location_id (value) - ticketData.location?.display_value || null, // location_name (display_value) - ticketData['variables.numero_ramal']?.value || null, - ticketData['variables.telephone_favorecido_rh']?.value || null, - ticketData.opened_at?.value ? new Date(ticketData.opened_at.value) : null, - typeTicket + ticketData.number?.value || null, // $1 - ticket_number + ticketData.short_description?.value || null, // $2 - short_description + ticketData.state?.display_value || null, // $3 - status + ticketData.description?.value || null, // $4 - description + ticketData.opened_by?.display_value || null, // $5 - caller_id + ticketData['opened_by.email']?.value || null, // $6 - caller_email + ticketData.location?.value || null, // $7 - location_id + ticketData.location?.display_value || null, // $8 - location_name + ticketData['variables.numero_ramal']?.value || null, // $9 - ramal + ticketData['variables.telephone_favorecido_rh']?.value || null, // $10 - telefone + ticketData.opened_at?.value ? new Date(ticketData.opened_at.value) : null, // $11 - opened_at + typeTicket // $12 - tipo ]; - - try { - const result = await pool.query(query, values); - const wasInserted = result.rows[0].inserted; // true = INSERT, false = UPDATE - - if (wasInserted) { - logInfo(`✅ NOVO ticket salvo! ID: ${result.rows[0].id}`, { - ticket_id: result.rows[0].id, - ticket_number: values[0], - action: 'insert' - }); - } else { - logInfo(`🔄 Ticket atualizado! ID: ${result.rows[0].id}`, { - ticket_id: result.rows[0].id, - ticket_number: values[0], - action: 'update' - }); + try { + const result = await pool.query(query, values); + const wasInserted = result.rows[0].inserted; // true = INSERT, false = UPDATE + + if (wasInserted) { + logInfo(`✅ NOVO ticket salvo! ID: ${result.rows[0].id}`, { + ticket_id: result.rows[0].id, + ticket_number: values[0], + action: 'insert' + }); + } else { + logInfo(`🔄 Ticket atualizado! ID: ${result.rows[0].id}`, { + ticket_id: result.rows[0].id, + ticket_number: values[0], + action: 'update' + }); + } + + return result.rows[0].id; + } catch (error) { + logError(error, `❌ Erro ao salvar ticket ${values[0]}`); + throw error; + } + } + + // 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 []; } - - return result.rows[0].id; - } catch (error) { - logError(error, `❌ Erro ao salvar ticket ${values[0]}`); - throw error; } } - -} module.exports = TicketSnModel; \ No newline at end of file diff --git a/src/scripts/database/01_create_tables.sql b/src/scripts/database/01_create_tables.sql index df40620..796b318 100644 --- a/src/scripts/database/01_create_tables.sql +++ b/src/scripts/database/01_create_tables.sql @@ -3,6 +3,7 @@ CREATE TABLE tickets_sn ( ticket_number VARCHAR(32) NOT NULL UNIQUE, -- Número do ticket (INC0140378) - Ú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 @@ -12,9 +13,14 @@ CREATE TABLE tickets_sn ( 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 + 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 LIMIT 3; + diff --git a/src/services/glpiService.js b/src/services/glpiService.js index ca7cac3..f5a7fef 100644 --- a/src/services/glpiService.js +++ b/src/services/glpiService.js @@ -1,57 +1,92 @@ // 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'); -const { GLPI_URL, GLPI_APP_TOKEN, GLPI_USER_TOKEN } = process.env; - -// Function to create a ticket in GLPI -const createTicketInGLPI = async (ticketData) => { - try { - const response = await axios.post(`${GLPI_URL}/ticket`, ticketData, { - headers: { - 'Content-Type': 'application/json', - 'App-Token': GLPI_APP_TOKEN, - 'Authorization': `Bearer ${GLPI_USER_TOKEN}` +class GlpiService { + + // Método principal que o controller vai chamar + static async syncTicketsToGlpi() { + try { + logInfo('🔄 Iniciando sincronização para GLPI...'); + + // 1. Buscar tickets pendentes usando SEU MODEL + const pendingTickets = await TicketSnModel.getPendingTickets(); + + if (pendingTickets.length === 0) { + logInfo('✅ Nenhum ticket pendente para sincronizar'); + return; } - }); - return response.data; - } catch (error) { - throw new Error(`Error creating ticket in GLPI: ${error.message}`); - } -}; - -// Function to update a ticket in GLPI -const updateTicketInGLPI = async (ticketId, ticketData) => { - try { - const response = await axios.put(`${GLPI_URL}/ticket/${ticketId}`, ticketData, { - headers: { - 'Content-Type': 'application/json', - 'App-Token': GLPI_APP_TOKEN, - 'Authorization': `Bearer ${GLPI_USER_TOKEN}` + + logInfo(`📋 Encontrados ${pendingTickets.length} tickets para sincronizar`); + + // 2. Processar cada ticket + for (const ticket of pendingTickets) { + await this.processSingleTicket(ticket); } - }); - return response.data; - } catch (error) { - throw new Error(`Error updating ticket in GLPI: ${error.message}`); + + logInfo('🎉 Sincronização com GLPI concluída!'); + + } catch (error) { + logError(error, '❌ Erro na sincronização GLPI'); + throw error; + } } -}; - -// 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}` + + // 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; } - }); - return response.data; - } catch (error) { - throw new Error(`Error fetching ticket from GLPI: ${error.message}`); + + // 2. Converter dados para formato GLPI (O MODEL vai fazer o mapeamento) + const glpiData = { + // Dados básicos do PostgreSQL + ticket_number: ticket.ticket_number, + short_description: ticket.short_description, + description: ticket.description, + + // Dados do ServiceNow originais (se necessário para mapeamento) + state: ticket.status, // Já vem do ServiceNow + urgency: '2', // Default - O MODEL vai mapear + impact: '2', // Default - O MODEL vai mapear + priority: '3', // Default - O MODEL vai mapear + + // Localização para de/para + location_name: ticket.location_name, + + // Datas + opened_at: ticket.opened_at, + updated_at: ticket.updated_at + }; + + // 3. Inserir no GLPI (O MODEL faz INSERT e mapeamento) + const glpiTicketId = await TicketGlpiModel.createTicket(glpiData); + + // 4. Atualizar status no PostgreSQL usando SEU MODEL + await TicketSnModel.updateSyncStatus(ticket.id, 'completed', glpiTicketId); + + logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`); + + } catch (error) { + logError(error, `❌ Erro no ticket ${ticket.ticket_number}`); + await TicketSnModel.updateSyncStatus(ticket.id, 'error'); + } } +} + +const syncTicketsToGlpi = async () => { + await GlpiService.syncTicketsToGlpi(); }; module.exports = { - createTicketInGLPI, - updateTicketInGLPI, - getTicketFromGLPI + syncTicketsToGlpi, + GlpiService }; \ No newline at end of file