REFACTOR: Integração refatorada utilizando conceitos de clean architeture e monolito modular

This commit is contained in:
Rafael Alves Lopes 2026-01-02 18:19:11 -03:00
parent 2cd00c65c5
commit 783eb2f081
72 changed files with 2328 additions and 2240 deletions

View File

@ -3,10 +3,10 @@
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start:api": "cross-env NODE_ENV=production node src/server.js",
"dev:api": "cross-env NODE_ENV=development nodemon src/server.js",
"start:worker": "cross-env NODE_ENV=production node src/cron.js",
"dev:worker": "cross-env NODE_ENV=development nodemon src/cron.js",
"start:api": "cross-env NODE_ENV=production node src/infra/http/server.js",
"dev:api": "cross-env NODE_ENV=development nodemon src/infra/http/server.js",
"start:worker": "cross-env NODE_ENV=production node src/infra/cron/sync.cron.js",
"dev:worker": "cross-env NODE_ENV=development nodemon src/infra/cron/sync.cron.js",
"test": "echo \"Error: no test specified\" && exit 1",
"postinstall": "node -e \"const fs = require('fs'); if (fs.existsSync('.env') && !fs.existsSync('.env.development')) { fs.copyFileSync('.env', '.env.development'); console.log('✅ .env.development criado a partir do .env'); }\""
},

View File

@ -1,3 +1,4 @@
// src/config/db.config.js
module.exports = {
hubsoft: {
databaseHost: process.env.HUBSOFT_DATABASE_HOST,

View File

@ -1,56 +0,0 @@
const {fechaTicket} = require('../services/ticketService.js');
const { logInfo, logError } = require('../utils/logger.js');
/**
* Controller para lidar com o webhook de fechamento de ticket do GLPI.
* @param {import('express').Request} req - O objeto de requisição do Express.
* @param {import('express').Response} res - O objeto de resposta do Express.
*/
const closeTicket = async (req, res) => {
let rawData = '';
req.on('data', chunk => {
rawData += chunk;
});
req.on('end', async () => {
try {
if (!rawData) {
logError('Webhook de fechamento recebido com corpo vazio.');
return res.status(400).json({ error: 'Corpo da requisição ausente.' });
}
const bodyRequest = JSON.parse(rawData);
if (!bodyRequest || !bodyRequest.item || !bodyRequest.item.items_id) {
logError('Webhook de fechamento recebido com dados incompletos.', bodyRequest);
return res.status(400).json({ error: 'Dados do ticket ausentes no corpo da requisição.' });
}
const ticketId = bodyRequest.item.items_id;
logInfo(`Ticket ${ticketId} acionado para encerramento.`);
const closingTicket = await fechaTicket(bodyRequest);
res.status(200).json(closingTicket);
} catch (error) {
if (error instanceof SyntaxError) {
logError('Erro ao parsear JSON do webhook de fechamento:', error.message);
res.status(400).json({ status: 'error', message: 'Corpo da requisição não é um JSON válido.' });
} else {
logError(`Erro ao processar webhook de fechamento:`, error);
res.status(500).json({ error: error.message });
}
}
});
}
module.exports = { closeTicket };
/**
* @module ClosureController
* @description Este controller é o ponto de entrada para as requisições de webhook enviadas pelo GLPI quando um ticket é fechado.
*
* Funções:
* - `closeTicket(req, res)`: Recebe a notificação do GLPI, extrai os dados do corpo da requisição e invoca o `ticketService` para orquestrar o processo de fechamento do ticket correspondente no HubSoft e a atualização no banco de dados local.
* Ele é responsável por validar a requisição e responder ao GLPI com o status do processamento.
*/

View File

@ -1,55 +0,0 @@
// src/controller/commentController.js
const commentService = require('../services/commentService.js');
const { logInfo, logError, logWarning } = require('../utils/logger.js');
/**
* Controller para lidar com o webhook de novo comentário do GLPI.
* @param {import('express').Request} req - O objeto de requisição do Express.
* @param {import('express').Response} res - O objeto de resposta do Express.
*/
async function handleNewComment(req, res) {
let rawData = '';
req.on('data', chunk => {
rawData += chunk;
});
req.on('end', async () => {
try {
if (!rawData) {
logWarning('Webhook de novo comentário recebido com corpo vazio.');
return res.status(400).json({ error: 'Corpo da requisição ausente.' });
}
const body = JSON.parse(rawData);
const { item } = body;
// Validação básica para garantir que os dados necessários existem.
if (!item || !item.items_id || !item.content) {
logWarning('Webhook de novo comentário recebido com dados incompletos.', body);
return res.status(400).json({ error: 'Dados do comentário ou ID do ticket ausentes.' });
}
const glpiTicketId = item.items_id;
const commentContent = item.content;
const messageId = item.id;
logInfo(`Webhook de novo comentário recebido para o ticket GLPI ID ${glpiTicketId}.`);
await commentService.syncGlpiCommentToHubsoft(glpiTicketId, messageId, commentContent);
res.status(200).json({ status: 'success', message: 'Comentário recebido e processado.' });
} catch (error) {
// Verifica se o erro é de parsing de JSON
if (error instanceof SyntaxError) {
logError('Erro ao parsear JSON do webhook de comentário:', error.message);
res.status(400).json({ status: 'error', message: 'Corpo da requisição não é um JSON válido.' });
} else {
logError(`Erro ao processar webhook de comentário:`, error);
res.status(500).json({ status: 'error', message: error.message });
}
}
});
}
module.exports = { handleNewComment };

View File

@ -1,320 +0,0 @@
// controller/processController.js
const hubsoftModel = require('../model/hubsoftModel.js');
const hubglpiModel = require('../model/hubglpiModel.js');
const glpiModel = require('../model/glpiModel.js');
const { logError, logInfo } = require('../utils/logger');
// ================================================================================
// Constantes e Configurações
// ================================================================================
const statusAtendimentoHubGlpi = {
1: 'Pendente',
2: 'Em atendimento',
3: 'Resolvido',
31: 'Pendente',
32: 'Pendente',
33: 'Novo'
};
const statusAtendimentoGLPI = {
'Novo': 1,
'Pendente': 4,
'Em atendimento': 2,
'Resolvido': 5
};
const categoriaGLPI = {
'Lan-to-Lan 100 Mbps' : 5708 ,
'Link de Internet Dedicado 2 Gbps Full Duplex' : 5707 ,
'Lan-to-Lan' : 5707 ,
'Lan-to-Lan 500 Mbps' : 5708 ,
'Link de internet - Banda Larga' : 5708 ,
'Lan-to-Lan 700 Mbps' : 5708 ,
'Link de Internet Dedicado 20 Mbps Full Duplex' : 5707 ,
'Lan-to-Lan 300 Mbps' : 5708 ,
'Link de Internet Dedicado 5Gbs + Burst 5Gbs Burs Full Duplex Anti DDOS' : 5707 ,
'Link de Internet - Banda Larga - 500 Mbps' : 5708 ,
'Link de Internet Dedicado 600Mbps Full Duplex' : 5707 ,
'Link de Internet - Banda Larga - 1Gbps' : 5707 ,
'Link de Internet Dedicado 1Gbps Full Duplex' : 5707 ,
'Link de Internet Dedicado Full Duplex' : 5707 ,
'Fibra Apagada' : 5706 ,
'Link de Internet - Banda Larga - 100 Mbps' : 5708 ,
'Canal de Voz IP' : 5715 ,
'Link de Internet Dedicado 500Mbps Full Duplex' : 5707 ,
'Lan-to-Lan 200 Mbps' : 5708 ,
'Lan-to-Lan 50 Mbps' : 5708 ,
'Linha IP - Limitado' : 5715 ,
'PABX IP Cloud' : 5717 ,
'Link de Internet Dedicado 100 Mbps Full Duplex' : 5707 ,
'Link de Internet Dedicado 200 Mbps Full Duplex' : 5707 ,
'Link de Internet - Banda Larga - 200 Mbps' : 5708 ,
'Link de Internet - Banda Larga - 300 Mbps' : 5708 ,
'Link de Internet - Banda Larga - 700 Mbps' : 5708 ,
'Link de Internet Dedicado 300 Mbps Full Duplex' : 5707 ,
'Link de Internet Dedicado 50Mbps Full Duplex' : 5707 ,
'Link de Internet Dedicado 10 Mbps Full Duplex' : 5707 ,
'Link de Internet Dedicado 30 Mbps Full Duplex' : 5707 ,
'PABX IP - Até 30 ramais com telefones IPs (CAOA ESTADO DE SP)' : 5717 ,
'Lan-to-Lan 1GB' : 5707 ,
'PABX IP - Até 30 ramais com telefones IPs (CAOA OUTROS ESTADOS)' : 5717 ,
'Link de Internet Dedicado 700MB Full Duplex' : 5707 ,
'Link de Internet Dedicado - Temporário' : 5707
}
// ================================================================================
// Funções Utilitárias
// ================================================================================
// Formata a descrição do ticket em HTML
const formatDescription = (ticketData) => {
let htmlDescription = `
<table style="width:100%; border-collapse: collapse;">
<tr style="background-color:#f2f2f2;">
<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Campo</th>
<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Valor</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Nome:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.cliente_nome}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Codigo:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.codigo_cliente}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Serviço:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.servico_nome}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Ticket Mundiale</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.ticket_mundiale}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Protocolo Hub:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.protocolo_hub || 'N/A'}</td>
</tr>
</table>
`;
return htmlDescription;
};
// Formata os dados do ticket para o GLPI
const formatTicketDataForGlpi = async (ticketData) => {
const formattedData = {
...ticketData,
status_atendimento: statusAtendimentoGLPI[ticketData.status_atendimento] || 1,
date_mod: new Date(),
user_id_recipient: process.env.GLPI_USER || 1,
descricao_abertura: formatDescription(ticketData),
urgency: 3,
impact: 3,
priority: 3,
type: 1,
itilcategories_id: categoriaGLPI[ticketData.servico_nome] || 1,
date_creation: new Date(),
// entidades_id: 0 //await glpiModel.selectEntityId() //TODO: Implementar a busca da entidade
};
return formattedData;
};
// ================================================================================
// Funções de Integração
// ================================================================================
const createGlpiTicket = async (ticketData) => {
try {
const formattedTicketData = await formatTicketDataForGlpi(ticketData);
const glpiTicket = await glpiModel.insertTicket(formattedTicketData);
logInfo(`Ticket criado no GLPI: ${glpiTicket.insertId} `);
//Atualiza que ticket foi criado
ticketData.status_sync = 'created_glpi';
ticketData.glpi_ticket_id = glpiTicket.insertId;
ticketData.created_at = new Date();
ticketData.updated_at = new Date();
const updateSyncData = await hubglpiModel.update_syncData(ticketData);
logInfo(`Sync Data atualizado com o ID do ticket do GLPI: ${updateSyncData.glpi_ticket_id}`);
try {
//Insetindo ao grupo Operacao NOC
await glpiModel.insertGroupTickets(glpiTicket.insertId);
logInfo(`Atribuido grupo Operação NOC ao ticket no GLPI: ${glpiTicket.insertId} `)
} catch (error) {
const updateSyncDataError = await hubglpiModel.update_syncaDataError(error.message, ticketData.id_atendimento)
logError(`Erro ao criar ticket no GLPI. Sync Data atualizado com a mensagem de erro: ${error}`);
}
} catch (error) {
const updateSyncDataError = await hubglpiModel.update_syncaDataError(error.message, ticketData.id_atendimento)
logError(`Erro ao criar ticket no GLPI. Sync Data atualizado com a mensagem de erro: ${error}`);
}
};
const processTicketFromHubSoft = async (atendimento) => {
const ticketData = {
id_atendimento: atendimento.id_atendimento,
id_atendimento_status: atendimento.id_atendimento_status,
codigo_servico: String(atendimento.id_cliente_servico || ''), // Garante que seja uma string
protocolo_hub: atendimento.protocolo,
servico_nome: atendimento.descricao,
descricao_abertura: atendimento.descricao_abertura,
data_cadastro: atendimento.data_cadastro,
cliente_nome: atendimento.nome_contato,
codigo_cliente: atendimento.codigo_cliente,
descricao: atendimento.descricao
};
ticketData.status_atendimento = statusAtendimentoHubGlpi[ticketData.id_atendimento_status] || ticketData.id_atendimento_status;
ticketData.descricao_abertura = ticketData.descricao_abertura.replace(/[^0-9]/g, '');
ticketData.ticket_mundiale = parseInt(ticketData.descricao_abertura) || null;
return ticketData;
};
const saveTicketToHubGlpi = async (ticketData) => {
try {
const insertedTicket = await hubglpiModel.insertTicket(ticketData);
logInfo(`Ticket inserido/atualizado na tabela hubsoft_tickets: ${insertedTicket.protocolo_hub}`);
await hubglpiModel.insertSyncData(ticketData.id_atendimento);
logInfo('Dados inseridos/atualizados na tabela sync_data');
return insertedTicket;
} catch (error) {
logError(`Erro ao salvar ticket no hubglpi: ${error}`);
throw error; // Rejeita a promise para que o erro seja tratado no nível superior
}
};
const processAtendimento = async (ticketData) => {
try {
const glpiTicketId = await hubglpiModel.getGlpiTicketIdByAtendimentoId(ticketData.id_atendimento);
if (glpiTicketId) {
logInfo(`Ticket já inserido no GLPI para id_atendimento ${ticketData.id_atendimento}, pulando criação.`);
ticketData.status_sync = 'created_glpi';
ticketData.glpi_ticket_id = glpiTicketId;
await hubglpiModel.update_syncData(ticketData);
return;
}
const titulo = `Mundiale - Protocolo: ${ticketData.ticket_mundiale} - ${ticketData.cliente_nome}`;
ticketData.titulo = titulo;
// Busca a entidade primeiro pelo código do serviço
// Se não encontrar, busca pelo código do cliente
// Se ainda assim não encontrar, atribui 0 (contratos ativos)
const selectedEntityCodServico = await glpiModel.selectEntityIdCodServico(ticketData.codigo_cliente, ticketData.codigo_servico);
if (selectedEntityCodServico) {
ticketData.entidades_id = selectedEntityCodServico;
logInfo(`Entidade encontrada por serviço: ${selectedEntityCodServico}`);
} else {
const selectedEntityCodCliente = await glpiModel.selectEntityIdCodCliente(ticketData.codigo_cliente);
if (selectedEntityCodCliente) {
ticketData.entidades_id = selectedEntityCodCliente;
logInfo(`Entidade encontrada por cliente: ${selectedEntityCodCliente}`);
} else {
ticketData.entidades_id = 0;
logInfo(`Nenhuma entidade encontrada para serviço="${ticketData.codigo_servico}" ou cliente="${ticketData.codigo_cliente}", atribuindo contratos ativos`);
}
}
await createGlpiTicket(ticketData);
} catch (error) {
console.error(`Erro ao processar atendimento ${ticketData.id_atendimento}:`, error);
}
};
// ================================================================================
// Função Principal (com opção de pular a etapa do HubSoft)
// ================================================================================
const processaAtendimentos = async (skipHubSoft = false) => {
let atendimentosDB = [];
if (!skipHubSoft) {
try {
logInfo('Buscando atendimentos do HubSoft...');
atendimentosDB = await hubsoftModel.getAtendimentosFromDB();
logInfo(`Total de atendimentos obtidos do HubSoft: ${atendimentosDB.length}`);
for (const atendimento of atendimentosDB) {
try {
const ticketData = await processTicketFromHubSoft(atendimento);
await saveTicketToHubGlpi(ticketData);
} catch (error) {
logError(`Erro ao processar atendimento ${atendimento.id_atendimento}:`, error);
}
}
} catch (error) {
logError(`[ETAPA 1 FALHOU] Erro crítico ao buscar ou salvar dados do HubSoft. Verifique a conexão com o banco de dados do HubSoft e do HubGLPI.`, error);
// Se a primeira etapa falhar, talvez não queiramos continuar.
// Retornar aqui evita que o resto do código execute com dados potencialmente vazios.
return;
}
} else {
logInfo('Pulando a busca de atendimentos do HubSoft (skipHubSoft = true)');
}
try {
logInfo('Buscando tickets pendentes no banco de dados intermediário (HubGLPI)...');
atendimentosDB = await hubglpiModel.getTicketDataPending();
logInfo(`Total de tickets pendentes para criar no GLPI: ${atendimentosDB.length}`);
} catch (error) {
logError(`[ETAPA 2 FALHOU] Erro crítico ao buscar tickets pendentes do HubGLPI. Verifique a conexão com o banco de dados.`, error);
return;
}
if (atendimentosDB.length > 0) {
for (const atendimento of atendimentosDB) {
try {
// Processa o atendimento para o GLPI
await processAtendimento(atendimento);
} catch (error) {
logError(`Erro ao processar atendimento ${atendimento.id_atendimento}:, ${error}`);
}
}
}
}
module.exports = {
processaAtendimentos,
processAtendimento,
formatTicketDataForGlpi,
formatDescription
};
/**
* @module processController
* @description Este módulo orquestra o fluxo principal de sincronização de tickets do HubSoft para o GLPI.
* Ele é executado periodicamente pelo cron job.
*
* O fluxo é o seguinte:
* 1. `processaAtendimentos`: Função principal que pode buscar novos atendimentos do banco de dados do HubSoft (opcional) e salvá-los no banco de dados intermediário (`hubglpi`).
* 2. Em seguida, busca os tickets pendentes de criação (`pending_create`) no banco `hubglpi`.
* 3. `processAtendimento`: Para cada ticket pendente, formata os dados e chama as funções do `glpiModel` para criar o ticket no GLPI.
* 4. Atualiza o status da sincronização no banco `hubglpi` para `created_glpi` ou `sync_error` em caso de falha.
*/

View File

@ -1,48 +0,0 @@
const loadEnv = require('./config/envLoader');
loadEnv();
const cron = require('node-cron');
const { processaAtendimentos } = require('./controller/processController.js');
const commentService = require('./services/commentService.js'); // 1. Importar o novo serviço
const { logInfo, logError } = require('./utils/logger.js');
let isCronRunning = false;
logInfo('⏰ Agendando cron job para processar atendimentos a cada 1 minuto.');
cron.schedule('* * * * *', async () => {
if (isCronRunning) {
logInfo('CRON: Tentativa de início, mas o processo anterior ainda está em execução. Pulando esta rodada.');
return;
}
isCronRunning = true;
logInfo('CRON: Iniciando ciclo de sincronização...');
try {
// --- Tarefa 1: Sincronizar criação de tickets ---
logInfo('CRON (Etapa 1/2): Processando criação de tickets...');
await processaAtendimentos();
logInfo('CRON (Etapa 1/2): Criação de tickets concluída.');
// --- Tarefa 2: Sincronizar comentários ---
logInfo('CRON (Etapa 2/2): Processando sincronização de comentários...');
await commentService.syncHubsoftCommentsToLocalDB();
await commentService.sendPendingCommentsToGlpi(); // E outras direções se necessário
logInfo('CRON (Etapa 2/2): Sincronização de comentários concluída.');
logInfo('CRON: Ciclo de sincronização concluído com sucesso.');
} catch (error) {
logError('CRON: Erro durante o processamento de atendimentos.', error);
} finally {
isCronRunning = false;
}
});
/**
* @module server
* @description Ponto de entrada principal da aplicação.
* Este módulo é responsável por:
* 1. Carregar as variáveis de ambiente.
* 2. Iniciar o servidor Express para escutar requisições HTTP (ex: webhooks do GLPI).
* 3. Agendar e executar um cron job que roda a função `processaAtendimentos` periodicamente para sincronizar novos tickets do HubSoft para o GLPI.
*/

View File

@ -1,31 +0,0 @@
// src/data/hubglpiDataBase.js
// Configuração da conexão com o banco de dados PostgreSQL
const { logInfo, logError } = require('../utils/logger');
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.HUBGLPI_DB_HOST,
port: process.env.HUBGLPI_DB_PORT,
database: process.env.HUBGLPI_DB_NAME,
user: process.env.HUBGLPI_DB_USER,
password: process.env.HUBGLPI_DB_PASSWORD,
});
// Teste de conexão
pool.on('connect', () => {
logInfo('Conexão com o banco de dados PostgreSQL estabelecida com sucesso.');
});
pool.on('error', (err) => {
logError('Erro na conexão com o banco de dados PostgreSQL', err);
});
module.exports = pool;
/**
* @module hubglpiDataBase
* @description Este módulo configura e exporta a conexão com o banco de dados PostgreSQL usado para armazenar dados sincronizados entre HubSoft e GLPI.
*/

View File

@ -1,11 +0,0 @@
const loadEnv = require('./config/envLoader');
loadEnv();
const hubsoftController = require('./controller/processController.js');
const { logInfo } = require('./utils/logger.js');
logInfo('Aplicação iniciada', {
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV || 'development'
});
hubsoftController.processaAtendimentos();

View File

@ -0,0 +1,27 @@
// src/shared/infra/api/hubsoft.auth.js
const axios = require('axios')
const qs = require('qs')
const { hubsoft } = require('./hubsoft.config')
const { logError } = require('../../shared/utils/logger')
const getAuthToken = async () => {
try {
const response = await axios.post(
hubsoft.authUrl,
qs.stringify(hubsoft.authPayload),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
)
return response.data.access_token
} catch (error) {
logError('Erro ao obter token de autenticação HubSoft', error)
throw error
}
}
module.exports = getAuthToken

View File

@ -0,0 +1,43 @@
// src/infra/api/hubsoft.client.js
const axios = require('axios')
const { hubsoft } = require('./hubsoft.config')
const getAuthToken = require('./hubsoft.auth')
const { logInfo, logError } = require('../../shared/utils/logger')
/**
* Envia uma mensagem para um atendimento no HubSoft
*/
async function sendHubsoftMessage(atendimentoId, mensagem) {
const token = await getAuthToken()
const url = `${hubsoft.atendimentosUrl}adicionar_mensagem/${atendimentoId}`
const payload = { mensagem }
console.log('URL:', url)
console.log('Payload:', payload)
try {
logInfo(`Enviando mensagem para atendimento HubSoft ${atendimentoId}`)
const response = await axios.post(url, payload, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
return response.data?.atendimento_mensagem?.id_atendimento_mensagem
} catch (err) {
logError(
`Erro ao enviar mensagem para atendimento HubSoft ${atendimentoId}`,
err.response?.data || err.message
)
throw err
}
}
module.exports = {
sendHubsoftMessage
}

View File

@ -0,0 +1,32 @@
// src/shared/infra/api/hubsoft.config.js
var hubsoft = {
baseUrl: process.env.HUBSOFT_BASE_URL,
authUrl: process.env.HUBSOFT_AUTH_URL,
authPayload: {
grant_type: 'password',
client_id: process.env.HUBSOFT_CLIENT_ID,
client_secret: process.env.HUBSOFT_CLIENT_SECRET,
username: process.env.HUBSOFT_USERNAME,
password: process.env.HUBSOFT_PASSWORD
},
};
module.exports = {
hubsoft: {
baseUrl: process.env.HUBSOFT_BASE_URL,
authUrl: process.env.HUBSOFT_AUTH_URL,
authPayload: {
grant_type: 'password',
client_id: process.env.HUBSOFT_CLIENT_ID,
client_secret: process.env.HUBSOFT_CLIENT_SECRET,
username: process.env.HUBSOFT_USERNAME,
password: process.env.HUBSOFT_PASSWORD
},
atendimentosUrl: process.env.HUBSOFT_CONSULTAR_ATENDIMENTO_URL
}
};

View File

@ -0,0 +1,48 @@
// infra/cron/sync.cron.js
const loadEnv = require('../../config/env.loader.js')
loadEnv()
const cron = require('node-cron')
const { syncTickets } = require('../../modules/tickets/controller/tickets.controller.js')
const { syncComments } = require('../../modules/comments/useCases/syncHubCommentToGlpi.usecase.js')
//const { retryFailedTickets } = require('../../modules/tickets/useCases/retryFailedTickets.usecase.js') //TODO
//const { retryFailedComments } = require('../../modules/comments/useCases/retryFailedComments.usecase.js') //TODO
const { logInfo, logError } = require('../../shared/utils/logger.js')
let isCronRunning = false
logInfo('Schedule agendado para rodar a cada minuto.')
cron.schedule('* * * * *', async () => {
if (isCronRunning) {
logInfo('Cron já está em execução. Ignorando nova execução.')
return
}
isCronRunning = true
try {
await runSyncPipeline()
} catch (error) {
logError('Erro no cron', error)
} finally {
isCronRunning = false
}
})
async function runSyncPipeline() {
logInfo('[CRON] Iniciando Sync Pipeline Hubsoft -> GLPI ')
logInfo('[CRON] Sincronizando Tickets')
await syncTickets()
logInfo('[CRON] Sincronizando comentários')
await syncComments()
logInfo('[CRON] Resincronizando Tickets que fallharam')
//await retryFailedTickets()
logInfo('[CRON] Resincronizando Comentarios que falharam')
//await retryFailedComments()
logInfo('[CRON] Pipeline Hubsoft -> GLPI finalizado')
}

View File

@ -0,0 +1,16 @@
// src/infra/db/connections/glpi.mysql.js
const mysql = require('mysql2/promise');
const dbConfig = require('../../../config/db.config.js')
const pool = mysql.createPool({
host: dbConfig.glpi.databaseHost,
port: dbConfig.glpi.databasePort,
user: dbConfig.glpi.databaseUser,
password: dbConfig.glpi.databasePassword,
database: dbConfig.glpi.databaseName,
charset: dbConfig.glpi.databaseCharset,
waitForConnections: true,
connectionLimit: 10
})
module.exports = pool

View File

@ -0,0 +1,18 @@
// src/infra/db/connections/hubglpi.pg.js
const { Pool } = require('pg')
const dbConfig = require('../../../config/db.config.js')
const pool = new Pool({
host: dbConfig.hubglpi.databaseHost,
port: dbConfig.hubglpi.databasePort,
user: dbConfig.hubglpi.databaseUser,
password: dbConfig.hubglpi.databasePassword,
database: dbConfig.hubglpi.databaseName,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000
})
module.exports = pool

View File

@ -0,0 +1,14 @@
// src/infra/db/connections/hubsoft.pg.js
const { Pool } = require('pg')
const dbConfig = require('../../../config/db.config')
const pool = new Pool({
host: dbConfig.hubsoft.databaseHost,
port: dbConfig.hubsoft.databasePort,
user: dbConfig.hubsoft.databaseUser,
password: dbConfig.hubsoft.databasePassword,
database: dbConfig.hubsoft.databaseName,
max: 10
})
module.exports = pool

View File

@ -0,0 +1,48 @@
// src/infra/db/repositories/glpi/comments.repository.js
const db = require('../../connections/glpi.mysql.js')
const { logError } = require('../../../../shared/utils/logger')
async function insert({ content }) {
const query = `
INSERT INTO glpi_itilfollowups (
itemtype,
items_id,
users_id,
content,
is_private,
requesttypes_id,
date,
date_creation,
date_mod
)
VALUES (
'Ticket',
?,
?,
?,
0,
1,
NOW(),
NOW(),
NOW()
)
`;
const values = [
content.ticketId,
Number(process.env.GLPI_USER_ID),
content.content
];
const [result] = await db.query(query, [...values])
return {
id: result.insertId
}
}
module.exports = {
insert
}

View File

@ -0,0 +1,57 @@
// src/infra/db/repositories/glpi/entities.repository.js
const db = require('../../connections/glpi.mysql.js')
const { logError, logInfo } = require('../../../../shared/utils/logger.js')
async function getEntitiesByClient(codigoCliente) {
const query = `
SELECT id
FROM glpi_entities
WHERE name LIKE ?
LIMIT 1
`
try {
const [rows] = await db.query(query, [`${codigoCliente} -%`])
if (!rows.length) {
logInfo(`Entidade não encontrada para cliente ${codigoCliente}`)
return null
}
return Number(rows[0].id)
} catch (err) {
logError('Erro ao buscar entidade por cliente', err)
throw err
}
}
async function getEntitiesByService(codigoCliente, codigoServico) {
const query = `
SELECT id
FROM glpi_entities
WHERE name LIKE ?
LIMIT 1
`
try {
const [rows] = await db.query(query, [
`${codigoCliente} - ${codigoServico} %`
])
if (!rows.length) {
logInfo(`Entidade não encontrada para serviço ${codigoServico}`)
return null
}
return Number(rows[0].id)
} catch (err) {
logError('Erro ao buscar entidade por serviço', err)
throw err
}
}
module.exports = {
getEntitiesByClient,
getEntitiesByService
}

View File

@ -0,0 +1,38 @@
// src/infra/db/repositories/glpi/groups.repository.js
const db = require('../../connections/glpi.mysql.js')
const { logError } = require('../../../../shared/utils/logger')
const GROUPS = {
NOC: 25,
IMPLANTACAO: 36
}
async function insertGroupNOC(ticketId) {
return insertGroup(ticketId, GROUPS.NOC)
}
async function insertGroupImplantacao(ticketId) {
return insertGroup(ticketId, GROUPS.IMPLANTACAO)
}
async function insertGroup(ticketId, groupId) {
const query = `
INSERT INTO glpi_groups_tickets (tickets_id, groups_id, type)
VALUES (?, ?, 2)
`
try {
const [result] = await db.query(query, [ticketId, groupId])
return result.insertId || null
} catch (err) {
logError('Erro ao inserir grupo no ticket', err)
throw err
}
}
module.exports = {
insertGroupNOC,
insertGroupImplantacao
}

View File

@ -0,0 +1,55 @@
// src/infra/db/repositories/glpi/tickets.repository.js
const db = require('../../connections/glpi.mysql.js')
const { logError } = require('../../../../shared/utils/logger.js')
async function insertTicket(ticketData) {
const query = `
INSERT INTO glpi_tickets (
entities_id,
name,
date,
date_mod,
status,
users_id_recipient,
content,
urgency,
impact,
priority,
type,
itilcategories_id,
date_creation,
slas_id_ttr
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
const values = [
ticketData.entities_id,
ticketData.name,
ticketData.date_creation,
ticketData.date_mod,
ticketData.status,
ticketData.users_id_recipient,
ticketData.content,
ticketData.urgency,
ticketData.impact,
ticketData.priority,
2,
ticketData.itilcategories_id,
ticketData.date_creation,
37
]
try {
const [result] = await db.query(query, values)
return result.insertId
} catch (err) {
logError('Erro ao inserir ticket no GLPI', err)
throw err
}
}
module.exports = {
insertTicket
}

View File

@ -0,0 +1,74 @@
// src/infra/db/repositories/hubglpi/comments.repository.js
const db = require('../../connections/hubglpi.pg.js');
const { logError } = require('../../../../shared/utils/logger');
/**
* Insere um comentário sincronizado no contexto de um ticket GLPI
*/
async function insertSyncComment({
syncDataId,
content,
author = 'system',
sourceSystem = 'SYSTEM',
sourceCommentId = null,
destinationCommentId = null
}) {
const query = `
INSERT INTO sync_comments (
sync_data_id,
source_system,
source_comment_id,
destination_comment_id,
content,
author,
sync_status,
sync_attempts,
created_at,
updated_at
)
VALUES (
$1, $2, $3, $4, $5, $6,
'synced',
1,
NOW(),
NOW()
)
RETURNING *;
`;
const values = [
syncDataId,
sourceSystem,
sourceCommentId,
destinationCommentId,
content,
author
];
try {
const { rows } = await db.query(query, values);
return rows[0];
} catch (err) {
logError('Erro ao inserir sync_comment', err);
throw err;
}
}
async function exists(source, sourceCommentId) {
const query = `
SELECT 1
FROM sync_comments
WHERE source_system = $1
AND source_comment_id = $2
LIMIT 1
`
const { rowCount } = await db.query(query, [source, sourceCommentId])
return rowCount > 0
}
module.exports = {
insertSyncComment,
exists
};

View File

@ -0,0 +1,152 @@
// src/infra/db/repositories/hubglpi/sync.repository.js
const db = require('../../connections/hubglpi.pg.js');
const { logError } = require('../../../../shared/utils/logger');
/**
* Insere registros em sync_data para tickets novos vindos do HubSoft
* @param {number[]} hubsoftIds
*/
async function insertSyncData(hubsoftIds) {
if (!hubsoftIds?.length) return [];
const query = `
INSERT INTO sync_data (hubsoft_ticket_id, status_sync)
SELECT id, 'pending_create'
FROM unnest($1::bigint[]) id
ON CONFLICT (hubsoft_ticket_id) DO NOTHING
RETURNING id, hubsoft_ticket_id;
`;
try {
const { rows } = await db.query(query, [hubsoftIds]);
return rows;
} catch (err) {
logError('Erro ao inserir sync_data', err);
throw err;
}
}
/**
* Atualiza sync_data após criação do ticket no GLPI
* @param {number, number[]} hubsoftid, glpiID
*/
async function updateSyncDataCreated(hubsoftId, glpiId) {
const query = `
UPDATE sync_data
SET glpi_ticket_id = $2,
status_sync = 'created',
updated_at = NOW()
WHERE hubsoft_ticket_id = $1
RETURNING *;
`;
try {
const { rows } = await db.query(query, [hubsoftId, glpiId]);
return rows[0] || null;
} catch (err) {
logError('Erro ao atualizar sync_data para created', err);
throw err;
}
}
/**
* Busca sync_data pelo id do HubSoft
*/
async function getSyncIdByHubsoftId(hubsoftTicketId) {
const query = `
SELECT id, glpi_ticket_id
FROM sync_data
WHERE hubsoft_ticket_id = $1;
`;
try {
const { rows } = await db.query(query, [hubsoftTicketId]);
return rows[0] || null;
} catch (err) {
logError('Erro ao buscar sync_data por hubsoft_ticket_id', err);
throw err;
}
}
async function lockTicketForClosing(syncId) {
const query = `
UPDATE sync_data
SET status_sync = 'processing_close',
updated_at = NOW()
WHERE id = $1
AND status_sync NOT IN ('processing_close', 'closed')
RETURNING *;
`;
try {
const { rows } = await db.query(query, [syncId]);
return rows[0] || null;
} catch (err) {
logError(`Erro ao travar ticket para fechamento (${syncId})`, err);
throw err;
}
}
async function updateSyncDataError(syncErrorMessage, syncId) {
const query = `
UPDATE sync_data
SET sync_error_message = $1,
status_sync = 'sync_error',
last_sync_attempt = NOW()
WHERE id = $2
RETURNING *;
`;
try {
const { rows } = await db.query(query, [syncErrorMessage, syncId]);
return rows[0];
} catch (err) {
logError('Erro ao atualizar erro de sync_data', err);
throw err;
}
}
/**
* Coleta info de sync a partir do GLPI ID
* @param {number[]} glpiTicketId
*/
async function getByGlpiId(glpiTicketId) {
const query = `
SELECT
id,
glpi_ticket_id,
hubsoft_ticket_id,
status_sync
FROM sync_data
WHERE glpi_ticket_id = $1
LIMIT 1;
`
const { rows } = await db.query(query, [glpiTicketId])
return rows[0] || null
}
async function markAsClosed(syncId) {
const query = `
UPDATE sync_data
SET
status_sync = 'closed',
updated_at = NOW()
WHERE id = $1
RETURNING hubsoft_ticket_id;
`
const { rows } = await db.query(query, [syncId])
return rows[0]?.hubsoft_ticket_id
}
module.exports = {
insertSyncData,
updateSyncDataCreated,
getSyncIdByHubsoftId,
lockTicketForClosing,
updateSyncDataError,
getByGlpiId,
markAsClosed
};

View File

@ -0,0 +1,168 @@
// src/infra/db/repositories/hubglpi/tickets.repository.js
const db = require('../../connections/hubglpi.pg.js');
const { logError } = require('../../../../shared/utils/logger.js');
/**
* Insere ou atualiza tickets vindos do HubSoft
* @param {Array<Object>} tickets
*/
async function insertTickets(tickets) {
if (!tickets?.length) return [];
const query = `
INSERT INTO hubsoft_tickets (
id_atendimento,
codigo_cliente,
status_atendimento,
codigo_servico,
servico_nome,
protocolo_hub,
ticket_mundiale,
cliente_nome,
cpf_cnpj,
telefone,
email,
tipo_pessoa,
nome_razaosocial,
endereco,
ticket_type,
descricao_abertura,
created_at,
vendedor
)
SELECT
t.id_atendimento,
t.codigo_cliente,
t.status_atendimento,
t.codigo_servico,
t.servico_nome,
t.protocolo_hub,
t.ticket_mundiale,
t.cliente_nome,
t.cpf_cnpj,
t.telefone,
t.email,
t.tipo_pessoa,
t.nome_razaosocial,
t.endereco,
t.ticket_type,
descricao_abertura,
t.created_at,
vendedor
FROM jsonb_to_recordset($1::jsonb) AS t(
id_atendimento BIGINT,
codigo_cliente INTEGER,
status_atendimento TEXT,
codigo_servico INTEGER,
servico_nome TEXT,
protocolo_hub TEXT,
ticket_mundiale INTEGER,
cliente_nome TEXT,
cpf_cnpj TEXT,
telefone TEXT,
email TEXT,
tipo_pessoa TEXT,
nome_razaosocial TEXT,
endereco TEXT,
ticket_type TEXT,
descricao_abertura TEXT,
created_at TIMESTAMP,
vendedor TEXT
)
ON CONFLICT (id_atendimento)
DO UPDATE SET
codigo_cliente = EXCLUDED.codigo_cliente,
status_atendimento = EXCLUDED.status_atendimento,
codigo_servico = EXCLUDED.codigo_servico,
servico_nome = EXCLUDED.servico_nome,
protocolo_hub = EXCLUDED.protocolo_hub,
ticket_mundiale = EXCLUDED.ticket_mundiale,
cliente_nome = EXCLUDED.cliente_nome,
cpf_cnpj = EXCLUDED.cpf_cnpj,
telefone = EXCLUDED.telefone,
email = EXCLUDED.email,
tipo_pessoa = EXCLUDED.tipo_pessoa,
nome_razaosocial = EXCLUDED.nome_razaosocial,
endereco = EXCLUDED.endereco,
ticket_type = EXCLUDED.ticket_type,
descricao_abertura = EXCLUDED.descricao_abertura,
vendedor = EXCLUDED.vendedor,
updated_at = NOW()
RETURNING id_atendimento;
`;
try {
const { rows } = await db.query(query, [JSON.stringify(tickets)]);
return rows.map(r => r.id_atendimento);
} catch (err) {
logError('Erro ao inserir tickets no hubglpi', err);
throw err;
}
}
async function fetchPendingTickets() {
const query = `
SELECT
ht.*,
sd.id AS sync_data_id,
sd.glpi_ticket_id,
sd.status_sync,
sd.sync_metadata,
sd.last_sync_attempt,
sd.sync_error_message,
sd.created_at AS sync_created_at,
sd.updated_at AS sync_updated_at
FROM hubsoft_tickets ht
LEFT JOIN sync_data sd
ON ht.id_atendimento = sd.hubsoft_ticket_id
WHERE sd.status_sync IS NULL
OR sd.status_sync = 'pending_create'
ORDER BY ht.created_at ASC;
`;
try {
const { rows } = await db.query(query);
return rows;
} catch (err) {
logError('Erro ao buscar tickets pendentes', err);
throw err;
}
}
async function updateCloseMessage(hubTicketId, message) {
const query = `
UPDATE hubsoft_tickets
SET
status_atendimento = 'Resolvido',
descricao_fechamento = $2,
data_fechamento = NOW(),
updated_at = NOW()
WHERE id_atendimento = $1
RETURNING *;
`
try {
const { rows } = await db.query(query, [hubTicketId, message])
return rows[0]
} catch (err) {
logError(`Erro ao atualizar fechamento do ticket ${hubTicketId}`, err)
throw err
}
}
module.exports = {
insertTickets,
fetchPendingTickets,
updateCloseMessage
};

View File

@ -0,0 +1,31 @@
// src/infra/db/repositories/hubglpi/watermark.repository.js
const db = require('../../connections/hubglpi.pg.js');
async function getJobWatermark(jobName) {
const { rows } = await db.query(`
SELECT last_run_timestamp
FROM sync_control
WHERE job_name = $1
LIMIT 1
`, [jobName]);
return rows?.[0]?.last_run_timestamp ?? null;
}
async function updateJobWatermark(jobName, newTimestamp) {
await db.query(`
INSERT INTO sync_control (job_name, last_run_timestamp)
VALUES ($1, $2::timestamptz - INTERVAL '1 minute')
ON CONFLICT (job_name)
DO UPDATE SET
last_run_timestamp = EXCLUDED.last_run_timestamp
`, [jobName, newTimestamp]);
}
module.exports = {
getJobWatermark,
updateJobWatermark
};

View File

@ -0,0 +1,27 @@
//src/infra/db/repositories/hubsoft/messages.repository.js
const db = require('../../connections/hubsoft.pg.js')
const { logError } = require('../../../../shared/utils/logger')
async function getNewMessages(watermark) {
const query = `
SELECT
am.id_atendimento_mensagem,
am.id_atendimento,
am.mensagem,
am.data_cadastro,
u.name AS usuario_nome
FROM atendimento_mensagem am
LEFT JOIN users u
ON u.id = am.id_usuario
WHERE am.data_cadastro > $1
ORDER BY am.data_cadastro ASC;
`
const { rows } = await db.query(query, [watermark])
return rows
}
module.exports = {
getNewMessages
}

View File

@ -0,0 +1,100 @@
//src/infra/db/repositories/hubsoft/ticket.repository.js
const db = require('../../connections/hubsoft.pg.js')
const { logError } = require('../../../../shared/utils/logger.js')
async function getTicketsByTipo({
tipoAtendimento,
usuarioAbertura = null,
watermark = null
}) {
try {
const isImplantacao = Number(tipoAtendimento) === 21;
const isCancelamento = Number(tipoAtendimento) === 27;
let select = `
a.id_atendimento,
a.id_usuario_abertura,
a.id_atendimento_status,
a.protocolo,
a.descricao_abertura,
a.data_cadastro,
a.nome_contato,
c.codigo_cliente::text AS codigo_cliente,
s.descricao AS servico_nome,
cs.id_cliente_servico
`;
let joins = `
FROM atendimento AS a
INNER JOIN cliente_servico AS cs ON a.id_cliente_servico = cs.id_cliente_servico
INNER JOIN cliente AS c ON cs.id_cliente = c.id_cliente
INNER JOIN servico AS s ON cs.id_servico = s.id_servico
`;
if (isImplantacao || isCancelamento) {
select += `,
u.name AS vendedor,
c.nome_razaosocial,
c.telefone_primario AS telefone,
c.cpf_cnpj,
c.tipo_pessoa,
c.email_principal AS email,
en.endereco,
en.numero,
en.complemento,
en.bairro,
en.cep,
ci.nome AS cidade,
es.sigla AS estado
`;
joins += `
INNER JOIN cliente_servico_endereco AS cse
ON cse.id_cliente_servico = cs.id_cliente_servico
AND cse.tipo = 'instalacao'
INNER JOIN endereco_numero AS en
ON en.id_endereco_numero = cse.id_endereco_numero
INNER JOIN cidade AS ci
ON ci.id_cidade = en.id_cidade
INNER JOIN estado AS es
ON es.id_estado = ci.id_estado
INNER JOIN users AS u
ON u.id = a.id_usuario_abertura
`;
}
let query = `
SELECT ${select}
${joins}
WHERE a.id_tipo_atendimento = $1
AND a.id_atendimento_status IN (1, 2, 33)
AND s.ativo = true
`;
const params = [tipoAtendimento];
let paramIndex = 2;
if (usuarioAbertura) {
query += ` AND a.id_usuario_abertura = $${paramIndex++}`;
params.push(usuarioAbertura);
}
if (watermark) {
query += ` AND a.data_cadastro > $${paramIndex++}`;
params.push(watermark);
}
const { rows } = await db.query(query, params);
return rows;
} catch (error) {
logError('Erro ao buscar tickets HubSoft', error);
throw error;
}
}
module.exports = {
getTicketsByTipo
}

View File

@ -1,5 +1,7 @@
// src/infra/http/app.js
const express = require('express');
const router = require('./routes.js')
const router = require('./routes')
function createApp() {
const app = express();

View File

@ -0,0 +1,16 @@
const { Router } = require('express');
const express = require('express');
const closureController = require('../../../modules/close/controller/close.controller.js');
const commentController = require('../../../modules/comments/controller/glpiComment.controller.js');
const router = Router();
router.use(express.json({ type: '*/*' }));
router.post('/webhook/close-ticket', closureController.closeTicket);
router.post('/webhook/new-comment', commentController.handleGlpiComment);
module.exports = router;

View File

@ -1,8 +1,10 @@
const loadEnv = require('./config/envLoader');
// src/infra/http/server.js
const loadEnv = require('../../config/env.loader.js');
loadEnv(); // Carrega as variáveis de ambiente
const createApp = require('./app.js');
const { logInfo } = require('./utils/logger.js');
const { logInfo } = require('../../shared/utils/logger.js');
const app = createApp();

View File

@ -1,142 +0,0 @@
const { logError, logInfo } = require('../utils/logger');
const pool = require('../data/hubglpiDataBase');
class CommentModel {
/**
* Busca o timestamp da última execução de um job de sincronização.
* @param {string} jobName - O nome do job (ex: 'hubsoft_comments_sync').
* @returns {Promise<Date>} O timestamp da última execução.
*/
static async getLastRunTimestamp(jobName) {
const query = 'SELECT last_run_timestamp FROM sync_control WHERE job_name = $1';
try {
const { rows } = await pool.query(query, [jobName]);
if (rows.length === 0) {
throw new Error(`Job de controle '${jobName}' não encontrado na tabela sync_control.`);
}
return rows[0].last_run_timestamp;
} catch (error) {
logError(`Erro ao buscar último timestamp para o job '${jobName}':`, error);
throw error;
}
}
/**
* Atualiza o timestamp da última execução de um job de sincronização.
* @param {string} jobName - O nome do job.
* @param {Date} timestamp - O novo timestamp.
*/
static async updateLastRunTimestamp(jobName, timestamp) {
const query = 'UPDATE sync_control SET last_run_timestamp = $1 WHERE job_name = $2';
try {
await pool.query(query, [timestamp, jobName]);
} catch (error) {
logError(`Erro ao atualizar o timestamp para o job '${jobName}':`, error);
throw error;
}
}
/**
* Insere um novo comentário na fila de sincronização.
* Usa ON CONFLICT para evitar duplicatas com base no sistema de origem e ID do comentário.
* @param {object} commentData - Dados do comentário.
* @param {number} commentData.hubsoftAtendimentoId - ID do atendimento no HubSoft.
* @param {string} commentData.sourceSystem - 'hubsoft' ou 'glpi'.
* @param {string} commentData.sourceCommentId - ID do comentário na origem.
* @param {string} commentData.content - Conteúdo do comentário.
* @param {string} [commentData.author] - Autor do comentário.
*/
static async insertComment(commentData) {
// Primeiro, precisamos do ID do sync_data correspondente ao atendimento do HubSoft.
const syncDataQuery = 'SELECT id FROM sync_data WHERE hubsoft_ticket_id = $1';
const syncDataRes = await pool.query(syncDataQuery, [commentData.hubsoftAtendimentoId]);
if (syncDataRes.rows.length === 0) {
logInfo(`Nenhum registro de sincronização encontrado para o atendimento HubSoft ID ${commentData.hubsoftAtendimentoId}. Comentário não será inserido.`);
return false;
}
const syncDataId = syncDataRes.rows[0].id;
const insertQuery = `
INSERT INTO sync_comments (
sync_data_id, source_system, source_comment_id, content, author
)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (source_system, source_comment_id)
DO UPDATE SET
-- Atualização "falsa" para forçar o RETURNING a funcionar no conflito.
-- Isso não altera o valor, mas permite obter o ID da linha existente.
source_comment_id = EXCLUDED.source_comment_id
RETURNING id, sync_status;
`;
const values = [
syncDataId,
commentData.sourceSystem,
commentData.sourceCommentId,
commentData.content,
commentData.author
];
try {
const result = await pool.query(insertQuery, values);
if (result.rows.length > 0) {
// Retorna o objeto inteiro com { id, sync_status }
return result.rows[0];
} else {
// Este bloco agora é menos provável de ser atingido, a menos que haja um problema diferente.
return false;
}
} catch (error) {
logError('Erro ao inserir comentário em sync_comments:', error);
throw error;
}
}
/**
* Busca comentários pendentes para um sistema de destino específico.
* @param {string} destinationSystem - O sistema de destino ('glpi' ou 'hubsoft').
* @returns {Promise<Array<object>>} Uma lista de comentários pendentes.
*/
static async getPendingCommentsForDestination(destinationSystem) {
const sourceSystem = destinationSystem === 'glpi' ? 'hubsoft' : 'glpi';
const query = `
SELECT sc.id, sc.content, sc.author, sd.glpi_ticket_id, sd.hubsoft_ticket_id
FROM sync_comments sc
JOIN sync_data sd ON sc.sync_data_id = sd.id
WHERE sc.source_system = $1
AND sc.sync_status = 'pending_sync'
AND sc.sync_attempts < 5; -- Evita retentativas infinitas
`;
const { rows } = await pool.query(query, [sourceSystem]);
return rows;
}
/**
* Atualiza o status de sincronização de um comentário.
* @param {number} commentId - O ID do comentário em sync_comments.
* @param {string} status - O novo status ('synced', 'sync_error').
* @param {string|null} destinationId - O ID do comentário no sistema de destino.
* @param {string|null} errorMessage - A mensagem de erro, se houver.
*/
static async updateCommentSyncStatus(commentId, status, destinationId = null, errorMessage = null) {
const query = `
UPDATE sync_comments
SET
sync_status = $1,
destination_comment_id = $2,
error_message = $3,
sync_attempts = sync_attempts + 1,
updated_at = NOW()
WHERE id = $4;
`;
try {
await pool.query(query, [status, destinationId, errorMessage, commentId]);
} catch (error) {
logError(`Erro ao atualizar status do comentário ID ${commentId}:`, error);
// Não relançamos o erro para não parar o loop de sincronização.
}
}
}
module.exports = CommentModel;

View File

@ -1,139 +0,0 @@
// src/models/glpiModel.js
const dbConfig = require('../config/dbConfig.js');
const { logError, logInfo} = require('../utils/logger');
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: dbConfig.glpi.databaseHost,
port: dbConfig.glpi.databasePort,
database: dbConfig.glpi.databaseName,
user: dbConfig.glpi.databaseUser,
password: dbConfig.glpi.databasePassword,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
class GlpiModel {
static async insertTicket(ticketData) {
const query = `
INSERT INTO glpi_tickets(
entities_id,
name,
date,
date_mod,
status,
users_id_recipient,
content,
urgency,
impact,
priority,
type,
itilcategories_id,
date_creation,
slas_id_ttr
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const values = [
ticketData.entidades_id,
ticketData.titulo,
ticketData.created_at,
ticketData.date_mod,
ticketData.status_atendimento,
ticketData.user_id_recipient,
ticketData.descricao_abertura,
ticketData.urgency,
ticketData.impact,
ticketData.priority,
2,
ticketData.itilcategories_id,
ticketData.date_creation,
37
];
try {
const [rows] = await pool.execute(query, values)
return rows;
} catch (error) {
logError('Erro ao inserir ticket:', error);
throw error;
}
}
static async selectEntityIdCodCliente(id) {
const query = `SELECT id FROM glpi_entities WHERE name LIKE ? LIMIT 1;`;
const values = [`${id} -%`];
try {
const [rows] = await pool.execute(query, values);
if (!rows || rows.length === 0) {
logInfo(`Entidade não encontrada por código de cliente para: ${id} `);
return null;
}
logInfo(`Entidade encontrada por código de cliente para: ${id}`);
return Number(rows[0].id);
} catch (err) {
logError(`Erro ao buscar entidade por código de cliente: ${err}`);
throw err;
}
}
static async selectEntityIdCodServico(idCliente, idServico) {
const query = `SELECT id FROM glpi_entities WHERE name LIKE ? LIMIT 1;`;
const values = [`${idCliente} - ${idServico} %`];
try {
const [rows] = await pool.execute(query, values);
if (!rows || rows.length === 0) {
logInfo(`Entidade não encontrada por código de serviço para: ${idServico} `);
return null;
}
logInfo(`Entidade encontrada por código de serviço para: ${idServico}`);
return Number(rows[0].id);
} catch (err) {
logError(`Erro ao buscar entidade por código de serviço: ${err}`);
throw err;
}
}
static async insertGroupTickets(glpiTicketId) {
const query = `
INSERT INTO glpi_groups_tickets (tickets_id, groups_id, type)
VALUES (?, ?, ?)
`;
const values = [glpiTicketId, 25, 2];
try {
const [rows] = await pool.query(query, values);
return rows && rows.insertId ? rows.insertId : null;
}
catch (err) {
logError(`Erro ao Adicionar Grupo Operação NOC: ${err}`);
throw err;
}
}
static async insertComment(commentData) {
const query = `INSERT INTO glpi_itilfollowups(itemtype, items_id, date, users_id, content, date_creation, date_mod) VALUES('Ticket',? , NOW(), ?, ?, NOW(), NOW())`;
const values = [commentData.tickets_id, parseInt(process.env.GLPI_USER), commentData.content];
try {
const [rows] = await pool.query(query, values);
return rows && rows.insertId ? { id: rows.insertId } : null;
}
catch (err) {
logError(`Erro ao inserir comentário no GLPI: ${err}`);
throw err;
}
}
}
module.exports = GlpiModel;

View File

@ -1,322 +0,0 @@
// src/models/hubsoft_ticketsModel.js
const { log } = require('winston');
const { logError, logInfo} = require('../utils/logger');
const pool = require('../data/hubglpiDataBase'); // <- Importa o pool centralizado
class HubglpiModel {
static async insertTicket(ticketData) {
const query = `
INSERT INTO hubsoft_tickets (
id_atendimento,
codigo_cliente,
status_atendimento,
codigo_servico,
servico_nome,
protocolo_hub,
ticket_mundiale,
cliente_nome,
created_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (id_atendimento)
DO UPDATE SET
codigo_cliente = $2,
status_atendimento = $3,
codigo_servico = $4,
servico_nome = $5,
protocolo_hub = $6,
ticket_mundiale = $7,
cliente_nome = $8,
created_at = $9
RETURNING *;
`;
const values = [
ticketData.id_atendimento,
ticketData.codigo_cliente,
ticketData.status_atendimento,
ticketData.codigo_servico,
ticketData.servico_nome,
ticketData.protocolo_hub,
ticketData.ticket_mundiale,
ticketData.cliente_nome,
ticketData.data_cadastro
];
try {
const res = await pool.query(query, values);
return res.rows[0];
} catch (err) {
logError(`Erro ao inserir/atualizar ticket na tabela hubsoft_tickets: ${err}`);
throw err;
}
}
static async insertSyncData(syncData) {
const query = `
INSERT INTO sync_data (
hubsoft_ticket_id
)
VALUES ($1)
ON CONFLICT (hubsoft_ticket_id)
DO UPDATE SET
hubsoft_ticket_id = $1
RETURNING *;
`;
const values = [
syncData
];
try {
const res = await pool.query(query, values);
return res.rows[0];
} catch (err) {
logError(`Erro ao inserir/atualizar dados na tabela sync_data: ${err}`);
throw err;
}
}
static async update_syncData(sync_update) {
const query = `
UPDATE sync_data
set glpi_ticket_id = $1,
status_sync = $2,
sync_metadata = $3,
last_sync_attempt = $4,
sync_error_message = $5,
created_at = $6,
updated_at = $7
WHERE id = $8
RETURNING *;
`;
const values = [
sync_update.glpi_ticket_id,
sync_update.status_sync,
sync_update.sync_metadata,
new Date(),
sync_update.sync_error_message,
sync_update.created_at,
sync_update.updated_at,
sync_update.sync_data_id
]; //Todo colocar parametros dinamicos || null se não tiver
try {
const res = await pool.query(query, values);
return res.rows[0];
} catch (err) {
logError(`Erro ao atualizar dados na tabela sync_data: ${err}`);
throw err;
}
}
static async get_idSyncByHubsoftId(hubsoft_ticket_id) {
const query = `
SELECT id FROM sync_data
WHERE hubsoft_ticket_id = $1;
`;
const values = [hubsoft_ticket_id];
try {
const res = await pool.query(query, values);
logInfo('ID de sync_data obtido com sucesso:', res.rows[0]);
return res.rows[0] ? res.rows[0].id : null;
} catch (err) {
logError(`Erro ao obter ID de sync_data, ${err}`);
throw err;
}
}
static async getIdByGlpiID(glpi_ticket_id) {
const query = `
SELECT
id,
glpi_ticket_id glpiId,
hubsoft_ticket_id hubsoftId
FROM sync_data
WHERE glpi_ticket_id = $1;
`;
const values = [parseInt(glpi_ticket_id)];
try {
const { rows } = await pool.query(query, values);
if (rows.length > 0) {
logInfo(`Dados de sincronização para o GLPI ID ${glpi_ticket_id} obtidos com sucesso.`);
return rows[0]; // Retorna o objeto completo
}
return null; // Retorna null se não encontrar
} catch (err) {
logError(`Erro ao obter ID de sync_data, ${err}`);
throw err;
}
}
static async getTicketDataPending() {
const query = ` SELECT ht.id_atendimento, ht.codigo_cliente, ht.status_atendimento, ht.codigo_servico, ht.servico_nome, ht.protocolo_hub, ht.ticket_mundiale, ht.cliente_nome, ht.created_at, sd.id AS sync_data_id, sd.glpi_ticket_id, sd.status_sync, sd.sync_metadata, sd.last_sync_attempt, sd.sync_error_message, sd.created_at AS sync_created_at, sd.updated_at AS sync_updated_at
FROM hubsoft_tickets AS ht
LEFT JOIN sync_data AS sd ON ht.id_atendimento = sd.hubsoft_ticket_id
WHERE sd.status_sync IS NULL OR sd.status_sync = 'pending_create'
ORDER BY ht.created_at ASC; `;
try {
const res = await pool.query(query);
return res.rows;
} catch (err) {
logError(`Erro ao obter tickets pendentes ${err}`);
throw err;
}
}
static async getGlpiTicketIdByAtendimentoId(id_atendimento) {
const query = `
SELECT glpi_ticket_id FROM sync_data
WHERE hubsoft_ticket_id = $1;
`;
const values = [id_atendimento];
try {
const res = await pool.query(query, values);
return res.rows[0] ? res.rows[0].glpi_ticket_id : null;
} catch (err) {
logError(`Erro ao obter glpi_ticket_id por id_atendimento, ${err}`);
throw err;
}
}
static async updateClosingTicket(syncId, closeMessage) {
const client = await pool.connect();
try {
await client.query('BEGIN'); // inicia transação
// 1. Atualiza sync_data para 'closed'
const querySync = `
UPDATE sync_data
SET status_sync = 'closed',
updated_at = NOW()
WHERE id = $1
RETURNING *;
`;
const resSync = await client.query(querySync, [syncId]);
const updatedSyncData = resSync.rows[0];
if (!updatedSyncData) {
throw new Error(`Nenhum registro sync_data encontrado com o id ${syncId} para atualizar.`);
}
// 2. Atualiza hubsoft_tickets com os dados de fechamento
const queryHubsoft = `
UPDATE hubsoft_tickets
SET updated_at = NOW(),
data_fechamento = NOW(),
descricao_fechamento = $2,
status_atendimento = 'Resolvido'
WHERE id_atendimento = $1
RETURNING *;
`;
const resHubsoft = await client.query(queryHubsoft, [updatedSyncData.hubsoft_ticket_id, closeMessage]);
await client.query('COMMIT'); // confirma transação
return {
sync_data: updatedSyncData,
hubsoft_tickets: resHubsoft.rows[0]
};
} catch (err) {
await client.query('ROLLBACK'); // desfaz se der erro
logError(`Erro ao atualizar ticket: ${err}`);
throw err;
} finally {
client.release();
}
}
static async updateSyncDataStatus( ticketId, status_sync, source_last){
const query = `
UPDATE sync_data
SET status_sync = $1,
source_last = $2
WHERE id = $3
RETURNING *;
`;
const values = [status_sync, source_last, ticketId];
try {
const res = await pool.query(query, values);
return res.rows[0];
} catch (err) {
logError(`Erro ao atualizar status_sync na tabela sync_data: ${err}`);
throw err;
}
}
static async updateSyncaDataError(sync_error_message, id_atendimento) {
const query = `
UPDATE sync_data
set sync_error_message = $1,
status_sync = 'sync_error',
last_sync_attempt = NOW()
WHERE id = $2
RETURNING *;
`;
const values = [sync_error_message, id_atendimento];
try {
const res = await pool.query(query, values);
return res.rows[0];
} catch (err){
logError(`Erro ao inserir mensagem de erro na tabela sync_data: ${err}`);
throw err;
}
}
static async lockTicketForClosing(syncId) {
const query = `
UPDATE sync_data
SET status_sync = 'processing_close',
updated_at = NOW()
WHERE id = $1 AND status_sync NOT IN ('processing_close', 'closed_glpi')
RETURNING *;
`;
const values = [syncId];
try {
const res = await pool.query(query, values);
// Se a consulta retornar uma linha, o lock foi bem-sucedido.
// Se não retornar nada, outro processo já pegou o ticket.
return res.rows[0] || null;
} catch (err) {
logError(`Erro ao tentar travar o ticket para fechamento (syncId: ${syncId}): ${err}`);
throw err;
}
}
}
module.exports = HubglpiModel;
/**
* @module HubglpiModel
* @description Este módulo contém a classe `HubglpiModel` que encapsula todas as interações com o banco de dados intermediário (`hubglpi`).
* Ele é responsável por inserir, atualizar e consultar dados nas tabelas `hubsoft_tickets` e `sync_data`.
*
* Funções principais:
* - Funções de CRUD para as tabelas `hubsoft_tickets` e `sync_data`.
* - `lockTicketForClosing(syncId)`: Implementa um mecanismo de trava para evitar processamento concorrente de fechamento de tickets.
* - `getTicketDataPending()`: Busca todos os tickets que estão pendentes de criação no GLPI para serem processados pelo cron job.
*/

View File

@ -1,102 +0,0 @@
const dbConfig = require('../config/dbConfig.js');
const { logError, logInfo } = require('../utils/logger');
const hubsoftDbConfig = {
host: dbConfig.hubsoft.databaseHost,
port: dbConfig.hubsoft.databasePort,
database: dbConfig.hubsoft.databaseName,
user: dbConfig.hubsoft.databaseUser,
password: dbConfig.hubsoft.databasePassword
};
const { Pool } = require('pg');
const pool = new Pool(hubsoftDbConfig);
pool.on('connect', () => {
logInfo('Conexão com o banco de dados HubSoft (leitura) estabelecida com sucesso.');
});
pool.on('error', (err) => {
logError('Erro na conexão com o banco de dados HubSoft', err);
});
const getAtendimentosFromDB = async () => {
try {
const query = 'SELECT a.id_atendimento, a.id_usuario_abertura, a.id_atendimento_status, a.protocolo, a.descricao_abertura, a.data_cadastro, a.nome_contato, c.codigo_cliente, s.descricao, cs.id_cliente_servico FROM atendimento AS a INNER JOIN cliente_servico AS cs ON a.id_cliente_servico = cs.id_cliente_servico INNER JOIN cliente AS c ON cs.id_cliente = c.id_cliente INNER JOIN servico AS s ON cs.id_servico = s.id_servico WHERE a.id_tipo_atendimento = 4 AND a.id_usuario_abertura = 248 AND a.id_atendimento_status IN (1, 2, 33) AND s.ativo = true;';
const { rows } = await pool.query(query);
return rows;
} catch (error) {
logError('Erro ao buscar atendimentos no banco de dados HubSoft.', error);
throw error; // Propaga o erro para ser tratado no controller
}
}
const validateAtendimentoStatus = async (id_atendimento) => {
// Corrigido para prevenir SQL Injection
const query = 'SELECT id_atendimento_status FROM atendimento WHERE id_atendimento = $1;';
const values = [id_atendimento];
const { rows } = await pool.query(query, values);
return rows;
}
const validateMensagensByAtendimento = async (id_atendimento) => {
// Corrigido para prevenir SQL Injection
const query = 'SELECT id_atendimento_mensagem, id_atendimento, mensagem, data_cadastro FROM atendimento_mensagem WHERE id_atendimento = $1;';
const values = [id_atendimento];
const { rows } = await pool.query(query, values);
return rows;
}
const getNewMessagesFromDB = async (lastFetchTimestamp) => {
// Busca novas mensagens e o id_atendimento correspondente
const query = `
SELECT
m.id_atendimento_mensagem,
m.id_atendimento,
m.mensagem,
m.data_cadastro
FROM
atendimento_mensagem AS m
INNER JOIN
atendimento AS a ON m.id_atendimento = a.id_atendimento
WHERE
m.data_cadastro > $1
AND a.id_tipo_atendimento = 4
AND a.id_usuario_abertura = 248;
`;
const { rows } = await pool.query(query, [lastFetchTimestamp]);
return rows;
}
const updateFechaAtendimento = async (id_atendimento, closingMessage) => {
const query = `
UPDATE atendimento
SET descricao_fechamento = $1,
data_fechamento = $2,
id_atendimento_status = $3 -- Assumindo que o status é um ID numérico
WHERE id_atendimento = $4;
`;
// Precisamos descobrir qual é o status de "Fechado" no Hubsoft
// ATENÇÃO: Usar o ID do status, não o nome. Ex: 5 para 'Solucionado'
const STATUS_FECHADO_ID = 5; // Substitua pelo ID correto
const values = [closingMessage, new Date(), STATUS_FECHADO_ID, id_atendimento];
try {
const res = await pool.query(query, values);
return res.rows[0];
} catch (err) {
logError(`Erro ao atualizar fechamento de atendimento: ${err}`);
throw err;
}
}
module.exports = {
getAtendimentosFromDB,
validateAtendimentoStatus,
validateMensagensByAtendimento,
updateFechaAtendimento, // Exportando a função
getNewMessagesFromDB
};

View File

@ -0,0 +1,30 @@
// src/modules/close/controller/close.controller.js
const { close } = require('../useCases/closeTickets.usecase')
const { logInfo, logError } = require('../../../shared/utils/logger');
/**
* Controller para lidar com o webhook de fechamento de ticket do GLPI.
* @param {import('express').Request} req - O objeto de requisição do Express.
* @param {import('express').Response} res - O objeto de resposta do Express.
*/
const closeTicket = async (req, res) => {
try {
const result = await close(req.body)
res.status(200).json(result)
} catch (err) {
logError(err)
res.status(500).json({ error: err.message })
}
}
module.exports = { closeTicket };
/**
* @module ClosureController
* @description Este controller é o ponto de entrada para as requisições de webhook enviadas pelo GLPI quando um ticket é fechado.
*
* Funções:
* - `closeTicket(req, res)`: Recebe a notificação do GLPI, extrai os dados do corpo da requisição e invoca o `ticketService` para orquestrar o processo de fechamento do ticket correspondente no HubSoft e a atualização no banco de dados local.
* Ele é responsável por validar a requisição e responder ao GLPI com o status do processamento.
*/

View File

@ -0,0 +1,37 @@
// src/modules/close/repositories/close.repository.js
const glpiTicketsRepo = require('../../../infra/db/repositories/hubglpi/tickets.repository')
const glpiSyncRepo = require('../../../infra/db/repositories/hubglpi/sync.repository')
const hubsoftApiClient = require('../../../infra/api/hubsoft.client.js')
async function getSyncByGlpiId(glpiId) {
return glpiSyncRepo.getByGlpiId(glpiId)
}
async function lockForClosing(syncId) {
return glpiSyncRepo.lockTicketForClosing(syncId)
}
async function markClosed(syncId, message) {
const hubTicketId = await glpiSyncRepo.markAsClosed(syncId)
if (!hubTicketId) {
throw new Error(`HubTicketId não encontrado ao fechar sync ${syncId}`)}
return glpiTicketsRepo.updateCloseMessage(
hubTicketId,
message
) //TODO
}
async function closeAtendimento(hubsoftId,closingMessage){
return hubsoftApiClient.close(hubsoftId, closingMessage) //TODO
}
module.exports = {
getSyncByGlpiId,
lockForClosing,
markClosed,
closeAtendimento
}

View File

@ -0,0 +1,47 @@
// src/modules/close/services/hubsoftClose.service.js
const repository = require('../repositories/close.repository')
const { logInfo, logError } = require('../../../shared/utils/logger.js')
async function close(hubsoftId, closingMessage) {
logInfo(
`[SERVICE][HUBSOFT][CLOSE] Fechando atendimento ${hubsoftId}`
)
try {
const response = await repository.closeAtendimento(
hubsoftId,
closingMessage
)
if (
response?.status === 'error' &&
response?.msg === 'Atendimento já finalizado'
) {
logInfo(
`[SERVICE][HUBSOFT][CLOSE] Atendimento ${hubsoftId} já estava fechado`
)
return
}
if (response?.status !== 'success') {
throw new Error(
`Resposta inesperada do HubSoft: ${JSON.stringify(response)}`
)
}
logInfo(
`[SERVICE][HUBSOFT][CLOSE] Atendimento ${hubsoftId} fechado com sucesso`
)
} catch (error) {
logError(
`[SERVICE][HUBSOFT][CLOSE] Erro ao fechar atendimento ${hubsoftId}`,
error
)
throw error
}
}
module.exports = {
close
}

View File

@ -0,0 +1,70 @@
const repository = require('../repositories/close.repository.js')
const hubsoftCloseService = require('../services/hubsoftClose.service.js')
const { sanitizeGLPIComment } = require('../../../shared/utils/commentSanitizer.js')
const { logInfo, logError } = require('../../../shared/utils/logger')
async function closeTicketUseCase(bodyRequest) {
if (!bodyRequest?.item?.items_id) {
logInfo('[USECASE][CLOSE] Payload inválido recebido')
return { status: 'ignored', message: 'Payload inválido' }
}
const glpiTicketId = bodyRequest.item.items_id
logInfo(`[USECASE][CLOSE] Iniciando fechamento GLPI ${glpiTicketId}`)
try {
const syncRecord = await repository.getSyncByGlpiId(glpiTicketId)
if (!syncRecord) {
logInfo(
`[USECASE][CLOSE] Ticket GLPI ${glpiTicketId} ignorado (fora do HubGlpi)`
)
return { status: 'ignored', message: 'Ticket não pertence ao HubGlpi' }
}
const locked = await repository.lockForClosing(syncRecord.id)
if (!locked) {
logInfo(
`[USECASE][CLOSE] Ticket GLPI ${glpiTicketId} já em processamento`
)
return { status: 'ignored', message: 'Ticket já em processamento' }
}
const rawClosingMessage =
bodyRequest.item.content || 'Fechamento automático do ticket.'
const closingMessage = sanitizeGLPIComment({
content: rawClosingMessage
})
logInfo(
`[USECASE][CLOSE] Fechando HubSoft ${syncRecord.hubsoft_ticket_id}`
)
await hubsoftCloseService.close(
syncRecord.hubsoft_ticket_id,
closingMessage
)
await repository.markClosed(syncRecord.id, closingMessage)
logInfo(
`[USECASE][CLOSE] Fechamento concluído GLPI ${glpiTicketId}`
)
return { status: 'success' }
} catch (error) {
logError(
`[USECASE][CLOSE] Erro ao fechar GLPI ${glpiTicketId}`,
error
)
// TODO: repository.markError(syncId, error.message)
throw error
}
}
module.exports = {
close: closeTicketUseCase
}

View File

@ -0,0 +1,20 @@
// src/modules/comments/controller/glpiComment.controller.js
const syncGlpiCommentToHub = require('../useCases/syncGlpiCommentToHub.usecase.js')
async function handleGlpiComment(req, res) {
const { item } = req.body;
if (!item?.items_id || !item?.content || !item?.id) {
return res.status(400).json({ error: 'Payload inválido' });
}
await syncGlpiCommentToHub.sync({
glpiTicketId: item.items_id,
glpiMessageId: item.id,
rawContent: item.content
});
res.status(200).json({ status: 'ok' });
}
module.exports = {handleGlpiComment}

View File

@ -0,0 +1,42 @@
//src/modules/comments/models/glpiComment.model.js
function escapeHtml(text = '') {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
function buildHtml({ author, message, source }) {
const safeAuthor = escapeHtml(author || 'Sistema')
const safeMessage = escapeHtml(message || '')
.replace(/\n/g, '<br>')
return `
<div style="font-family: Arial, sans-serif; font-size: 13px;">
<div style="margin-bottom: 4px; color: #555;">
<strong>${safeAuthor}</strong>
<span style="color:#999;">(${source})</span>
</div>
<div style="padding-left: 6px;">
${safeMessage}
</div>
</div>
`.trim()
}
function mapMessageToGlpiComment(glpiTicketId, msg) {
return {
ticketId: glpiTicketId,
content: buildHtml({
author: msg.usuario_nome,
message: msg.mensagem,
source: 'Hubsoft'
})
}
}
module.exports = {
mapMessageToGlpiComment
}

View File

@ -0,0 +1,65 @@
// src/modules/comments/repositories/comment.repository.js
const syncRepo = require('../../../infra/db/repositories/hubglpi/sync.repository')
const commentsRepo = require('../../../infra/db/repositories/hubglpi/comments.repository')
const hubsoftCommentsRepo = require('../../../infra/db/repositories/hubsoft/messages.repository.js')
const hubsoftApiClient = require('../../../infra/api/hubsoft.client.js')
const glpiCommentsRepo = require('../../../infra/db/repositories/glpi/comments.repository')
const watermarkRepo = require('../../../infra/db/repositories/hubglpi/watermark.repository')
//DONE
async function getSyncByGlpiId(glpiTicketId) {
return syncRepo.getByGlpiId(glpiTicketId)
}
//DONE
async function getSyncByHubsoftId(hubsoftTicketId) {
return syncRepo.getSyncIdByHubsoftId(hubsoftTicketId)
}
async function commentExists({ source, sourceCommentId }) {
return commentsRepo.exists(source, sourceCommentId)
}
async function getHubsoftMessagesAfter(watermark) {
return hubsoftCommentsRepo.getNewMessages(watermark) //TODO
}
async function insertGlpiComment(content) {
return glpiCommentsRepo.insert({
content
})
}
//DONE
async function addHubsoftComment(hubsoftTicketId, content) {
return hubsoftApiClient.sendHubsoftMessage(
hubsoftTicketId,
content
)
}
async function insertSyncComment(data) {
return commentsRepo.insertSyncComment(data) //TODO: Review the code and payload
}
//DONE
async function getWatermark() {
return watermarkRepo.getJobWatermark('hubsoft_comments_sync')
}
//DONE
async function updateWatermark(date) {
return watermarkRepo.updateJobWatermark('hubsoft_comments_sync', date)
}
module.exports = {
getSyncByGlpiId,
getSyncByHubsoftId,
commentExists,
getHubsoftMessagesAfter,
insertGlpiComment,
addHubsoftComment,
insertSyncComment,
getWatermark,
updateWatermark
}

View File

@ -0,0 +1,50 @@
// src/modules/comments/useCases/syncGlpiCommentToHub.usecase.js
const repository = require('../repositories/comment.repository')
const { sanitizeGLPIComment } = require('../../../shared/utils/commentSanitizer')
const { logInfo, logError, logWarn } = require('../../../shared/utils/logger')
async function sync({ glpiTicketId, glpiMessageId, rawContent }) {
logInfo(`[COMMENTS][GLPI->HUB] Ticket ${glpiTicketId}`)
try {
const syncRecord = await repository.getSyncByGlpiId(glpiTicketId)
if (!syncRecord?.hubsoft_ticket_id) {
logWarn('[COMMENTS][GLPI->HUB] Ticket sem vínculo com Hubsoft')
return
}
const exists = await repository.commentExists({
source: 'glpi',
sourceCommentId: glpiMessageId
})
if (exists) {
logInfo('[COMMENTS][GLPI->HUB] Comentário duplicado ignorado')
return
}
const content = sanitizeGLPIComment({ content: rawContent })
const hubsoftMessageId = await repository.addHubsoftComment(
syncRecord.hubsoft_ticket_id,
content
)
await repository.insertSyncComment({
source: 'glpi',
sourceCommentId: glpiMessageId,
destinationCommentId: hubsoftMessageId,
hubsoftTicketId: syncRecord.hubsoft_ticket_id,
glpiTicketId,
content,
status: 'synced'
})
logInfo('[COMMENTS][GLPI->HUB] Comentário sincronizado com sucesso')
} catch (err) {
logError('[COMMENTS][GLPI->HUB] Erro ao sincronizar comentário', err)
}
}
module.exports = { sync }

View File

@ -0,0 +1,61 @@
// src/modules/comments/useCases/syncHubCommentToGlpi.usecase.js
const model = require('../models/glpiComment.model.js')
const repository = require('../repositories/comment.repository')
const { logInfo, logError } = require('../../../shared/utils/logger')
async function sync() {
logInfo('[COMMENTS][HUB->GLPI] Iniciando sincronização')
const watermark = await repository.getWatermark()
const messages = await repository.getHubsoftMessagesAfter(watermark)
if (!messages.length) {
logInfo('[COMMENTS][HUB->GLPI] Nenhum comentário novo')
return
}
for (const msg of messages) {
try {
const syncRecord = await repository.getSyncByHubsoftId(msg.id_atendimento)
if (!syncRecord?.glpi_ticket_id) continue
const exists = await repository.commentExists({
source: 'hubsoft',
sourceCommentId: msg.id_atendimento_mensagem
})
if (exists) continue
const payload = await model.mapMessageToGlpiComment(syncRecord.glpi_ticket_id, msg)
const glpiComment = await repository.insertGlpiComment(
payload
)
await repository.insertSyncComment({
syncDataId: syncRecord.id,
sourceSystem: 'hubsoft',
author: msg.usuario_nome,
sourceCommentId: msg.id_atendimento_mensagem,
destinationCommentId: glpiComment.id,
hubsoftTicketId: msg.id_atendimento,
glpiTicketId: syncRecord.glpi_ticket_id,
content: msg.mensagem,
status: 'synced'
})
} catch (err) {
logError('[COMMENTS][HUB->GLPI] Erro ao processar comentário', err)
}
}
await repository.updateWatermark(new Date())
logInfo('[COMMENTS][HUB->GLPI] Sincronização finalizada')
}
module.exports = { syncComments: sync }

View File

@ -1,55 +0,0 @@
// src/modules/createTickets/controller/createTickets.controller.js
const mundialeService = require('../services/mundiale.service.js');
const implantacaoService = require('../services/implantacao.service.js');
const sacService = require('../services/sac.service.js');
const ticketShared = require('../services/createTickets.service.js');
const { logInfo } = require('../../../utils/logger.js');
async function processAtendimentos() {
logInfo("[CONTROLLER] Iniciando processamento");
// 1⃣ Buscar por fonte
const mundiale = await mundialeService.fetchNew();
logInfo(`Encontrados ${mundiale.length} tickets Mundiale para processar.`);
const implantacao = await implantacaoService.fetchNew();
logInfo(`Encontrados ${implantacao.length} tickets de Implantação para processar.`);
//const sac = await sacService.fetchNew();
// 2⃣ Salvar no hubglpi
await mundialeService.saveHubGlpi(mundiale);
await implantacaoService.saveHubGlpi(implantacao);
//await sacService.saveHubGlpi(sac);
// 3⃣ Buscar pendentes que foram salvos no banco hubglpi
const pendentes = await ticketShared.fetchPendingTickets();
// 4⃣ Roteamento por tipo
for (const ticket of pendentes) {
let glpiId;
if (ticket.ticket_type === 'MUNDIALE') {
glpiId = await mundialeService.sendToGlpi(ticket);
} else if (ticket.ticket_type === 'IMPLANTACAO') {
glpiId = await implantacaoService.sendToGlpi(ticket);
} else if (ticket.ticket_type === 'SAC') {
glpiId = await sacService.sendToGlpi(ticket);
} else {
logInfo(`Tipo desconhecido, ignorando ticket id=${ticket.id}`);
continue;
}
ticketShared.insertCreateComment(ticket.id_atendimento, glpiId);
}
logInfo("[CONTROLLER] Finalizado.");
}
module.exports = { processAtendimentos };

View File

@ -1,73 +0,0 @@
// src/modules/createTickets/services/createTickets.service.js
const repositoryHubGlpi = require('../../../shared/repositories/hubglpi.repository.js');
const repositoryGlpi = require('../../../shared/repositories/glpi.repository.js');
const repositoryHubSoft = require('../../../shared/repositories/hubsoftAPI.repository.js');
const modelHubsoft = require('../../../shared/model/hubsoft.model.js');
const { hubglpi } = require('../../../config/dbConfig.js');
// --------------------------------------
// Funções principais do serviço
// --------------------------------------
async function fetchPendingTickets() {
return repositoryHubGlpi.fetchPendingTickets();
}
async function resolveEntityId(ticketData) {
const entityByService = await repositoryGlpi.getEntitiesByService(
ticketData.codigo_cliente,
ticketData.codigo_servico
);
if (entityByService) {
ticketData.entities_id = entityByService;
return ticketData;
}
const entityByClient = await repositoryGlpi.getEntitiesByClient(
ticketData.codigo_cliente
);
if (entityByClient) {
ticketData.entities_id = entityByClient;
return ticketData;
}
ticketData.entities_id = 0;
return ticketData;
}
// Funcao para definir ticket como processado
async function setAsCreated(dataTicket) {
}
//Cria uma nova entidade no GLPI
async function createEntity(ticketData) {
const entity_name = `${ticketData.codigo_cliente} - ${ticketData.nome_razao_social}`;
await repositoryGlpi.insertEntity(entity_name);
return;
}
async function insertCreateComment(ticketData, glpiId) {
const message = `Atendimento ${glpiId} criado com sucesso`;
await repositoryHubSoft.sendMessage(ticketData.id_atendimento, message);
await repositoryHubGlpi.sendMessage(glpiId, message);
return;
}
module.exports = {
fetchPendingTickets,
resolveEntityId,
setAsCreated,
createEntity
}

View File

@ -1,128 +0,0 @@
// src/modules/createTickets/services/mundiale.service.js
const repositoryHubGlpi = require('../../../shared/repositories/hubglpi.repository.js');
const repositoryGlpi = require('../../../shared/repositories/glpi.repository.js');
const repositoryHubsoft = require('../../../shared/repositories/hubsoftDB.repository.js');
const modelHubGlpi = require('../../../shared/model/hubglpi.model.js');
const modelGlpi = require('../../../shared/model/glpi.model.js');
const ticketShared = require('./createTickets.service.js');
const { logInfo, logError, logWarning } = require('../../../utils/logger.js');
const { log } = require('winston');
// --------------------------------------
// Funções principais do serviço
// --------------------------------------
async function fetchNew() {
return repositoryHubsoft.getMundialeTickets();
}
async function saveHubGlpi(tickets) {
if (!tickets.length) return;
const ticketsFormatted = tickets.map(ticket => modelHubGlpi.mapHubsoftToHubglpi(ticket, 'MUNDIALE'));
const inserted = await repositoryHubGlpi.insertTickets(ticketsFormatted);
if (inserted) {
await repositoryHubGlpi.insertSyncData(ticketsFormatted.map(ticket => ticket.id_atendimento));
}
return inserted;
}
async function sendToGlpi(ticket) {
const ticketsResolved = await ticketShared.resolveEntityId(ticket);
formatGlpiPayload(ticketsResolved);
const payload = modelGlpi.mapHubGlpiToGlpi(ticketsResolved);
const glpiId = await repositoryGlpi.insertTicket(payload);
logInfo(`Ticket GLPI criado com ID: ${glpiId} para ticket Hubsoft ID: ${ticket.id_atendimento}`);
await repositoryGlpi.insertGroupTicket(glpiId, 'MUNDIALE');
logInfo(`Grupo padrão atribuído ao ticket GLPI ID: ${glpiId}`);
await repositoryHubGlpi.updateSyncDataCreated(ticket.id_atendimento, glpiId);
logInfo(`Ticket HubGlpi ID: ${ticket.id_atendimento} marcado como criado no Banco intermediario com ID: ${glpiId}`);
return glpiId;
}
// --------------------------------------
// Mapeamento e formatação de dados
// --------------------------------------
const statusAtendimentoGLPI = {
'Novo': 1,
'Pendente': 4,
'Em atendimento': 2,
'Resolvido': 5
};
// --------------------------------------
// Formatar dados antes de enviar para o GLPI
// --------------------------------------
function formatGlpiPayload(ticket) {
ticket.status = statusAtendimentoGLPI[ticket.status_atendimento] || 1;
ticket.name = `Mundiale - Protocolo: ${ticket.ticket_mundiale} - ${ticket.cliente_nome}`;
ticket.content = formatDescription(ticket);
return;
}
const formatDescription = (ticketData) => {
let htmlDescription = `
<table style="width:100%; border-collapse: collapse;">
<tr style="background-color:#f2f2f2;">
<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Campo</th>
<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Valor</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Nome:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.cliente_nome}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Codigo:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.codigo_cliente}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Serviço:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.servico_nome}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Ticket Mundiale</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.ticket_mundiale}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Protocolo Hub:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.protocolo_hub || 'N/A'}</td>
</tr>
</table>
`;
return htmlDescription;
};
module.exports = {
fetchNew,
saveHubGlpi,
sendToGlpi
}
/**
* @module CreateTickets/MundialeService
* @description Serviço responsável por interagir com o Hubsoft e GLPI criação de tickets que provêm da Mundiale.
*/

View File

@ -0,0 +1,13 @@
// modules/tickets/controllers/controller.js
const { syncTicketsUseCase } = require('../useCases/syncTickets.usecase')
const { logInfo } = require('../../../shared/utils/logger')
async function syncTickets() {
logInfo('[TICKETS] Iniciando sincronização de tickets')
await syncTicketsUseCase()
logInfo('[TICKETS] Sincronização finalizada')
}
module.exports = {syncTickets}

View File

@ -0,0 +1,195 @@
// src/modules/tickets/models/glpi/cancelamento.model.js
function toGlpiPayload(ticket) {
return {
entities_id: ticket.entities_id || 0,
name: buildTitle(ticket),
content: buildHtml(ticket),
status: resolveGlpiStatus(ticket.status_atendimento),
date: ticket.created_at,
date_mod: new Date(),
users_id_recipient: Number(process.env.GLPI_USER_ID) || 0,
urgency: 3,
impact: 3,
priority: 3,
type: 2,
date_creation: ticket.created_at || new Date(),
itilcategories_id: 0,
slas_id_ttr: 37
}
}
function resolveGlpiStatus(status) {
const map = {
'Novo': 1,
'Pendente': 4,
'Em atendimento': 2,
'Resolvido': 5
}
return map[status] || 1
}
function buildTitle(ticket) {
return `CANCELAMENTO - ${ticket.codigo_cliente}-${ticket.codigo_servico} - ${ticket.nome_razaosocial} - ${ticket.servico_nome}`
}
function buildHtml(ticket) {
const docLabel = ticket.tipo_pessoa === 'pf' ? 'CPF' : 'CNPJ'
const documento = formatDocument(ticket.cpf_cnpj, ticket.tipo_pessoa)
const servico = resolveService(ticket.servico_nome)
return `
<table style="width:100%; border-collapse: collapse;">
<!-- ===== CABEÇALHO DADOS HUBSOFT ===== -->
<tr style="background-color:#f2f2f2;">
<th colspan="2" style="padding: 8px; border: 1px solid #ddd; text-align:left;">
Dados HubSoft
</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong> de Operação</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.protocolo_hub || "N/A"}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Solicitante</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.vendedor || "N/A"}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Código do Cliente</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.codigo_cliente}</td>
</tr>
<!-- ===== CABEÇALHO DADOS CLIENTE ===== -->
<tr style="background-color:#f2f2f2;">
<th colspan="2" style="padding: 8px; border: 1px solid #ddd; text-align:left;">
Dados Cliente
</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Cliente</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.nome_razaosocial}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>${docLabel}</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${documento}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Nome Contato</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.cliente_nome || "N/A"}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Email Contato</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.email || "N/A"}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Telefone Contato</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.telefone || "N/A"}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Endereço Instalação</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.endereco || "N/A"}</td>
</tr>
<!-- ===== CABEÇALHO DADOS DO SERVIÇO ===== -->
<tr style="background-color:#f2f2f2;">
<th colspan="2" style="padding: 8px; border: 1px solid #ddd; text-align:left;">
Dados do Serviço Cancelado
</th>
</tr>
<tr>
<td colspan="2" style="padding: 0; border: 1px solid #ddd;">
<table style="width:100%; border-collapse: collapse;">
<tr style="background-color:#fafafa;">
<th style="padding: 8px; border: 1px solid #ddd; text-align:left;">Produto</th>
<th style="padding: 8px; border: 1px solid #ddd; text-align:left;">Quantidade</th>
<th style="padding: 8px; border: 1px solid #ddd; text-align:left;">Descrição</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">${servico.produto}</td>
<td style="padding: 8px; border: 1px solid #ddd;">${servico.qtd}</td>
<td style="padding: 8px; border: 1px solid #ddd;">${servico.descricao || ""}</td>
</tr>
</table>
</td>
</tr>
<!-- ===== OBSERVAÇÕES ===== -->
<tr style="background-color:#f2f2f2;">
<th colspan="2" style="padding: 8px; border: 1px solid #ddd; text-align:left;">
Observações
</th>
</tr>
<tr>
<td colspan="2" style="padding: 8px; border: 1px solid #ddd;">
${nl2br(ticket.descricao_abertura) || "N/A"}
</td>
</tr>
</table>
`
}
function formatCPF(cpf) {
return cpf?.replace(/\D/g, '')
.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4')
}
function formatCNPJ(cnpj) {
return cnpj?.replace(/\D/g, '')
.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, '$1.$2.$3/$4-$5')
}
function formatDocument(doc, tipo) {
if (!doc) return 'N/A'
return tipo === 'pf' ? formatCPF(doc) : formatCNPJ(doc)
}
const serviceDictionary = {
// ---------- LAN-TO-LAN ----------
"Lan-to-Lan 50 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "50 Mbps" },
"Lan-to-Lan 100 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "100 Mbps" },
"Lan-to-Lan 200 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "200 Mbps" },
"Lan-to-Lan 300 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "300 Mbps" },
"Lan-to-Lan 500 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "500 Mbps" },
"Lan-to-Lan 700 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "700 Mbps" },
"Lan-to-Lan": { produto: "Lan-to-Lan", qtd: 1, descricao: null },
// ---------- LINK DEDICADO ----------
"Link de Internet Dedicado 20 Mbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "20 Mbps Full Duplex" },
"Link de Internet Dedicado 100 Mbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "100 Mbps Full Duplex" },
"Link de Internet Dedicado 1Gbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "1 Gbps Full Duplex" },
"Link de Internet Dedicado 2 Gbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "2 Gbps Full Duplex" },
// Default genérico caso venha coisa nova
};
function resolveService(name) {
return serviceDictionary[name] || { produto: name, qtd: 1, descricao: null }
}
function nl2br(text) {
if (!text) return ''
return text.replace(/\r\n|\n|\r/g, '<br>')
}
module.exports = { toGlpiPayload }

View File

@ -1,113 +1,44 @@
// src/modules/createTickets/services/mundiale.service.js
const repositoryHubGlpi = require('../../../shared/repositories/hubglpi.repository.js');
const repositoryGlpi = require('../../../shared/repositories/glpi.repository.js');
const repositoryHubsoft = require('../../../shared/repositories/hubsoftDB.repository.js');
const modelHubGlpi = require('../../../shared/model/hubglpi.model.js');
const modelGlpi = require('../../../shared/model/glpi.model.js');
const ticketShared = require('./createTickets.service.js');
const { logInfo, logWarning } = require('../../../utils/logger.js');
// src/modules/tickets/models/glpi/implantacao.model.js
// --------------------------------------
// Funções principais do serviço
// --------------------------------------
async function fetchNew() {
return repositoryHubsoft.getImplantacaoTickets();
function toGlpiPayload(ticket) {
return {
entities_id: ticket.entities_id || 0,
name: buildTitle(ticket),
content: buildHtml(ticket),
status: resolveGlpiStatus(ticket.status_atendimento),
date: ticket.created_at,
date_mod: new Date(),
users_id_recipient: process.env.GLPI_USER_ID || 0,
urgency: 3,
impact: 3,
priority: 3,
type: 2,
date_creation: ticket.created_at || new Date(),
itilcategories_id: 0,
slas_id_ttr: 37,
}
}
async function saveHubGlpi(tickets) {
if (!tickets.length) return;
const ticketsFormatted = tickets.map(ticket =>
modelHubGlpi.mapHubsoftToHubglpi(ticket, 'IMPLANTACAO')
);
const inserted = await repositoryHubGlpi.insertTickets(ticketsFormatted);
if (inserted) {
await repositoryHubGlpi.insertSyncData(
ticketsFormatted.map(ticket => ticket.id_atendimento)
);
}
return inserted;
function resolveGlpiStatus(status) {
const map = {
'Novo': 1,
'Pendente': 4,
'Em atendimento': 2,
'Resolvido': 5
}
return map[status] || 1
}
async function sendToGlpi(ticket) {
const ticketsResolved = await ticketShared.resolveEntityId(ticket);
if (ticketsResolved.entities_id === 0) {
logWarning(`Entidade não encontrada para o ticket id=${ticket.id_atendimento}.`);
}
formatGlpiPayload(ticketsResolved);
const payload = modelGlpi.mapHubGlpiToGlpi(ticketsResolved);
const glpiId = await repositoryGlpi.insertTicket(payload);
logInfo(`Ticket GLPI criado com ID=${glpiId} para o ticket id=${ticket.id_atendimento}.`);
await repositoryGlpi.insertGroupTicket(glpiId, 'IMPLANTACAO');
await repositoryHubGlpi.updateSyncDataCreated(ticket.id_atendimento, glpiId);
function buildTitle(ticket) {
return `IMPLANTAÇÃO - ${ticket.codigo_cliente}-${ticket.codigo_servico} - ${ticket.nome_razaosocial} - ${ticket.servico_nome}`
}
function buildHtml(ticket) {
const docLabel = ticket.tipo_pessoa === 'pf' ? 'CPF' : 'CNPJ'
const documento = formatDocument(ticket.cpf_cnpj, ticket.tipo_pessoa)
const servico = resolveService(ticket.servico_nome)
// --------------------------------------
// Formatar dados antes de enviar para o GLPI
// --------------------------------------
function formatGlpiPayload(ticket) {
const title = `IMPLANTAÇÃO - ${ticket.codigo_cliente}-${ticket.codigo_servico}-${ticket.nome_razaosocial} - ${ticket.servico_nome}`;
const description = formatDescription(ticket);
ticket.name = title;
ticket.content = description;
return;
}
// --------------------------------------
// Dicionário de Serviços
// --------------------------------------
const serviceDictionary = {
// ---------- LAN-TO-LAN ----------
"Lan-to-Lan 50 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "50 Mbps" },
"Lan-to-Lan 100 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "100 Mbps" },
"Lan-to-Lan 200 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "200 Mbps" },
"Lan-to-Lan 300 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "300 Mbps" },
"Lan-to-Lan 500 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "500 Mbps" },
"Lan-to-Lan 700 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "700 Mbps" },
"Lan-to-Lan": { produto: "Lan-to-Lan", qtd: 1, descricao: null },
// ---------- LINK DEDICADO ----------
"Link de Internet Dedicado 20 Mbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "20 Mbps Full Duplex" },
"Link de Internet Dedicado 100 Mbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "100 Mbps Full Duplex" },
"Link de Internet Dedicado 1Gbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "1 Gbps Full Duplex" },
"Link de Internet Dedicado 2 Gbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "2 Gbps Full Duplex" },
// Default genérico caso venha coisa nova
};
// --------------------------------------
// Tabela HTML da descrição
// --------------------------------------
const formatDescription = (ticket) => {
const documentoFormatado = formatDocument(ticket.cpf_cnpj, ticket.tipo_pessoa);
const docLabel = ticket.tipo_pessoa === "pf" ? "CPF" : "CNPJ";
const servico = resolveService(ticket.servico_nome);
return `
return `
<table style="width:100%; border-collapse: collapse;">
<!-- ===== CABEÇALHO DADOS COMERCIAL ===== -->
@ -124,7 +55,7 @@ const formatDescription = (ticket) => {
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Gerente Responsável</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.usuario_que_abriu || "N/A"}</td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.vendedor || "N/A"}</td>
</tr>
<tr>
@ -146,7 +77,7 @@ const formatDescription = (ticket) => {
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>${docLabel}</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${documentoFormatado}</td>
<td style="padding: 8px; border: 1px solid #ddd;">${documento}</td>
</tr>
<tr>
@ -201,54 +132,64 @@ const formatDescription = (ticket) => {
</tr>
<tr>
<td colspan="2" style="padding: 8px; border: 1px solid #ddd;">${ticket.descricao_abertura || "N/A"}</td>
<td colspan="2" style="padding: 8px; border: 1px solid #ddd;">
${nl2br(ticket.descricao_abertura) || "N/A"}
</td>
</tr>
</table>
`;
};
`
}
// --------------------------------------
// Formatadores de Documento
// --------------------------------------
function formatCPF(cpf) {
cpf = cpf.replace(/\D/g, "");
return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
return cpf?.replace(/\D/g, '')
.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4')
}
function formatCNPJ(cnpj) {
cnpj = cnpj.replace(/\D/g, "");
return cnpj.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, "$1.$2.$3/$4-$5");
return cnpj?.replace(/\D/g, '')
.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, '$1.$2.$3/$4-$5')
}
function formatDocument(doc, tipo) {
if (!doc) return "N/A";
return tipo === "pf" ? formatCPF(doc) : formatCNPJ(doc);
if (!doc) return 'N/A'
return tipo === 'pf' ? formatCPF(doc) : formatCNPJ(doc)
}
const serviceDictionary = {
// ---------- LAN-TO-LAN ----------
"Lan-to-Lan 50 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "50 Mbps" },
"Lan-to-Lan 100 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "100 Mbps" },
"Lan-to-Lan 200 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "200 Mbps" },
"Lan-to-Lan 300 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "300 Mbps" },
"Lan-to-Lan 500 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "500 Mbps" },
"Lan-to-Lan 700 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "700 Mbps" },
"Lan-to-Lan": { produto: "Lan-to-Lan", qtd: 1, descricao: null },
// --------------------------------------
// Resolve Serviço
// --------------------------------------
// ---------- LINK DEDICADO ----------
"Link de Internet Dedicado 20 Mbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "20 Mbps Full Duplex" },
function resolveService(servicoNome) {
return serviceDictionary[servicoNome] || {
produto: servicoNome,
qtd: 1,
descricao: null
};
}
"Link de Internet Dedicado 100 Mbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "100 Mbps Full Duplex" },
"Link de Internet Dedicado 1Gbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "1 Gbps Full Duplex" },
// --------------------------------------
// Exportação
// --------------------------------------
"Link de Internet Dedicado 2 Gbps Full Duplex":
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "2 Gbps Full Duplex" },
module.exports = {
fetchNew,
saveHubGlpi,
sendToGlpi
// Default genérico caso venha coisa nova
};
function resolveService(name) {
return serviceDictionary[name] || { produto: name, qtd: 1, descricao: null }
}
function nl2br(text) {
if (!text) return ''
return text.replace(/\r\n|\n|\r/g, '<br>')
}
module.exports = { toGlpiPayload }

View File

@ -0,0 +1,69 @@
// src/modules/tickets/models/glpi/mundiale.model.js
function toGlpiPayload(ticket) {
return {
entities_id: ticket.entities_id || 0,
name: buildTitle(ticket),
content: buildHtml(ticket),
status: resolveGlpiStatus(ticket.status_atendimento),
date: ticket.created_at,
date_mod: new Date(),
users_id_recipient: process.env.GLPI_USER_ID || 0,
urgency: 3,
impact: 3,
priority: 3,
type: 2,
date_creation: ticket.create_at || new Date(),
itilcategories_id: 0,
slas_id_ttr: 37
}
}
function resolveGlpiStatus(status) {
const map = {
'Novo': 1,
'Pendente': 4,
'Em atendimento': 2,
'Resolvido': 5
}
return map[status] || 1
}
function buildHtml(ticket) {
return `
<table style="width:100%; border-collapse: collapse;">
<tr style="background-color:#f2f2f2;">
<th style="padding: 8px; border: 1px solid #ddd;">Campo</th>
<th style="padding: 8px; border: 1px solid #ddd;">Valor</th>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Nome</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.cliente_nome}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Código</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.codigo_cliente}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Serviço</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.servico_nome}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Ticket Mundiale</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.ticket_mundiale}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Protocolo Hub</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${ticket.protocolo_hub || 'N/A'}</td>
</tr>
</table>
`
}
function buildTitle(ticket){
return `Mundiale - Protocolo: ${ticket.ticket_mundiale} - ${ticket.cliente_nome} `
}
module.exports = {
toGlpiPayload
}

View File

@ -0,0 +1,88 @@
// src/modules/tickets/models/hubglpi/model.js
function fromHubsoft(ticket, type) {
return {
id_atendimento: ticket.id_atendimento,
codigo_cliente: toInt(ticket.codigo_cliente),
status_atendimento: resolveHubsoftStatus(ticket.id_atendimento_status),
servico_nome: ticket.servico_nome || ticket.descricao || null,
protocolo_hub: ticket.protocolo,
ticket_mundiale: extractMundialeTicket(ticket.descricao_abertura, type),
codigo_servico: toInt(ticket.id_cliente_servico),
cliente_nome: ticket.nome_contato,
cpf_cnpj: sanitizeCpfCnpj(ticket.cpf_cnpj),
telefone: sanitizePhone(ticket.telefone),
email: ticket.email || null,
tipo_pessoa: ticket.tipo_pessoa || null,
nome_razaosocial: ticket.nome_razaosocial || null,
descricao_abertura: ticket.descricao_abertura || null,
endereco: buildEndereco(ticket),
descricao_fechamento: ticket.descricao_fechamento || null,
data_fechamento: ticket.data_fechamento || null,
created_at: ticket.data_cadastro,
ticket_type: type,
vendedor: ticket.vendedor
}
}
function toInt(value) {
const n = Number(value)
return Number.isInteger(n) ? n : null
}
function sanitizeCpfCnpj(value) {
if (!value) return null
return String(value).replace(/\D/g, '') || null
}
function sanitizePhone(value) {
if (!value) return null
return String(value).replace(/\D/g, '') || null
}
function buildEndereco(ticket) {
if (!ticket.endereco || !ticket.numero || !ticket.cidade || !ticket.estado) {
return null
}
const complemento = ticket.complemento ? ` - ${ticket.complemento}` : ''
const bairro = ticket.bairro ? `${ticket.bairro}` : ''
const cep = ticket.cep ? ` - CEP ${ticket.cep}` : ''
return `${ticket.endereco}, ${ticket.numero}${complemento}, ${bairro} - ${ticket.cidade}/${ticket.estado}${cep}`
}
function resolveHubsoftStatus(status) {
const map = {
1: 'Pendente',
2: 'Em atendimento',
3: 'Resolvido',
31: 'Pendente',
32: 'Pendente',
33: 'Novo'
}
return map[status] || 'Novo'
}
function extractMundialeTicket(text, type) {
if (type !== 'MUNDIALE') return null
if (!text) return null
const onlyDigits = text.replace(/[^0-9]/g, '')
if (!onlyDigits) return null
//Somente digitos maiores que 10
if (onlyDigits.length > 18) return null
return Number(onlyDigits)
}
module.exports = {
fromHubsoft
}

View File

@ -0,0 +1,158 @@
// src/modules/tickets/respository.js
const hubglpiTicketsRepo = require('../../../infra/db/repositories/hubglpi/tickets.repository.js')
const hubsoftTicketsRepo = require('../../../infra/db/repositories/hubsoft/tickets.repository.js')
const hubglpiSyncRepo = require('../../../infra/db/repositories/hubglpi/sync.repository.js')
const hubglpiCommentsRepo = require('../../../infra/db/repositories/hubglpi/comments.repository.js')
const glpiTicketsRepo = require('../../../infra/db/repositories/glpi/tickets.repository.js')
const glpiEntitiesRepo = require('../../../infra/db/repositories/glpi/entities.repository.js')
const glpiGroupsRepo = require('../../../infra/db/repositories/glpi/groups.repository.js')
const hubsoftApiClient = require('../../../infra/api/hubsoft.client.js')
const watermarkRepository = require('../../../infra/db/repositories/hubglpi/watermark.repository.js');
const JOB_NAME = 'hubsoft_tickets_sync';
async function getWaterMark() {
return watermarkRepository.getJobWatermark(JOB_NAME);
}
async function updateWaterMark(newWatermark) {
return watermarkRepository.updateJobWatermark(JOB_NAME, newWatermark);
}
const TYPES = Object.freeze({
MUNDIALE: 4,
IMPLANTACAO: 21,
CANCELAMENTO: 27,
SAC: 41,
TITULARIDADE: 60
});
async function getMundialeTickets(watermark) {
return hubsoftTicketsRepo.getTicketsByTipo({
tipoAtendimento: TYPES.MUNDIALE,
usuarioAbertura: 248,
watermark
});
}
async function getImplantacaoTickets(watermark) {
return hubsoftTicketsRepo.getTicketsByTipo({
tipoAtendimento: TYPES.IMPLANTACAO,
watermark
});
}
async function getCancelamentoTickets(watermark) {
return hubsoftTicketsRepo.getTicketsByTipo({
tipoAtendimento: TYPES.CANCELAMENTO,
watermark
});
}
async function getSacTickets(watermark) {
return hubsoftTicketsRepo.getTicketsByTipo({
tipoAtendimento: TYPES.SAC,
watermark
});
}
async function getTrocaTTickets(watermark) {
return hubsoftTicketsRepo.get(watermark)
}
async function insertTicketsHubGlpi(tickets){
return hubglpiTicketsRepo.insertTickets(tickets)
}
async function insertSyncDataByIds(ids){
return hubglpiSyncRepo.insertSyncData(ids)
}
async function fetchPendingTickets(){
return hubglpiTicketsRepo.fetchPendingTickets()
}
async function insertTicketGlpi(ticket){
return glpiTicketsRepo.insertTicket(ticket)
}
async function getEntitiesByService(codigoCliente, codigoServico){
return glpiEntitiesRepo.getEntitiesByService(codigoCliente, codigoServico)
}
async function getEntitiesByClient(codigoCliente){
return glpiEntitiesRepo.getEntitiesByClient(codigoCliente)
}
const GROUP_BY_TYPE = {
IMPLANTACAO: 'IMPLANTACAO',
TITULARIDADE: 'IMPLANTACAO',
CANCELAMENTO: 'IMPLANTACAO',
MUNDIALE: 'NOC',
SAC: 'NOC'
}
async function insertGroupTicket(id, type){
const group = GROUP_BY_TYPE[type] || 'NOC'
if (group === 'IMPLANTACAO') {
return glpiGroupsRepo.insertGroupImplantacao(id)
}
return glpiGroupsRepo.insertGroupNOC(id)
}
async function updateSyncDataCreated(hubId, glpiId){
return hubglpiSyncRepo.updateSyncDataCreated(hubId, glpiId)
}
async function sendHubsoftMessage(hubId, message){
return hubsoftApiClient.sendHubsoftMessage(hubId, message)
}
async function sendHubglpiMessage(glpiId, message) {
const syncDataId = await hubglpiSyncRepo.getSyncIdByHubsoftId(glpiId);
if (!syncDataId) {
throw new Error(`sync_data não encontrado para ticket ${glpiId}`);
}
return hubglpiCommentsRepo.insertSyncComment({
syncDataId,
content: message,
author: 'hubsoft-sync'
});
}
module.exports = {
// watermark
getWaterMark,
updateWaterMark,
// hubsoft reads
getMundialeTickets,
getImplantacaoTickets,
getCancelamentoTickets,
getSacTickets,
getTrocaTTickets,
// hubglpi
insertTicketsHubGlpi,
insertSyncDataByIds,
fetchPendingTickets,
updateSyncDataCreated,
sendHubglpiMessage,
// glpi
insertTicketGlpi,
getEntitiesByService,
getEntitiesByClient,
insertGroupTicket,
// hubsoft api
sendHubsoftMessage,
}

View File

@ -0,0 +1,63 @@
// src/modules/createTickets/services/cancelamento.service.js
const repository = require('../repositories/ticket.repository.js');
const modelHubGlpi = require('../models/hubglpi/model.js');
const ticketEntityResolver = require('./resolveTicketEntity.service.js')
const cancelamentoGlpiModel = require('../models/glpi/cancelamento.model.js');
const { logInfo, logError, logWarning } = require('../../../shared/utils/logger.js');
// --------------------------------------
// Funções principais do serviço
// --------------------------------------
async function fetchNew(watermark) {
logInfo('[CANCELAMENTO] Coletando novos chamados')
const rawTickets = await repository.getCancelamentoTickets(watermark)
return rawTickets.map(t => modelHubGlpi.fromHubsoft(t, 'CANCELAMENTO'))
}
async function saveHubGlpi(tickets) {
if (!tickets.length) return
logInfo('[CANCELAMENTO] Inserindo chamados no HubGlpi')
await repository.insertTicketsHubGlpi(tickets)
logInfo('[CANCELAMENTO] Inserindo dado de sincronia dos chamados')
await repository.insertSyncDataByIds(tickets.map(t => t.id_atendimento)
)
}
async function sendToGlpi(ticket) {
logInfo(`[CANCELAMENTO] Iniciando envio para GLPI - id_atendimento=${ticket.id_atendimento}`)
try {
logInfo('[CANCELAMENTO] Verificando id da entidade do ticket', { id_atendimento: ticket.id_atendimento })
const resolved = await ticketEntityResolver.resolveEntityId(ticket)
logInfo('[CANCELAMENTO] Entidade resolvida')
const payload = cancelamentoGlpiModel.toGlpiPayload(resolved)
logInfo('[CANCELAMENTO] Payload preparado para GLPI', payload)
logInfo('[CANCELAMENTO] Inserindo ticket no GLPI', { id_atendimento: ticket.id_atendimento })
const glpiId = await repository.insertTicketGlpi(payload)
logInfo('[CANCELAMENTO] Ticket inserido no GLPI', { id_atendimento: ticket.id_atendimento, glpiId })
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 })
return glpiId
} catch (error) {
logError(error, `[CANCELAMENTO] Falha ao enviar ticket para GLPI - id_atendimento=${ticket.id_atendimento}`)
throw error
}
}
module.exports = {
fetchNew,
saveHubGlpi,
sendToGlpi
}
/**
* @module CreateTickets/CancelamentoService
* @description Serviço responsável por interagir com o Hubsoft e GLPI criação de tickets de cancelamento.
*/

View File

@ -0,0 +1,14 @@
// src/modules/createTickets/services/createTickets.service.js
const repository = require('../repositories/ticket.repository')
// --------------------------------------
// Funções principais do serviço
// --------------------------------------
async function fetchPendingTickets() {
return repository.fetchPendingTickets();
}
module.exports = {
fetchPendingTickets
}

View File

@ -0,0 +1,43 @@
// src/modules/tickets/services/implantacao.service.js
const repository = require('../repositories/ticket.repository.js')
const modelHubGlpi = require('../models/hubglpi/model.js')
const ticketEntityResolver = require('./resolveTicketEntity.service.js')
const implantacaoGlpiModel = require('../models/glpi/implantacao.model.js')
const { logInfo, logError } = require('../../../shared/utils/logger.js')
async function fetchNew(watermark) {
logInfo('[IMPLANTACAO] Coletando novos chamados')
const raw = await repository.getImplantacaoTickets(watermark)
return raw.map(t => modelHubGlpi.fromHubsoft(t, 'IMPLANTACAO'))
}
async function saveHubGlpi(tickets) {
if (!tickets.length) return
logInfo('[IMPLANTACAO] Salvando tickets no HubGLPI')
await repository.insertTicketsHubGlpi(tickets)
await repository.insertSyncDataByIds(tickets.map(t => t.id_atendimento))
}
async function sendToGlpi(ticket) {
logInfo(`[IMPLANTACAO] Enviando ticket ${ticket.id_atendimento} para GLPI`)
try {
const resolved = await ticketEntityResolver.resolveEntityId(ticket)
const payload = implantacaoGlpiModel.toGlpiPayload(resolved)
const glpiId = await repository.insertTicketGlpi(payload)
await repository.insertGroupTicket(glpiId, 'IMPLANTACAO')
await repository.updateSyncDataCreated(ticket.id_atendimento, glpiId)
return glpiId
} catch (err) {
logError(err, `[IMPLANTACAO] Erro ao enviar ticket ${ticket.id_atendimento}`)
throw err
}
}
module.exports = {
fetchNew,
saveHubGlpi,
sendToGlpi
}

View File

@ -0,0 +1,63 @@
// src/modules/createTickets/services/mundiale.service.js
const repository = require('../repositories/ticket.repository.js');
const modelHubGlpi = require('../models/hubglpi/model.js');
const ticketEntityResolver = require('./resolveTicketEntity.service.js')
const mundialeGlpiModel = require('../models/glpi/mundiale.model.js');
const { logInfo, logError, logWarning } = require('../../../shared/utils/logger.js');
// --------------------------------------
// Funções principais do serviço
// --------------------------------------
async function fetchNew(watermark) {
logInfo('[MUNDIALE] Coletando novos chamados')
const rawTickets = await repository.getMundialeTickets(watermark)
return rawTickets.map(t => modelHubGlpi.fromHubsoft(t, 'MUNDIALE'))
}
async function saveHubGlpi(tickets) {
if (!tickets.length) return
logInfo('[MUNDIALE] Inserindo chamados no HubGlpi')
await repository.insertTicketsHubGlpi(tickets)
logInfo('[MUNDIALE] Inserindo dado de sincronia dos chamados')
await repository.insertSyncDataByIds(tickets.map(t => t.id_atendimentox)
)
}
async function sendToGlpi(ticket) {
logInfo(`[MUNDIALE] Iniciando envio para GLPI - id_atendimento=${ticket.id_atendimento}`)
try {
logInfo('[MUNDIALE] Verificando id da entidade do ticket', { id_atendimento: ticket.id_atendimento })
const resolved = await ticketEntityResolver.resolveEntityId(ticket)
logInfo('[MUNDIALE] Entidade resolvida', { id_atendimento: ticket.id_atendimento, resolved })
const payload = mundialeGlpiModel.toGlpiPayload(resolved)
logInfo('[MUNDIALE] Payload preparado para GLPI')
logInfo('[MUNDIALE] Inserindo ticket no GLPI', { id_atendimento: ticket.id_atendimento })
const glpiId = await repository.insertTicketGlpi(payload)
logInfo('[MUNDIALE] Ticket inserido no GLPI', { id_atendimento: ticket.id_atendimento, glpiId })
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 })
return glpiId
} catch (error) {
logError(error, `[MUNDIALE] Falha ao enviar ticket para GLPI - id_atendimento=${ticket.id_atendimento}`)
throw error
}
}
module.exports = {
fetchNew,
saveHubGlpi,
sendToGlpi
}
/**
* @module CreateTickets/MundialeService
* @description Serviço responsável por interagir com o Hubsoft e GLPI criação de tickets que provêm da Mundiale.
*/

View File

@ -0,0 +1,36 @@
// src/modules/tickets/services/resolveTicketEntity.service.js
const repository = require('../repositories/ticket.repository.js')
async function resolveEntityId(ticketData) {
const entityByService = await repository.getEntitiesByService(
ticketData.codigo_cliente,
ticketData.codigo_servico
);
if (entityByService) {
return {
...ticketData,
entities_id: entityByService
}
}
const entityByClient = await repository.getEntitiesByClient(
ticketData.codigo_cliente
);
if (entityByClient) {
return {
...ticketData,
entities_id: entityByClient
}
}
return {
...ticketData,
entities_id: 0
}
}
module.exports = { resolveEntityId }

View File

@ -0,0 +1,18 @@
// src/modules/tickets/services/ticketNotifications.service.js
const repository = require('../repositories/ticket.repository')
async function fetchPendingTickets() {
return repository.fetchPendingTickets()
}
async function notifyTicketCreated(hubId, glpiId) {
const message = `Atendimento ${glpiId} criado com sucesso`
//await repository.sendHubsoftMessage(hubId, message)
await repository.sendHubglpiMessage(hubId, message)
}
module.exports = {
fetchPendingTickets,
notifyTicketCreated
}

View File

@ -0,0 +1,92 @@
//src/modules/tickes/useCases/syncTickets.usecase.js
const notifyTicketCreated = require('../services/ticketNotifications.service.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')
//const sacService = require('../services/sac.service.js') //TODO
const getAuthToken = require('../../../infra/api/hubsoft.auth.js')
const ticketShared = require('../services/createTickets.service.js')
const { logInfo, logError } = require('../../../shared/utils/logger.js')
async function syncTicketsUseCase() {
logInfo('[USECASE] Buscando novos tickets no Hubsoft')
const waterMark = await repository.getWaterMark()
logInfo(`Buscando Tickets novos desde de: ${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 cancelamento = await cancelamentoService.fetchNew(waterMark)
logInfo(`[USECASE] ${cancelamento.length} tickets Cancelamento encontrados`)
await mundialeService.saveHubGlpi(mundiale)
await implantacaoService.saveHubGlpi(implantacao)
await cancelamentoService.saveHubGlpi(cancelamento)
const allFetchedTickets = [
...mundiale,
...implantacao,
...cancelamento
]
const newWaterMark = resolveNewWatermark(allFetchedTickets, waterMark)
if (newWaterMark !== waterMark) {
await repository.updateWaterMark(newWaterMark)
logInfo(`[USECASE] Watermark atualizada para: ${newWaterMark}`)
}
const pendentes = await ticketShared.fetchPendingTickets()
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
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}`)
}
}
}
function resolveTicketService(type) {
const map = {
MUNDIALE: mundialeService,
IMPLANTACAO: implantacaoService,
CANCELAMENTO: cancelamentoService,
//SAC: sacService, //TODO
//TITULARIDADE: trocaTitularidadeService //TODO
}
return map[type]
}
function resolveNewWatermark(tickets, currentWatermark) {
if (!tickets?.length) return currentWatermark
return tickets.reduce((max, ticket) => {
if (!ticket.created_at) return max
return ticket.created_at > max ? ticket.created_at : max
}, currentWatermark)
}
module.exports = { syncTicketsUseCase }

View File

@ -1,17 +0,0 @@
const { Router } = require('express');
const express = require('express');
const closureController = require('./controller/closureController.js');
const commentController = require('./controller/commentController'); // Novo
const router = Router();
// Aplica o middleware de parsing de JSON para todas as rotas deste router.
// Força o parsing como JSON mesmo que o Content-Type não seja application/json.
router.use(express.json({ type: '*/*' }));
router.post('/webhook/close-ticket', closureController.closeTicket);
router.post('/webhook/new-comment', commentController.handleNewComment); // Nova rota
module.exports = router;

View File

@ -1,141 +0,0 @@
// src/services/commentService.js
const hubsoftModel = require('../model/hubsoftModel.js');
const commentModel = require('../model/commentModel.js'); // a ser criado
const glpiModel = require('../model/glpiModel.js');
const hubglpiModel = require('../model/hubglpiModel.js');
const hubsoftService = require('./hubsoftService.js');
const { sanitizeGLPIComment } = require('../utils/commentSanitizer.js');
const { logInfo, logError, logWarning } = require('../utils/logger');
const { log } = require('winston');
/**
* Busca novos comentários no HubSoft e os salva no banco intermediário.
*/
async function syncHubsoftCommentsToLocalDB() {
logInfo('Iniciando sincronização de comentários do HubSoft...');
const lastRun = await commentModel.getLastRunTimestamp('hubsoft_comments_sync');
const newMessages = await hubsoftModel.getNewMessagesFromDB(lastRun);
if (newMessages.length === 0) {
logInfo('Nenhum comentário novo encontrado no HubSoft.');
return;
}
logInfo(`Encontrados ${newMessages.length} novos comentários no HubSoft.`);
for (const message of newMessages) {
try {
// A função insertComment deve ter uma cláusula ON CONFLICT para não duplicar
const commentInserted = await commentModel.insertComment({
hubsoftAtendimentoId: message.id_atendimento,
sourceSystem: 'hubsoft',
sourceCommentId: message.id_atendimento_mensagem,
content: message.mensagem,
// author: ... se disponível
});
if (commentInserted){
logInfo(`Comentário HubSoft ID ${message.id_atendimento_mensagem} salvo para sincronização.`);
}else{
logInfo(`Chamado HubSoft ID ${message.id_atendimento} não possui registro de sincronização. Comentário ignorado.`);
}
} catch (error) {
// Se o erro for de violação de chave única, apenas ignoramos.
if (error.code !== '23505') { // Código de erro do PostgreSQL para unique_violation
logError(`Erro ao salvar comentário HubSoft ID ${message.id_atendimento_mensagem}:`, error);
}
}
}
// Atualiza o timestamp da última execução
await commentModel.updateLastRunTimestamp('hubsoft_comments_sync', new Date());
logInfo('Sincronização de comentários do HubSoft para o banco local concluída.');
}
/**
* Envia comentários pendentes do banco local para o GLPI.
*/
async function sendPendingCommentsToGlpi() {
// Busca comentários com origem 'hubsoft' que estão pendentes de envio para o GLPI
const pendingComments = await commentModel.getPendingCommentsForDestination('glpi');
if (pendingComments.length === 0) {
return; // Nenhum comentário para processar
}
logInfo(`Encontrados ${pendingComments.length} comentários pendentes para envio ao GLPI.`);
for (const comment of pendingComments) {
try {
const newGlpiComment = await glpiModel.insertComment({
tickets_id: comment.glpi_ticket_id, //
content: comment.content
});
// Sucesso: atualiza o status no nosso banco
await commentModel.updateCommentSyncStatus(comment.id, 'synced', newGlpiComment.id);
logInfo(`Comentário ID ${comment.id} sincronizado com sucesso para o GLPI. Novo ID de comentário no GLPI: ${newGlpiComment.id}`);
} catch (error) {
// Falha: atualiza o status para erro e incrementa a tentativa no nosso banco
await commentModel.updateCommentSyncStatus(comment.id, 'sync_error', null, error.message);
logError(`Falha ao sincronizar comentário ID ${comment.id} para o GLPI:`, error);
}
}
}
/**
* Recebe um novo comentário do GLPI (via webhook) e o envia para o HubSoft.
* @param {number} glpiTicketId - O ID do ticket no GLPI.
* @param {string} rawContent - O conteúdo bruto do comentário.
*/
async function syncGlpiCommentToHubsoft(glpiTicketId, glpiMessageId, rawContent) {
// 1. Encontrar o registro de sincronização para obter o ID do HubSoft
const syncRecord = await hubglpiModel.getIdByGlpiID(glpiTicketId);
if (!syncRecord || !syncRecord.hubsoftid) {
logWarning(`Recebido comentário para o ticket GLPI ID ${glpiTicketId}, mas não há registro de sincronização correspondente. Ignorando.`);
// Não lançamos um erro, pois o comentário pode ser de um ticket não sincronizado.
return;
}
const hubsoftTicketId = syncRecord.hubsoftid;
// 2. Sanitizar o comentário (remover HTML, etc.)
const sanitizedContent = sanitizeGLPIComment({ content: rawContent });
//3. Envia o comentario para o banco intermediário (sync_comments) com status 'pending'
try {
const commentRecord = await commentModel.insertComment({
hubsoftAtendimentoId: hubsoftTicketId,
sourceSystem: 'glpi',
sourceCommentId: glpiMessageId,
content: sanitizedContent
});
if (!commentRecord) {
throw new Error('Não foi possível inserir o comentário na fila de sincronização.');
}
// Se o comentário já existia e estava sincronizado, não fazemos nada.
if (commentRecord && commentRecord.sync_status === 'synced') {
logWarning(`Comentário do GLPI Ticket ${glpiTicketId} já existe e está sincronizado. Ignorando envio duplicado.`);
return;
}
// 4. Enviar o comentário para a API do HubSoft
logInfo(`Enviando comentário do GLPI Ticket ${glpiTicketId} para o HubSoft Atendimento ${hubsoftTicketId}.`);
const hubsoftMessageId = await hubsoftService.addMensagem(hubsoftTicketId, sanitizedContent);
logInfo(`Comentário do GLPI Ticket ${glpiTicketId} enviado com sucesso para o HubSoft. Novo ID de mensagem: ${hubsoftMessageId}`);
// 5. Atualizar o status no banco local com o ID do comentário e o ID da mensagem de destino
await commentModel.updateCommentSyncStatus(commentRecord.id, 'synced', hubsoftMessageId, null);
logInfo(`Comentário do GLPI Ticket ${glpiTicketId} (ID local: ${commentRecord.id}) marcado como sincronizado.`);
} catch (error) {
logError(`Erro no processo de sincronização do comentário do GLPI Ticket ${glpiTicketId}:`, error);
// Não relançamos o erro para não quebrar o webhook, mas o erro já foi logado.
}
}
module.exports = { syncHubsoftCommentsToLocalDB, sendPendingCommentsToGlpi, syncGlpiCommentToHubsoft };

View File

@ -1,119 +0,0 @@
const apiConfig = require('../config/apiConfig');
const axios = require('axios');
const qs = require('qs');
const { logError, logInfo } = require('../utils/logger');
const getAuthToken = async () => {
try {
const response = await axios.post(apiConfig.hubsoft.authUrl, qs.stringify(apiConfig.hubsoft.authPayload), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
return response.data.access_token;
} catch (error) {
logError('Erro ao obter token de autenticação:', error);
throw error;
}
};
const updateAtendimentoStatus = async (id_atendimento, newStatus) => {
try {
const token = await getAuthToken();
if (newStatus === 3) {
const response = await axios.put(`${apiConfig.hubsoft.atendimentosUrl}${id_atendimento}`, {
"fechar_atendimento": true
}, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.data;
} else {
const response = await axios.put(`${apiConfig.hubsoft.atendimentosUrl}${id_atendimento}`, {
"id_atendimento_status": newStatus,
"fechar_atendimento": false
}, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.data;
}
} catch (error) {
logError(`Erro ao atualizar status do atendimento ID ${id_atendimento}:`, error.response ? error.response.data : error.message);
throw error;
}
};
const closeAtendimento = async (id_atendimento, closingMessage) => {
try {
const token = await getAuthToken();
const response = await axios.put(`${apiConfig.hubsoft.atendimentosUrl}${id_atendimento}`, {
"fechar_atendimento": true,
"parametros_fechamento": {
"descricao_fechamento": closingMessage
}
},
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.data;
} catch (error) {
logError(`Erro ao fechar atendimento ID ${id_atendimento}:`, error.response ? error.response.data : error.message);
throw error;
}
};
/**
* Adiciona uma nova mensagem a um atendimento no HubSoft.
* @param {number} id_atendimento - O ID do atendimento no HubSoft.
* @param {string} mensagem - O conteúdo da mensagem a ser adicionada.
* @returns {Promise<object>} A resposta da API do HubSoft.
*/
async function addMensagem(id_atendimento, mensagem) {
// 1. Obter o token de autenticação
const token = await getAuthToken();
// 2. Construir a URL completa do endpoint
const url = `${apiConfig.hubsoft.atendimentosUrl}adicionar_mensagem/${id_atendimento}`;
const payload = { mensagem };
try {
logInfo(`Enviando nova mensagem para o atendimento HubSoft ID ${id_atendimento}...`);
//PRint emulando o curl inteiro que será enviado
const response = await axios.post(url, payload, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const idAtendimentoMensagem = response.data.atendimento_mensagem.id_atendimento_mensagem;
return idAtendimentoMensagem;
} catch (error) {
logError(`Erro ao adicionar mensagem no atendimento HubSoft ID ${id_atendimento}:`, error.response?.data || error.message);
throw error;
}
}
module.exports = {
updateAtendimentoStatus,
closeAtendimento,
addMensagem
};

View File

@ -1,144 +0,0 @@
// HUBXGLPI/src/services/ticketService.js
const { logError, logInfo, logWarning} = require('../utils/logger');
const { getIdByGlpiID, updateClosingTicket, updateSyncaDataError, lockTicketForClosing } = require("../model/hubglpiModel.js");
const { sanitizeGLPIComment } = require('../utils/commentSanitizer.js');
const { closeAtendimento } = require("./hubsoftService.js");
const { hubsoft } = require('../config/apiConfig.js');
const e = require('express');
/**
* Verifica se um ticket é da "Mundiale" e atualiza seu status para 'pending_close'.
* @param {number} glpiTicketId - O ID do ticket no GLPI.
* @param {string} ticketTitle - O título do ticket.
* @returns {Promise<object|null>} Um objeto com os IDs { hubsoftId, glpiId, syncId } ou null se não for aplicável.
*/
const handleMundialeTicket = async (glpiTicketId, ticketTitle) => {
try {
if (!ticketTitle.includes("Mundiale")) {
logInfo(`Ticket ID ${glpiTicketId} não é da Mundiale. Ignorando fechamento.`);
return null;
}
const syncRecord = await getIdByGlpiID(glpiTicketId);
if (!syncRecord) {
logInfo(`Nenhum registro de sincronização encontrado para o ticket GLPI ID ${glpiTicketId}.`);
return null;
}
logInfo(`-> Tentando obter trava para fechar o ticket GLPI ID ${glpiTicketId}.`);
const lockedTicket = await lockTicketForClosing(syncRecord.id);
if (lockedTicket) {
logInfo(`Trava obtida com sucesso. Status atualizado para 'processing_close' para o ticket GLPI ID ${glpiTicketId}.`);
return {
hubsoftId: lockedTicket.hubsoft_ticket_id,
glpiId: glpiTicketId,
syncId: lockedTicket.id,
};
} else {
// Se não conseguimos a trava, é porque outro processo já está tratando dele.
// Isso não é um erro, apenas uma requisição duplicada.
logWarning(`Não foi possível obter a trava para o ticket GLPI ID ${glpiTicketId}. Provavelmente já está sendo processado.`);
return null;
}
} catch (error) {
logError(`Erro em handleMundialeTicket para o ticket ID ${glpiTicketId}:`, error);
throw error; // Propaga o erro para o chamador
}
};
/**
* Fecha o atendimento correspondente no HubSoft.
* @param {number} hubsoftId - O ID do atendimento no HubSoft.
* @param {string} closingMessage - A mensagem de fechamento.
* @returns {Promise<object>} A resposta da API do HubSoft.
*/
const closeHubsoftTicket = async (hubsoftId, closingMessage) => {
logInfo(`-> Iniciando fechamento do atendimento ${hubsoftId} no HubSoft.`);
const closeResponse = await closeAtendimento(hubsoftId, closingMessage);
// Se o atendimento já estiver finalizado, não é um erro. Apenas retornamos a resposta.
if (closeResponse?.status === 'error' && closeResponse?.msg === 'Atendimento já finalizado') {
return closeResponse;
}
// Se a API retornar qualquer outro erro, lançamos uma exceção.
if (closeResponse.status !== "success") {
const errorMessage = `Falha ao fechar atendimento no HubSoft: ${JSON.stringify(closeResponse) || 'Resposta inesperada'}`;
throw new Error(errorMessage);
}
logInfo(`Atendimento ${hubsoftId} no HubSoft fechado com sucesso.`);
return closeResponse;
};
/**
* Atualiza os registros no banco de dados local após o fechamento bem-sucedido.
* @param {number} syncId - O ID do registro de sincronização.
* @param {string} closingMessage - A mensagem de fechamento.
*/
const updateLocalDatabaseOnClose = async (syncId, closingMessage) => {
logInfo(`-> Atualizando banco de dados local para o sync ID ${syncId}.`);
const dbUpdateResult = await updateClosingTicket(syncId, closingMessage);
if (dbUpdateResult) {
logInfo(`Banco de dados local atualizado com sucesso para o sync ID ${syncId}.`);
} else {
// Mesmo que a API do Hubsoft tenha funcionado, o DB local falhou.
// Isso deve ser logado como um erro para investigação.
logError(`Falha crítica ao atualizar o banco de dados local para o sync ID ${syncId} após o fechamento no HubSoft.`);
}
};
const fechaTicket = async (bodyRequest) => {
const glpiTicketId = bodyRequest.item.items_id;
const ticketTitle = bodyRequest.parent_item.name;
const rawClosingMessage = bodyRequest.item.content || "Fechamento automático do ticket.";
// Sanitiza a mensagem de fechamento para remover tags HTML e formatar o texto.
const closingMessage = sanitizeGLPIComment({ content: rawClosingMessage });
try {
const ticketInfo = await handleMundialeTicket(glpiTicketId, ticketTitle);
if (ticketInfo) {
const closeResponse = await closeHubsoftTicket(ticketInfo.hubsoftId, closingMessage);
// Se o atendimento já estava fechado, registramos um aviso e continuamos para garantir que nosso DB local esteja sincronizado.
if (closeResponse?.msg === 'Atendimento já finalizado') {
logInfo(`Atendimento ${ticketInfo.hubsoftId} no HubSoft já estava fechado. Garantindo a sincronização do banco de dados local.`);
}
await updateLocalDatabaseOnClose(ticketInfo.syncId, closingMessage);
return { status: 'success', message: `Ticket ${glpiTicketId} e atendimento ${ticketInfo.hubsoftId} fechados com sucesso.` };
}
return { status: 'ignored', message: `Ticket ${glpiTicketId} não processado.` };
} catch (error) {
logError(`Erro no processo de fechamento do ticket GLPI ID ${glpiTicketId}: ${error.message}`);
// Se ticketInfo existir, podemos tentar registrar o erro no banco
const syncId = (await getIdByGlpiID(glpiTicketId))?.id;
if (syncId) {
await updateSyncaDataError(error.message, syncId);
}
// Retorna um erro para o webhook do GLPI
throw new Error(`Falha ao processar fechamento do ticket ${glpiTicketId}: ${error.message}`);
}
};
module.exports = { fechaTicket };
/**
* @module ticketService
* @description Este serviço contém a lógica de negócio para o processo de fechamento de tickets.
* Ele é invocado pelo `ClosureController` quando um webhook de fechamento do GLPI é recebido.
*
* Funções:
* - `fechaTicket(bodyRequest)`: Orquestra todo o processo de fechamento.
* - `handleMundialeTicket(...)`: Verifica se o ticket é elegível para o fluxo e tenta obter uma trava no banco de dados para evitar processamento duplicado.
* - `closeHubsoftTicket(...)`: Interage com o `hubsoftService` para fechar o atendimento no HubSoft. Trata o caso onde o ticket está fechado.
* - `updateLocalDatabaseOnClose(...)`: Atualiza o status do ticket no banco de dados local após o fechamento bem-sucedido.
*/

View File

@ -1,20 +0,0 @@
// src/shared/infra/api/hubsoft.auth.js
const getAuthToken = async () => {
try {
const response = await axios.post(
apiConfig.hubsoft.authUrl,
qs.stringify(apiConfig.hubsoft.authPayload),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return response.data.access_token;
} catch (error) {
logError('Erro ao obter token de autenticação:', error);
throw error;
}
};

View File

@ -1,19 +0,0 @@
// src/shared/infra/api/hubsoft.config.js
module.exports = {
hubsoft: {
baseUrl: process.env.HUBSOFT_BASE_URL,
authUrl: `${process.env.HUBSOFT_BASE_URL}/oauth/token`,
authPayload: {
grant_type: 'password',
client_id: process.env.HUBSOFT_CLIENT_ID,
client_secret: process.env.HUBSOFT_CLIENT_SECRET,
username: process.env.HUBSOFT_USER,
password: process.env.HUBSOFT_PASS
},
atendimentosUrl: `${process.env.HUBSOFT_BASE_URL}/atendimentos/`
}
};

View File

@ -1,38 +0,0 @@
// src/shared/repositories/hubsoftAPI.repository.js
const axios = require('axios');
const {getAuthToken} = require('../infra/api/hubsoft.auth.js');
const apiConfig = require('../infra/api/hubsoft.config');
const { logError, logInfo } = require('../../utils/logger');
async function sendMessage(idAtendimento, mensagem) {
try {
const token = await getAuthToken();
const url = `${apiConfig.hubsoft.atendimentosUrl}adicionar_mensagem/${idAtendimento}`;
logInfo(`[HubSoft API] Enviando comentário para atendimento ${idAtendimento}`);
await axios.post(
url,
{ mensagem },
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
} catch (error) {
logError(
`[HubSoft API] Erro ao enviar comentário`,
error.response?.data || error.message
);
throw error;
}
}
module.exports = {
sendMessage
};

View File

@ -1,118 +0,0 @@
// src/shared/repositories/hubsoft.repository.js
const { get } = require('pm2');
const { hubsoft } = require("../infra/database/index.js");
const { logInfo, logError } = require('../../utils/logger.js');
async function getMundialeTickets() {
try {
const query = `SELECT a.id_atendimento, a.id_usuario_abertura, a.id_atendimento_status, a.protocolo, a.descricao_abertura, a.data_cadastro, a.nome_contato, c.codigo_cliente, s.descricao, cs.id_cliente_servico FROM atendimento AS a INNER JOIN cliente_servico AS cs ON a.id_cliente_servico = cs.id_cliente_servico INNER JOIN cliente AS c ON cs.id_cliente = c.id_cliente INNER JOIN servico AS s ON cs.id_servico = s.id_servico WHERE a.id_tipo_atendimento = 4 AND a.id_usuario_abertura = 248 AND a.id_atendimento_status IN (1, 2, 33) AND s.ativo = true;`;
const { rows } = await hubsoft.query(query);
return rows;
} catch (error) {
logError("Erro ao buscar tickets Mundiale:", error);
throw error;
}
}
async function getImplantacaoTickets() {
try {
const query = `
SELECT
a.id_atendimento,
a.id_usuario_abertura,
a.id_atendimento_status,
a.protocolo,
a.descricao_abertura,
a.data_cadastro,
a.nome_contato,
-- DADOS DO CLIENTE (novos)
c.nome_razaosocial,
c.telefone_primario AS telefone,
c.cpf_cnpj,
c.tipo_pessoa,
c.email_principal AS email,
c.codigo_cliente,
s.descricao AS descricao,
cs.id_cliente_servico,
-- ENDEREÇO DE INSTALAÇÃO
en.endereco,
en.numero,
en.complemento,
en.bairro,
en.cep,
ci.nome AS cidade,
es.sigla AS estado
FROM atendimento AS a
INNER JOIN cliente_servico AS cs
ON a.id_cliente_servico = cs.id_cliente_servico
INNER JOIN cliente AS c
ON cs.id_cliente = c.id_cliente
INNER JOIN servico AS s
ON cs.id_servico = s.id_servico
-- endereço do cliente
INNER JOIN cliente_servico_endereco AS cse
ON cse.id_cliente_servico = cs.id_cliente_servico
AND cse.tipo = 'instalacao'
INNER JOIN endereco_numero AS en
ON en.id_endereco_numero = cse.id_endereco_numero
INNER JOIN cidade AS ci
ON ci.id_cidade = en.id_cidade
INNER JOIN estado AS es
ON es.id_estado = ci.id_estado
WHERE
a.id_tipo_atendimento = 21
AND a.id_atendimento_status IN (1, 2, 33)
AND s.ativo = TRUE;
`;
const { rows } = await hubsoft.query(query);
return rows;
} catch (error) {
logError("Erro ao buscar tickets Implantação:", error);
throw error;
}
}
async function getCancelamentoTickets() {
try {
const query = `SELECT a.id_atendimento, a.id_usuario_abertura, a.id_atendimento_status, a.protocolo, a.descricao_abertura, a.data_cadastro, a.nome_contato, c.codigo_cliente, s.descricao, cs.id_cliente_servico FROM atendimento AS a INNER JOIN cliente_servico AS cs ON a.id_cliente_servico = cs.id_cliente_servico INNER JOIN cliente AS c ON cs.id_cliente = c.id_cliente INNER JOIN servico AS s ON cs.id_servico = s.id_servico WHERE a.id_tipo_atendimento = 27 AND a.id_atendimento_status IN (1, 2, 33) AND s.ativo = true;`;
const { rows } = await hubsoft.query(query);
return rows;
} catch (error) {
logError("Erro ao buscar tickets Cancelamento:", error);
throw error;
}
}
async function getSacTickets() {
try {
const query = `SELECT a.id_atendimento, a.id_usuario_abertura, a.id_atendimento_status, a.protocolo, a.descricao_abertura, a.data_cadastro, a.nome_contato, c.codigo_cliente, s.descricao, cs.id_cliente_servico FROM atendimento AS a INNER JOIN cliente_servico AS cs ON a.id_cliente_servico = cs.id_cliente_servico INNER JOIN cliente AS c ON cs.id_cliente = c.id_cliente INNER JOIN servico AS s ON cs.id_servico = s.id_servico WHERE a.id_tipo_atendimento = 41 AND a.id_atendimento_status IN (1, 2, 33) AND s.ativo = true;`;
const { rows } = await hubsoft.query(query);
return rows;
} catch (error) {
logError("Erro ao buscar tickets SAC:", error);
throw error;
}
}
module.exports = {
getMundialeTickets,
getImplantacaoTickets,
getCancelamentoTickets,
getSacTickets
};

View File

@ -1,3 +1,5 @@
// src/shared/utils/logger.js
const winston = require('winston');
const path = require('path');
require('winston-daily-rotate-file');