From 53bdd1c5a5b18dbe3a008ca4d9c1f5b9b78fb8f2 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 20 Jan 2026 17:36:28 -0300 Subject: [PATCH] =?UTF-8?q?FEATURE:=20Implementado=20servi=C3=A7o=20de=20n?= =?UTF-8?q?otifica=C3=A7=C3=A3o=20para=20Admin=20e=20colaboradores,=20Adic?= =?UTF-8?q?ionado=20validador=20de=20ticket=20updade,=20e=20modificado=20e?= =?UTF-8?q?mail=20para=20envio=20bonito?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repositories/glpi/tickets.repository.js | 21 ++- .../hubglpi/watchdog.repository.js | 94 +++++++++++-- .../hubsoft/tickets.repository.js | 12 +- .../tickets/useCases/syncTickets.usecase.js | 6 + src/modules/watchdog/job/job.js | 61 +-------- src/modules/watchdog/model/email.model.js | 125 +++++++++++------- .../repository/watchdog.repository.js | 25 +++- .../watchdog/services/notifyAdmins.service.js | 62 +++++++++ .../services/notifyCollaborators.service.js | 58 ++++++++ 9 files changed, 344 insertions(+), 120 deletions(-) create mode 100644 src/modules/watchdog/services/notifyAdmins.service.js create mode 100644 src/modules/watchdog/services/notifyCollaborators.service.js diff --git a/src/infra/db/repositories/glpi/tickets.repository.js b/src/infra/db/repositories/glpi/tickets.repository.js index 1c677a5..e9aa14c 100644 --- a/src/infra/db/repositories/glpi/tickets.repository.js +++ b/src/infra/db/repositories/glpi/tickets.repository.js @@ -45,11 +45,28 @@ async function insertTicket(ticketData) { const [result] = await db.query(query, values) return result.insertId } 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 } } module.exports = { - insertTicket + insertTicket, + checkTicketUpdateDate } diff --git a/src/infra/db/repositories/hubglpi/watchdog.repository.js b/src/infra/db/repositories/hubglpi/watchdog.repository.js index 145afad..50c829b 100644 --- a/src/infra/db/repositories/hubglpi/watchdog.repository.js +++ b/src/infra/db/repositories/hubglpi/watchdog.repository.js @@ -17,7 +17,7 @@ function buildStatus(kind, type) { /** * Verifica se a notificaÇõÇœo jÇ­ foi enviada com sucesso */ -async function notificationAlreadySentFunc(hubsoftTicketId, type = 'func') { +async function notificationAlreadySent(hubsoftTicketId, type = 'func') { const status = buildStatus('sent', type); const query = ` SELECT 1 @@ -38,12 +38,12 @@ async function notificationAlreadySentFunc(hubsoftTicketId, type = 'func') { /** * Marca tickets como pendente de notificaÇõÇœo */ -async function markNotificationsAsPendingFunc(hubsoftTicketIds, type = 'func') { +async function markNotificationsAsPending(hubsoftTicketIds, type = 'func') { if (!hubsoftTicketIds || hubsoftTicketIds.length === 0) { return; } - const status = buildStatus('pending', type); + const status = buildStatus('pending', type); // Define o status como 'pending_func' ou 'pending_adm' const query = ` INSERT INTO watchdog_notifications (ticket_id, notified_at, status) SELECT @@ -68,7 +68,7 @@ async function markNotificationsAsPendingFunc(hubsoftTicketIds, type = 'func') { /** * Marca tickets como notificados com sucesso */ -async function markNotificationsAsSentFunc(hubsoftTicketIds, type = 'func') { +async function markNotificationsAsSent(hubsoftTicketIds, type = 'func') { const ids = (hubsoftTicketIds || []) .map(id => Number(id)) .filter(Number.isFinite); @@ -99,7 +99,7 @@ async function markNotificationsAsSentFunc(hubsoftTicketIds, type = 'func') { /** * Marca tickets como falha de notificaÇõÇœo */ -async function markNotificationsAsFailedFunc(hubsoftTicketIds, type = 'func') { +async function markNotificationsAsFailed(hubsoftTicketIds, type = 'func') { const ids = (hubsoftTicketIds || []) .map(id => Number(id)) .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 query = ` 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 = { - notificationAlreadySent: notificationAlreadySentFunc, - markNotificationsAsSent: markNotificationsAsSentFunc, - markNotificationsAsFailed: markNotificationsAsFailedFunc, - markNotificationsAsPending: markNotificationsAsPendingFunc, - getPendingTicketsForNotification: getPendingTicketsForNotificationFunc + notificationAlreadySent, + markNotificationsAsSent, + markNotificationsAsFailed, + markNotificationsAsPending, + markNotificationsAsCompleted, + getPendingTicketsForNotification, + getSentNotificationsBefore, + getTicketsPendingResponse }; diff --git a/src/infra/db/repositories/hubsoft/tickets.repository.js b/src/infra/db/repositories/hubsoft/tickets.repository.js index 4668b60..a30a72d 100644 --- a/src/infra/db/repositories/hubsoft/tickets.repository.js +++ b/src/infra/db/repositories/hubsoft/tickets.repository.js @@ -101,18 +101,18 @@ async function getTicketsClosedSince(thresholdDate) { if (process.env.HUBSOFT_MOCK_ENABLED === 'true') { return [ { - id_atendimento: 2780, - protocolo: '20260106155510498970', + id_atendimento: 2949, + protocolo: '20260120133512641803', hubsoft_closed_at: new Date('2026-01-06T08:20:45') }, { - id_atendimento: 2769, - protocolo: '20260105170715994323', + id_atendimento: 2950, + protocolo: '20260120133618108141', hubsoft_closed_at: new Date('2026-01-06T10:02:13') }, { - id_atendimento: 2779, - protocolo: '20260106145016864639', + id_atendimento: 2955, + protocolo: '20260120134024457448', hubsoft_closed_at: new Date('2025-12-18T14:35:56') } ] diff --git a/src/modules/tickets/useCases/syncTickets.usecase.js b/src/modules/tickets/useCases/syncTickets.usecase.js index f59b713..a48589a 100644 --- a/src/modules/tickets/useCases/syncTickets.usecase.js +++ b/src/modules/tickets/useCases/syncTickets.usecase.js @@ -65,7 +65,13 @@ async function syncTicketsUseCase() { if (!service) continue 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) + } catch (err) { logError(err, `[USECASE] Falha ao processar ticket ${ticket.id_atendimento}`) } diff --git a/src/modules/watchdog/job/job.js b/src/modules/watchdog/job/job.js index 6dbc7e5..4267ba5 100644 --- a/src/modules/watchdog/job/job.js +++ b/src/modules/watchdog/job/job.js @@ -3,67 +3,18 @@ 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') +const { notifyCollaborators } = require('../services/notifyCollaborators.service.js') +const { notifyAdmins } = require('../services/notifyAdmins.service.js') 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') - - 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`) + logInfo(`[WATCHDOG] [JOB] Enviadas ${sentFunc} notificacoes para colaboradores e ${sentAdm} para ADM.`) } - runWatchdog().catch((error) => { logError('[WATCHDOG] [JOB] Erro ao executar o job do Watchdog', error) }) -module.exports = { runWatchdog} +module.exports = { runWatchdog } diff --git a/src/modules/watchdog/model/email.model.js b/src/modules/watchdog/model/email.model.js index 1e97d27..69a6093 100644 --- a/src/modules/watchdog/model/email.model.js +++ b/src/modules/watchdog/model/email.model.js @@ -1,83 +1,118 @@ // src/modules/watchdog/model/email.model.js -function prepareNotificationPayload(tickets) { - return { - subject: buildSubject(tickets), - bodyEmail: buildBodyEmail(tickets), - recipients: getRecipients(), - cc: getCc() - }; +function prepareNotificationPayload(tickets, type = 'func') { + const normalizedType = String(type || 'func').toLowerCase() + const isAdm = normalizedType === 'adm' + + return { + subject: buildSubject(tickets, normalizedType), + bodyEmail: isAdm ? buildBodyEmailAdm(tickets) : buildBodyEmail(tickets), + recipients: getRecipients(normalizedType), + cc: getCc(normalizedType) + } } -function buildSubject(tickets) { - +function buildSubject(tickets, type = 'func') { + if (type === 'adm') { 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) { let body = ` -

🚨 Atenção!

-

O goleiro defendeu os seguintes chamados:

-

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

+

🧤 Atenção, time!

+

O goleiro entrou em ação e 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.

+

⚠️ Favor verificar e alinhar os status no GLPI.

Watchdog Hub × GLPI

- `; + ` - return body; + return body +} + +function buildBodyEmailAdm(tickets) { + let body = ` +

🚨⚽ Pênalti!

+

Os seguintes chamados passaram da defesa inicial:

+

+ Esses chamados foram fechados no Hubsoft há mais de 1 hora + e ainda não tiveram atualização no GLPI. +

+
+ +
+

📢 Favor acionar o time responsável e alinhar os status no GLPI.

+

Watchdog Hub × GLPI

+ ` + + return body } function formatDate(value) { - if (!value) return 'não informado'; + if (!value) return 'não informado' - const d = new Date(value); - if (Number.isNaN(d.getTime())) return String(value); // fallback seguro + const d = new Date(value) + 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() { - return process.env.WATCHDOG_RECIPIENT_EMAILS?.split(',') || []; + return process.env.WATCHDOG_RECIPIENT_EMAILS?.split(',') || [] } function getCc() { - return process.env.WATCHDOG_CC_EMAILS?.split(',') || []; + return process.env.WATCHDOG_CC_EMAILS?.split(',') || [] } - module.exports = { - prepareNotificationPayload -}; \ No newline at end of file + prepareNotificationPayload +} diff --git a/src/modules/watchdog/repository/watchdog.repository.js b/src/modules/watchdog/repository/watchdog.repository.js index b26f78e..9f164c9 100644 --- a/src/modules/watchdog/repository/watchdog.repository.js +++ b/src/modules/watchdog/repository/watchdog.repository.js @@ -5,6 +5,8 @@ const wdRepository = require('../../../infra/db/repositories/hubglpi/watchdog.re 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') +const glpiRepository = require('../../../infra/db/repositories/glpi/tickets.repository.js') + /** * Busca tickets encerrados no Hubsoft a partir de uma data @@ -71,13 +73,34 @@ async function getPendingTicketsForNotification(type = 'func') { 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 = { getClosedTicketsSince, checkTicketInHubGlpi, + checkTicketIsUpdated, notificationAlreadySent, sendClosureNotifications, markNotificationsAsSent, markNotificationsAsFailed, markNotificationsAsPending, - getPendingTicketsForNotification + getPendingTicketsForNotification, + getTicketsPendingResponse, + markNotificationAsCompleted }; diff --git a/src/modules/watchdog/services/notifyAdmins.service.js b/src/modules/watchdog/services/notifyAdmins.service.js new file mode 100644 index 0000000..9dd42b5 --- /dev/null +++ b/src/modules/watchdog/services/notifyAdmins.service.js @@ -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 } diff --git a/src/modules/watchdog/services/notifyCollaborators.service.js b/src/modules/watchdog/services/notifyCollaborators.service.js new file mode 100644 index 0000000..7ad8d6d --- /dev/null +++ b/src/modules/watchdog/services/notifyCollaborators.service.js @@ -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 }