diff --git a/src/app.js b/src/app.js index c743185..9755067 100644 --- a/src/app.js +++ b/src/app.js @@ -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; } diff --git a/src/controller/ClosureController.js b/src/controller/ClosureController.js index db76a3c..726dda8 100644 --- a/src/controller/ClosureController.js +++ b/src/controller/ClosureController.js @@ -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 }); + } + } + }); } diff --git a/src/controller/commentController.js b/src/controller/commentController.js index 2ae298f..96984c5 100644 --- a/src/controller/commentController.js +++ b/src/controller/commentController.js @@ -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 }; \ No newline at end of file diff --git a/src/controller/processController.js b/src/controller/processController.js index c3c23ac..0d14cb9 100644 --- a/src/controller/processController.js +++ b/src/controller/processController.js @@ -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, diff --git a/src/model/commentModel.js b/src/model/commentModel.js index 668e6ad..0a2af3f 100644 --- a/src/model/commentModel.js +++ b/src/model/commentModel.js @@ -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; diff --git a/src/model/commentSyncModel.js b/src/model/commentSyncModel.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/model/glpiModel.js b/src/model/glpiModel.js index 9bb5e39..2b82ff3 100644 --- a/src/model/glpiModel.js +++ b/src/model/glpiModel.js @@ -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; diff --git a/src/model/hubglpiModel.js b/src/model/hubglpiModel.js index f74415d..499795d 100644 --- a/src/model/hubglpiModel.js +++ b/src/model/hubglpiModel.js @@ -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) { diff --git a/src/routes.js b/src/routes.js index c8386fa..b5697e7 100644 --- a/src/routes.js +++ b/src/routes.js @@ -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 diff --git a/src/services/commentService.js b/src/services/commentService.js index 43f6342..e45c0e2 100644 --- a/src/services/commentService.js +++ b/src/services/commentService.js @@ -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 }; diff --git a/src/services/hubsoftService.js b/src/services/hubsoftService.js index d185b87..4f63379 100644 --- a/src/services/hubsoftService.js +++ b/src/services/hubsoftService.js @@ -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;