2025-09-05 20:28:15 -03:00
|
|
|
const pool = require('../data/database');
|
2026-02-19 18:22:38 -03:00
|
|
|
const { logInfo, logError } = require('../utils/logger');
|
2025-08-30 19:04:35 -03:00
|
|
|
|
2025-09-05 20:28:15 -03:00
|
|
|
class TicketSnModel {
|
2025-09-15 20:55:19 -03:00
|
|
|
|
2025-09-05 20:28:15 -03:00
|
|
|
static async saveTicket(ticketData, typeTicket) {
|
2025-09-08 20:35:49 -03:00
|
|
|
const query = `
|
2025-09-05 20:28:15 -03:00
|
|
|
INSERT INTO tickets_sn
|
2025-09-15 20:55:19 -03:00
|
|
|
(ticket_number, sys_id, short_description, status, description, caller_id, caller_email,
|
2026-02-19 18:22:38 -03:00
|
|
|
location_id, location_name, ramal, telefone, justificativa, opened_at, tipo)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
2025-09-05 20:28:15 -03:00
|
|
|
ON CONFLICT (ticket_number)
|
|
|
|
|
DO UPDATE SET
|
2025-09-15 20:55:19 -03:00
|
|
|
sys_id = EXCLUDED.sys_id,
|
2025-09-05 20:28:15 -03:00
|
|
|
short_description = EXCLUDED.short_description,
|
2025-09-11 20:57:43 -03:00
|
|
|
status = EXCLUDED.status,
|
2025-09-05 20:28:15 -03:00
|
|
|
description = EXCLUDED.description,
|
2025-09-08 20:35:49 -03:00
|
|
|
caller_id = EXCLUDED.caller_id,
|
2025-09-05 20:28:15 -03:00
|
|
|
caller_email = EXCLUDED.caller_email,
|
2025-09-11 20:57:43 -03:00
|
|
|
location_id = EXCLUDED.location_id,
|
|
|
|
|
location_name = EXCLUDED.location_name,
|
2025-09-05 20:28:15 -03:00
|
|
|
ramal = EXCLUDED.ramal,
|
|
|
|
|
telefone = EXCLUDED.telefone,
|
2026-02-19 18:22:38 -03:00
|
|
|
justificativa = EXCLUDED.justificativa,
|
2025-09-11 20:57:43 -03:00
|
|
|
opened_at = EXCLUDED.opened_at,
|
2025-10-13 06:06:52 -03:00
|
|
|
tipo = EXCLUDED.tipo
|
2025-10-29 14:20:21 -03:00
|
|
|
RETURNING id, (xmax = 0) AS "wasInserted"`;
|
2025-08-30 19:04:35 -03:00
|
|
|
|
2025-10-24 13:13:08 -03:00
|
|
|
const ticketNumber = typeof ticketData.number === 'object' ? ticketData.number?.value : ticketData.number;
|
2026-02-19 18:22:38 -03:00
|
|
|
let callerName, callerEmail, ramal, telefone, justificativa, sysId, shortDescription, state, description, locationId, locationName, openedAt;
|
2025-10-28 14:06:00 -03:00
|
|
|
|
|
|
|
|
|
2025-10-24 13:13:08 -03:00
|
|
|
if (typeTicket === 'incidente') {
|
|
|
|
|
callerName = ticketData.caller_id?.display_value;
|
|
|
|
|
const emailObject = ticketData['caller_id.email'] || {};
|
|
|
|
|
callerEmail = (typeof emailObject === 'object' ? emailObject.value : emailObject) || null;
|
|
|
|
|
ramal = null; // Incidentes não têm essa variável por padrão
|
2025-10-28 14:06:00 -03:00
|
|
|
telefone = ticketData['caller_id.phone'] || ticketData['caller_id.mobile_phone'] || null; // Incidentes não têm essa variável por padrão
|
|
|
|
|
sysId = ticketData.sys_id || null;
|
|
|
|
|
shortDescription = ticketData.short_description || null;
|
|
|
|
|
state = ticketData.state || null;
|
|
|
|
|
description = ticketData.description || null;
|
2026-02-19 18:22:38 -03:00
|
|
|
justificativa = null;
|
2025-10-28 14:06:00 -03:00
|
|
|
const locationLink = ticketData.location?.link;
|
|
|
|
|
locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null;
|
2025-10-24 13:13:08 -03:00
|
|
|
locationName = ticketData.location?.display_value || null;
|
2025-10-28 14:06:00 -03:00
|
|
|
openedAt = ticketData.opened_at || null;
|
2025-10-24 13:13:08 -03:00
|
|
|
} else { // 'requisicao'
|
|
|
|
|
callerName = ticketData.opened_by?.display_value || null;
|
|
|
|
|
callerEmail = ticketData['opened_by.email'] || null;
|
|
|
|
|
ramal = ticketData['variables.numero_ramal'] || null;
|
2026-02-19 18:22:38 -03:00
|
|
|
telefone = ticketData['variables.telephone_favorecido_rh'] || ticketData['variables.telephone_favorecido_ti'] || ticketData.telefone || null;
|
2025-10-24 13:13:08 -03:00
|
|
|
sysId = ticketData.sys_id || null;
|
|
|
|
|
shortDescription = ticketData.short_description || null;
|
|
|
|
|
state = ticketData.state || null;
|
|
|
|
|
description = ticketData.description || null;
|
2026-02-19 18:22:38 -03:00
|
|
|
justificativa = ticketData.justificativa || null;
|
2025-10-24 13:13:08 -03:00
|
|
|
const locationLink = ticketData.location?.link;
|
|
|
|
|
locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null;
|
|
|
|
|
locationName = ticketData.location?.display_value || null;
|
|
|
|
|
openedAt = ticketData.opened_at || null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-15 20:55:19 -03:00
|
|
|
const values = [
|
2025-10-24 13:13:08 -03:00
|
|
|
ticketNumber,
|
|
|
|
|
sysId,
|
|
|
|
|
shortDescription,
|
|
|
|
|
state,
|
|
|
|
|
description,
|
|
|
|
|
callerName,
|
|
|
|
|
callerEmail,
|
|
|
|
|
locationId,
|
|
|
|
|
locationName,
|
|
|
|
|
ramal,
|
|
|
|
|
telefone,
|
2026-02-19 18:22:38 -03:00
|
|
|
justificativa,
|
2025-10-24 13:13:08 -03:00
|
|
|
openedAt
|
|
|
|
|
? (typeof openedAt === 'string' && openedAt.includes('/')
|
|
|
|
|
? new Date(openedAt.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$2-$1'))
|
|
|
|
|
: new Date(openedAt))
|
|
|
|
|
: null,
|
2025-09-15 20:55:19 -03:00
|
|
|
typeTicket
|
2025-09-05 20:28:15 -03:00
|
|
|
];
|
2025-09-15 20:55:19 -03:00
|
|
|
|
2025-09-08 20:35:49 -03:00
|
|
|
|
2025-09-11 20:57:43 -03:00
|
|
|
try {
|
|
|
|
|
const result = await pool.query(query, values);
|
2025-10-29 14:20:21 -03:00
|
|
|
const wasInserted = result.rows[0].wasInserted;
|
2025-09-11 20:57:43 -03:00
|
|
|
|
|
|
|
|
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'
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-15 20:55:19 -03:00
|
|
|
|
2025-10-23 17:30:03 -03:00
|
|
|
return { id: result.rows[0].id, wasInserted };
|
2025-09-11 20:57:43 -03:00
|
|
|
} catch (error) {
|
|
|
|
|
logError(error, `❌ Erro ao salvar ticket ${values[0]}`);
|
2025-09-15 20:55:19 -03:00
|
|
|
throw error;
|
2025-09-11 20:57:43 -03:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-30 19:04:35 -03:00
|
|
|
|
2025-09-15 20:55:19 -03:00
|
|
|
static async getPendingTickets() {
|
2025-09-11 20:57:43 -03:00
|
|
|
try {
|
|
|
|
|
const query = `
|
|
|
|
|
SELECT
|
2026-03-04 16:37:04 -03:00
|
|
|
tsync.id AS ticket_sync_id,
|
2025-09-17 20:56:32 -03:00
|
|
|
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,
|
2026-02-19 18:22:38 -03:00
|
|
|
ts.justificativa,
|
2025-09-17 20:56:32 -03:00
|
|
|
ts.opened_at,
|
|
|
|
|
ts.tipo,
|
|
|
|
|
ts.created_at,
|
2025-10-17 16:58:14 -03:00
|
|
|
ts.updated_at,
|
|
|
|
|
tsync.glpi_ticket_id
|
2025-09-17 20:56:32 -03:00
|
|
|
FROM tickets_sn ts
|
|
|
|
|
LEFT JOIN ticket_sync tsync ON ts.id = tsync.sn_ticket_id
|
2025-10-13 06:06:52 -03:00
|
|
|
WHERE tsync.glpi_sync_status = 'pending_check'
|
2025-09-17 20:56:32 -03:00
|
|
|
ORDER BY ts.opened_at
|
2025-09-11 20:57:43 -03:00
|
|
|
`;
|
2025-09-15 20:55:19 -03:00
|
|
|
|
|
|
|
|
const result = await pool.query(query);
|
2025-09-11 20:57:43 -03:00
|
|
|
logInfo(`📋 Tickets pendentes encontrados: ${result.rows.length}`);
|
|
|
|
|
return result.rows;
|
2025-09-15 20:55:19 -03:00
|
|
|
|
2025-09-11 20:57:43 -03:00
|
|
|
} catch (error) {
|
|
|
|
|
logError(error, '❌ Erro ao buscar tickets pendentes');
|
|
|
|
|
return [];
|
2025-09-05 20:28:15 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-17 20:56:32 -03:00
|
|
|
static async findByTicketNumber(ticketNumber) {
|
2025-09-11 20:57:43 -03:00
|
|
|
try {
|
|
|
|
|
const query = `
|
2025-09-17 20:56:32 -03:00
|
|
|
SELECT * FROM tickets_sn
|
|
|
|
|
WHERE ticket_number = $1
|
2025-09-11 20:57:43 -03:00
|
|
|
`;
|
2025-09-15 20:55:19 -03:00
|
|
|
|
2025-09-17 20:56:32 -03:00
|
|
|
const result = await pool.query(query, [ticketNumber]);
|
|
|
|
|
return result.rows[0] || null;
|
2025-09-15 20:55:19 -03:00
|
|
|
|
2025-09-11 20:57:43 -03:00
|
|
|
} catch (error) {
|
2025-09-17 20:56:32 -03:00
|
|
|
logError(error, `❌ Erro ao buscar ticket por número: ${ticketNumber}`);
|
|
|
|
|
return null;
|
2025-09-11 20:57:43 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-23 17:30:03 -03:00
|
|
|
static async getStatus(ticketId) {
|
|
|
|
|
try {
|
|
|
|
|
const query = `
|
|
|
|
|
SELECT status,tipo FROM tickets_sn
|
|
|
|
|
WHERE id = $1`;
|
|
|
|
|
const result = await pool.query(query, [ticketId]);
|
|
|
|
|
return result.rows || null;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logError(error, `❌ Erro ao buscar status do ticket: ${ticketId}`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-17 20:56:32 -03:00
|
|
|
static async findById(ticketId) {
|
|
|
|
|
try {
|
|
|
|
|
const query = `
|
|
|
|
|
SELECT * FROM tickets_sn
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, [ticketId]);
|
|
|
|
|
return result.rows[0] || null;
|
2025-09-05 20:28:15 -03:00
|
|
|
|
2025-09-17 20:56:32 -03:00
|
|
|
} catch (error) {
|
|
|
|
|
logError(error, `❌ Erro ao buscar ticket por ID: ${ticketId}`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async updateTicket(ticketId, updateData) {
|
2025-09-11 20:57:43 -03:00
|
|
|
try {
|
2025-09-17 20:56:32 -03:00
|
|
|
const fields = [];
|
|
|
|
|
const values = [];
|
|
|
|
|
let paramCount = 1;
|
|
|
|
|
|
|
|
|
|
for (const [key, value] of Object.entries(updateData)) {
|
|
|
|
|
fields.push(`${key} = $${paramCount}`);
|
|
|
|
|
values.push(value);
|
|
|
|
|
paramCount++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fields.push(`updated_at = NOW()`);
|
|
|
|
|
|
|
|
|
|
values.push(ticketId);
|
|
|
|
|
|
2025-09-11 20:57:43 -03:00
|
|
|
const query = `
|
2025-09-17 20:56:32 -03:00
|
|
|
UPDATE tickets_sn
|
|
|
|
|
SET ${fields.join(', ')}
|
|
|
|
|
WHERE id = $${paramCount}
|
|
|
|
|
RETURNING *
|
2025-09-11 20:57:43 -03:00
|
|
|
`;
|
2025-09-15 20:55:19 -03:00
|
|
|
|
2025-09-17 20:56:32 -03:00
|
|
|
const result = await pool.query(query, values);
|
|
|
|
|
|
|
|
|
|
if (result.rows.length > 0) {
|
|
|
|
|
logInfo(`✅ Ticket atualizado: ${ticketId}`);
|
|
|
|
|
return result.rows[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
2025-09-15 20:55:19 -03:00
|
|
|
|
2025-09-11 20:57:43 -03:00
|
|
|
} catch (error) {
|
2025-09-17 20:56:32 -03:00
|
|
|
logError(error, `❌ Erro ao atualizar ticket ${ticketId}`);
|
|
|
|
|
throw error;
|
2025-09-11 20:57:43 -03:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-23 21:01:01 -03:00
|
|
|
|
|
|
|
|
|
2025-09-05 20:28:15 -03:00
|
|
|
}
|
2025-09-11 20:57:43 -03:00
|
|
|
|
2025-09-15 20:55:19 -03:00
|
|
|
/**
|
|
|
|
|
* @module TicketSnModel
|
2025-10-29 14:20:21 -03:00
|
|
|
* @description Este módulo serve como a camada de acesso a dados para a tabela `tickets_sn`, que armazena uma cópia local dos dados dos tickets do ServiceNow.
|
|
|
|
|
* Manter uma cópia local otimiza as consultas e reduz a dependência de chamadas de API repetitivas ao ServiceNow.
|
2025-09-15 20:55:19 -03:00
|
|
|
*
|
2025-10-29 14:20:21 -03:00
|
|
|
* Principais Funcionalidades:
|
|
|
|
|
* - `saveTicket(ticketData, typeTicket)`: Insere um novo ticket ou atualiza um existente (UPSERT) na tabela `tickets_sn`. Ele mapeia de forma inteligente os campos de incidentes e requisições, que possuem estruturas diferentes na API do ServiceNow. Retorna se o registro foi inserido ou atualizado.
|
|
|
|
|
* - `getPendingTickets()`: Busca todos os tickets que estão no estado 'pending_check', indicando que foram coletados do ServiceNow mas ainda não foram processados para criação no GLPI.
|
|
|
|
|
* - Funções de Busca (`findByTicketNumber`, `findById`, `getStatus`): Métodos para recuperar registros de tickets ou seus status com base em diferentes identificadores.
|
|
|
|
|
* - `updateTicket(ticketId, updateData)`: Atualiza dinamicamente os campos de um registro de ticket existente.
|
2025-09-15 20:55:19 -03:00
|
|
|
*/
|
2026-02-19 18:22:38 -03:00
|
|
|
module.exports = TicketSnModel;
|