FEAT: Sincronia de comentarios bidirecional finalizada
- Comentários oriundos do GLPI estão sincronizando com Hub através de um webhook - Comentários oriundos do Hub estão sincronizando com GLPI através de um CronJob - Validação de comentário existente sendo realizada através do banco de dados intermediário - Ainda há a necessidade função via nodecron para que verifique os chamados syncError com retry menor igual a 3
This commit is contained in:
parent
d359880f10
commit
14949bf4df
@ -4,8 +4,7 @@ const router = require('./routes.js')
|
||||
function createApp() {
|
||||
const app = express();
|
||||
|
||||
app.use(express.json({ type: '*/*' }));
|
||||
app.use('/api', router);
|
||||
app.use('/api', router); // O router agora tem seu próprio middleware de JSON.
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@ -8,32 +8,40 @@ const { logInfo, logError } = require('../utils/logger.js');
|
||||
*/
|
||||
|
||||
const closeTicket = async (req, res) => {
|
||||
try {
|
||||
let rawData = '';
|
||||
const bodyRequest = req.body;
|
||||
let rawData = '';
|
||||
|
||||
req.on('data', chunk => {
|
||||
rawData += chunk;
|
||||
});
|
||||
req.on('data', chunk => {
|
||||
rawData += chunk;
|
||||
});
|
||||
|
||||
req.on('end', async () => {
|
||||
let bodyRequest;
|
||||
try {
|
||||
bodyRequest = JSON.parse(rawData);
|
||||
} catch (err) {
|
||||
logError('Erro ao parsear JSON:', err);
|
||||
bodyRequest = {};
|
||||
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) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
} 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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -7,25 +7,49 @@ const { logInfo, logError, logWarning } = require('../utils/logger.js');
|
||||
* @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) {
|
||||
const body = req.body;
|
||||
const glpiTicketId = body?.item?.items_id;
|
||||
const commentContent = body?.item?.content;
|
||||
let rawData = '';
|
||||
|
||||
if (!glpiTicketId || !commentContent) {
|
||||
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.' });
|
||||
}
|
||||
req.on('data', chunk => {
|
||||
rawData += chunk;
|
||||
});
|
||||
|
||||
logInfo(`Webhook de novo comentário recebido para o ticket GLPI ID ${glpiTicketId}.`);
|
||||
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.' });
|
||||
}
|
||||
|
||||
try {
|
||||
await commentService.syncGlpiCommentToHubsoft(glpiTicketId, commentContent);
|
||||
res.status(200).json({ status: 'success', message: 'Comentário recebido e processado.' });
|
||||
} catch (error) {
|
||||
logError(`Erro ao processar webhook de comentário para o ticket GLPI ID ${glpiTicketId}:`, error);
|
||||
res.status(500).json({ status: 'error', message: error.message });
|
||||
}
|
||||
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 };
|
||||
@ -4,6 +4,7 @@ const hubglpiModel = require('../model/hubglpiModel.js');
|
||||
const glpiModel = require('../model/glpiModel.js');
|
||||
const { logError, logInfo } = require('../utils/logger');
|
||||
|
||||
|
||||
// ================================================================================
|
||||
// Constantes e Configurações
|
||||
// ================================================================================
|
||||
@ -112,7 +113,7 @@ const formatTicketDataForGlpi = async (ticketData) => {
|
||||
...ticketData,
|
||||
status_atendimento: statusAtendimentoGLPI[ticketData.status_atendimento] || 1,
|
||||
date_mod: new Date(),
|
||||
user_id_recipient: 1100,
|
||||
user_id_recipient: process.env.GLPI_USER || 1,
|
||||
descricao_abertura: formatDescription(ticketData),
|
||||
urgency: 3,
|
||||
impact: 3,
|
||||
|
||||
@ -61,8 +61,13 @@ class CommentModel {
|
||||
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 NOTHING;
|
||||
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,
|
||||
@ -73,8 +78,14 @@ class CommentModel {
|
||||
];
|
||||
|
||||
try {
|
||||
await pool.query(insertQuery, values);
|
||||
return true;
|
||||
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;
|
||||
@ -125,6 +136,7 @@ class CommentModel {
|
||||
// Não relançamos o erro para não parar o loop de sincronização.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = CommentModel;
|
||||
|
||||
@ -124,11 +124,8 @@ class GlpiModel {
|
||||
}
|
||||
}
|
||||
static async insertComment(commentData) {
|
||||
const query = `
|
||||
INSERT INTO glpi_tickets_comments (tickets_id, content, date_creation)
|
||||
VALUES (?, ?, ?)
|
||||
`;
|
||||
const values = [commentData.tickets_id, commentData.content, new Date()];
|
||||
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;
|
||||
|
||||
@ -144,7 +144,9 @@ class HubglpiModel {
|
||||
FROM sync_data
|
||||
WHERE glpi_ticket_id = $1;
|
||||
`;
|
||||
const values = [glpi_ticket_id];
|
||||
const values = [parseInt(glpi_ticket_id)];
|
||||
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(query, values);
|
||||
if (rows.length > 0) {
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
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
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ 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.
|
||||
@ -65,11 +66,9 @@ async function sendPendingCommentsToGlpi() {
|
||||
|
||||
for (const comment of pendingComments) {
|
||||
try {
|
||||
// 1. Inserir o comentário diretamente no banco de dados do GLPI
|
||||
const newGlpiComment = await glpiModel.insertComment({
|
||||
tickets_id: comment.glpi_ticket_id, // ID do ticket no GLPI
|
||||
content: comment.content,
|
||||
// Outros campos necessários, como autor, data, etc.
|
||||
tickets_id: comment.glpi_ticket_id, //
|
||||
content: comment.content
|
||||
});
|
||||
|
||||
// Sucesso: atualiza o status no nosso banco
|
||||
@ -88,28 +87,55 @@ async function sendPendingCommentsToGlpi() {
|
||||
* @param {number} glpiTicketId - O ID do ticket no GLPI.
|
||||
* @param {string} rawContent - O conteúdo bruto do comentário.
|
||||
*/
|
||||
async function syncGlpiCommentToHubsoft(glpiTicketId, rawContent) {
|
||||
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) {
|
||||
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;
|
||||
const hubsoftTicketId = syncRecord.hubsoftid;
|
||||
|
||||
// 2. Sanitizar o comentário (remover HTML, etc.)
|
||||
const sanitizedContent = sanitizeGLPIComment({ content: rawContent });
|
||||
|
||||
// 3. Enviar o comentário para a API do HubSoft
|
||||
logInfo(`Enviando comentário do GLPI Ticket ${glpiTicketId} para o HubSoft Atendimento ${hubsoftTicketId}.`);
|
||||
await hubsoftService.addMensagem(hubsoftTicketId, sanitizedContent);
|
||||
|
||||
// Opcional: Salvar o comentário no banco intermediário (sync_comments) para ter um log.
|
||||
// Isso pode ser útil para auditoria, mas não é estritamente necessário para o envio.
|
||||
logInfo(`Comentário do GLPI Ticket ${glpiTicketId} enviado com sucesso para o HubSoft.`);
|
||||
//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 };
|
||||
|
||||
@ -88,20 +88,24 @@ async function addMensagem(id_atendimento, mensagem) {
|
||||
const token = await getAuthToken();
|
||||
|
||||
// 2. Construir a URL completa do endpoint
|
||||
const url = `${apiConfig.hubsoft.baseUrl}/integracao/atendimento/adicionar_mensagem/${id_atendimento}`;
|
||||
const url = `${apiConfig.hubsoft.atendimentosUrl}adicionar_mensagem/${id_atendimento}`;
|
||||
const payload = { mensagem };
|
||||
|
||||
|
||||
try {
|
||||
logInfo(`Enviando nova mensagem para o atendimento HubSoft ID ${id_atendimento}...`);
|
||||
// 3. Usar a instância 'axios' e passar o token no header
|
||||
|
||||
//PRint emulando o curl inteiro que será enviado
|
||||
const response = await axios.post(url, payload, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
logInfo(`Resposta da API HubSoft para adição de mensagem: ${JSON.stringify(response.data)}`);
|
||||
return response.data;
|
||||
|
||||
|
||||
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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user