REFACTOR: Integração refatorada utilizando conceitos de clean architeture e monolito modular
This commit is contained in:
parent
2cd00c65c5
commit
783eb2f081
@ -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'); }\""
|
||||
},
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// src/config/db.config.js
|
||||
module.exports = {
|
||||
hubsoft: {
|
||||
databaseHost: process.env.HUBSOFT_DATABASE_HOST,
|
||||
@ -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.
|
||||
*/
|
||||
@ -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 };
|
||||
@ -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.
|
||||
*/
|
||||
48
src/cron.js
48
src/cron.js
@ -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.
|
||||
*/
|
||||
@ -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.
|
||||
*/
|
||||
11
src/index.js
11
src/index.js
@ -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();
|
||||
27
src/infra/api/hubsoft.auth.js
Normal file
27
src/infra/api/hubsoft.auth.js
Normal 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
|
||||
43
src/infra/api/hubsoft.client.js
Normal file
43
src/infra/api/hubsoft.client.js
Normal 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
|
||||
}
|
||||
32
src/infra/api/hubsoft.config.js
Normal file
32
src/infra/api/hubsoft.config.js
Normal 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
|
||||
}
|
||||
};
|
||||
48
src/infra/cron/sync.cron.js
Normal file
48
src/infra/cron/sync.cron.js
Normal 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')
|
||||
}
|
||||
|
||||
16
src/infra/db/connections/glpi.mysql.js
Normal file
16
src/infra/db/connections/glpi.mysql.js
Normal 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
|
||||
18
src/infra/db/connections/hubglpi.pg.js
Normal file
18
src/infra/db/connections/hubglpi.pg.js
Normal 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
|
||||
14
src/infra/db/connections/hubsoft.pg.js
Normal file
14
src/infra/db/connections/hubsoft.pg.js
Normal 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
|
||||
48
src/infra/db/repositories/glpi/comments.repository.js
Normal file
48
src/infra/db/repositories/glpi/comments.repository.js
Normal 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
|
||||
}
|
||||
57
src/infra/db/repositories/glpi/entities.repository.js
Normal file
57
src/infra/db/repositories/glpi/entities.repository.js
Normal 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
|
||||
}
|
||||
38
src/infra/db/repositories/glpi/groups.repository.js
Normal file
38
src/infra/db/repositories/glpi/groups.repository.js
Normal 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
|
||||
}
|
||||
55
src/infra/db/repositories/glpi/tickets.repository.js
Normal file
55
src/infra/db/repositories/glpi/tickets.repository.js
Normal 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
|
||||
}
|
||||
74
src/infra/db/repositories/hubglpi/comments.repository.js
Normal file
74
src/infra/db/repositories/hubglpi/comments.repository.js
Normal 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
|
||||
};
|
||||
152
src/infra/db/repositories/hubglpi/sync.repository.js
Normal file
152
src/infra/db/repositories/hubglpi/sync.repository.js
Normal 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
|
||||
};
|
||||
168
src/infra/db/repositories/hubglpi/tickets.repository.js
Normal file
168
src/infra/db/repositories/hubglpi/tickets.repository.js
Normal 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
|
||||
};
|
||||
|
||||
31
src/infra/db/repositories/hubglpi/watermark.repository.js
Normal file
31
src/infra/db/repositories/hubglpi/watermark.repository.js
Normal 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
|
||||
};
|
||||
27
src/infra/db/repositories/hubsoft/messages.repository.js
Normal file
27
src/infra/db/repositories/hubsoft/messages.repository.js
Normal 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
|
||||
}
|
||||
100
src/infra/db/repositories/hubsoft/tickets.repository.js
Normal file
100
src/infra/db/repositories/hubsoft/tickets.repository.js
Normal 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
|
||||
}
|
||||
@ -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();
|
||||
16
src/infra/http/routes/index.js
Normal file
16
src/infra/http/routes/index.js
Normal 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;
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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.
|
||||
*/
|
||||
@ -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
|
||||
};
|
||||
30
src/modules/close/controller/close.controller.js
Normal file
30
src/modules/close/controller/close.controller.js
Normal 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.
|
||||
*/
|
||||
37
src/modules/close/repositories/close.repository.js
Normal file
37
src/modules/close/repositories/close.repository.js
Normal 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
|
||||
}
|
||||
47
src/modules/close/services/hubsoftClose.service.js
Normal file
47
src/modules/close/services/hubsoftClose.service.js
Normal 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
|
||||
}
|
||||
70
src/modules/close/useCases/closeTickets.usecase.js
Normal file
70
src/modules/close/useCases/closeTickets.usecase.js
Normal 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
|
||||
}
|
||||
20
src/modules/comments/controller/glpiComment.controller.js
Normal file
20
src/modules/comments/controller/glpiComment.controller.js
Normal 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}
|
||||
42
src/modules/comments/models/glpiComment.model.js
Normal file
42
src/modules/comments/models/glpiComment.model.js
Normal file
@ -0,0 +1,42 @@
|
||||
//src/modules/comments/models/glpiComment.model.js
|
||||
function escapeHtml(text = '') {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
65
src/modules/comments/repositories/comment.repository.js
Normal file
65
src/modules/comments/repositories/comment.repository.js
Normal 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
|
||||
}
|
||||
@ -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 }
|
||||
@ -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 }
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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.
|
||||
*/
|
||||
13
src/modules/tickets/controller/tickets.controller.js
Normal file
13
src/modules/tickets/controller/tickets.controller.js
Normal 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}
|
||||
195
src/modules/tickets/models/glpi/cancelamento.model.js
Normal file
195
src/modules/tickets/models/glpi/cancelamento.model.js
Normal 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>Nº 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 }
|
||||
@ -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 }
|
||||
69
src/modules/tickets/models/glpi/mundiale.model.js
Normal file
69
src/modules/tickets/models/glpi/mundiale.model.js
Normal 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
|
||||
}
|
||||
88
src/modules/tickets/models/hubglpi/model.js
Normal file
88
src/modules/tickets/models/hubglpi/model.js
Normal 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
|
||||
}
|
||||
158
src/modules/tickets/repositories/ticket.repository.js
Normal file
158
src/modules/tickets/repositories/ticket.repository.js
Normal 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,
|
||||
}
|
||||
63
src/modules/tickets/services/cancelamento.service.js
Normal file
63
src/modules/tickets/services/cancelamento.service.js
Normal 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.
|
||||
*/
|
||||
14
src/modules/tickets/services/createTickets.service.js
Normal file
14
src/modules/tickets/services/createTickets.service.js
Normal 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
|
||||
}
|
||||
43
src/modules/tickets/services/implantacao.service.js
Normal file
43
src/modules/tickets/services/implantacao.service.js
Normal 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
|
||||
}
|
||||
63
src/modules/tickets/services/mundiale.service.js
Normal file
63
src/modules/tickets/services/mundiale.service.js
Normal 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.
|
||||
*/
|
||||
36
src/modules/tickets/services/resolveTicketEntity.service.js
Normal file
36
src/modules/tickets/services/resolveTicketEntity.service.js
Normal 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 }
|
||||
18
src/modules/tickets/services/ticketNotifications.service.js
Normal file
18
src/modules/tickets/services/ticketNotifications.service.js
Normal 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
|
||||
}
|
||||
92
src/modules/tickets/useCases/syncTickets.usecase.js
Normal file
92
src/modules/tickets/useCases/syncTickets.usecase.js
Normal 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 }
|
||||
@ -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;
|
||||
@ -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 };
|
||||
@ -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
|
||||
};
|
||||
@ -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 já está fechado.
|
||||
* - `updateLocalDatabaseOnClose(...)`: Atualiza o status do ticket no banco de dados local após o fechamento bem-sucedido.
|
||||
*/
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
@ -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/`
|
||||
}
|
||||
};
|
||||
@ -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
|
||||
};
|
||||
@ -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
|
||||
|
||||
};
|
||||
@ -1,3 +1,5 @@
|
||||
// src/shared/utils/logger.js
|
||||
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
require('winston-daily-rotate-file');
|
||||
Loading…
Reference in New Issue
Block a user