diff --git a/.env.development b/.env.development index 95977cf..f5dfbcc 100644 --- a/.env.development +++ b/.env.development @@ -39,6 +39,8 @@ GLPI_DB_CHARSET=utf8mb4 # ID do usuario padrao do GLPI para criacao de tickets e comentarios GLPI_DEFAULT_USER_ID=11111 +GLPI_REQ_DEFAULT_JUSTIFICATIVA=Chamado aberto por colaborador CAOA sem Justificativa +GLPI_REQ_DEFAULT_DESCRIPTION=Chamado aberto por colaborador CAOA sem Descricao # Banco intermediario (PostgreSQL) SNGLPI_DB_HOST=10.0.120.75 diff --git a/.env.example b/.env.example index 35abf9e..10f7f06 100644 --- a/.env.example +++ b/.env.example @@ -44,6 +44,8 @@ GLPI_DB_CHARSET=utf8mb4 # ID do usuario padrao do GLPI para criacao de tickets e comentarios GLPI_DEFAULT_USER_ID=1118 +GLPI_REQ_DEFAULT_JUSTIFICATIVA=Chamado aberto por colaborador CAOA sem Justificativa +GLPI_REQ_DEFAULT_DESCRIPTION=Chamado aberto por colaborador CAOA sem Descricao # --- Configuracao do Banco de Dados Intermediario (PostgreSQL) --- SNGLPI_DB_HOST=localhost diff --git a/src/controllers/processSyncController.js b/src/controllers/processSyncController.js index 0e28a9f..1378393 100644 --- a/src/controllers/processSyncController.js +++ b/src/controllers/processSyncController.js @@ -30,28 +30,28 @@ const processAndSaveTickets = async (tickets, type) => { let latestUpdateTimestamp = new Date(0); for (const ticket of tickets) { try { - if (type === 'requisicao' && (!ticket.description || ticket.description.trim() === '')) { - logInfo(`Descricao vazia para requisicao ${ticket.number}. Buscando em sc_item_option...`); + if (type === 'requisicao') { + const ticketNumber = typeof ticket.number === 'object' ? ticket.number?.value : ticket.number; + logInfo(`Buscando variaveis de catalogo para requisicao ${ticketNumber}.`); const extraData = await fetchScItemOptionValue(ticket.sys_id, 'justificativa'); const vars = {}; - (extraData || []).forEach(item => { - const question = item['sc_item_option.item_option_new.question_text']; - const value = item['sc_item_option.value']; - if (question === 'Select what you want') vars.whatYouWant = value; - if (question === 'Please justify the need') vars.justification = value; - if (question === 'Telephone') vars.telephone = value; + (extraData || []).forEach((item) => { + const question = item.question_text; + const name = item.variable_name; + const value = item.value; + + if (name === 'select_what_you_want' || question === 'Select what you want') vars.whatYouWant = value; + if (name === 'please_justify_the_need' || question === 'Please justify the need') vars.justification = value; + if (name === 'telephone_favorecido_ti' || question === 'Telephone') vars.telephone = value; }); - - if (vars.justification) { - ticket.description = vars.justification; - logInfo(`Descricao da requisicao ${ticket.number} atualizada com valor de sc_item_option.`); - } - if (vars.telephone){ + ticket.justificativa = vars.justification || null; + if (vars.telephone) { ticket.telefone = vars.telephone; - logInfo(`Telefone da requisicao ${ticket.number} atualizado com valor de sc_item_option.`); } + + logInfo(`Variaveis da requisicao ${ticketNumber}: justificativa=${ticket.justificativa ? 'ok' : 'vazia'}, telefone=${ticket.telefone ? 'ok' : 'vazio'}.`); } const { id: idTableTicketSn, wasInserted } = await TicketSnModel.saveTicket(ticket, type); @@ -148,4 +148,4 @@ module.exports = { * - Cria ou atualiza o registro de controle na tabela `ticket_sync`, definindo o status inicial e a origem da atualizacao (`source_last`). * - Implementa uma logica de enriquecimento de dados para requisicoes com descricao vazia, buscando informacoes de variaveis de catalogo (`sc_item_option`). * - Ao final, atualiza a marca d'agua para a proxima execucao, apenas se houverem tickets atualizados na execucao vigente. - */ \ No newline at end of file + */ diff --git a/src/models/ticketGlpiModel.js b/src/models/ticketGlpiModel.js index e11a181..6d1d1df 100644 --- a/src/models/ticketGlpiModel.js +++ b/src/models/ticketGlpiModel.js @@ -4,6 +4,8 @@ const LocationMappingModel = require('./locationMappingModel'); // Carrega o ID do usuário padrão do GLPI a partir das variáveis de ambiente const GLPI_DEFAULT_USER_ID = parseInt(process.env.GLPI_DEFAULT_USER_ID, 10) || 1118; +const GLPI_REQ_DEFAULT_JUSTIFICATIVA = process.env.GLPI_REQ_DEFAULT_JUSTIFICATIVA || 'Chamado aberto por colaborador CAOA sem Justificativa'; +const GLPI_REQ_DEFAULT_DESCRIPTION = process.env.GLPI_REQ_DEFAULT_DESCRIPTION || 'Chamado aberto por colaborador CAOA sem Descricao'; class TicketGlpiModel { @@ -118,9 +120,31 @@ class TicketGlpiModel { return extractInfo(/(\d{3,4})/i, description); }; - const email = ticketData.caller_email || extractEmailFromDescription(ticketData.description) || 'N/A'; - const phone = ticketData.caller_phone || extractPhoneFromDescription(ticketData.description) || 'N/A'; - const ramal = ticketData.caller_ramal || extractRamalFromDescription(ticketData.description) || 'N/A'; + const rawDescription = ticketData.description; + const isRequest = ticketData.type === 'requisicao'; + const justificationForDisplay = isRequest + ? (ticketData.justificativa && ticketData.justificativa.trim() + ? ticketData.justificativa + : GLPI_REQ_DEFAULT_JUSTIFICATIVA) + : null; + const descriptionForDisplay = isRequest + ? (rawDescription && rawDescription.trim() + ? rawDescription + : GLPI_REQ_DEFAULT_DESCRIPTION) + : (rawDescription || 'N/A'); + + const email = ticketData.caller_email || extractEmailFromDescription(rawDescription) || 'N/A'; + const phone = ticketData.caller_phone || extractPhoneFromDescription(rawDescription) || 'N/A'; + const ramal = ticketData.caller_ramal || extractRamalFromDescription(rawDescription) || 'N/A'; + + const requestExtraHtml = isRequest + ? ` +
+
Justificativa
+
${justificationForDisplay}
+
+ ` + : ''; let htmlDescription = ` @@ -149,10 +173,11 @@ class TicketGlpiModel {
${ticketData.ticket_number || 'N/A'}
-
- Descrição: -
- ${ticketData.description || 'N/A'} + ${requestExtraHtml} +
+
Descricao
+
${descriptionForDisplay}
+
`; return htmlDescription; } @@ -418,4 +443,4 @@ class TicketGlpiModel { * * Este modelo é fundamental para abstrair a complexidade do esquema de banco de dados do GLPI e garantir que as operações sejam realizadas de forma transacional e segura. */ -module.exports = TicketGlpiModel; \ No newline at end of file +module.exports = TicketGlpiModel; diff --git a/src/models/ticketSnModel.js b/src/models/ticketSnModel.js index 30e204f..fe5ebdc 100644 --- a/src/models/ticketSnModel.js +++ b/src/models/ticketSnModel.js @@ -1,5 +1,5 @@ const pool = require('../data/database'); -const { logInfo, logError, logWarning } = require('../utils/logger'); +const { logInfo, logError } = require('../utils/logger'); class TicketSnModel { @@ -7,8 +7,8 @@ class TicketSnModel { const query = ` INSERT INTO tickets_sn (ticket_number, sys_id, 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, $12, $13) + 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) ON CONFLICT (ticket_number) DO UPDATE SET sys_id = EXCLUDED.sys_id, @@ -21,12 +21,13 @@ class TicketSnModel { location_name = EXCLUDED.location_name, ramal = EXCLUDED.ramal, telefone = EXCLUDED.telefone, + justificativa = EXCLUDED.justificativa, opened_at = EXCLUDED.opened_at, tipo = EXCLUDED.tipo RETURNING id, (xmax = 0) AS "wasInserted"`; const ticketNumber = typeof ticketData.number === 'object' ? ticketData.number?.value : ticketData.number; - let callerName, callerEmail, ramal, telefone, sysId, shortDescription, state, description, locationId, locationName, openedAt; + let callerName, callerEmail, ramal, telefone, justificativa, sysId, shortDescription, state, description, locationId, locationName, openedAt; if (typeTicket === 'incidente') { @@ -39,6 +40,7 @@ class TicketSnModel { shortDescription = ticketData.short_description || null; state = ticketData.state || null; description = ticketData.description || null; + justificativa = null; const locationLink = ticketData.location?.link; locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null; locationName = ticketData.location?.display_value || null; @@ -47,11 +49,12 @@ class TicketSnModel { callerName = ticketData.opened_by?.display_value || null; callerEmail = ticketData['opened_by.email'] || null; ramal = ticketData['variables.numero_ramal'] || null; - telefone = ticketData['variables.telephone_favorecido_rh'] || ticketData.telefone || null; + telefone = ticketData['variables.telephone_favorecido_rh'] || ticketData['variables.telephone_favorecido_ti'] || ticketData.telefone || null; sysId = ticketData.sys_id || null; shortDescription = ticketData.short_description || null; state = ticketData.state || null; description = ticketData.description || null; + justificativa = ticketData.justificativa || null; const locationLink = ticketData.location?.link; locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null; locationName = ticketData.location?.display_value || null; @@ -70,6 +73,7 @@ class TicketSnModel { locationName, ramal, telefone, + justificativa, openedAt ? (typeof openedAt === 'string' && openedAt.includes('/') ? new Date(openedAt.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$2-$1')) @@ -119,6 +123,7 @@ class TicketSnModel { ts.location_name, ts.ramal, ts.telefone, + ts.justificativa, ts.opened_at, ts.tipo, ts.created_at, @@ -237,4 +242,4 @@ class TicketSnModel { * - 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. */ -module.exports = TicketSnModel; \ No newline at end of file +module.exports = TicketSnModel; diff --git a/src/scripts/database/scriptBD.sql b/src/scripts/database/scriptBD.sql index 59ae0f8..ad4ad17 100644 --- a/src/scripts/database/scriptBD.sql +++ b/src/scripts/database/scriptBD.sql @@ -34,6 +34,7 @@ CREATE TABLE tickets_sn ( location_name VARCHAR(255), -- Nome da localização no ServiceNow ramal VARCHAR(100), -- Ramal do solicitante telefone VARCHAR(100), -- Telefone do solicitante + justificativa TEXT, -- Justificativa da requisicao (catalog variable) 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 @@ -225,6 +226,7 @@ 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.justificativa IS 'Justificativa da requisicao vinda das variaveis de catalogo'; 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'; @@ -293,3 +295,8 @@ DELETE FROM tickets_sn WHERE status != 'Encerrado' AND status != 'Encerrado - Om UPDATE sync_control SET last_watermark = '1970-01-01 00:00:00+00'; + +-- Migracao para bases existentes: +ALTER TABLE tickets_sn ADD COLUMN IF NOT EXISTS justificativa TEXT; + + diff --git a/src/services/glpiTicketService.js b/src/services/glpiTicketService.js index d15ab0a..6b3e609 100644 --- a/src/services/glpiTicketService.js +++ b/src/services/glpiTicketService.js @@ -2,7 +2,7 @@ const TicketSnModel = require('../models/ticketSnModel'); const TicketGlpiModel = require('../models/ticketGlpiModel'); const TicketUpdateModel = require('../models/ticketUpdateModel'); const TicketSyncModel = require('../models/ticketSyncModel'); -const { logInfo, logError, logWarning } = require('../utils/logger'); +const { logInfo, logError } = require('../utils/logger'); const { addWorkNoteToServiceNow } = require('./servicenowService'); const syncTicketsToGlpi = async () => { @@ -61,6 +61,7 @@ const processSingleTicket = async (ticket) => { ticket_number: ticket.ticket_number, short_description: ticket.short_description, description: ticket.description, + justificativa: ticket.justificativa, caller_id: ticket.caller_id, caller_email: ticket.caller_email, caller_phone: ticket.telefone, @@ -73,18 +74,6 @@ const processSingleTicket = async (ticket) => { opened_at: ticket.opened_at, updated_at: ticket.updated_at }; - - if (glpiData.type === 'requisicao' && (!glpiData.description || glpiData.description.trim() === '')) { - logInfo(`Descrição vazia para requisição ${ticket.ticket_number}. Tentando buscar de sc_item_option.`); - const scItemOptionDescription = await require('./servicenowService').fetchScItemOptionValue(ticket.sys_id, 'justificativa'); - if (scItemOptionDescription) { - glpiData.description = scItemOptionDescription; - logInfo(`Descrição atualizada para requisição ${ticket.ticket_number} a partir de sc_item_option.`); - } else { - logWarning(`Não foi possível obter descrição de sc_item_option para requisição ${ticket.ticket_number}.`); - } - } - const glpiTicketId = await TicketGlpiModel.createTicket(glpiData); await TicketSyncModel.updateLastSync(ticket.id, 'glpi'); await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', glpiTicketId); @@ -191,4 +180,4 @@ module.exports = { * - `processSingleTicket(ticket)`: Processa um único ticket. Verifica se ele já existe no GLPI para evitar duplicatas. Se não existir, formata os dados (incluindo o enriquecimento da descrição para requisições) e chama o `TicketGlpiModel` para criar o ticket. Se já existir, apenas atualiza o registro de sincronização. * - `checkGlpiStatusChanges()`: (Atualmente não utilizado no fluxo principal, sua lógica foi integrada ao `processStatusController`) Função projetada para detectar proativamente mudanças de status no GLPI. * - `closeTicketInGlpi(ticket)`: Orquestra o processo de fechamento de um ticket no GLPI. Ele busca a nota de resolução vinda do ServiceNow e a utiliza para adicionar uma solução no GLPI antes de mudar o status para "Solucionado". - */ \ No newline at end of file + */ diff --git a/src/services/servicenowService.js b/src/services/servicenowService.js index 6fdd44f..a11ebbe 100644 --- a/src/services/servicenowService.js +++ b/src/services/servicenowService.js @@ -495,14 +495,29 @@ const fetchScItemOptionValue = async (ritmSysId, variableName) => { try { const baseUrl = apiConfig.snScItemOptionConfig.baseUrl.replace(/\/$/, ""); - const url = `${baseUrl}?sysparm_query=request_item=${ritmSysId}^sc_item_option.item_option_new.question_textINPlease justify the need,Telephone,Select what you want&sysparm_fields=sc_item_option.item_option_new.question_text,sc_item_option.value`; + const url = `${baseUrl}?sysparm_query=request_item=${ritmSysId}^sc_item_option.item_option_new.question_textINPlease justify the need,Telephone,Select what you want&sysparm_fields=sc_item_option.item_option_new.question_text,sc_item_option.item_option_new.name,sc_item_option.value`; const response = await axios.get(url, { auth: apiConfig.servicenowAuthentication.auth }); - const result = response.data.result; - return (result && result.length > 0) ? result : null; + const result = response.data.result || []; + if (result.length === 0) return null; + + const normalize = (value) => { + if (value === null || value === undefined) return ''; + if (typeof value === 'string') return value; + if (typeof value === 'object') { + return value.value || value.display_value || ''; + } + return String(value); + }; + + return result.map((item) => ({ + question_text: normalize(item['sc_item_option.item_option_new.question_text']), + variable_name: normalize(item['sc_item_option.item_option_new.name']), + value: normalize(item['sc_item_option.value']) + })); } catch (error) { logError(error, `ERRO: Falha ao buscar valor da variavel '${variableName}' para RITM ${ritmSysId}`); return null;