FEATURE: Implementado serviço de notificação para Admin e colaboradores, Adicionado validador de ticket updade, e modificado email para envio bonito

This commit is contained in:
Rafael Alves Lopes 2026-01-20 17:36:28 -03:00
parent ad28159185
commit 53bdd1c5a5
9 changed files with 344 additions and 120 deletions

View File

@ -45,11 +45,28 @@ async function insertTicket(ticketData) {
const [result] = await db.query(query, values) const [result] = await db.query(query, values)
return result.insertId return result.insertId
} catch (err) { } catch (err) {
logError('Erro ao inserir ticket no GLPI', err) logError('[GLPI][REPOSITORY]Erro ao inserir ticket no GLPI', err)
throw err
}
}
async function checkTicketUpdateDate(glpiTicketId) {
const query = `
SELECT date_mod
FROM glpi_tickets
WHERE id = ?
`
try {
const [rows] = await db.query(query, [glpiTicketId])
return rows[0]
} catch (err) {
logError('[GLPI][REPOSITORY]Erro ao verificar data de atualização do ticket no GLPI', err)
throw err throw err
} }
} }
module.exports = { module.exports = {
insertTicket insertTicket,
checkTicketUpdateDate
} }

View File

@ -17,7 +17,7 @@ function buildStatus(kind, type) {
/** /**
* Verifica se a notificaÇõÇœo ­ foi enviada com sucesso * Verifica se a notificaÇõÇœo ­ foi enviada com sucesso
*/ */
async function notificationAlreadySentFunc(hubsoftTicketId, type = 'func') { async function notificationAlreadySent(hubsoftTicketId, type = 'func') {
const status = buildStatus('sent', type); const status = buildStatus('sent', type);
const query = ` const query = `
SELECT 1 SELECT 1
@ -38,12 +38,12 @@ async function notificationAlreadySentFunc(hubsoftTicketId, type = 'func') {
/** /**
* Marca tickets como pendente de notificaÇõÇœo * Marca tickets como pendente de notificaÇõÇœo
*/ */
async function markNotificationsAsPendingFunc(hubsoftTicketIds, type = 'func') { async function markNotificationsAsPending(hubsoftTicketIds, type = 'func') {
if (!hubsoftTicketIds || hubsoftTicketIds.length === 0) { if (!hubsoftTicketIds || hubsoftTicketIds.length === 0) {
return; return;
} }
const status = buildStatus('pending', type); const status = buildStatus('pending', type); // Define o status como 'pending_func' ou 'pending_adm'
const query = ` const query = `
INSERT INTO watchdog_notifications (ticket_id, notified_at, status) INSERT INTO watchdog_notifications (ticket_id, notified_at, status)
SELECT SELECT
@ -68,7 +68,7 @@ async function markNotificationsAsPendingFunc(hubsoftTicketIds, type = 'func') {
/** /**
* Marca tickets como notificados com sucesso * Marca tickets como notificados com sucesso
*/ */
async function markNotificationsAsSentFunc(hubsoftTicketIds, type = 'func') { async function markNotificationsAsSent(hubsoftTicketIds, type = 'func') {
const ids = (hubsoftTicketIds || []) const ids = (hubsoftTicketIds || [])
.map(id => Number(id)) .map(id => Number(id))
.filter(Number.isFinite); .filter(Number.isFinite);
@ -99,7 +99,7 @@ async function markNotificationsAsSentFunc(hubsoftTicketIds, type = 'func') {
/** /**
* Marca tickets como falha de notificaÇõÇœo * Marca tickets como falha de notificaÇõÇœo
*/ */
async function markNotificationsAsFailedFunc(hubsoftTicketIds, type = 'func') { async function markNotificationsAsFailed(hubsoftTicketIds, type = 'func') {
const ids = (hubsoftTicketIds || []) const ids = (hubsoftTicketIds || [])
.map(id => Number(id)) .map(id => Number(id))
.filter(Number.isFinite); .filter(Number.isFinite);
@ -127,8 +127,39 @@ async function markNotificationsAsFailedFunc(hubsoftTicketIds, type = 'func') {
} }
} }
/**
* Marca tickets como fluxo concluido
*/
async function markNotificationsAsCompleted(hubsoftTicketIds, type = 'func') {
const ids = (hubsoftTicketIds || [])
.map(id => Number(id))
.filter(Number.isFinite);
async function getPendingTicketsForNotificationFunc(type = 'func') { if (ids.length === 0) return;
const status = buildStatus('completed', type);
const query = `
INSERT INTO watchdog_notifications (ticket_id, notified_at, status)
SELECT
unnest($1::bigint[]),
NOW(),
$2
ON CONFLICT (ticket_id)
DO UPDATE SET
notified_at = EXCLUDED.notified_at,
status = $2;
`;
try {
await db.query(query, [ids, status]);
} catch (error) {
logError('[WATCHDOG][REPOSITORY] Erro ao marcar notificaÇÎÇæÇÎÇÝes como concluido', error);
throw error;
}
}
async function getPendingTicketsForNotification(type = 'func') {
const status = buildStatus('pending', type); const status = buildStatus('pending', type);
const query = ` const query = `
SELECT SELECT
@ -154,10 +185,51 @@ async function getPendingTicketsForNotificationFunc(type = 'func') {
} }
} }
/**
* Busca chamados com status sent_func com notified_at anterior a thresholdDate
*/
async function getSentNotificationsBefore(thresholdDate, type = 'func') {
const status = buildStatus('sent', type);
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 = $1 AND wn.notified_at > $2;
`;
//
try {
const { rows } = await db.query(query, [status, thresholdDate]);
return rows;
} catch (error) {
logError('Erro ao buscar notificaÇõÇœes enviadas antes da data limite', error);
throw error;
}
}
async function getTicketsPendingResponse(sinceMinutes = 30, type = 'func') {
const thresholdDate = new Date(Date.now() - sinceMinutes * 60 * 1000);
return getSentNotificationsBefore(thresholdDate, type);
}
module.exports = { module.exports = {
notificationAlreadySent: notificationAlreadySentFunc, notificationAlreadySent,
markNotificationsAsSent: markNotificationsAsSentFunc, markNotificationsAsSent,
markNotificationsAsFailed: markNotificationsAsFailedFunc, markNotificationsAsFailed,
markNotificationsAsPending: markNotificationsAsPendingFunc, markNotificationsAsPending,
getPendingTicketsForNotification: getPendingTicketsForNotificationFunc markNotificationsAsCompleted,
getPendingTicketsForNotification,
getSentNotificationsBefore,
getTicketsPendingResponse
}; };

View File

@ -101,18 +101,18 @@ async function getTicketsClosedSince(thresholdDate) {
if (process.env.HUBSOFT_MOCK_ENABLED === 'true') { if (process.env.HUBSOFT_MOCK_ENABLED === 'true') {
return [ return [
{ {
id_atendimento: 2780, id_atendimento: 2949,
protocolo: '20260106155510498970', protocolo: '20260120133512641803',
hubsoft_closed_at: new Date('2026-01-06T08:20:45') hubsoft_closed_at: new Date('2026-01-06T08:20:45')
}, },
{ {
id_atendimento: 2769, id_atendimento: 2950,
protocolo: '20260105170715994323', protocolo: '20260120133618108141',
hubsoft_closed_at: new Date('2026-01-06T10:02:13') hubsoft_closed_at: new Date('2026-01-06T10:02:13')
}, },
{ {
id_atendimento: 2779, id_atendimento: 2955,
protocolo: '20260106145016864639', protocolo: '20260120134024457448',
hubsoft_closed_at: new Date('2025-12-18T14:35:56') hubsoft_closed_at: new Date('2025-12-18T14:35:56')
} }
] ]

View File

@ -65,7 +65,13 @@ async function syncTicketsUseCase() {
if (!service) continue if (!service) continue
const glpiId = await service.sendToGlpi(ticket) const glpiId = await service.sendToGlpi(ticket)
if (process.env.NODE_ENV !== 'production') {
logInfo(`[USECASE] Ambiente de desenvolvimento. Ignorando notificação do ticket ${ticket.id_atendimento}`)
continue
}
await notifyTicketCreated.notifyTicketCreated(ticket.id_atendimento, glpiId) await notifyTicketCreated.notifyTicketCreated(ticket.id_atendimento, glpiId)
} catch (err) { } catch (err) {
logError(err, `[USECASE] Falha ao processar ticket ${ticket.id_atendimento}`) logError(err, `[USECASE] Falha ao processar ticket ${ticket.id_atendimento}`)
} }

View File

@ -3,67 +3,18 @@
require('dotenv').config({ path: '.env.development' }) require('dotenv').config({ path: '.env.development' })
const { logError, logInfo } = require('../../../shared/utils/logger') const { logError, logInfo } = require('../../../shared/utils/logger')
const repository = require('../repository/watchdog.repository.js') const { notifyCollaborators } = require('../services/notifyCollaborators.service.js')
const model = require('../model/email.model.js') const { notifyAdmins } = require('../services/notifyAdmins.service.js')
async function runWatchdog() { async function runWatchdog() {
const { sentCount: sentFunc } = await notifyCollaborators({ thresholdMinutes: 31 })
const { sentCount: sentAdm } = await notifyAdmins({ sinceMinutes: 30 })
logInfo('[WATCHDOG] [JOB] Coletando chamados fechados hÇ­ 31 minutos') logInfo(`[WATCHDOG] [JOB] Enviadas ${sentFunc} notificacoes para colaboradores e ${sentAdm} para ADM.`)
const thresholdDate = new Date(Date.now() - 31 * 60 * 1000)
const closedTickets = await repository.getClosedTicketsSince(thresholdDate)
logInfo(`[WATCHDOG] [JOB] Encontrados ${closedTickets.length} chamados fechados`)
let notificationType = 'func'
for (const ticket of closedTickets) {
const hubGlpiTicket = await repository.checkTicketInHubGlpi(ticket.id_atendimento)
if (!hubGlpiTicket.exists) {
logInfo(`[WATCHDOG] [JOB] Chamado ${ticket.id_atendimento} nao encontrado no HubGlpi. Ignorando.`)
continue
}
if (hubGlpiTicket.status === 'closed') {
logInfo(`[WATCHDOG] [JOB] Chamado ${ticket.id_atendimento} ja esta fechado no HubGlpi. Ignorando.`)
continue
}
if (await repository.notificationAlreadySent(ticket.id_atendimento, notificationType)) {
logInfo(`[WATCHDOG] [JOB] NotificaÇõÇœo jÇ­ enviada para o chamado ${ticket.id_atendimento}.`)
continue
}
await repository.markNotificationsAsPending([ticket.id_atendimento], notificationType)
}
const ticketsToNotify = await repository.getPendingTicketsForNotification(notificationType)
logInfo(`[WATCHDOG] [JOB] ${ticketsToNotify.length} chamados pendentes para notificaÇõÇœo.`)
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(hubsoftTicketIds, notificationType)
} catch (err) {
logError('[WATCHDOG] Erro ao enviar notificaÇõÇæes', err)
await repository.markNotificationsAsFailed(hubsoftTicketIds, notificationType)
}
logInfo(`[WATCHDOG] [JOB] Enviadas ${ticketsToNotify.length} notificaÇõÇæes`)
} }
runWatchdog().catch((error) => { runWatchdog().catch((error) => {
logError('[WATCHDOG] [JOB] Erro ao executar o job do Watchdog', error) logError('[WATCHDOG] [JOB] Erro ao executar o job do Watchdog', error)
}) })
module.exports = { runWatchdog} module.exports = { runWatchdog }

View File

@ -1,83 +1,118 @@
// src/modules/watchdog/model/email.model.js // src/modules/watchdog/model/email.model.js
function prepareNotificationPayload(tickets) { function prepareNotificationPayload(tickets, type = 'func') {
const normalizedType = String(type || 'func').toLowerCase()
const isAdm = normalizedType === 'adm'
return { return {
subject: buildSubject(tickets), subject: buildSubject(tickets, normalizedType),
bodyEmail: buildBodyEmail(tickets), bodyEmail: isAdm ? buildBodyEmailAdm(tickets) : buildBodyEmail(tickets),
recipients: getRecipients(), recipients: getRecipients(normalizedType),
cc: getCc() cc: getCc(normalizedType)
}; }
} }
function buildSubject(tickets) { function buildSubject(tickets, type = 'func') {
if (type === 'adm') {
if (tickets.length === 0) { if (tickets.length === 0) {
return '[GOLEIRO] Nenhum chamado foi agarrado'; return '🧤⚽ [GOLEIRO ADM] Nenhum chamado foi para os pênaltis'
} }
return `🧤 [GOLEIRO] ${tickets.length} chamados foram agarrados`; return `🚨⚽ [GOLEIRO ADM] ${tickets.length} chamados foram para os pênaltis`
}
if (tickets.length === 0) {
return '🧤 [GOLEIRO] Nenhum chamado foi agarrado'
}
return `🧤 [GOLEIRO] ${tickets.length} chamados foram agarrados`
} }
function buildBodyEmail(tickets) { function buildBodyEmail(tickets) {
let body = ` let body = `
<p>🚨 <strong>Atenção!</strong></p> <p>🧤 <strong>Atenção, time!</strong></p>
<p>O goleiro defendeu os seguintes chamados:</p> <p>O goleiro entrou em ação e defendeu os seguintes chamados:</p>
<p>Esses chamados foram <strong>fechados no Hubsoft</strong>, mas ainda constam como <strong>abertos no GLPI</strong>.</p> <p>
Esses chamados foram <strong>fechados no Hubsoft</strong>,
mas ainda constam como <strong>abertos no GLPI</strong>.
</p>
<br> <br>
<ul> <ul>
`; `
tickets.forEach(ticket => { tickets.forEach(ticket => {
body += ` body += `
<li> <li>
Protocolo Hubsoft: <strong>${ticket.protocolo_hub}</strong><br> 🧾 <strong>Protocolo Hubsoft:</strong> ${ticket.protocolo_hub}<br>
Mundiale ID: <strong>${ticket.ticket_mundiale}</strong><br> 🧠 <strong>Mundiale ID:</strong> ${ticket.ticket_mundiale}<br>
GLPI ID: <strong>${ticket.glpi_ticket_id ?? 'não encontrado'}</strong><br> 🛠 <strong>GLPI ID:</strong> ${ticket.glpi_ticket_id ?? 'não encontrado'}<br>
Fechado em: ${formatDate(ticket.closed_at)} <strong>Fechado em:</strong> ${formatDate(ticket.closed_at)}
</li> </li>
<br> <br>
`; `
}); })
body += ` body += `
</ul> </ul>
<br> <br>
<p> Favor verificar e alinhar os status no GLPI.</p> <p> Favor verificar e alinhar os status no GLPI.</p>
<p><em>Watchdog Hub × GLPI</em></p> <p><em>Watchdog Hub × GLPI</em></p>
`; `
return body; return body
}
function buildBodyEmailAdm(tickets) {
let body = `
<p>🚨 <strong>Pênalti!</strong></p>
<p>Os seguintes chamados passaram da defesa inicial:</p>
<p>
Esses chamados foram <strong>fechados no Hubsoft mais de 1 hora</strong>
e ainda <strong>não tiveram atualização no GLPI</strong>.
</p>
<br>
<ul>
`
tickets.forEach(ticket => {
body += `
<li>
🧾 <strong>Protocolo Hubsoft:</strong> ${ticket.protocolo_hub}<br>
🧠 <strong>Mundiale ID:</strong> ${ticket.ticket_mundiale}<br>
🛠 <strong>GLPI ID:</strong> ${ticket.glpi_ticket_id ?? 'não encontrado'}<br>
<strong>Fechado em:</strong> ${formatDate(ticket.closed_at)}
</li>
<br>
`
})
body += `
</ul>
<br>
<p>📢 Favor acionar o time responsável e alinhar os status no GLPI.</p>
<p><em>Watchdog Hub × GLPI</em></p>
`
return body
} }
function formatDate(value) { function formatDate(value) {
if (!value) return 'não informado'; if (!value) return 'não informado'
const d = new Date(value); const d = new Date(value)
if (Number.isNaN(d.getTime())) return String(value); // fallback seguro if (Number.isNaN(d.getTime())) return String(value)
return d.toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' }); return d.toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })
} }
function formatDate(date) {
try {
return new Date(date).toLocaleString('pt-BR');
} catch {
return date;
}
}
function getRecipients() { function getRecipients() {
return process.env.WATCHDOG_RECIPIENT_EMAILS?.split(',') || []; return process.env.WATCHDOG_RECIPIENT_EMAILS?.split(',') || []
} }
function getCc() { function getCc() {
return process.env.WATCHDOG_CC_EMAILS?.split(',') || []; return process.env.WATCHDOG_CC_EMAILS?.split(',') || []
} }
module.exports = { module.exports = {
prepareNotificationPayload prepareNotificationPayload
}; }

View File

@ -5,6 +5,8 @@ const wdRepository = require('../../../infra/db/repositories/hubglpi/watchdog.re
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') const senderMail = require('../../../infra/mail/sender.js')
const glpiRepository = require('../../../infra/db/repositories/glpi/tickets.repository.js')
/** /**
* Busca tickets encerrados no Hubsoft a partir de uma data * Busca tickets encerrados no Hubsoft a partir de uma data
@ -71,13 +73,34 @@ async function getPendingTicketsForNotification(type = 'func') {
return wdRepository.getPendingTicketsForNotification(type); return wdRepository.getPendingTicketsForNotification(type);
} }
async function getTicketsPendingResponse(sinceMinutes = 30, type = 'func') {
return wdRepository.getTicketsPendingResponse(sinceMinutes, type);
}
async function checkTicketIsUpdated(glpiTicketId, sinceMinutes = 30) {
const ticketUpdateDate = await glpiRepository.checkTicketUpdateDate(glpiTicketId);
if (ticketUpdateDate.date_mod < new Date(Date.now() - sinceMinutes * 60000)) {
return false;
}
return true;
}
async function markNotificationAsCompleted(hubsoftTicketId, type = 'func') {
return wdRepository.markNotificationsAsCompleted([hubsoftTicketId], type);
}
module.exports = { module.exports = {
getClosedTicketsSince, getClosedTicketsSince,
checkTicketInHubGlpi, checkTicketInHubGlpi,
checkTicketIsUpdated,
notificationAlreadySent, notificationAlreadySent,
sendClosureNotifications, sendClosureNotifications,
markNotificationsAsSent, markNotificationsAsSent,
markNotificationsAsFailed, markNotificationsAsFailed,
markNotificationsAsPending, markNotificationsAsPending,
getPendingTicketsForNotification getPendingTicketsForNotification,
getTicketsPendingResponse,
markNotificationAsCompleted
}; };

View File

@ -0,0 +1,62 @@
// src/modules/watchdog/services/notifyAdmins.service.js
const { logError, logInfo } = require('../../../shared/utils/logger')
const repository = require('../repository/watchdog.repository.js')
const model = require('../model/email.model.js')
async function notifyAdmins({ sinceMinutes = 30 } = {}) {
logInfo(`[WATCHDOG] [ADM] Verificando chamados sem interacao ha mais de ${sinceMinutes} minutos`)
const ticketsPendingVerify = await repository.getTicketsPendingResponse(sinceMinutes, 'func')
logInfo(`[WATCHDOG] [ADM] ${ticketsPendingVerify.length} chamados com notificacao enviada e pendentes de resposta.`)
const notificationType = 'adm'
for (const ticket of ticketsPendingVerify) {
const hubsoftTicketId = Number(ticket.hubsoft_ticket_id ?? ticket.id_atendimento)
const glpiTicketId = Number(ticket.glpi_ticket_id)
if (!Number.isFinite(hubsoftTicketId)) {
logInfo('[WATCHDOG] [ADM] Ticket pendente sem id valido. Ignorando.')
continue
}
const isUpdated = await repository.checkTicketIsUpdated(glpiTicketId, sinceMinutes)
if (isUpdated) {
logInfo(`[WATCHDOG] [ADM] Chamado ${hubsoftTicketId} atualizado no GLPI. Encerrando fluxo.`)
await repository.markNotificationAsCompleted(hubsoftTicketId, 'func')
continue
}
if (await repository.notificationAlreadySent(hubsoftTicketId, notificationType)) {
logInfo(`[WATCHDOG] [ADM] Notificacao ADM ja enviada para o chamado ${hubsoftTicketId}.`)
continue
}
await repository.markNotificationsAsPending([hubsoftTicketId], notificationType)
}
const ticketsToNotify = await repository.getPendingTicketsForNotification(notificationType)
logInfo(`[WATCHDOG] [ADM] ${ticketsToNotify.length} chamados pendentes para notificacao.`)
if (!ticketsToNotify.length) {
return { sentCount: 0, failedCount: 0 }
}
const payload = await model.prepareNotificationPayload(ticketsToNotify, notificationType)
const hubsoftTicketIds = ticketsToNotify
.map(t => Number(t.hubsoft_ticket_id))
.filter(Number.isFinite)
try {
await repository.sendClosureNotifications(payload)
await repository.markNotificationsAsSent(hubsoftTicketIds, notificationType)
return { sentCount: ticketsToNotify.length, failedCount: 0 }
} catch (err) {
logError('[WATCHDOG] Erro ao enviar notificacoes para ADM', err)
await repository.markNotificationsAsFailed(hubsoftTicketIds, notificationType)
return { sentCount: 0, failedCount: hubsoftTicketIds.length }
}
}
module.exports = { notifyAdmins }

View File

@ -0,0 +1,58 @@
// src/modules/watchdog/services/notifyCollaborators.service.js
const { logError, logInfo } = require('../../../shared/utils/logger')
const repository = require('../repository/watchdog.repository.js')
const model = require('../model/email.model.js')
async function notifyCollaborators({ thresholdMinutes = 31 } = {}) {
logInfo(`[WATCHDOG] [FUNC] Coletando chamados fechados ha mais de ${thresholdMinutes} minutos`)
const thresholdDate = new Date(Date.now() - thresholdMinutes * 60 * 1000)
const closedTickets = await repository.getClosedTicketsSince(thresholdDate)
logInfo(`[WATCHDOG] [FUNC] Encontrados ${closedTickets.length} chamados fechados`)
const notificationType = 'func'
for (const ticket of closedTickets) {
const hubGlpiTicket = await repository.checkTicketInHubGlpi(ticket.id_atendimento)
if (!hubGlpiTicket.exists) {
logInfo(`[WATCHDOG] [FUNC] Chamado ${ticket.id_atendimento} nao encontrado no HubGlpi. Ignorando.`)
continue
}
if (hubGlpiTicket.status === 'closed') {
logInfo(`[WATCHDOG] [FUNC] Chamado ${ticket.id_atendimento} ja esta fechado no HubGlpi. Ignorando.`)
continue
}
if (await repository.notificationAlreadySent(ticket.id_atendimento, notificationType)) {
logInfo(`[WATCHDOG] [FUNC] Notificacao ja enviada para o chamado ${ticket.id_atendimento}.`)
continue
}
await repository.markNotificationsAsPending([ticket.id_atendimento], notificationType)
}
const ticketsToNotify = await repository.getPendingTicketsForNotification(notificationType)
logInfo(`[WATCHDOG] [FUNC] ${ticketsToNotify.length} chamados pendentes para notificacao.`)
if (!ticketsToNotify.length) {
return { sentCount: 0, failedCount: 0 }
}
const payload = await model.prepareNotificationPayload(ticketsToNotify, notificationType)
const hubsoftTicketIds = ticketsToNotify
.map(t => Number(t.hubsoft_ticket_id))
.filter(Number.isFinite)
try {
await repository.sendClosureNotifications(payload)
await repository.markNotificationsAsSent(hubsoftTicketIds, notificationType)
return { sentCount: ticketsToNotify.length, failedCount: 0 }
} catch (err) {
logError('[WATCHDOG] Erro ao enviar notificacoes para colaboradores', err)
await repository.markNotificationsAsFailed(hubsoftTicketIds, notificationType)
return { sentCount: 0, failedCount: hubsoftTicketIds.length }
}
}
module.exports = { notifyCollaborators }