diff --git a/ecosystem.config.js b/ecosystem.config.js index 786cb5f..984ae87 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -1,73 +1,58 @@ +// ecosystem.config.js + module.exports = { apps: [ - // š¢ --- PRIMEIRA APLICAĆĆO: API PRINCIPAL (servidor HTTP) --- + // š¢ API PRINCIPAL { - name: "hubxglpi-api", // Nome que aparecerĆ” no PM2 - script: "src/infra/http/server.js", // Caminho do arquivo principal da API + name: "hubxglpi-api", + script: "src/infra/http/server.js", - // š Execução em modo "cluster" (um processo por core da mĆ”quina) exec_mode: "cluster", - instances: "max", // "max" = usa todos os nĆŗcleos disponĆveis + instances: "max", - // āļø VariĆ”veis de ambiente padrĆ£o (modo development) env: { - watch: true, NODE_ENV: "development", - PORT: 3000 // Porta usada no ambiente de desenvolvimento + PORT: 3000 }, - // āļø VariĆ”veis de ambiente quando rodar com `--env production` env_production: { - watch: false, 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 - script: "src/infra/cron/sync.cron.js", // Arquivo onde ficam as tarefas agendadas + name: "hubxglpi-cron", + script: "src/infra/cron/sync.cron.js", - // š Modo "fork" = apenas 1 instĆ¢ncia, sem cluster (evita rodar crons duplicados) 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: { - watch: true, NODE_ENV: "development" }, - // āļø VariĆ”veis de ambiente para produção env_production: { - watch: false, 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. - */ \ No newline at end of file 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/tickets/repositories/ticket.repository.js b/src/modules/tickets/repositories/ticket.repository.js index 63fd175..1d5eaff 100644 --- a/src/modules/tickets/repositories/ticket.repository.js +++ b/src/modules/tickets/repositories/ticket.repository.js @@ -33,7 +33,7 @@ const TYPES = Object.freeze({ async function getMundialeTickets(watermark) { return hubsoftTicketsRepo.getTicketsByTipo({ tipoAtendimento: TYPES.MUNDIALE, - usuarioAbertura: 248, + usuarioAbertura: process.env.HUBSOFT_MUNDIALE_USER_ID, watermark }); } @@ -66,27 +66,27 @@ async function getTrocaTTickets(watermark) { }); } -async function insertTicketsHubGlpi(tickets){ +async function insertTicketsHubGlpi(tickets) { return hubglpiTicketsRepo.insertTickets(tickets) } -async function insertSyncDataByIds(ids){ +async function insertSyncDataByIds(ids) { return hubglpiSyncRepo.insertSyncData(ids) } -async function fetchPendingTickets(){ +async function fetchPendingTickets() { return hubglpiTicketsRepo.fetchPendingTickets() } -async function insertTicketGlpi(ticket){ +async function insertTicketGlpi(ticket) { return glpiTicketsRepo.insertTicket(ticket) } -async function getEntitiesByService(codigoCliente, codigoServico){ +async function getEntitiesByService(codigoCliente, codigoServico) { return glpiEntitiesRepo.getEntitiesByService(codigoCliente, codigoServico) } -async function getEntitiesByClient(codigoCliente){ +async function getEntitiesByClient(codigoCliente) { return glpiEntitiesRepo.getEntitiesByClient(codigoCliente) } @@ -98,7 +98,7 @@ const GROUP_BY_TYPE = { SAC: 'NOC' } -async function insertGroupTicket(id, type){ +async function insertGroupTicket(id, type) { const group = GROUP_BY_TYPE[type] || 'NOC' if (group === 'IMPLANTACAO') { @@ -108,11 +108,11 @@ async function insertGroupTicket(id, type){ return glpiGroupsRepo.insertGroupNOC(id) } -async function updateSyncDataCreated(hubId, glpiId){ +async function updateSyncDataCreated(hubId, glpiId) { return hubglpiSyncRepo.updateSyncDataCreated(hubId, glpiId) } -async function sendHubsoftMessage(hubId, message){ +async function sendHubsoftMessage(hubId, message) { return hubsoftApiClient.sendHubsoftMessage(hubId, message) } diff --git a/src/modules/tickets/services/cancelamento.service.js b/src/modules/tickets/services/cancelamento.service.js index 9237aa0..3227ebd 100644 --- a/src/modules/tickets/services/cancelamento.service.js +++ b/src/modules/tickets/services/cancelamento.service.js @@ -16,7 +16,7 @@ async function fetchNew(watermark) { } async function saveHubGlpi(tickets) { - if (!tickets.length) return + if (!tickets.length) return logInfo('[CANCELAMENTO] Inserindo chamados no HubGlpi') await repository.insertTicketsHubGlpi(tickets) logInfo('[CANCELAMENTO] Inserindo dado de sincronia dos chamados') @@ -31,7 +31,7 @@ async function sendToGlpi(ticket) { const resolved = await ticketEntityResolver.resolveEntityId(ticket) logInfo('[CANCELAMENTO] Entidade resolvida') - const payload = cancelamentoGlpiModel.toGlpiPayload(resolved) + const payload = cancelamentoGlpiModel.toGlpiPayload(resolved) logInfo('[CANCELAMENTO] Payload preparado para GLPI', payload) logInfo('[CANCELAMENTO] Inserindo ticket no GLPI', { id_atendimento: ticket.id_atendimento }) @@ -40,7 +40,7 @@ async function sendToGlpi(ticket) { await repository.insertGroupTicket(glpiId, 'CANCELAMENTO') logInfo('[CANCELAMENTO] Grupo associado ao ticket GLPI', { glpiId, group: 'CANCELAMENTO' }) - + await repository.updateSyncDataCreated(ticket.id_atendimento, glpiId) logInfo('[CANCELAMENTO] Dados de sincronização atualizados', { id_atendimento: ticket.id_atendimento, glpiId }) @@ -52,9 +52,9 @@ async function sendToGlpi(ticket) { } module.exports = { - fetchNew, - saveHubGlpi, - sendToGlpi + fetchNew, + saveHubGlpi, + sendToGlpi } /** diff --git a/src/modules/tickets/services/mundiale.service.js b/src/modules/tickets/services/mundiale.service.js index 5c7b7fa..0bf34e3 100644 --- a/src/modules/tickets/services/mundiale.service.js +++ b/src/modules/tickets/services/mundiale.service.js @@ -16,7 +16,7 @@ async function fetchNew(watermark) { } async function saveHubGlpi(tickets) { - if (!tickets.length) return + if (!tickets.length) return logInfo('[MUNDIALE] Inserindo chamados no HubGlpi') await repository.insertTicketsHubGlpi(tickets) logInfo('[MUNDIALE] Inserindo dado de sincronia dos chamados') @@ -40,7 +40,7 @@ async function sendToGlpi(ticket) { await repository.insertGroupTicket(glpiId, 'MUNDIALE') logInfo('[MUNDIALE] Grupo associado ao ticket GLPI', { glpiId, group: 'MUNDIALE' }) - + await repository.updateSyncDataCreated(ticket.id_atendimento, glpiId) logInfo('[MUNDIALE] Dados de sincronização atualizados', { id_atendimento: ticket.id_atendimento, glpiId }) @@ -52,9 +52,9 @@ async function sendToGlpi(ticket) { } module.exports = { - fetchNew, - saveHubGlpi, - sendToGlpi + fetchNew, + saveHubGlpi, + sendToGlpi } /** diff --git a/src/modules/tickets/services/resolveTicketEntity.service.js b/src/modules/tickets/services/resolveTicketEntity.service.js index 3e8d730..e065534 100644 --- a/src/modules/tickets/services/resolveTicketEntity.service.js +++ b/src/modules/tickets/services/resolveTicketEntity.service.js @@ -5,14 +5,14 @@ const repository = require('../repositories/ticket.repository.js') async function resolveEntityId(ticketData) { const entityByService = await repository.getEntitiesByService( - ticketData.codigo_cliente, + ticketData.codigo_clasiente, ticketData.codigo_servico ); if (entityByService) { - return { - ...ticketData, - entities_id: entityByService + return { + ...ticketData, + entities_id: entityByService } } @@ -21,15 +21,15 @@ async function resolveEntityId(ticketData) { ); if (entityByClient) { - return { - ...ticketData, - entities_id: entityByClient + return { + ...ticketData, + entities_id: entityByClient } } - return { - ...ticketData, - entities_id: 0 + return { + ...ticketData, + entities_id: 0 } } diff --git a/src/modules/tickets/useCases/syncTickets.usecase.js b/src/modules/tickets/useCases/syncTickets.usecase.js index cfd6e50..ba71824 100644 --- a/src/modules/tickets/useCases/syncTickets.usecase.js +++ b/src/modules/tickets/useCases/syncTickets.usecase.js @@ -1,7 +1,7 @@ //src/modules/tickes/useCases/syncTickets.usecase.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 implantacaoService = require('../services/implantacao.service.js') const cancelamentoService = require('../services/cancelamento.service.js') @@ -22,34 +22,34 @@ async function syncTicketsUseCase() { const waterMark = await repository.getWaterMark() logInfo(`Buscando Tickets novos desde de: ${waterMark}`) - const mundiale = await mundialeService.fetchNew(waterMark) + const mundiale = await mundialeService.fetchNew(waterMark) logInfo(`[USECASE] ${mundiale.length} tickets Mundiale encontrados`) - const implantacao = await implantacaoService.fetchNew(waterMark) - logInfo(`[USECASE] ${implantacao.length} tickets Implantação encontrados`) + //const implantacao = await implantacaoService.fetchNew(waterMark) + //logInfo(`[USECASE] ${implantacao.length} tickets Implantação encontrados`) - const cancelamento = await cancelamentoService.fetchNew(waterMark) - logInfo(`[USECASE] ${cancelamento.length} tickets Cancelamento encontrados`) + //const cancelamento = await cancelamentoService.fetchNew(waterMark) + //logInfo(`[USECASE] ${cancelamento.length} tickets Cancelamento encontrados`) //const sac = await sacService.fetchNew(waterMark) //TODO //logInfo(`[USECASE] ${sac.length} tickets SAC encontrados`) - - const trocaTitularidade = await trocaTitularidadeService.fetchNew(waterMark) //TODO - logInfo(`[USECASE] ${trocaTitularidade.length} tickets Troca de Titularidade encontrados`) - + + //const trocaTitularidade = await trocaTitularidadeService.fetchNew(waterMark) //TODO + //logInfo(`[USECASE] ${trocaTitularidade.length} tickets Troca de Titularidade encontrados`) + await mundialeService.saveHubGlpi(mundiale) - await implantacaoService.saveHubGlpi(implantacao) - await cancelamentoService.saveHubGlpi(cancelamento) + //await implantacaoService.saveHubGlpi(implantacao) + //await cancelamentoService.saveHubGlpi(cancelamento) //await sacService.saveHubGlpi(sac) //TODO - await trocaTitularidadeService.saveHubGlpi(trocaTitularidade) //TODO + //await trocaTitularidadeService.saveHubGlpi(trocaTitularidade) const allFetchedTickets = [ ...mundiale, - ...implantacao, - ...cancelamento, + //...implantacao, + //...cancelamento, //...sac, - ...trocaTitularidade + //...trocaTitularidade ] const newWaterMark = resolveNewWatermark(allFetchedTickets, waterMark) @@ -63,16 +63,16 @@ async function syncTicketsUseCase() { logInfo(`[USECASE] ${pendentes.length} tickets pendentes para envio ao GLPI`) for (const ticket of pendentes) { - try { - const service = resolveTicketService(ticket.ticket_type) - if (!service) continue + try { + const service = resolveTicketService(ticket.ticket_type) + if (!service) continue - const glpiId = await service.sendToGlpi(ticket) - await notifyTicketCreated.notifyTicketCreated(ticket.id_atendimento, glpiId) - } catch (err) { - logError(err, `[USECASE] Falha ao processar ticket ${ticket.id_atendimento}`) + const glpiId = await service.sendToGlpi(ticket) + await notifyTicketCreated.notifyTicketCreated(ticket.id_atendimento, glpiId) + } catch (err) { + logError(err, `[USECASE] Falha ao processar ticket ${ticket.id_atendimento}`) + } } -} } @@ -80,10 +80,10 @@ async function syncTicketsUseCase() { function resolveTicketService(type) { const map = { MUNDIALE: mundialeService, - IMPLANTACAO: implantacaoService, - CANCELAMENTO: cancelamentoService, + //IMPLANTACAO: implantacaoService, + //CANCELAMENTO: cancelamentoService, //SAC: sacService, //TODO - TITULARIDADE: trocaTitularidadeService //TODO + //TITULARIDADE: trocaTitularidadeService } return map[type] 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.
-šØ 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
+ā½ 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