From d610f32575dbe7d2913cc42a868f7e2cefb398d8 Mon Sep 17 00:00:00 2001 From: Desenvolvimento Date: Thu, 8 Jan 2026 16:30:54 -0300 Subject: [PATCH 1/5] CONFIG: Alterado config PM2 --- ecosystem.config.js | 2 +- src/controller/closureController.js | 48 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/controller/closureController.js diff --git a/ecosystem.config.js b/ecosystem.config.js index 786cb5f..3667ffd 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -20,7 +20,7 @@ module.exports = { env_production: { watch: false, NODE_ENV: "production", - PORT: 8080 // Porta usada no ambiente de produção + PORT: 3000 // Porta usada no ambiente de produção } }, diff --git a/src/controller/closureController.js b/src/controller/closureController.js new file mode 100644 index 0000000..db76a3c --- /dev/null +++ b/src/controller/closureController.js @@ -0,0 +1,48 @@ +const {fechaTicket} = require('../services/ticketService.js'); +const { logInfo, logError } = require('../utils/logger.js'); + +/** + * Controller para lidar com o webhook de fechamento de ticket do GLPI. + * @param {import('express').Request} req - O objeto de requisição do Express. + * @param {import('express').Response} res - O objeto de resposta do Express. + */ + +const closeTicket = async (req, res) => { + try { + let rawData = ''; + const bodyRequest = req.body; + + req.on('data', chunk => { + rawData += chunk; + }); + + req.on('end', async () => { + let bodyRequest; + try { + bodyRequest = JSON.parse(rawData); + } catch (err) { + logError('Erro ao parsear JSON:', err); + bodyRequest = {}; + } + + const ticketId = bodyRequest.item.items_id; + logInfo(`Ticket ${ticketId} acionado para encerramento.`); + const closingTicket = await fechaTicket(bodyRequest); + res.status(200).json(closingTicket); + }); + + } catch (error) { + res.status(500).json({ error: error.message }); + } +} + + +module.exports = { closeTicket }; +/** + * @module ClosureController + * @description Este controller é o ponto de entrada para as requisições de webhook enviadas pelo GLPI quando um ticket é fechado. + * + * Funções: + * - `closeTicket(req, res)`: Recebe a notificação do GLPI, extrai os dados do corpo da requisição e invoca o `ticketService` para orquestrar o processo de fechamento do ticket correspondente no HubSoft e a atualização no banco de dados local. + * Ele é responsável por validar a requisição e responder ao GLPI com o status do processamento. + */ \ No newline at end of file From b398b222866b94e916467fb9ad9eb8caf2f3e3b0 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Thu, 8 Jan 2026 16:40:07 -0300 Subject: [PATCH 2/5] =?UTF-8?q?RAW:=20Coment=C3=A1rios=20regra=20temporari?= =?UTF-8?q?a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adiciona comentários para impedir abertura de chamados de: - cancelamento - SAC - troca de titularidade Regra temporária válida até 19/01, conforme definição da gerência. --- .../tickets/repositories/ticket.repository.js | 18 +++---- .../tickets/services/cancelamento.service.js | 12 ++--- .../tickets/services/mundiale.service.js | 10 ++-- .../services/resolveTicketEntity.service.js | 20 +++---- .../tickets/useCases/syncTickets.usecase.js | 54 +++++++++---------- 5 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/modules/tickets/repositories/ticket.repository.js b/src/modules/tickets/repositories/ticket.repository.js index 63fd175..bb5b66b 100644 --- a/src/modules/tickets/repositories/ticket.repository.js +++ b/src/modules/tickets/repositories/ticket.repository.js @@ -66,27 +66,27 @@ async function getTrocaTTickets(watermark) { }); } -async function insertTicketsHubGlpi(tickets){ +async function insertTicketsHubGlpi(tickets) { return hubglpiTicketsRepo.insertTickets(tickets) } -async function insertSyncDataByIds(ids){ +async function insertSyncDataByIds(ids) { return hubglpiSyncRepo.insertSyncData(ids) } -async function fetchPendingTickets(){ +async function fetchPendingTickets() { return hubglpiTicketsRepo.fetchPendingTickets() } -async function insertTicketGlpi(ticket){ +async function insertTicketGlpi(ticket) { return glpiTicketsRepo.insertTicket(ticket) } -async function getEntitiesByService(codigoCliente, codigoServico){ +async function getEntitiesByService(codigoCliente, codigoServico) { return glpiEntitiesRepo.getEntitiesByService(codigoCliente, codigoServico) } -async function getEntitiesByClient(codigoCliente){ +async function getEntitiesByClient(codigoCliente) { return glpiEntitiesRepo.getEntitiesByClient(codigoCliente) } @@ -98,7 +98,7 @@ const GROUP_BY_TYPE = { SAC: 'NOC' } -async function insertGroupTicket(id, type){ +async function insertGroupTicket(id, type) { const group = GROUP_BY_TYPE[type] || 'NOC' if (group === 'IMPLANTACAO') { @@ -108,11 +108,11 @@ async function insertGroupTicket(id, type){ return glpiGroupsRepo.insertGroupNOC(id) } -async function updateSyncDataCreated(hubId, glpiId){ +async function updateSyncDataCreated(hubId, glpiId) { return hubglpiSyncRepo.updateSyncDataCreated(hubId, glpiId) } -async function sendHubsoftMessage(hubId, message){ +async function sendHubsoftMessage(hubId, message) { return hubsoftApiClient.sendHubsoftMessage(hubId, message) } diff --git a/src/modules/tickets/services/cancelamento.service.js b/src/modules/tickets/services/cancelamento.service.js index 9237aa0..3227ebd 100644 --- a/src/modules/tickets/services/cancelamento.service.js +++ b/src/modules/tickets/services/cancelamento.service.js @@ -16,7 +16,7 @@ async function fetchNew(watermark) { } async function saveHubGlpi(tickets) { - if (!tickets.length) return + if (!tickets.length) return logInfo('[CANCELAMENTO] Inserindo chamados no HubGlpi') await repository.insertTicketsHubGlpi(tickets) logInfo('[CANCELAMENTO] Inserindo dado de sincronia dos chamados') @@ -31,7 +31,7 @@ async function sendToGlpi(ticket) { const resolved = await ticketEntityResolver.resolveEntityId(ticket) logInfo('[CANCELAMENTO] Entidade resolvida') - const payload = cancelamentoGlpiModel.toGlpiPayload(resolved) + const payload = cancelamentoGlpiModel.toGlpiPayload(resolved) logInfo('[CANCELAMENTO] Payload preparado para GLPI', payload) logInfo('[CANCELAMENTO] Inserindo ticket no GLPI', { id_atendimento: ticket.id_atendimento }) @@ -40,7 +40,7 @@ async function sendToGlpi(ticket) { await repository.insertGroupTicket(glpiId, 'CANCELAMENTO') logInfo('[CANCELAMENTO] Grupo associado ao ticket GLPI', { glpiId, group: 'CANCELAMENTO' }) - + await repository.updateSyncDataCreated(ticket.id_atendimento, glpiId) logInfo('[CANCELAMENTO] Dados de sincronização atualizados', { id_atendimento: ticket.id_atendimento, glpiId }) @@ -52,9 +52,9 @@ async function sendToGlpi(ticket) { } module.exports = { - fetchNew, - saveHubGlpi, - sendToGlpi + fetchNew, + saveHubGlpi, + sendToGlpi } /** diff --git a/src/modules/tickets/services/mundiale.service.js b/src/modules/tickets/services/mundiale.service.js index 5c7b7fa..0bf34e3 100644 --- a/src/modules/tickets/services/mundiale.service.js +++ b/src/modules/tickets/services/mundiale.service.js @@ -16,7 +16,7 @@ async function fetchNew(watermark) { } async function saveHubGlpi(tickets) { - if (!tickets.length) return + if (!tickets.length) return logInfo('[MUNDIALE] Inserindo chamados no HubGlpi') await repository.insertTicketsHubGlpi(tickets) logInfo('[MUNDIALE] Inserindo dado de sincronia dos chamados') @@ -40,7 +40,7 @@ async function sendToGlpi(ticket) { await repository.insertGroupTicket(glpiId, 'MUNDIALE') logInfo('[MUNDIALE] Grupo associado ao ticket GLPI', { glpiId, group: 'MUNDIALE' }) - + await repository.updateSyncDataCreated(ticket.id_atendimento, glpiId) logInfo('[MUNDIALE] Dados de sincronização atualizados', { id_atendimento: ticket.id_atendimento, glpiId }) @@ -52,9 +52,9 @@ async function sendToGlpi(ticket) { } module.exports = { - fetchNew, - saveHubGlpi, - sendToGlpi + fetchNew, + saveHubGlpi, + sendToGlpi } /** diff --git a/src/modules/tickets/services/resolveTicketEntity.service.js b/src/modules/tickets/services/resolveTicketEntity.service.js index 3e8d730..e065534 100644 --- a/src/modules/tickets/services/resolveTicketEntity.service.js +++ b/src/modules/tickets/services/resolveTicketEntity.service.js @@ -5,14 +5,14 @@ const repository = require('../repositories/ticket.repository.js') async function resolveEntityId(ticketData) { const entityByService = await repository.getEntitiesByService( - ticketData.codigo_cliente, + ticketData.codigo_clasiente, ticketData.codigo_servico ); if (entityByService) { - return { - ...ticketData, - entities_id: entityByService + return { + ...ticketData, + entities_id: entityByService } } @@ -21,15 +21,15 @@ async function resolveEntityId(ticketData) { ); if (entityByClient) { - return { - ...ticketData, - entities_id: entityByClient + return { + ...ticketData, + entities_id: entityByClient } } - return { - ...ticketData, - entities_id: 0 + return { + ...ticketData, + entities_id: 0 } } diff --git a/src/modules/tickets/useCases/syncTickets.usecase.js b/src/modules/tickets/useCases/syncTickets.usecase.js index cfd6e50..ba71824 100644 --- a/src/modules/tickets/useCases/syncTickets.usecase.js +++ b/src/modules/tickets/useCases/syncTickets.usecase.js @@ -1,7 +1,7 @@ //src/modules/tickes/useCases/syncTickets.usecase.js const notifyTicketCreated = require('../services/ticketNotifications.service.js') -const repository = require ('../repositories/ticket.repository.js') +const repository = require('../repositories/ticket.repository.js') const mundialeService = require('../services/mundiale.service.js') const implantacaoService = require('../services/implantacao.service.js') const cancelamentoService = require('../services/cancelamento.service.js') @@ -22,34 +22,34 @@ async function syncTicketsUseCase() { const waterMark = await repository.getWaterMark() logInfo(`Buscando Tickets novos desde de: ${waterMark}`) - const mundiale = await mundialeService.fetchNew(waterMark) + const mundiale = await mundialeService.fetchNew(waterMark) logInfo(`[USECASE] ${mundiale.length} tickets Mundiale encontrados`) - const implantacao = await implantacaoService.fetchNew(waterMark) - logInfo(`[USECASE] ${implantacao.length} tickets Implantação encontrados`) + //const implantacao = await implantacaoService.fetchNew(waterMark) + //logInfo(`[USECASE] ${implantacao.length} tickets Implantação encontrados`) - const cancelamento = await cancelamentoService.fetchNew(waterMark) - logInfo(`[USECASE] ${cancelamento.length} tickets Cancelamento encontrados`) + //const cancelamento = await cancelamentoService.fetchNew(waterMark) + //logInfo(`[USECASE] ${cancelamento.length} tickets Cancelamento encontrados`) //const sac = await sacService.fetchNew(waterMark) //TODO //logInfo(`[USECASE] ${sac.length} tickets SAC encontrados`) - - const trocaTitularidade = await trocaTitularidadeService.fetchNew(waterMark) //TODO - logInfo(`[USECASE] ${trocaTitularidade.length} tickets Troca de Titularidade encontrados`) - + + //const trocaTitularidade = await trocaTitularidadeService.fetchNew(waterMark) //TODO + //logInfo(`[USECASE] ${trocaTitularidade.length} tickets Troca de Titularidade encontrados`) + await mundialeService.saveHubGlpi(mundiale) - await implantacaoService.saveHubGlpi(implantacao) - await cancelamentoService.saveHubGlpi(cancelamento) + //await implantacaoService.saveHubGlpi(implantacao) + //await cancelamentoService.saveHubGlpi(cancelamento) //await sacService.saveHubGlpi(sac) //TODO - await trocaTitularidadeService.saveHubGlpi(trocaTitularidade) //TODO + //await trocaTitularidadeService.saveHubGlpi(trocaTitularidade) const allFetchedTickets = [ ...mundiale, - ...implantacao, - ...cancelamento, + //...implantacao, + //...cancelamento, //...sac, - ...trocaTitularidade + //...trocaTitularidade ] const newWaterMark = resolveNewWatermark(allFetchedTickets, waterMark) @@ -63,16 +63,16 @@ async function syncTicketsUseCase() { logInfo(`[USECASE] ${pendentes.length} tickets pendentes para envio ao GLPI`) for (const ticket of pendentes) { - try { - const service = resolveTicketService(ticket.ticket_type) - if (!service) continue + try { + const service = resolveTicketService(ticket.ticket_type) + if (!service) continue - const glpiId = await service.sendToGlpi(ticket) - await notifyTicketCreated.notifyTicketCreated(ticket.id_atendimento, glpiId) - } catch (err) { - logError(err, `[USECASE] Falha ao processar ticket ${ticket.id_atendimento}`) + const glpiId = await service.sendToGlpi(ticket) + await notifyTicketCreated.notifyTicketCreated(ticket.id_atendimento, glpiId) + } catch (err) { + logError(err, `[USECASE] Falha ao processar ticket ${ticket.id_atendimento}`) + } } -} } @@ -80,10 +80,10 @@ async function syncTicketsUseCase() { function resolveTicketService(type) { const map = { MUNDIALE: mundialeService, - IMPLANTACAO: implantacaoService, - CANCELAMENTO: cancelamentoService, + //IMPLANTACAO: implantacaoService, + //CANCELAMENTO: cancelamentoService, //SAC: sacService, //TODO - TITULARIDADE: trocaTitularidadeService //TODO + //TITULARIDADE: trocaTitularidadeService } return map[type] From 08b3bf2639ae2edb488dca281c988a05b1c3e9b5 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Mon, 12 Jan 2026 16:40:47 -0300 Subject: [PATCH 3/5] CHORE: Preparando ambiente PM2 para novas Features --- ecosystem.config.js | 73 ++++++++----------- .../tickets/repositories/ticket.repository.js | 2 +- 2 files changed, 30 insertions(+), 45 deletions(-) diff --git a/ecosystem.config.js b/ecosystem.config.js index 3667ffd..984ae87 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -1,73 +1,58 @@ +// ecosystem.config.js + module.exports = { apps: [ - // 🟢 --- PRIMEIRA APLICAÇÃO: API PRINCIPAL (servidor HTTP) --- + // 🟢 API PRINCIPAL { - name: "hubxglpi-api", // Nome que aparecerá no PM2 - script: "src/infra/http/server.js", // Caminho do arquivo principal da API + name: "hubxglpi-api", + script: "src/infra/http/server.js", - // 👇 Execução em modo "cluster" (um processo por core da máquina) exec_mode: "cluster", - instances: "max", // "max" = usa todos os núcleos disponíveis + instances: "max", - // ⚙️ Variáveis de ambiente padrão (modo development) env: { - watch: true, NODE_ENV: "development", - PORT: 3000 // Porta usada no ambiente de desenvolvimento + PORT: 3000 }, - // ⚙️ Variáveis de ambiente quando rodar com `--env production` env_production: { - watch: false, NODE_ENV: "production", - PORT: 3000 // Porta usada no ambiente de produção + PORT: 3000 } }, - // 🕒 --- SEGUNDA APLICAÇÃO: CRON JOBS (tarefas agendadas) --- + // 🕒 CRON DE SINCRONIZAÇÃO { - name: "hubxglpi-cron", // Nome do serviço de crons - script: "src/infra/cron/sync.cron.js", // Arquivo onde ficam as tarefas agendadas + name: "hubxglpi-cron", + script: "src/infra/cron/sync.cron.js", - // 👇 Modo "fork" = apenas 1 instância, sem cluster (evita rodar crons duplicados) exec_mode: "fork", - instances: 1, // Força a rodar somente um processo + instances: 1, + + env: { + NODE_ENV: "development" + }, + + env_production: { + NODE_ENV: "production" + } + }, + + // 🐶 WATCHDOG + { + name: "hubxglpi-watchdog", + script: "src/infra/cron/observer.cron.js", + + exec_mode: "fork", + instances: 1, - // ⚙️ Variáveis de ambiente para desenvolvimento env: { - watch: true, NODE_ENV: "development" }, - // ⚙️ Variáveis de ambiente para produção env_production: { - watch: false, NODE_ENV: "production" } } ] }; - - -/** * @module server - * @description Ponto de entrada principal da aplicação. - * Este módulo é responsável por: - * Definir a configuração do PM2 para gerenciar a aplicação principal e o serviço de cron jobs. - * 1. A aplicação principal (`hubxglpi-api`) roda em modo cluster para lidar com requisições HTTP. Para encerrar chamados. - * 2. O serviço de cron jobs (`hubxglpi-cron`) roda em modo fork para evitar execuções duplicadas das tarefas agendadas. -*/ - -/** - * 💡 Dicas de uso: - * - * 🧪 Ambiente de desenvolvimento: - * pm2 start ecosystem.config.js --env development - * - * 🚀 Ambiente de produção: - * pm2 start ecosystem.config.js --env production - * pm2 startup systemd - * sudo env PATH=$PATH:/usr/bin /usr/local/lib/node_modules/pm2/bin/pm2 startup systemd -u desenvolvimento --hp /home/desenvolvimento - * pm2 save - * - * ✅ Após isso, o PM2 inicializa automaticamente os dois processos no boot do servidor. - */ \ No newline at end of file diff --git a/src/modules/tickets/repositories/ticket.repository.js b/src/modules/tickets/repositories/ticket.repository.js index bb5b66b..1d5eaff 100644 --- a/src/modules/tickets/repositories/ticket.repository.js +++ b/src/modules/tickets/repositories/ticket.repository.js @@ -33,7 +33,7 @@ const TYPES = Object.freeze({ async function getMundialeTickets(watermark) { return hubsoftTicketsRepo.getTicketsByTipo({ tipoAtendimento: TYPES.MUNDIALE, - usuarioAbertura: 248, + usuarioAbertura: process.env.HUBSOFT_MUNDIALE_USER_ID, watermark }); } From 4833310ec12b6dfac1fdd40495d6d2160407e050 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Wed, 14 Jan 2026 17:51:49 -0300 Subject: [PATCH 4/5] =?UTF-8?q?FEATURE:=20Finalizado=20Feature=20Watchdog,?= =?UTF-8?q?=20respons=C3=A1vel=20por=20coletar=20chamados=20pendentes=20de?= =?UTF-8?q?=20fechamento=20no=20GLPI=20e=20notificar=20algum=20grupo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 2 - src/infra/cron/observer.cron.js | 8 +- .../hubglpi/watchdog.repository.js | 130 +++++++++++++----- .../hubsoft/tickets.repository.js | 24 ++++ src/infra/mail/sender.js | 17 ++- src/modules/watchdog/job/job.js | 41 +++--- src/modules/watchdog/model/email.model.js | 57 ++++---- .../repository/watchdog.repository.js | 20 ++- src/shared/utils/sendEmail.js | 0 9 files changed, 207 insertions(+), 92 deletions(-) delete mode 100644 src/shared/utils/sendEmail.js diff --git a/package-lock.json b/package-lock.json index cb26d6d..2cdad64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2090,7 +2090,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -3114,7 +3113,6 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", diff --git a/src/infra/cron/observer.cron.js b/src/infra/cron/observer.cron.js index 26771c3..b4fdf05 100644 --- a/src/infra/cron/observer.cron.js +++ b/src/infra/cron/observer.cron.js @@ -1,18 +1,18 @@ // src/infra/cron/observer.cron.js const cron = require('node-cron') -const logger = require('../../shared/utils/logger') +const {logError, logInfo} = require('../../shared/utils/logger') const runWatchdog = require('../../modules/watchdog/job/job') -logger.info('[CRON] 🐶 Watchdog cron iniciado') +logInfo('[CRON] 🐶 Watchdog cron iniciado') cron.schedule('*/30 * * * *', async () => { - logger.info('[CRON] 🐶 Watchdog executando verificação') + logInfo('[CRON] 🐶 Watchdog executando verificação') try { await runWatchdog() } catch (error) { - logger.error('[CRON] ❌ Erro no Watchdog', { error }) + logError('[CRON] ❌ Erro no Watchdog', { error }) } }) \ No newline at end of file diff --git a/src/infra/db/repositories/hubglpi/watchdog.repository.js b/src/infra/db/repositories/hubglpi/watchdog.repository.js index 180c6f9..700ad30 100644 --- a/src/infra/db/repositories/hubglpi/watchdog.repository.js +++ b/src/infra/db/repositories/hubglpi/watchdog.repository.js @@ -1,6 +1,6 @@ // src/infra/db/repositories/hubglpi/watchdog.repository.js -const db = require('../connections/hubglpi.pg.js'); +const db = require('../../connections/hubglpi.pg.js'); const { logError } = require('../../../../shared/utils/logger.js'); /** @@ -23,11 +23,10 @@ async function notificationAlreadySent(hubsoftTicketId) { throw error; } } - /** - * Marca tickets como notificados com sucesso + * Marca tickets como pendente de notificação */ -async function markNotificationsAsSent(hubsoftTicketIds) { +async function markNotificationsAsPending(hubsoftTicketIds) { if (!hubsoftTicketIds || hubsoftTicketIds.length === 0) { return; } @@ -37,39 +36,11 @@ async function markNotificationsAsSent(hubsoftTicketIds) { SELECT unnest($1::bigint[]), NOW(), - 'SENT' + 'pending' ON CONFLICT (ticket_id) DO UPDATE SET notified_at = EXCLUDED.notified_at, - status = 'SENT'; - `; - - try { - await db.query(query, [hubsoftTicketIds]); - } catch (error) { - logError('Erro ao marcar notificações como enviadas', error); - throw error; - } -} - -/** - * Marca tickets como falha de notificação - */ -async function markNotificationsAsFailed(hubsoftTicketIds) { - if (!hubsoftTicketIds || hubsoftTicketIds.length === 0) { - return; - } - - const query = ` - INSERT INTO watchdog_notifications (ticket_id, notified_at, status) - SELECT - unnest($1::bigint[]), - NOW(), - 'FAILED' - ON CONFLICT (ticket_id) - DO UPDATE SET - notified_at = EXCLUDED.notified_at, - status = 'FAILED'; + status = 'pending'; `; try { @@ -80,8 +51,97 @@ async function markNotificationsAsFailed(hubsoftTicketIds) { } } + +/** + * Marca tickets como notificados com sucesso + */ +async function markNotificationsAsSent(hubsoftTicketIds) { + const ids = (hubsoftTicketIds || []) + .map(id => Number(id)) + .filter(Number.isFinite); + + if (ids.length === 0) return; + + const query = ` + INSERT INTO watchdog_notifications (ticket_id, notified_at, status) + SELECT + unnest($1::bigint[]), + NOW(), + 'sent' + ON CONFLICT (ticket_id) + DO UPDATE SET + notified_at = EXCLUDED.notified_at, + status = 'sent'; + `; + + try { + await db.query(query, [ids]); + } catch (error) { + logError('[WATCHDOG][REPOSITORY] Erro ao marcar notificações como enviadas', error); + throw error; + } +} + +/** + * Marca tickets como falha de notificação + */ +async function markNotificationsAsFailed(hubsoftTicketIds) { + const ids = (hubsoftTicketIds || []) + .map(id => Number(id)) + .filter(Number.isFinite); + + if (ids.length === 0) return; + + const query = ` + INSERT INTO watchdog_notifications (ticket_id, notified_at, status) + SELECT + unnest($1::bigint[]), + NOW(), + 'sent' + ON CONFLICT (ticket_id) + DO UPDATE SET + notified_at = EXCLUDED.notified_at, + status = 'failed'; + `; + + try { + await db.query(query, [ids]); + } catch (error) { + logError('[WATCHDOG][REPOSITORY] Erro ao marcar notificações como enviadas', error); + throw error; + } +} + + +async function getPendingTicketsForNotification() { + const query = ` + SELECT + wn.ticket_id AS hubsoft_ticket_id, + ht.protocolo_hub as protocolo_hub, + ht.ticket_mundiale AS ticket_mundiale, + wn.notified_at AS closed_at, + sd.glpi_ticket_id AS glpi_ticket_id + FROM watchdog_notifications wn + INNER JOIN sync_data sd + ON wn.ticket_id = sd.hubsoft_ticket_id + INNER JOIN hubsoft_tickets ht + ON wn.ticket_id = ht.id_atendimento + WHERE wn.status = 'pending'; + `; + + try { + const { rows } = await db.query(query); + return rows; + } catch (error) { + logError('Erro ao buscar tickets pendentes para notificação', error); + throw error; + } +} + module.exports = { notificationAlreadySent, markNotificationsAsSent, - markNotificationsAsFailed + markNotificationsAsFailed, + markNotificationsAsPending, + getPendingTicketsForNotification }; diff --git a/src/infra/db/repositories/hubsoft/tickets.repository.js b/src/infra/db/repositories/hubsoft/tickets.repository.js index 8d83d61..4668b60 100644 --- a/src/infra/db/repositories/hubsoft/tickets.repository.js +++ b/src/infra/db/repositories/hubsoft/tickets.repository.js @@ -97,6 +97,30 @@ async function getTicketsByTipo({ async function getTicketsClosedSince(thresholdDate) { try { + + if (process.env.HUBSOFT_MOCK_ENABLED === 'true') { + return [ + { + id_atendimento: 2780, + protocolo: '20260106155510498970', + hubsoft_closed_at: new Date('2026-01-06T08:20:45') + }, + { + id_atendimento: 2769, + protocolo: '20260105170715994323', + hubsoft_closed_at: new Date('2026-01-06T10:02:13') + }, + { + id_atendimento: 2779, + protocolo: '20260106145016864639', + hubsoft_closed_at: new Date('2025-12-18T14:35:56') + } + ] + } + + + + const mundialeUserId = process.env.HUBSOFT_MUNDIALE_USER_ID; if (!mundialeUserId) { diff --git a/src/infra/mail/sender.js b/src/infra/mail/sender.js index 55985e3..caf4df4 100644 --- a/src/infra/mail/sender.js +++ b/src/infra/mail/sender.js @@ -1,16 +1,15 @@ // src/infra/mail/sender.js const nodemailer = require('nodemailer'); -const logger = require('../../shared/utils/logger.js'); +const {logError, logInfo} = require('../../shared/utils/logger.js'); const transporter = nodemailer.createTransport({ host: process.env.MAIL_HOST, - port: Number(process.env.MAIL_PORT), - secure: process.env.MAIL_SECURE === 'true', - auth: { - user: process.env.MAIL_USER, - pass: process.env.MAIL_PASS - } + port: Number(process.env.MAIL_PORT), + secure: Number(process.env.MAIL_PORT) === 465, // 465 = TLS direto; 587/25 = STARTTLS + tls: { + rejectUnauthorized: false, // aceita self-signed + }, }); @@ -38,11 +37,11 @@ async function send({ subject, bodyEmail, recipients, cc = [] }) { try { const info = await transporter.sendMail(mailOptions); - logger.info(`[MAIL] Email enviado com sucesso. MessageId=${info.messageId}`); + logInfo(`[MAIL] Email enviado com sucesso. MessageId=${info.messageId}`); return info; } catch (error) { - logger.error('[MAIL] Erro ao enviar email', error); + logError('[MAIL] Erro ao enviar email', error); throw error; } } diff --git a/src/modules/watchdog/job/job.js b/src/modules/watchdog/job/job.js index 272199d..92d95f9 100644 --- a/src/modules/watchdog/job/job.js +++ b/src/modules/watchdog/job/job.js @@ -1,58 +1,67 @@ // src/modules/watchdog/job/job.js -const logger = require('../../../shared/utils/logger') +require('dotenv').config({ path: '.env.development' }) + +const { logError, logInfo } = require('../../../shared/utils/logger') const repository = require('../repository/watchdog.repository.js') const model = require('../model/email.model.js') async function runWatchdog() { - logger.info('[WATCHDOG] [JOB] Coletando chamados fechados há 31 minutos') + logInfo('[WATCHDOG] [JOB] Coletando chamados fechados há 31 minutos') const thresholdDate = new Date(Date.now() - 31 * 60 * 1000) const closedTickets = await repository.getClosedTicketsSince(thresholdDate) - logger.info(`[WATCHDOG] [JOB] Encontrados ${closedTickets.length} chamados fechados`) + logInfo(`[WATCHDOG] [JOB] Encontrados ${closedTickets.length} chamados fechados`) for (const ticket of closedTickets) { const hubGlpiTicket = await repository.checkTicketInHubGlpi(ticket.id_atendimento) if (!hubGlpiTicket.exists) { - logger.info(`[WATCHDOG] [JOB] Chamado ${ticket.id_atendimento} não encontrado no HubGlpi. Ignorando.`) + logInfo(`[WATCHDOG] [JOB] Chamado ${ticket.id_atendimento} não encontrado no HubGlpi. Ignorando.`) continue } if (hubGlpiTicket.status === 'closed') { - logger.info(`[WATCHDOG] [JOB] Chamado ${ticket.id_atendimento} já está fechado no HubGlpi. Ignorando.`) + logInfo(`[WATCHDOG] [JOB] Chamado ${ticket.id_atendimento} já está fechado no HubGlpi. Ignorando.`) continue } if (await repository.notificationAlreadySent(ticket.id_atendimento)) { - logger.info(`[WATCHDOG] [JOB] Notificação já enviada para o chamado ${ticket.id_atendimento}.`) + logInfo(`[WATCHDOG] [JOB] Notificação já enviada para o chamado ${ticket.id_atendimento}.`) continue } - await repository.insertAsPending(ticket.id_atendimento) + await repository.markNotificationsAsPending([ticket.id_atendimento]) } const ticketsToNotify = await repository.getPendingTicketsForNotification() - logger.info(`[WATCHDOG] [JOB] ${ticketsToNotify.length} chamados pendentes para notificação.`) + logInfo(`[WATCHDOG] [JOB] ${ticketsToNotify.length} chamados pendentes para notificação.`) - const payload = await model.prepareNotificationPayload(ticketsToNotify) - - if (!payload.length) { - logger.info('[WATCHDOG] [JOB] Nenhuma notificação para enviar') + if (!ticketsToNotify.length) { + logInfo('[WATCHDOG] [JOB] Nenhum chamado pendente para notificação') return } + const payload = await model.prepareNotificationPayload(ticketsToNotify) + const hubsoftTicketIds = ticketsToNotify.map(t => Number(t.hubsoft_ticket_id)).filter(Number.isFinite); + try { await repository.sendClosureNotifications(payload) - await repository.markNotificationsAsSent(ticketsToNotify) + + + await repository.markNotificationsAsSent(hubsoftTicketIds) } catch (err) { - logger.error('[WATCHDOG] Erro ao enviar notificações', err) - await repository.markNotificationsAsFailed(ticketsToNotify) + logError('[WATCHDOG] Erro ao enviar notificações', err) + await repository.markNotificationsAsFailed(hubsoftTicketIds) } - logger.info(`[WATCHDOG] [JOB] Enviadas ${payload.length} notificações`) + logInfo(`[WATCHDOG] [JOB] Enviadas ${ticketsToNotify.length} notificações`) } +runWatchdog().catch((error) => { + logError('[WATCHDOG] [JOB] Erro ao executar o job do Watchdog', error) +}) + module.exports = { runWatchdog} \ No newline at end of file diff --git a/src/modules/watchdog/model/email.model.js b/src/modules/watchdog/model/email.model.js index a085998..1e97d27 100644 --- a/src/modules/watchdog/model/email.model.js +++ b/src/modules/watchdog/model/email.model.js @@ -19,35 +19,46 @@ function buildSubject(tickets) { } function buildBodyEmail(tickets) { - let body = ` -

🚨 Atenção!

-

O goleiro defendeu os seguintes chamados:

-

Esses chamados foram fechados no Hubsoft, mas ainda constam como abertos no GLPI.

-
-
    - `; - - tickets.forEach(ticket => { - body += ` -
  • - Hubsoft ID: ${ticket.hubsoft_ticket_id}
    - GLPI ID: ${ticket.glpi_ticket_id ?? 'não encontrado'}
    - Fechado em: ${formatDate(ticket.hubsoft_closed_at)} -
  • -
    - `; - }); + let body = ` +

    🚨 Atenção!

    +

    O goleiro defendeu os seguintes chamados:

    +

    Esses chamados foram fechados no Hubsoft, mas ainda constam como abertos no GLPI.

    +
    +
      + `; + tickets.forEach(ticket => { body += ` -
    -
    -

    ⚽ Favor verificar e alinhar os status no GLPI.

    -

    Watchdog Hub × GLPI

    +
  • + Protocolo Hubsoft: ${ticket.protocolo_hub}
    + Mundiale ID: ${ticket.ticket_mundiale}
    + GLPI ID: ${ticket.glpi_ticket_id ?? 'não encontrado'}
    + Fechado em: ${formatDate(ticket.closed_at)} +
  • +
    `; + }); - return body; + body += ` +
+
+

⚽ Favor verificar e alinhar os status no GLPI.

+

Watchdog Hub × GLPI

+ `; + + return body; } +function formatDate(value) { + if (!value) return 'não informado'; + + const d = new Date(value); + if (Number.isNaN(d.getTime())) return String(value); // fallback seguro + + return d.toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' }); +} + + function formatDate(date) { try { diff --git a/src/modules/watchdog/repository/watchdog.repository.js b/src/modules/watchdog/repository/watchdog.repository.js index 33669e2..7a78998 100644 --- a/src/modules/watchdog/repository/watchdog.repository.js +++ b/src/modules/watchdog/repository/watchdog.repository.js @@ -4,6 +4,7 @@ const wdRepository = require('../../../infra/db/repositories/hubglpi/watchdog.repository.js'); const hubsoftTicketsRepo = require('../../../infra/db/repositories/hubsoft/tickets.repository.js'); const hubglpiSyncRepo = require('../../../infra/db/repositories/hubglpi/sync.repository.js'); +const senderMail = require('../../../infra/mail/sender.js') /** * Busca tickets encerrados no Hubsoft a partir de uma data @@ -16,7 +17,7 @@ async function getClosedTicketsSince(thresholdDate) { * Verifica se um ticket do Hubsoft existe no GLPI e seu status de sync */ async function checkTicketInHubGlpi(hubsoftTicketId) { - const syncData = await hubglpiSyncRepo.getSyncByHubsoftTicketId(hubsoftTicketId); + const syncData = await hubglpiSyncRepo.getSyncIdByHubsoftId(hubsoftTicketId); if (!syncData) { return { @@ -42,7 +43,7 @@ async function notificationAlreadySent(hubsoftTicketId) { * Envia notificações de encerramento */ async function sendClosureNotifications(payload) { - return enviadordeEmailNaoseicomoserafeito.sendClosureNotifications(payload); + return senderMail.send(payload); } /** @@ -59,11 +60,24 @@ async function markNotificationsAsFailed(hubsoftTicketIds, error = null) { return wdRepository.markNotificationsAsFailed(hubsoftTicketIds, error); } +/** + * Marca notificações como pendentes + */ +async function markNotificationsAsPending(hubsoftTicketId, error = null) { + return wdRepository.markNotificationsAsPending(hubsoftTicketId, error); +} + +async function getPendingTicketsForNotification() { + return wdRepository.getPendingTicketsForNotification(); +} + module.exports = { getClosedTicketsSince, checkTicketInHubGlpi, notificationAlreadySent, sendClosureNotifications, markNotificationsAsSent, - markNotificationsAsFailed + markNotificationsAsFailed, + markNotificationsAsPending, + getPendingTicketsForNotification }; diff --git a/src/shared/utils/sendEmail.js b/src/shared/utils/sendEmail.js deleted file mode 100644 index e69de29..0000000 From f0ff2ba1296302701dedb3998c366ca8ad595712 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Thu, 15 Jan 2026 09:41:25 -0300 Subject: [PATCH 5/5] REMOVE: Removendo conteudo de commits antigos --- src/controller/closureController.js | 48 ----------------------------- 1 file changed, 48 deletions(-) delete mode 100644 src/controller/closureController.js diff --git a/src/controller/closureController.js b/src/controller/closureController.js deleted file mode 100644 index db76a3c..0000000 --- a/src/controller/closureController.js +++ /dev/null @@ -1,48 +0,0 @@ -const {fechaTicket} = require('../services/ticketService.js'); -const { logInfo, logError } = require('../utils/logger.js'); - -/** - * Controller para lidar com o webhook de fechamento de ticket do GLPI. - * @param {import('express').Request} req - O objeto de requisição do Express. - * @param {import('express').Response} res - O objeto de resposta do Express. - */ - -const closeTicket = async (req, res) => { - try { - let rawData = ''; - const bodyRequest = req.body; - - req.on('data', chunk => { - rawData += chunk; - }); - - req.on('end', async () => { - let bodyRequest; - try { - bodyRequest = JSON.parse(rawData); - } catch (err) { - logError('Erro ao parsear JSON:', err); - bodyRequest = {}; - } - - const ticketId = bodyRequest.item.items_id; - logInfo(`Ticket ${ticketId} acionado para encerramento.`); - const closingTicket = await fechaTicket(bodyRequest); - res.status(200).json(closingTicket); - }); - - } catch (error) { - res.status(500).json({ error: error.message }); - } -} - - -module.exports = { closeTicket }; -/** - * @module ClosureController - * @description Este controller é o ponto de entrada para as requisições de webhook enviadas pelo GLPI quando um ticket é fechado. - * - * Funções: - * - `closeTicket(req, res)`: Recebe a notificação do GLPI, extrai os dados do corpo da requisição e invoca o `ticketService` para orquestrar o processo de fechamento do ticket correspondente no HubSoft e a atualização no banco de dados local. - * Ele é responsável por validar a requisição e responder ao GLPI com o status do processamento. - */ \ No newline at end of file