Merge branch 'master' into feature/entity-creator

This commit is contained in:
Rafael Alves Lopes 2026-01-15 10:41:01 -03:00
commit c1f52d741f
15 changed files with 294 additions and 194 deletions

View File

@ -1,73 +1,58 @@
// ecosystem.config.js
module.exports = { module.exports = {
apps: [ apps: [
// 🟢 --- PRIMEIRA APLICAÇÃO: API PRINCIPAL (servidor HTTP) --- // 🟢 API PRINCIPAL
{ {
name: "hubxglpi-api", // Nome que aparecerá no PM2 name: "hubxglpi-api",
script: "src/infra/http/server.js", // Caminho do arquivo principal da API script: "src/infra/http/server.js",
// 👇 Execução em modo "cluster" (um processo por core da máquina)
exec_mode: "cluster", exec_mode: "cluster",
instances: "max", // "max" = usa todos os núcleos disponíveis instances: "max",
// ⚙️ Variáveis de ambiente padrão (modo development)
env: { env: {
watch: true,
NODE_ENV: "development", NODE_ENV: "development",
PORT: 3000 // Porta usada no ambiente de desenvolvimento PORT: 3000
}, },
// ⚙️ Variáveis de ambiente quando rodar com `--env production`
env_production: { env_production: {
watch: false,
NODE_ENV: "production", NODE_ENV: "production",
PORT: 8080 // 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 name: "hubxglpi-cron",
script: "src/infra/cron/sync.cron.js", // Arquivo onde ficam as tarefas agendadas script: "src/infra/cron/sync.cron.js",
// 👇 Modo "fork" = apenas 1 instância, sem cluster (evita rodar crons duplicados)
exec_mode: "fork", 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: { env: {
watch: true,
NODE_ENV: "development" NODE_ENV: "development"
}, },
// ⚙️ Variáveis de ambiente para produção
env_production: { env_production: {
watch: false,
NODE_ENV: "production" 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.
*/

2
package-lock.json generated
View File

@ -2090,7 +2090,6 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"pg-connection-string": "^2.9.1", "pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1", "pg-pool": "^3.10.1",
@ -3114,7 +3113,6 @@
"resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz",
"integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@colors/colors": "^1.6.0", "@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.8", "@dabh/diagnostics": "^2.0.8",

View File

@ -1,18 +1,18 @@
// src/infra/cron/observer.cron.js // src/infra/cron/observer.cron.js
const cron = require('node-cron') 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') const runWatchdog = require('../../modules/watchdog/job/job')
logger.info('[CRON] 🐶 Watchdog cron iniciado') logInfo('[CRON] 🐶 Watchdog cron iniciado')
cron.schedule('*/30 * * * *', async () => { cron.schedule('*/30 * * * *', async () => {
logger.info('[CRON] 🐶 Watchdog executando verificação') logInfo('[CRON] 🐶 Watchdog executando verificação')
try { try {
await runWatchdog() await runWatchdog()
} catch (error) { } catch (error) {
logger.error('[CRON] ❌ Erro no Watchdog', { error }) logError('[CRON] ❌ Erro no Watchdog', { error })
} }
}) })

View File

@ -1,6 +1,6 @@
// src/infra/db/repositories/hubglpi/watchdog.repository.js // 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'); const { logError } = require('../../../../shared/utils/logger.js');
/** /**
@ -23,11 +23,10 @@ async function notificationAlreadySent(hubsoftTicketId) {
throw error; 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) { if (!hubsoftTicketIds || hubsoftTicketIds.length === 0) {
return; return;
} }
@ -37,39 +36,11 @@ async function markNotificationsAsSent(hubsoftTicketIds) {
SELECT SELECT
unnest($1::bigint[]), unnest($1::bigint[]),
NOW(), NOW(),
'SENT' 'pending'
ON CONFLICT (ticket_id) ON CONFLICT (ticket_id)
DO UPDATE SET DO UPDATE SET
notified_at = EXCLUDED.notified_at, notified_at = EXCLUDED.notified_at,
status = 'SENT'; status = 'pending';
`;
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';
`; `;
try { 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 = { module.exports = {
notificationAlreadySent, notificationAlreadySent,
markNotificationsAsSent, markNotificationsAsSent,
markNotificationsAsFailed markNotificationsAsFailed,
markNotificationsAsPending,
getPendingTicketsForNotification
}; };

View File

@ -97,6 +97,30 @@ async function getTicketsByTipo({
async function getTicketsClosedSince(thresholdDate) { async function getTicketsClosedSince(thresholdDate) {
try { 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; const mundialeUserId = process.env.HUBSOFT_MUNDIALE_USER_ID;
if (!mundialeUserId) { if (!mundialeUserId) {

View File

@ -1,16 +1,15 @@
// src/infra/mail/sender.js // src/infra/mail/sender.js
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const logger = require('../../shared/utils/logger.js'); const {logError, logInfo} = require('../../shared/utils/logger.js');
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST, host: process.env.MAIL_HOST,
port: Number(process.env.MAIL_PORT), port: Number(process.env.MAIL_PORT),
secure: process.env.MAIL_SECURE === 'true', secure: Number(process.env.MAIL_PORT) === 465, // 465 = TLS direto; 587/25 = STARTTLS
auth: { tls: {
user: process.env.MAIL_USER, rejectUnauthorized: false, // aceita self-signed
pass: process.env.MAIL_PASS },
}
}); });
@ -38,11 +37,11 @@ async function send({ subject, bodyEmail, recipients, cc = [] }) {
try { try {
const info = await transporter.sendMail(mailOptions); 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; return info;
} catch (error) { } catch (error) {
logger.error('[MAIL] Erro ao enviar email', error); logError('[MAIL] Erro ao enviar email', error);
throw error; throw error;
} }
} }

View File

@ -33,7 +33,7 @@ const TYPES = Object.freeze({
async function getMundialeTickets(watermark) { async function getMundialeTickets(watermark) {
return hubsoftTicketsRepo.getTicketsByTipo({ return hubsoftTicketsRepo.getTicketsByTipo({
tipoAtendimento: TYPES.MUNDIALE, tipoAtendimento: TYPES.MUNDIALE,
usuarioAbertura: 248, usuarioAbertura: process.env.HUBSOFT_MUNDIALE_USER_ID,
watermark watermark
}); });
} }
@ -66,27 +66,27 @@ async function getTrocaTTickets(watermark) {
}); });
} }
async function insertTicketsHubGlpi(tickets){ async function insertTicketsHubGlpi(tickets) {
return hubglpiTicketsRepo.insertTickets(tickets) return hubglpiTicketsRepo.insertTickets(tickets)
} }
async function insertSyncDataByIds(ids){ async function insertSyncDataByIds(ids) {
return hubglpiSyncRepo.insertSyncData(ids) return hubglpiSyncRepo.insertSyncData(ids)
} }
async function fetchPendingTickets(){ async function fetchPendingTickets() {
return hubglpiTicketsRepo.fetchPendingTickets() return hubglpiTicketsRepo.fetchPendingTickets()
} }
async function insertTicketGlpi(ticket){ async function insertTicketGlpi(ticket) {
return glpiTicketsRepo.insertTicket(ticket) return glpiTicketsRepo.insertTicket(ticket)
} }
async function getEntitiesByService(codigoCliente, codigoServico){ async function getEntitiesByService(codigoCliente, codigoServico) {
return glpiEntitiesRepo.getEntitiesByService(codigoCliente, codigoServico) return glpiEntitiesRepo.getEntitiesByService(codigoCliente, codigoServico)
} }
async function getEntitiesByClient(codigoCliente){ async function getEntitiesByClient(codigoCliente) {
return glpiEntitiesRepo.getEntitiesByClient(codigoCliente) return glpiEntitiesRepo.getEntitiesByClient(codigoCliente)
} }
@ -98,7 +98,7 @@ const GROUP_BY_TYPE = {
SAC: 'NOC' SAC: 'NOC'
} }
async function insertGroupTicket(id, type){ async function insertGroupTicket(id, type) {
const group = GROUP_BY_TYPE[type] || 'NOC' const group = GROUP_BY_TYPE[type] || 'NOC'
if (group === 'IMPLANTACAO') { if (group === 'IMPLANTACAO') {
@ -108,11 +108,11 @@ async function insertGroupTicket(id, type){
return glpiGroupsRepo.insertGroupNOC(id) return glpiGroupsRepo.insertGroupNOC(id)
} }
async function updateSyncDataCreated(hubId, glpiId){ async function updateSyncDataCreated(hubId, glpiId) {
return hubglpiSyncRepo.updateSyncDataCreated(hubId, glpiId) return hubglpiSyncRepo.updateSyncDataCreated(hubId, glpiId)
} }
async function sendHubsoftMessage(hubId, message){ async function sendHubsoftMessage(hubId, message) {
return hubsoftApiClient.sendHubsoftMessage(hubId, message) return hubsoftApiClient.sendHubsoftMessage(hubId, message)
} }

View File

@ -5,7 +5,7 @@ const repository = require('../repositories/ticket.repository.js')
async function resolveEntityId(ticketData) { async function resolveEntityId(ticketData) {
const entityByService = await repository.getEntitiesByService( const entityByService = await repository.getEntitiesByService(
ticketData.codigo_cliente, ticketData.codigo_clasiente,
ticketData.codigo_servico ticketData.codigo_servico
); );

View File

@ -1,7 +1,7 @@
//src/modules/tickes/useCases/syncTickets.usecase.js //src/modules/tickes/useCases/syncTickets.usecase.js
const notifyTicketCreated = require('../services/ticketNotifications.service.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 mundialeService = require('../services/mundiale.service.js')
const implantacaoService = require('../services/implantacao.service.js') const implantacaoService = require('../services/implantacao.service.js')
const cancelamentoService = require('../services/cancelamento.service.js') const cancelamentoService = require('../services/cancelamento.service.js')
@ -25,31 +25,31 @@ async function syncTicketsUseCase() {
const mundiale = await mundialeService.fetchNew(waterMark) const mundiale = await mundialeService.fetchNew(waterMark)
logInfo(`[USECASE] ${mundiale.length} tickets Mundiale encontrados`) logInfo(`[USECASE] ${mundiale.length} tickets Mundiale encontrados`)
const implantacao = await implantacaoService.fetchNew(waterMark) //const implantacao = await implantacaoService.fetchNew(waterMark)
logInfo(`[USECASE] ${implantacao.length} tickets Implantação encontrados`) //logInfo(`[USECASE] ${implantacao.length} tickets Implantação encontrados`)
const cancelamento = await cancelamentoService.fetchNew(waterMark) //const cancelamento = await cancelamentoService.fetchNew(waterMark)
logInfo(`[USECASE] ${cancelamento.length} tickets Cancelamento encontrados`) //logInfo(`[USECASE] ${cancelamento.length} tickets Cancelamento encontrados`)
//const sac = await sacService.fetchNew(waterMark) //TODO //const sac = await sacService.fetchNew(waterMark) //TODO
//logInfo(`[USECASE] ${sac.length} tickets SAC encontrados`) //logInfo(`[USECASE] ${sac.length} tickets SAC encontrados`)
const trocaTitularidade = await trocaTitularidadeService.fetchNew(waterMark) //TODO //const trocaTitularidade = await trocaTitularidadeService.fetchNew(waterMark) //TODO
logInfo(`[USECASE] ${trocaTitularidade.length} tickets Troca de Titularidade encontrados`) //logInfo(`[USECASE] ${trocaTitularidade.length} tickets Troca de Titularidade encontrados`)
await mundialeService.saveHubGlpi(mundiale) await mundialeService.saveHubGlpi(mundiale)
await implantacaoService.saveHubGlpi(implantacao) //await implantacaoService.saveHubGlpi(implantacao)
await cancelamentoService.saveHubGlpi(cancelamento) //await cancelamentoService.saveHubGlpi(cancelamento)
//await sacService.saveHubGlpi(sac) //TODO //await sacService.saveHubGlpi(sac) //TODO
await trocaTitularidadeService.saveHubGlpi(trocaTitularidade) //TODO //await trocaTitularidadeService.saveHubGlpi(trocaTitularidade)
const allFetchedTickets = [ const allFetchedTickets = [
...mundiale, ...mundiale,
...implantacao, //...implantacao,
...cancelamento, //...cancelamento,
//...sac, //...sac,
...trocaTitularidade //...trocaTitularidade
] ]
const newWaterMark = resolveNewWatermark(allFetchedTickets, waterMark) const newWaterMark = resolveNewWatermark(allFetchedTickets, waterMark)
@ -72,7 +72,7 @@ async function syncTicketsUseCase() {
} catch (err) { } catch (err) {
logError(err, `[USECASE] Falha ao processar ticket ${ticket.id_atendimento}`) logError(err, `[USECASE] Falha ao processar ticket ${ticket.id_atendimento}`)
} }
} }
} }
@ -80,10 +80,10 @@ async function syncTicketsUseCase() {
function resolveTicketService(type) { function resolveTicketService(type) {
const map = { const map = {
MUNDIALE: mundialeService, MUNDIALE: mundialeService,
IMPLANTACAO: implantacaoService, //IMPLANTACAO: implantacaoService,
CANCELAMENTO: cancelamentoService, //CANCELAMENTO: cancelamentoService,
//SAC: sacService, //TODO //SAC: sacService, //TODO
TITULARIDADE: trocaTitularidadeService //TODO //TITULARIDADE: trocaTitularidadeService
} }
return map[type] return map[type]

View File

@ -1,58 +1,67 @@
// src/modules/watchdog/job/job.js // 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 repository = require('../repository/watchdog.repository.js')
const model = require('../model/email.model.js') const model = require('../model/email.model.js')
async function runWatchdog() { 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 thresholdDate = new Date(Date.now() - 31 * 60 * 1000)
const closedTickets = await repository.getClosedTicketsSince(thresholdDate) 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) { for (const ticket of closedTickets) {
const hubGlpiTicket = await repository.checkTicketInHubGlpi(ticket.id_atendimento) const hubGlpiTicket = await repository.checkTicketInHubGlpi(ticket.id_atendimento)
if (!hubGlpiTicket.exists) { 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 continue
} }
if (hubGlpiTicket.status === 'closed') { 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 continue
} }
if (await repository.notificationAlreadySent(ticket.id_atendimento)) { 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 continue
} }
await repository.insertAsPending(ticket.id_atendimento) await repository.markNotificationsAsPending([ticket.id_atendimento])
} }
const ticketsToNotify = await repository.getPendingTicketsForNotification() 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 (!ticketsToNotify.length) {
logInfo('[WATCHDOG] [JOB] Nenhum chamado pendente para notificação')
if (!payload.length) {
logger.info('[WATCHDOG] [JOB] Nenhuma notificação para enviar')
return return
} }
const payload = await model.prepareNotificationPayload(ticketsToNotify)
const hubsoftTicketIds = ticketsToNotify.map(t => Number(t.hubsoft_ticket_id)).filter(Number.isFinite);
try { try {
await repository.sendClosureNotifications(payload) await repository.sendClosureNotifications(payload)
await repository.markNotificationsAsSent(ticketsToNotify)
await repository.markNotificationsAsSent(hubsoftTicketIds)
} catch (err) { } catch (err) {
logger.error('[WATCHDOG] Erro ao enviar notificações', err) logError('[WATCHDOG] Erro ao enviar notificações', err)
await repository.markNotificationsAsFailed(ticketsToNotify) 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} module.exports = { runWatchdog}

View File

@ -30,9 +30,10 @@ function buildBodyEmail(tickets) {
tickets.forEach(ticket => { tickets.forEach(ticket => {
body += ` body += `
<li> <li>
Hubsoft ID: <strong>${ticket.hubsoft_ticket_id}</strong><br> 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> GLPI ID: <strong>${ticket.glpi_ticket_id ?? 'não encontrado'}</strong><br>
Fechado em: ${formatDate(ticket.hubsoft_closed_at)} Fechado em: ${formatDate(ticket.closed_at)}
</li> </li>
<br> <br>
`; `;
@ -48,6 +49,16 @@ function buildBodyEmail(tickets) {
return body; 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) { function formatDate(date) {
try { try {

View File

@ -4,6 +4,7 @@
const wdRepository = require('../../../infra/db/repositories/hubglpi/watchdog.repository.js'); const wdRepository = require('../../../infra/db/repositories/hubglpi/watchdog.repository.js');
const hubsoftTicketsRepo = require('../../../infra/db/repositories/hubsoft/tickets.repository.js'); const hubsoftTicketsRepo = require('../../../infra/db/repositories/hubsoft/tickets.repository.js');
const hubglpiSyncRepo = require('../../../infra/db/repositories/hubglpi/sync.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 * 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 * Verifica se um ticket do Hubsoft existe no GLPI e seu status de sync
*/ */
async function checkTicketInHubGlpi(hubsoftTicketId) { async function checkTicketInHubGlpi(hubsoftTicketId) {
const syncData = await hubglpiSyncRepo.getSyncByHubsoftTicketId(hubsoftTicketId); const syncData = await hubglpiSyncRepo.getSyncIdByHubsoftId(hubsoftTicketId);
if (!syncData) { if (!syncData) {
return { return {
@ -42,7 +43,7 @@ async function notificationAlreadySent(hubsoftTicketId) {
* Envia notificações de encerramento * Envia notificações de encerramento
*/ */
async function sendClosureNotifications(payload) { 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); 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 = { module.exports = {
getClosedTicketsSince, getClosedTicketsSince,
checkTicketInHubGlpi, checkTicketInHubGlpi,
notificationAlreadySent, notificationAlreadySent,
sendClosureNotifications, sendClosureNotifications,
markNotificationsAsSent, markNotificationsAsSent,
markNotificationsAsFailed markNotificationsAsFailed,
markNotificationsAsPending,
getPendingTicketsForNotification
}; };