FEATURE: Finalizado Feature Watchdog, responsável por coletar chamados pendentes de fechamento no GLPI e notificar algum grupo
This commit is contained in:
parent
1bf3f744ab
commit
4833310ec1
2
package-lock.json
generated
2
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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 })
|
||||
}
|
||||
})
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}
|
||||
@ -19,35 +19,46 @@ function buildSubject(tickets) {
|
||||
}
|
||||
|
||||
function buildBodyEmail(tickets) {
|
||||
let body = `
|
||||
<p>🚨 <strong>Atenção!</strong></p>
|
||||
<p>O goleiro defendeu os seguintes chamados:</p>
|
||||
<p>Esses chamados foram <strong>fechados no Hubsoft</strong>, mas ainda constam como <strong>abertos no GLPI</strong>.</p>
|
||||
<br>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
tickets.forEach(ticket => {
|
||||
body += `
|
||||
<li>
|
||||
Hubsoft ID: <strong>${ticket.hubsoft_ticket_id}</strong><br>
|
||||
GLPI ID: <strong>${ticket.glpi_ticket_id ?? 'não encontrado'}</strong><br>
|
||||
Fechado em: ${formatDate(ticket.hubsoft_closed_at)}
|
||||
</li>
|
||||
<br>
|
||||
`;
|
||||
});
|
||||
let body = `
|
||||
<p>🚨 <strong>Atenção!</strong></p>
|
||||
<p>O goleiro defendeu os seguintes chamados:</p>
|
||||
<p>Esses chamados foram <strong>fechados no Hubsoft</strong>, mas ainda constam como <strong>abertos no GLPI</strong>.</p>
|
||||
<br>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
tickets.forEach(ticket => {
|
||||
body += `
|
||||
</ul>
|
||||
<br>
|
||||
<p>⚽ Favor verificar e alinhar os status no GLPI.</p>
|
||||
<p><em>Watchdog Hub × GLPI</em></p>
|
||||
<li>
|
||||
Protocolo Hubsoft: <strong>${ticket.protocolo_hub}</strong><br>
|
||||
Mundiale ID: <strong>${ticket.ticket_mundiale}</strong><br>
|
||||
GLPI ID: <strong>${ticket.glpi_ticket_id ?? 'não encontrado'}</strong><br>
|
||||
Fechado em: ${formatDate(ticket.closed_at)}
|
||||
</li>
|
||||
<br>
|
||||
`;
|
||||
});
|
||||
|
||||
return body;
|
||||
body += `
|
||||
</ul>
|
||||
<br>
|
||||
<p>⚽ Favor verificar e alinhar os status no GLPI.</p>
|
||||
<p><em>Watchdog Hub × GLPI</em></p>
|
||||
`;
|
||||
|
||||
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 {
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user