Merge branch 'master' into feature/entity-creator
This commit is contained in:
commit
c1f52d741f
@ -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
2
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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 })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -52,9 +52,9 @@ async function sendToGlpi(ticket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
fetchNew,
|
fetchNew,
|
||||||
saveHubGlpi,
|
saveHubGlpi,
|
||||||
sendToGlpi
|
sendToGlpi
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -52,9 +52,9 @@ async function sendToGlpi(ticket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
fetchNew,
|
fetchNew,
|
||||||
saveHubGlpi,
|
saveHubGlpi,
|
||||||
sendToGlpi
|
sendToGlpi
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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')
|
||||||
@ -22,34 +22,34 @@ async function syncTicketsUseCase() {
|
|||||||
const waterMark = await repository.getWaterMark()
|
const waterMark = await repository.getWaterMark()
|
||||||
logInfo(`Buscando Tickets novos desde de: ${waterMark}`)
|
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`)
|
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)
|
||||||
@ -63,16 +63,16 @@ async function syncTicketsUseCase() {
|
|||||||
logInfo(`[USECASE] ${pendentes.length} tickets pendentes para envio ao GLPI`)
|
logInfo(`[USECASE] ${pendentes.length} tickets pendentes para envio ao GLPI`)
|
||||||
|
|
||||||
for (const ticket of pendentes) {
|
for (const ticket of pendentes) {
|
||||||
try {
|
try {
|
||||||
const service = resolveTicketService(ticket.ticket_type)
|
const service = resolveTicketService(ticket.ticket_type)
|
||||||
if (!service) continue
|
if (!service) continue
|
||||||
|
|
||||||
const glpiId = await service.sendToGlpi(ticket)
|
const glpiId = await service.sendToGlpi(ticket)
|
||||||
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}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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]
|
||||||
|
|||||||
@ -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}
|
||||||
@ -19,35 +19,46 @@ function buildSubject(tickets) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildBodyEmail(tickets) {
|
function buildBodyEmail(tickets) {
|
||||||
let body = `
|
let body = `
|
||||||
<p>🚨 <strong>Atenção!</strong></p>
|
<p>🚨 <strong>Atenção!</strong></p>
|
||||||
<p>O goleiro defendeu os seguintes chamados:</p>
|
<p>O goleiro defendeu os seguintes chamados:</p>
|
||||||
<p>Esses chamados foram <strong>fechados no Hubsoft</strong>, mas ainda constam como <strong>abertos no GLPI</strong>.</p>
|
<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 => {
|
|
||||||
body += `
|
|
||||||
<li>
|
|
||||||
Hubsoft ID: <strong>${ticket.hubsoft_ticket_id}</strong><br>
|
|
||||||
GLPI ID: <strong>${ticket.glpi_ticket_id ?? 'não encontrado'}</strong><br>
|
|
||||||
Fechado em: ${formatDate(ticket.hubsoft_closed_at)}
|
|
||||||
</li>
|
|
||||||
<br>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
tickets.forEach(ticket => {
|
||||||
body += `
|
body += `
|
||||||
</ul>
|
<li>
|
||||||
<br>
|
Protocolo Hubsoft: <strong>${ticket.protocolo_hub}</strong><br>
|
||||||
<p>⚽ Favor verificar e alinhar os status no GLPI.</p>
|
Mundiale ID: <strong>${ticket.ticket_mundiale}</strong><br>
|
||||||
<p><em>Watchdog Hub × GLPI</em></p>
|
GLPI ID: <strong>${ticket.glpi_ticket_id ?? 'não encontrado'}</strong><br>
|
||||||
|
Fechado em: ${formatDate(ticket.closed_at)}
|
||||||
|
</li>
|
||||||
|
<br>
|
||||||
`;
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
return body;
|
body += `
|
||||||
|
</ul>
|
||||||
|
<br>
|
||||||
|
<p>⚽ Favor verificar e alinhar os status no GLPI.</p>
|
||||||
|
<p><em>Watchdog Hub × GLPI</em></p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDate(value) {
|
||||||
|
if (!value) return 'não informado';
|
||||||
|
|
||||||
|
const d = new Date(value);
|
||||||
|
if (Number.isNaN(d.getTime())) return String(value); // fallback seguro
|
||||||
|
|
||||||
|
return d.toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function formatDate(date) {
|
function formatDate(date) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user