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",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start:api": "cross-env NODE_ENV=production node src/server.js",
|
"start:api": "cross-env NODE_ENV=production node src/infra/http/server.js",
|
||||||
"dev:api": "cross-env NODE_ENV=development nodemon src/server.js",
|
"dev:api": "cross-env NODE_ENV=development nodemon src/infra/http/server.js",
|
||||||
"start:worker": "cross-env NODE_ENV=production node src/cron.js",
|
"start:worker": "cross-env NODE_ENV=production node src/infra/cron/sync.cron.js",
|
||||||
"dev:worker": "cross-env NODE_ENV=development nodemon src/cron.js",
|
"dev:worker": "cross-env NODE_ENV=development nodemon src/infra/cron/sync.cron.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"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'); }\""
|
"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 = {
|
module.exports = {
|
||||||
hubsoft: {
|
hubsoft: {
|
||||||
databaseHost: process.env.HUBSOFT_DATABASE_HOST,
|
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 express = require('express');
|
||||||
const router = require('./routes.js')
|
const router = require('./routes')
|
||||||
|
|
||||||
function createApp() {
|
function createApp() {
|
||||||
const app = express();
|
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
|
loadEnv(); // Carrega as variáveis de ambiente
|
||||||
|
|
||||||
const createApp = require('./app.js');
|
const createApp = require('./app.js');
|
||||||
const { logInfo } = require('./utils/logger.js');
|
const { logInfo } = require('../../shared/utils/logger.js');
|
||||||
|
|
||||||
const app = createApp();
|
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
|
// src/modules/tickets/models/glpi/implantacao.model.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');
|
|
||||||
|
|
||||||
// --------------------------------------
|
function toGlpiPayload(ticket) {
|
||||||
// Funções principais do serviço
|
return {
|
||||||
// --------------------------------------
|
entities_id: ticket.entities_id || 0,
|
||||||
|
name: buildTitle(ticket),
|
||||||
async function fetchNew() {
|
content: buildHtml(ticket),
|
||||||
return repositoryHubsoft.getImplantacaoTickets();
|
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) {
|
function resolveGlpiStatus(status) {
|
||||||
if (!tickets.length) return;
|
const map = {
|
||||||
|
'Novo': 1,
|
||||||
const ticketsFormatted = tickets.map(ticket =>
|
'Pendente': 4,
|
||||||
modelHubGlpi.mapHubsoftToHubglpi(ticket, 'IMPLANTACAO')
|
'Em atendimento': 2,
|
||||||
);
|
'Resolvido': 5
|
||||||
|
}
|
||||||
const inserted = await repositoryHubGlpi.insertTickets(ticketsFormatted);
|
return map[status] || 1
|
||||||
|
|
||||||
if (inserted) {
|
|
||||||
await repositoryHubGlpi.insertSyncData(
|
|
||||||
ticketsFormatted.map(ticket => ticket.id_atendimento)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return inserted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendToGlpi(ticket) {
|
function buildTitle(ticket) {
|
||||||
|
return `IMPLANTAÇÃO - ${ticket.codigo_cliente}-${ticket.codigo_servico} - ${ticket.nome_razaosocial} - ${ticket.servico_nome}`
|
||||||
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 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 `
|
||||||
// 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 `
|
|
||||||
<table style="width:100%; border-collapse: collapse;">
|
<table style="width:100%; border-collapse: collapse;">
|
||||||
|
|
||||||
<!-- ===== CABEÇALHO DADOS COMERCIAL ===== -->
|
<!-- ===== CABEÇALHO DADOS COMERCIAL ===== -->
|
||||||
@ -124,7 +55,7 @@ const formatDescription = (ticket) => {
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Gerente Responsável</strong></td>
|
<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>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
@ -146,7 +77,7 @@ const formatDescription = (ticket) => {
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 8px; border: 1px solid #ddd;"><strong>${docLabel}</strong></td>
|
<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>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
@ -201,54 +132,64 @@ const formatDescription = (ticket) => {
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<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>
|
</tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
`;
|
`
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --------------------------------------
|
|
||||||
// Formatadores de Documento
|
|
||||||
// --------------------------------------
|
|
||||||
|
|
||||||
function formatCPF(cpf) {
|
function formatCPF(cpf) {
|
||||||
cpf = cpf.replace(/\D/g, "");
|
return cpf?.replace(/\D/g, '')
|
||||||
return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
|
.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4')
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCNPJ(cnpj) {
|
function formatCNPJ(cnpj) {
|
||||||
cnpj = cnpj.replace(/\D/g, "");
|
return cnpj?.replace(/\D/g, '')
|
||||||
return cnpj.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, "$1.$2.$3/$4-$5");
|
.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, '$1.$2.$3/$4-$5')
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDocument(doc, tipo) {
|
function formatDocument(doc, tipo) {
|
||||||
if (!doc) return "N/A";
|
if (!doc) return 'N/A'
|
||||||
return tipo === "pf" ? formatCPF(doc) : formatCNPJ(doc);
|
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 ----------
|
||||||
// Resolve Serviço
|
"Link de Internet Dedicado 20 Mbps Full Duplex":
|
||||||
// --------------------------------------
|
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "20 Mbps Full Duplex" },
|
||||||
|
|
||||||
function resolveService(servicoNome) {
|
"Link de Internet Dedicado 100 Mbps Full Duplex":
|
||||||
return serviceDictionary[servicoNome] || {
|
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "100 Mbps Full Duplex" },
|
||||||
produto: servicoNome,
|
|
||||||
qtd: 1,
|
|
||||||
descricao: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
"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":
|
||||||
// Exportação
|
{ produto: "Link de Internet Dedicado", qtd: 1, descricao: "2 Gbps Full Duplex" },
|
||||||
// --------------------------------------
|
|
||||||
|
|
||||||
module.exports = {
|
// Default genérico caso venha coisa nova
|
||||||
fetchNew,
|
|
||||||
saveHubGlpi,
|
|
||||||
sendToGlpi
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 winston = require('winston');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
require('winston-daily-rotate-file');
|
require('winston-daily-rotate-file');
|
||||||
Loading…
Reference in New Issue
Block a user