From 4833310ec12b6dfac1fdd40495d6d2160407e050 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Wed, 14 Jan 2026 17:51:49 -0300 Subject: [PATCH] =?UTF-8?q?FEATURE:=20Finalizado=20Feature=20Watchdog,=20r?= =?UTF-8?q?espons=C3=A1vel=20por=20coletar=20chamados=20pendentes=20de=20f?= =?UTF-8?q?echamento=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.

-
- +
+

⚽ 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