From 2cd00c65c5f932a68ad0d027ca1e3a47d4cd3142 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 16 Dec 2025 14:37:14 -0300 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20restaura=20altera=C3=A7=C3=B5es?= =?UTF-8?q?=20do=20createTickets=20ap=C3=B3s=20hotfix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/createTickets.controller.js | 55 ++++ .../services/createTickets.service.js | 73 +++++ .../services/implantacao.service.js | 254 ++++++++++++++++++ .../services/mundiale.service.js | 128 +++++++++ src/shared/infra/api/hubsoft.auth.js | 20 ++ src/shared/infra/api/hubsoft.config.js | 19 ++ src/shared/model/hubsoft.model.js | 0 .../repositories/hubsoftAPI.repository.js | 38 +++ .../repositories/hubsoftDB.repository.js | 118 ++++++++ 9 files changed, 705 insertions(+) create mode 100644 src/modules/createTickets/controller/createTickets.controller.js create mode 100644 src/modules/createTickets/services/createTickets.service.js create mode 100644 src/modules/createTickets/services/implantacao.service.js create mode 100644 src/modules/createTickets/services/mundiale.service.js create mode 100644 src/shared/infra/api/hubsoft.auth.js create mode 100644 src/shared/infra/api/hubsoft.config.js create mode 100644 src/shared/model/hubsoft.model.js create mode 100644 src/shared/repositories/hubsoftAPI.repository.js create mode 100644 src/shared/repositories/hubsoftDB.repository.js diff --git a/src/modules/createTickets/controller/createTickets.controller.js b/src/modules/createTickets/controller/createTickets.controller.js new file mode 100644 index 0000000..449bf30 --- /dev/null +++ b/src/modules/createTickets/controller/createTickets.controller.js @@ -0,0 +1,55 @@ +// 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 }; + + diff --git a/src/modules/createTickets/services/createTickets.service.js b/src/modules/createTickets/services/createTickets.service.js new file mode 100644 index 0000000..e7715b1 --- /dev/null +++ b/src/modules/createTickets/services/createTickets.service.js @@ -0,0 +1,73 @@ +// 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 +} \ No newline at end of file diff --git a/src/modules/createTickets/services/implantacao.service.js b/src/modules/createTickets/services/implantacao.service.js new file mode 100644 index 0000000..0b85e62 --- /dev/null +++ b/src/modules/createTickets/services/implantacao.service.js @@ -0,0 +1,254 @@ +// src/modules/createTickets/services/mundiale.service.js +const repositoryHubGlpi = require('../../../shared/repositories/hubglpi.repository.js'); +const repositoryGlpi = require('../../../shared/repositories/glpi.repository.js'); +const repositoryHubsoft = require('../../../shared/repositories/hubsoftDB.repository.js'); +const modelHubGlpi = require('../../../shared/model/hubglpi.model.js'); +const modelGlpi = require('../../../shared/model/glpi.model.js'); +const ticketShared = require('./createTickets.service.js'); +const { logInfo, logWarning } = require('../../../utils/logger.js'); + +// -------------------------------------- +// Funções principais do serviço +// -------------------------------------- + +async function fetchNew() { + return repositoryHubsoft.getImplantacaoTickets(); +} + +async function saveHubGlpi(tickets) { + if (!tickets.length) return; + + const ticketsFormatted = tickets.map(ticket => + modelHubGlpi.mapHubsoftToHubglpi(ticket, 'IMPLANTACAO') + ); + + const inserted = await repositoryHubGlpi.insertTickets(ticketsFormatted); + + if (inserted) { + await repositoryHubGlpi.insertSyncData( + ticketsFormatted.map(ticket => ticket.id_atendimento) + ); + } + + return inserted; +} + +async function sendToGlpi(ticket) { + + const ticketsResolved = await ticketShared.resolveEntityId(ticket); + + if (ticketsResolved.entities_id === 0) { + logWarning(`Entidade não encontrada para o ticket id=${ticket.id_atendimento}.`); + } + + formatGlpiPayload(ticketsResolved); + + const payload = modelGlpi.mapHubGlpiToGlpi(ticketsResolved); + + const glpiId = await repositoryGlpi.insertTicket(payload); + logInfo(`Ticket GLPI criado com ID=${glpiId} para o ticket id=${ticket.id_atendimento}.`); + + await repositoryGlpi.insertGroupTicket(glpiId, 'IMPLANTACAO'); + await repositoryHubGlpi.updateSyncDataCreated(ticket.id_atendimento, glpiId); +} + + +// -------------------------------------- +// 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 ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Dados Comercial +
Nº de Operação${ticket.protocolo_hub || "N/A"}
Gerente Responsável${ticket.usuario_que_abriu || "N/A"}
Código do Cliente${ticket.codigo_cliente}
+ Dados Cliente +
Cliente${ticket.nome_razaosocial}
${docLabel}${documentoFormatado}
Nome Contato${ticket.cliente_nome || "N/A"}
Email Contato${ticket.email || "N/A"}
Telefone Contato${ticket.telefone || "N/A"}
Endereço Instalação${ticket.endereco || "N/A"}
+ Dados do Serviço Contratado +
+ + + + + + + + + + + +
ProdutoQuantidadeDescrição
${servico.produto}${servico.qtd}${servico.descricao || ""}
+
+ Observações +
${ticket.descricao_abertura || "N/A"}
+ `; +}; + + + +// -------------------------------------- +// Formatadores de Documento +// -------------------------------------- + +function formatCPF(cpf) { + cpf = cpf.replace(/\D/g, ""); + return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4"); +} + +function formatCNPJ(cnpj) { + cnpj = cnpj.replace(/\D/g, ""); + return cnpj.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, "$1.$2.$3/$4-$5"); +} + +function formatDocument(doc, tipo) { + if (!doc) return "N/A"; + return tipo === "pf" ? formatCPF(doc) : formatCNPJ(doc); +} + + +// -------------------------------------- +// Resolve Serviço +// -------------------------------------- + +function resolveService(servicoNome) { + return serviceDictionary[servicoNome] || { + produto: servicoNome, + qtd: 1, + descricao: null + }; +} + + +// -------------------------------------- +// Exportação +// -------------------------------------- + +module.exports = { + fetchNew, + saveHubGlpi, + sendToGlpi +}; diff --git a/src/modules/createTickets/services/mundiale.service.js b/src/modules/createTickets/services/mundiale.service.js new file mode 100644 index 0000000..051550a --- /dev/null +++ b/src/modules/createTickets/services/mundiale.service.js @@ -0,0 +1,128 @@ +// 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 = ` + + + + + + + + + + + + + + + + + + + + + + + + + +
CampoValor
Nome:${ticketData.cliente_nome}
Codigo:${ticketData.codigo_cliente}
Serviço:${ticketData.servico_nome}
Ticket Mundiale${ticketData.ticket_mundiale}
Protocolo Hub:${ticketData.protocolo_hub || 'N/A'}
+ `; + + 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. + */ \ No newline at end of file diff --git a/src/shared/infra/api/hubsoft.auth.js b/src/shared/infra/api/hubsoft.auth.js new file mode 100644 index 0000000..7fe1512 --- /dev/null +++ b/src/shared/infra/api/hubsoft.auth.js @@ -0,0 +1,20 @@ +// 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; + } +}; diff --git a/src/shared/infra/api/hubsoft.config.js b/src/shared/infra/api/hubsoft.config.js new file mode 100644 index 0000000..d1581a6 --- /dev/null +++ b/src/shared/infra/api/hubsoft.config.js @@ -0,0 +1,19 @@ +// 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/` + } +}; \ No newline at end of file diff --git a/src/shared/model/hubsoft.model.js b/src/shared/model/hubsoft.model.js new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/repositories/hubsoftAPI.repository.js b/src/shared/repositories/hubsoftAPI.repository.js new file mode 100644 index 0000000..1b61c92 --- /dev/null +++ b/src/shared/repositories/hubsoftAPI.repository.js @@ -0,0 +1,38 @@ +// 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 +}; diff --git a/src/shared/repositories/hubsoftDB.repository.js b/src/shared/repositories/hubsoftDB.repository.js new file mode 100644 index 0000000..524c967 --- /dev/null +++ b/src/shared/repositories/hubsoftDB.repository.js @@ -0,0 +1,118 @@ +// 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 + +}; \ No newline at end of file From 783eb2f08134ca6d0f7ed1b83f99ca81ae546474 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Fri, 2 Jan 2026 18:19:11 -0300 Subject: [PATCH 2/4] =?UTF-8?q?REFACTOR:=20Integra=C3=A7=C3=A3o=20refatora?= =?UTF-8?q?da=20utilizando=20conceitos=20de=20clean=20architeture=20e=20mo?= =?UTF-8?q?nolito=20modular?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 8 +- .../{apiConfig.js => apiHubsoft.config.js} | 0 src/config/{dbConfig.js => db.config.js} | 1 + src/config/{envLoader.js => env.loader.js} | 0 src/controller/ClosureController.js | 56 --- src/controller/commentController.js | 55 --- src/controller/processController.js | 320 ----------------- src/cron.js | 48 --- src/data/hubglpiDataBase.js | 31 -- src/index.js | 11 - src/infra/api/hubsoft.auth.js | 27 ++ src/infra/api/hubsoft.client.js | 43 +++ src/infra/api/hubsoft.config.js | 32 ++ src/infra/cron/sync.cron.js | 48 +++ src/infra/db/connections/glpi.mysql.js | 16 + src/infra/db/connections/hubglpi.pg.js | 18 + src/infra/db/connections/hubsoft.pg.js | 14 + .../repositories/glpi/comments.repository.js | 48 +++ .../repositories/glpi/entities.repository.js | 57 ++++ .../db/repositories/glpi/groups.repository.js | 38 +++ .../repositories/glpi/tickets.repository.js | 55 +++ .../hubglpi/comments.repository.js | 74 ++++ .../repositories/hubglpi/sync.repository.js | 152 +++++++++ .../hubglpi/tickets.repository.js | 168 +++++++++ .../hubglpi/watermark.repository.js | 31 ++ .../hubsoft/messages.repository.js | 27 ++ .../hubsoft/tickets.repository.js | 100 ++++++ src/{ => infra/http}/app.js | 4 +- src/infra/http/routes/index.js | 16 + src/{ => infra/http}/server.js | 6 +- src/model/commentModel.js | 142 -------- src/model/glpiModel.js | 139 -------- src/model/hubglpiModel.js | 322 ------------------ src/model/hubsoftModel.js | 102 ------ .../close/controller/close.controller.js | 30 ++ .../close/repositories/close.repository.js | 37 ++ .../close/services/hubsoftClose.service.js | 47 +++ .../close/useCases/closeTickets.usecase.js | 70 ++++ .../controller/glpiComment.controller.js | 20 ++ .../comments/models/glpiComment.model.js | 42 +++ .../repositories/comment.repository.js | 65 ++++ .../useCases/syncGlpiCommentToHub.usecase.js | 50 +++ .../useCases/syncHubCommentToGlpi.usecase.js | 61 ++++ .../controller/createTickets.controller.js | 55 --- .../services/createTickets.service.js | 73 ---- .../services/mundiale.service.js | 128 ------- .../tickets/controller/tickets.controller.js | 13 + .../tickets/models/glpi/cancelamento.model.js | 195 +++++++++++ .../models/glpi/implantacao.model.js} | 211 +++++------- .../tickets/models/glpi/mundiale.model.js | 69 ++++ src/modules/tickets/models/hubglpi/model.js | 88 +++++ .../tickets/repositories/ticket.repository.js | 158 +++++++++ .../tickets/services/cancelamento.service.js | 63 ++++ .../tickets/services/createTickets.service.js | 14 + .../tickets/services/implantacao.service.js | 43 +++ .../tickets/services/mundiale.service.js | 63 ++++ .../services/resolveTicketEntity.service.js | 36 ++ .../services/ticketNotifications.service.js | 18 + .../tickets/useCases/syncTickets.usecase.js | 92 +++++ src/routes.js | 17 - src/services/commentService.js | 141 -------- src/services/hubsoftService.js | 119 ------- src/services/ticketService.js | 144 -------- src/shared/infra/api/hubsoft.auth.js | 20 -- src/shared/infra/api/hubsoft.config.js | 19 -- src/shared/model/hubsoft.model.js | 0 .../repositories/hubsoftAPI.repository.js | 38 --- .../repositories/hubsoftDB.repository.js | 118 ------- src/{ => shared}/scripts/data/database.sql | 0 src/{ => shared}/scripts/data/database2.sql | Bin src/{ => shared}/utils/commentSanitizer.js | 0 src/{ => shared}/utils/logger.js | 2 + 72 files changed, 2328 insertions(+), 2240 deletions(-) rename src/config/{apiConfig.js => apiHubsoft.config.js} (100%) rename src/config/{dbConfig.js => db.config.js} (97%) rename src/config/{envLoader.js => env.loader.js} (100%) delete mode 100644 src/controller/ClosureController.js delete mode 100644 src/controller/commentController.js delete mode 100644 src/controller/processController.js delete mode 100644 src/cron.js delete mode 100644 src/data/hubglpiDataBase.js delete mode 100644 src/index.js create mode 100644 src/infra/api/hubsoft.auth.js create mode 100644 src/infra/api/hubsoft.client.js create mode 100644 src/infra/api/hubsoft.config.js create mode 100644 src/infra/cron/sync.cron.js create mode 100644 src/infra/db/connections/glpi.mysql.js create mode 100644 src/infra/db/connections/hubglpi.pg.js create mode 100644 src/infra/db/connections/hubsoft.pg.js create mode 100644 src/infra/db/repositories/glpi/comments.repository.js create mode 100644 src/infra/db/repositories/glpi/entities.repository.js create mode 100644 src/infra/db/repositories/glpi/groups.repository.js create mode 100644 src/infra/db/repositories/glpi/tickets.repository.js create mode 100644 src/infra/db/repositories/hubglpi/comments.repository.js create mode 100644 src/infra/db/repositories/hubglpi/sync.repository.js create mode 100644 src/infra/db/repositories/hubglpi/tickets.repository.js create mode 100644 src/infra/db/repositories/hubglpi/watermark.repository.js create mode 100644 src/infra/db/repositories/hubsoft/messages.repository.js create mode 100644 src/infra/db/repositories/hubsoft/tickets.repository.js rename src/{ => infra/http}/app.js (90%) create mode 100644 src/infra/http/routes/index.js rename src/{ => infra/http}/server.js (77%) delete mode 100644 src/model/commentModel.js delete mode 100644 src/model/glpiModel.js delete mode 100644 src/model/hubglpiModel.js delete mode 100644 src/model/hubsoftModel.js create mode 100644 src/modules/close/controller/close.controller.js create mode 100644 src/modules/close/repositories/close.repository.js create mode 100644 src/modules/close/services/hubsoftClose.service.js create mode 100644 src/modules/close/useCases/closeTickets.usecase.js create mode 100644 src/modules/comments/controller/glpiComment.controller.js create mode 100644 src/modules/comments/models/glpiComment.model.js create mode 100644 src/modules/comments/repositories/comment.repository.js create mode 100644 src/modules/comments/useCases/syncGlpiCommentToHub.usecase.js create mode 100644 src/modules/comments/useCases/syncHubCommentToGlpi.usecase.js delete mode 100644 src/modules/createTickets/controller/createTickets.controller.js delete mode 100644 src/modules/createTickets/services/createTickets.service.js delete mode 100644 src/modules/createTickets/services/mundiale.service.js create mode 100644 src/modules/tickets/controller/tickets.controller.js create mode 100644 src/modules/tickets/models/glpi/cancelamento.model.js rename src/modules/{createTickets/services/implantacao.service.js => tickets/models/glpi/implantacao.model.js} (58%) create mode 100644 src/modules/tickets/models/glpi/mundiale.model.js create mode 100644 src/modules/tickets/models/hubglpi/model.js create mode 100644 src/modules/tickets/repositories/ticket.repository.js create mode 100644 src/modules/tickets/services/cancelamento.service.js create mode 100644 src/modules/tickets/services/createTickets.service.js create mode 100644 src/modules/tickets/services/implantacao.service.js create mode 100644 src/modules/tickets/services/mundiale.service.js create mode 100644 src/modules/tickets/services/resolveTicketEntity.service.js create mode 100644 src/modules/tickets/services/ticketNotifications.service.js create mode 100644 src/modules/tickets/useCases/syncTickets.usecase.js delete mode 100644 src/routes.js delete mode 100644 src/services/commentService.js delete mode 100644 src/services/hubsoftService.js delete mode 100644 src/services/ticketService.js delete mode 100644 src/shared/infra/api/hubsoft.auth.js delete mode 100644 src/shared/infra/api/hubsoft.config.js delete mode 100644 src/shared/model/hubsoft.model.js delete mode 100644 src/shared/repositories/hubsoftAPI.repository.js delete mode 100644 src/shared/repositories/hubsoftDB.repository.js rename src/{ => shared}/scripts/data/database.sql (100%) rename src/{ => shared}/scripts/data/database2.sql (100%) rename src/{ => shared}/utils/commentSanitizer.js (100%) rename src/{ => shared}/utils/logger.js (98%) diff --git a/package.json b/package.json index 0deca21..4853b7d 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "version": "1.0.0", "main": "index.js", "scripts": { - "start:api": "cross-env NODE_ENV=production node src/server.js", - "dev:api": "cross-env NODE_ENV=development nodemon src/server.js", - "start:worker": "cross-env NODE_ENV=production node src/cron.js", - "dev:worker": "cross-env NODE_ENV=development nodemon src/cron.js", + "start:api": "cross-env NODE_ENV=production node src/infra/http/server.js", + "dev:api": "cross-env NODE_ENV=development nodemon src/infra/http/server.js", + "start:worker": "cross-env NODE_ENV=production node src/infra/cron/sync.cron.js", + "dev:worker": "cross-env NODE_ENV=development nodemon src/infra/cron/sync.cron.js", "test": "echo \"Error: no test specified\" && exit 1", "postinstall": "node -e \"const fs = require('fs'); if (fs.existsSync('.env') && !fs.existsSync('.env.development')) { fs.copyFileSync('.env', '.env.development'); console.log('✅ .env.development criado a partir do .env'); }\"" }, diff --git a/src/config/apiConfig.js b/src/config/apiHubsoft.config.js similarity index 100% rename from src/config/apiConfig.js rename to src/config/apiHubsoft.config.js diff --git a/src/config/dbConfig.js b/src/config/db.config.js similarity index 97% rename from src/config/dbConfig.js rename to src/config/db.config.js index e026ebe..6083710 100644 --- a/src/config/dbConfig.js +++ b/src/config/db.config.js @@ -1,3 +1,4 @@ +// src/config/db.config.js module.exports = { hubsoft: { databaseHost: process.env.HUBSOFT_DATABASE_HOST, diff --git a/src/config/envLoader.js b/src/config/env.loader.js similarity index 100% rename from src/config/envLoader.js rename to src/config/env.loader.js diff --git a/src/controller/ClosureController.js b/src/controller/ClosureController.js deleted file mode 100644 index 726dda8..0000000 --- a/src/controller/ClosureController.js +++ /dev/null @@ -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. - */ \ No newline at end of file diff --git a/src/controller/commentController.js b/src/controller/commentController.js deleted file mode 100644 index 96984c5..0000000 --- a/src/controller/commentController.js +++ /dev/null @@ -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 }; \ No newline at end of file diff --git a/src/controller/processController.js b/src/controller/processController.js deleted file mode 100644 index 0d14cb9..0000000 --- a/src/controller/processController.js +++ /dev/null @@ -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 = ` - - - - - - - - - - - - - - - - - - - - - - - - - -
CampoValor
Nome:${ticketData.cliente_nome}
Codigo:${ticketData.codigo_cliente}
Serviço:${ticketData.servico_nome}
Ticket Mundiale${ticketData.ticket_mundiale}
Protocolo Hub:${ticketData.protocolo_hub || 'N/A'}
- `; - - 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. - */ \ No newline at end of file diff --git a/src/cron.js b/src/cron.js deleted file mode 100644 index 4fcb4ae..0000000 --- a/src/cron.js +++ /dev/null @@ -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. - */ \ No newline at end of file diff --git a/src/data/hubglpiDataBase.js b/src/data/hubglpiDataBase.js deleted file mode 100644 index 9ddf9a3..0000000 --- a/src/data/hubglpiDataBase.js +++ /dev/null @@ -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. -*/ \ No newline at end of file diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 15d48b1..0000000 --- a/src/index.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/src/infra/api/hubsoft.auth.js b/src/infra/api/hubsoft.auth.js new file mode 100644 index 0000000..6ad1f6d --- /dev/null +++ b/src/infra/api/hubsoft.auth.js @@ -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 diff --git a/src/infra/api/hubsoft.client.js b/src/infra/api/hubsoft.client.js new file mode 100644 index 0000000..d18bae3 --- /dev/null +++ b/src/infra/api/hubsoft.client.js @@ -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 +} diff --git a/src/infra/api/hubsoft.config.js b/src/infra/api/hubsoft.config.js new file mode 100644 index 0000000..ff919c7 --- /dev/null +++ b/src/infra/api/hubsoft.config.js @@ -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 + } +}; \ No newline at end of file diff --git a/src/infra/cron/sync.cron.js b/src/infra/cron/sync.cron.js new file mode 100644 index 0000000..07e2220 --- /dev/null +++ b/src/infra/cron/sync.cron.js @@ -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') +} + diff --git a/src/infra/db/connections/glpi.mysql.js b/src/infra/db/connections/glpi.mysql.js new file mode 100644 index 0000000..c5ad75e --- /dev/null +++ b/src/infra/db/connections/glpi.mysql.js @@ -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 \ No newline at end of file diff --git a/src/infra/db/connections/hubglpi.pg.js b/src/infra/db/connections/hubglpi.pg.js new file mode 100644 index 0000000..047328d --- /dev/null +++ b/src/infra/db/connections/hubglpi.pg.js @@ -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 diff --git a/src/infra/db/connections/hubsoft.pg.js b/src/infra/db/connections/hubsoft.pg.js new file mode 100644 index 0000000..b5aff29 --- /dev/null +++ b/src/infra/db/connections/hubsoft.pg.js @@ -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 diff --git a/src/infra/db/repositories/glpi/comments.repository.js b/src/infra/db/repositories/glpi/comments.repository.js new file mode 100644 index 0000000..b90ad94 --- /dev/null +++ b/src/infra/db/repositories/glpi/comments.repository.js @@ -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 +} diff --git a/src/infra/db/repositories/glpi/entities.repository.js b/src/infra/db/repositories/glpi/entities.repository.js new file mode 100644 index 0000000..b58f0e5 --- /dev/null +++ b/src/infra/db/repositories/glpi/entities.repository.js @@ -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 +} diff --git a/src/infra/db/repositories/glpi/groups.repository.js b/src/infra/db/repositories/glpi/groups.repository.js new file mode 100644 index 0000000..9140db1 --- /dev/null +++ b/src/infra/db/repositories/glpi/groups.repository.js @@ -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 +} diff --git a/src/infra/db/repositories/glpi/tickets.repository.js b/src/infra/db/repositories/glpi/tickets.repository.js new file mode 100644 index 0000000..1c677a5 --- /dev/null +++ b/src/infra/db/repositories/glpi/tickets.repository.js @@ -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 +} diff --git a/src/infra/db/repositories/hubglpi/comments.repository.js b/src/infra/db/repositories/hubglpi/comments.repository.js new file mode 100644 index 0000000..d41f4a8 --- /dev/null +++ b/src/infra/db/repositories/hubglpi/comments.repository.js @@ -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 +}; diff --git a/src/infra/db/repositories/hubglpi/sync.repository.js b/src/infra/db/repositories/hubglpi/sync.repository.js new file mode 100644 index 0000000..ad6de75 --- /dev/null +++ b/src/infra/db/repositories/hubglpi/sync.repository.js @@ -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 +}; diff --git a/src/infra/db/repositories/hubglpi/tickets.repository.js b/src/infra/db/repositories/hubglpi/tickets.repository.js new file mode 100644 index 0000000..fc7d656 --- /dev/null +++ b/src/infra/db/repositories/hubglpi/tickets.repository.js @@ -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} 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 +}; + diff --git a/src/infra/db/repositories/hubglpi/watermark.repository.js b/src/infra/db/repositories/hubglpi/watermark.repository.js new file mode 100644 index 0000000..fa3a3c4 --- /dev/null +++ b/src/infra/db/repositories/hubglpi/watermark.repository.js @@ -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 +}; diff --git a/src/infra/db/repositories/hubsoft/messages.repository.js b/src/infra/db/repositories/hubsoft/messages.repository.js new file mode 100644 index 0000000..33999c1 --- /dev/null +++ b/src/infra/db/repositories/hubsoft/messages.repository.js @@ -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 +} diff --git a/src/infra/db/repositories/hubsoft/tickets.repository.js b/src/infra/db/repositories/hubsoft/tickets.repository.js new file mode 100644 index 0000000..ec00d4e --- /dev/null +++ b/src/infra/db/repositories/hubsoft/tickets.repository.js @@ -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 +} diff --git a/src/app.js b/src/infra/http/app.js similarity index 90% rename from src/app.js rename to src/infra/http/app.js index 9755067..2ce9df5 100644 --- a/src/app.js +++ b/src/infra/http/app.js @@ -1,5 +1,7 @@ +// src/infra/http/app.js + const express = require('express'); -const router = require('./routes.js') +const router = require('./routes') function createApp() { const app = express(); diff --git a/src/infra/http/routes/index.js b/src/infra/http/routes/index.js new file mode 100644 index 0000000..2f96423 --- /dev/null +++ b/src/infra/http/routes/index.js @@ -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; \ No newline at end of file diff --git a/src/server.js b/src/infra/http/server.js similarity index 77% rename from src/server.js rename to src/infra/http/server.js index 4c49f4d..d47256e 100644 --- a/src/server.js +++ b/src/infra/http/server.js @@ -1,8 +1,10 @@ -const loadEnv = require('./config/envLoader'); +// src/infra/http/server.js + +const loadEnv = require('../../config/env.loader.js'); loadEnv(); // Carrega as variáveis de ambiente const createApp = require('./app.js'); -const { logInfo } = require('./utils/logger.js'); +const { logInfo } = require('../../shared/utils/logger.js'); const app = createApp(); diff --git a/src/model/commentModel.js b/src/model/commentModel.js deleted file mode 100644 index 0a2af3f..0000000 --- a/src/model/commentModel.js +++ /dev/null @@ -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} 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>} 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; diff --git a/src/model/glpiModel.js b/src/model/glpiModel.js deleted file mode 100644 index 2b82ff3..0000000 --- a/src/model/glpiModel.js +++ /dev/null @@ -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; diff --git a/src/model/hubglpiModel.js b/src/model/hubglpiModel.js deleted file mode 100644 index 499795d..0000000 --- a/src/model/hubglpiModel.js +++ /dev/null @@ -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. - */ diff --git a/src/model/hubsoftModel.js b/src/model/hubsoftModel.js deleted file mode 100644 index b05cfc1..0000000 --- a/src/model/hubsoftModel.js +++ /dev/null @@ -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 -}; \ No newline at end of file diff --git a/src/modules/close/controller/close.controller.js b/src/modules/close/controller/close.controller.js new file mode 100644 index 0000000..0929884 --- /dev/null +++ b/src/modules/close/controller/close.controller.js @@ -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. + */ \ No newline at end of file diff --git a/src/modules/close/repositories/close.repository.js b/src/modules/close/repositories/close.repository.js new file mode 100644 index 0000000..8712f76 --- /dev/null +++ b/src/modules/close/repositories/close.repository.js @@ -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 +} diff --git a/src/modules/close/services/hubsoftClose.service.js b/src/modules/close/services/hubsoftClose.service.js new file mode 100644 index 0000000..6cce5a9 --- /dev/null +++ b/src/modules/close/services/hubsoftClose.service.js @@ -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 +} diff --git a/src/modules/close/useCases/closeTickets.usecase.js b/src/modules/close/useCases/closeTickets.usecase.js new file mode 100644 index 0000000..c0aaff0 --- /dev/null +++ b/src/modules/close/useCases/closeTickets.usecase.js @@ -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 +} diff --git a/src/modules/comments/controller/glpiComment.controller.js b/src/modules/comments/controller/glpiComment.controller.js new file mode 100644 index 0000000..8394013 --- /dev/null +++ b/src/modules/comments/controller/glpiComment.controller.js @@ -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} \ No newline at end of file diff --git a/src/modules/comments/models/glpiComment.model.js b/src/modules/comments/models/glpiComment.model.js new file mode 100644 index 0000000..b97ca0e --- /dev/null +++ b/src/modules/comments/models/glpiComment.model.js @@ -0,0 +1,42 @@ +//src/modules/comments/models/glpiComment.model.js +function escapeHtml(text = '') { + return text + .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, '
') + + return ` +
+
+ ${safeAuthor} + (${source}) +
+
+ ${safeMessage} +
+
+`.trim() +} + +function mapMessageToGlpiComment(glpiTicketId, msg) { + return { + ticketId: glpiTicketId, + content: buildHtml({ + author: msg.usuario_nome, + message: msg.mensagem, + source: 'Hubsoft' + }) + } +} + +module.exports = { + mapMessageToGlpiComment +} diff --git a/src/modules/comments/repositories/comment.repository.js b/src/modules/comments/repositories/comment.repository.js new file mode 100644 index 0000000..77447c9 --- /dev/null +++ b/src/modules/comments/repositories/comment.repository.js @@ -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 +} diff --git a/src/modules/comments/useCases/syncGlpiCommentToHub.usecase.js b/src/modules/comments/useCases/syncGlpiCommentToHub.usecase.js new file mode 100644 index 0000000..2aa4861 --- /dev/null +++ b/src/modules/comments/useCases/syncGlpiCommentToHub.usecase.js @@ -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 } diff --git a/src/modules/comments/useCases/syncHubCommentToGlpi.usecase.js b/src/modules/comments/useCases/syncHubCommentToGlpi.usecase.js new file mode 100644 index 0000000..f8659c9 --- /dev/null +++ b/src/modules/comments/useCases/syncHubCommentToGlpi.usecase.js @@ -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 } diff --git a/src/modules/createTickets/controller/createTickets.controller.js b/src/modules/createTickets/controller/createTickets.controller.js deleted file mode 100644 index 449bf30..0000000 --- a/src/modules/createTickets/controller/createTickets.controller.js +++ /dev/null @@ -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 }; - - diff --git a/src/modules/createTickets/services/createTickets.service.js b/src/modules/createTickets/services/createTickets.service.js deleted file mode 100644 index e7715b1..0000000 --- a/src/modules/createTickets/services/createTickets.service.js +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/src/modules/createTickets/services/mundiale.service.js b/src/modules/createTickets/services/mundiale.service.js deleted file mode 100644 index 051550a..0000000 --- a/src/modules/createTickets/services/mundiale.service.js +++ /dev/null @@ -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 = ` - - - - - - - - - - - - - - - - - - - - - - - - - -
CampoValor
Nome:${ticketData.cliente_nome}
Codigo:${ticketData.codigo_cliente}
Serviço:${ticketData.servico_nome}
Ticket Mundiale${ticketData.ticket_mundiale}
Protocolo Hub:${ticketData.protocolo_hub || 'N/A'}
- `; - - 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. - */ \ No newline at end of file diff --git a/src/modules/tickets/controller/tickets.controller.js b/src/modules/tickets/controller/tickets.controller.js new file mode 100644 index 0000000..4f47e32 --- /dev/null +++ b/src/modules/tickets/controller/tickets.controller.js @@ -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} \ No newline at end of file diff --git a/src/modules/tickets/models/glpi/cancelamento.model.js b/src/modules/tickets/models/glpi/cancelamento.model.js new file mode 100644 index 0000000..41e69ff --- /dev/null +++ b/src/modules/tickets/models/glpi/cancelamento.model.js @@ -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 ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Dados HubSoft +
Nº de Operação${ticket.protocolo_hub || "N/A"}
Solicitante${ticket.vendedor || "N/A"}
Código do Cliente${ticket.codigo_cliente}
+ Dados Cliente +
Cliente${ticket.nome_razaosocial}
${docLabel}${documento}
Nome Contato${ticket.cliente_nome || "N/A"}
Email Contato${ticket.email || "N/A"}
Telefone Contato${ticket.telefone || "N/A"}
Endereço Instalação${ticket.endereco || "N/A"}
+ Dados do Serviço Cancelado +
+ + + + + + + + + + + +
ProdutoQuantidadeDescrição
${servico.produto}${servico.qtd}${servico.descricao || ""}
+
+ Observações +
+ ${nl2br(ticket.descricao_abertura) || "N/A"} +
+` +} + + +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, '
') +} + + +module.exports = { toGlpiPayload } \ No newline at end of file diff --git a/src/modules/createTickets/services/implantacao.service.js b/src/modules/tickets/models/glpi/implantacao.model.js similarity index 58% rename from src/modules/createTickets/services/implantacao.service.js rename to src/modules/tickets/models/glpi/implantacao.model.js index 0b85e62..5f69b53 100644 --- a/src/modules/createTickets/services/implantacao.service.js +++ b/src/modules/tickets/models/glpi/implantacao.model.js @@ -1,113 +1,44 @@ -// src/modules/createTickets/services/mundiale.service.js -const repositoryHubGlpi = require('../../../shared/repositories/hubglpi.repository.js'); -const repositoryGlpi = require('../../../shared/repositories/glpi.repository.js'); -const repositoryHubsoft = require('../../../shared/repositories/hubsoftDB.repository.js'); -const modelHubGlpi = require('../../../shared/model/hubglpi.model.js'); -const modelGlpi = require('../../../shared/model/glpi.model.js'); -const ticketShared = require('./createTickets.service.js'); -const { logInfo, logWarning } = require('../../../utils/logger.js'); +// src/modules/tickets/models/glpi/implantacao.model.js -// -------------------------------------- -// Funções principais do serviço -// -------------------------------------- - -async function fetchNew() { - return repositoryHubsoft.getImplantacaoTickets(); +function toGlpiPayload(ticket) { + return { + entities_id: ticket.entities_id || 0, + name: buildTitle(ticket), + content: buildHtml(ticket), + status: resolveGlpiStatus(ticket.status_atendimento), + date: ticket.created_at, + date_mod: new Date(), + users_id_recipient: process.env.GLPI_USER_ID || 0, + urgency: 3, + impact: 3, + priority: 3, + type: 2, + date_creation: ticket.created_at || new Date(), + itilcategories_id: 0, + slas_id_ttr: 37, + } } -async function saveHubGlpi(tickets) { - if (!tickets.length) return; - - const ticketsFormatted = tickets.map(ticket => - modelHubGlpi.mapHubsoftToHubglpi(ticket, 'IMPLANTACAO') - ); - - const inserted = await repositoryHubGlpi.insertTickets(ticketsFormatted); - - if (inserted) { - await repositoryHubGlpi.insertSyncData( - ticketsFormatted.map(ticket => ticket.id_atendimento) - ); - } - - return inserted; +function resolveGlpiStatus(status) { + const map = { + 'Novo': 1, + 'Pendente': 4, + 'Em atendimento': 2, + 'Resolvido': 5 + } + return map[status] || 1 } -async function sendToGlpi(ticket) { - - const ticketsResolved = await ticketShared.resolveEntityId(ticket); - - if (ticketsResolved.entities_id === 0) { - logWarning(`Entidade não encontrada para o ticket id=${ticket.id_atendimento}.`); - } - - formatGlpiPayload(ticketsResolved); - - const payload = modelGlpi.mapHubGlpiToGlpi(ticketsResolved); - - const glpiId = await repositoryGlpi.insertTicket(payload); - logInfo(`Ticket GLPI criado com ID=${glpiId} para o ticket id=${ticket.id_atendimento}.`); - - await repositoryGlpi.insertGroupTicket(glpiId, 'IMPLANTACAO'); - await repositoryHubGlpi.updateSyncDataCreated(ticket.id_atendimento, glpiId); +function buildTitle(ticket) { + return `IMPLANTAÇÃO - ${ticket.codigo_cliente}-${ticket.codigo_servico} - ${ticket.nome_razaosocial} - ${ticket.servico_nome}` } +function buildHtml(ticket) { + const docLabel = ticket.tipo_pessoa === 'pf' ? 'CPF' : 'CNPJ' + const documento = formatDocument(ticket.cpf_cnpj, ticket.tipo_pessoa) + const servico = resolveService(ticket.servico_nome) -// -------------------------------------- -// Formatar dados antes de enviar para o GLPI -// -------------------------------------- - -function formatGlpiPayload(ticket) { - const title = `IMPLANTAÇÃO - ${ticket.codigo_cliente}-${ticket.codigo_servico}-${ticket.nome_razaosocial} - ${ticket.servico_nome}`; - const description = formatDescription(ticket); - ticket.name = title; - ticket.content = description; - return; -} - - -// -------------------------------------- -// Dicionário de Serviços -// -------------------------------------- - -const serviceDictionary = { - // ---------- LAN-TO-LAN ---------- - "Lan-to-Lan 50 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "50 Mbps" }, - "Lan-to-Lan 100 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "100 Mbps" }, - "Lan-to-Lan 200 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "200 Mbps" }, - "Lan-to-Lan 300 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "300 Mbps" }, - "Lan-to-Lan 500 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "500 Mbps" }, - "Lan-to-Lan 700 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "700 Mbps" }, - "Lan-to-Lan": { produto: "Lan-to-Lan", qtd: 1, descricao: null }, - - // ---------- LINK DEDICADO ---------- - "Link de Internet Dedicado 20 Mbps Full Duplex": - { produto: "Link de Internet Dedicado", qtd: 1, descricao: "20 Mbps Full Duplex" }, - - "Link de Internet Dedicado 100 Mbps Full Duplex": - { produto: "Link de Internet Dedicado", qtd: 1, descricao: "100 Mbps Full Duplex" }, - - "Link de Internet Dedicado 1Gbps Full Duplex": - { produto: "Link de Internet Dedicado", qtd: 1, descricao: "1 Gbps Full Duplex" }, - - "Link de Internet Dedicado 2 Gbps Full Duplex": - { produto: "Link de Internet Dedicado", qtd: 1, descricao: "2 Gbps Full Duplex" }, - - // Default genérico caso venha coisa nova -}; - - -// -------------------------------------- -// Tabela HTML da descrição -// -------------------------------------- - -const formatDescription = (ticket) => { - - const documentoFormatado = formatDocument(ticket.cpf_cnpj, ticket.tipo_pessoa); - const docLabel = ticket.tipo_pessoa === "pf" ? "CPF" : "CNPJ"; - const servico = resolveService(ticket.servico_nome); - - return ` + return ` @@ -124,7 +55,7 @@ const formatDescription = (ticket) => { - + @@ -146,7 +77,7 @@ const formatDescription = (ticket) => { - + @@ -201,54 +132,64 @@ const formatDescription = (ticket) => { - +
Gerente Responsável${ticket.usuario_que_abriu || "N/A"}${ticket.vendedor || "N/A"}
${docLabel}${documentoFormatado}${documento}
${ticket.descricao_abertura || "N/A"} + ${nl2br(ticket.descricao_abertura) || "N/A"} +
- `; -}; +` +} - -// -------------------------------------- -// Formatadores de Documento -// -------------------------------------- - function formatCPF(cpf) { - cpf = cpf.replace(/\D/g, ""); - return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4"); + return cpf?.replace(/\D/g, '') + .replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4') } function formatCNPJ(cnpj) { - cnpj = cnpj.replace(/\D/g, ""); - return cnpj.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, "$1.$2.$3/$4-$5"); + return cnpj?.replace(/\D/g, '') + .replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, '$1.$2.$3/$4-$5') } function formatDocument(doc, tipo) { - if (!doc) return "N/A"; - return tipo === "pf" ? formatCPF(doc) : formatCNPJ(doc); + if (!doc) return 'N/A' + return tipo === 'pf' ? formatCPF(doc) : formatCNPJ(doc) } +const serviceDictionary = { + // ---------- LAN-TO-LAN ---------- + "Lan-to-Lan 50 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "50 Mbps" }, + "Lan-to-Lan 100 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "100 Mbps" }, + "Lan-to-Lan 200 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "200 Mbps" }, + "Lan-to-Lan 300 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "300 Mbps" }, + "Lan-to-Lan 500 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "500 Mbps" }, + "Lan-to-Lan 700 Mbps": { produto: "Lan-to-Lan", qtd: 1, descricao: "700 Mbps" }, + "Lan-to-Lan": { produto: "Lan-to-Lan", qtd: 1, descricao: null }, -// -------------------------------------- -// Resolve Serviço -// -------------------------------------- + // ---------- LINK DEDICADO ---------- + "Link de Internet Dedicado 20 Mbps Full Duplex": + { produto: "Link de Internet Dedicado", qtd: 1, descricao: "20 Mbps Full Duplex" }, -function resolveService(servicoNome) { - return serviceDictionary[servicoNome] || { - produto: servicoNome, - qtd: 1, - descricao: null - }; -} + "Link de Internet Dedicado 100 Mbps Full Duplex": + { produto: "Link de Internet Dedicado", qtd: 1, descricao: "100 Mbps Full Duplex" }, + "Link de Internet Dedicado 1Gbps Full Duplex": + { produto: "Link de Internet Dedicado", qtd: 1, descricao: "1 Gbps Full Duplex" }, -// -------------------------------------- -// Exportação -// -------------------------------------- + "Link de Internet Dedicado 2 Gbps Full Duplex": + { produto: "Link de Internet Dedicado", qtd: 1, descricao: "2 Gbps Full Duplex" }, -module.exports = { - fetchNew, - saveHubGlpi, - sendToGlpi + // Default genérico caso venha coisa nova }; + +function resolveService(name) { + return serviceDictionary[name] || { produto: name, qtd: 1, descricao: null } +} + +function nl2br(text) { + if (!text) return '' + return text.replace(/\r\n|\n|\r/g, '
') +} + +module.exports = { toGlpiPayload } \ No newline at end of file diff --git a/src/modules/tickets/models/glpi/mundiale.model.js b/src/modules/tickets/models/glpi/mundiale.model.js new file mode 100644 index 0000000..1c73b70 --- /dev/null +++ b/src/modules/tickets/models/glpi/mundiale.model.js @@ -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 ` + + + + + + + + + + + + + + + + + + + + + + + + + +
CampoValor
Nome${ticket.cliente_nome}
Código${ticket.codigo_cliente}
Serviço${ticket.servico_nome}
Ticket Mundiale${ticket.ticket_mundiale}
Protocolo Hub${ticket.protocolo_hub || 'N/A'}
+` +} + +function buildTitle(ticket){ + return `Mundiale - Protocolo: ${ticket.ticket_mundiale} - ${ticket.cliente_nome} ` +} + +module.exports = { + toGlpiPayload +} diff --git a/src/modules/tickets/models/hubglpi/model.js b/src/modules/tickets/models/hubglpi/model.js new file mode 100644 index 0000000..b64762e --- /dev/null +++ b/src/modules/tickets/models/hubglpi/model.js @@ -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 +} diff --git a/src/modules/tickets/repositories/ticket.repository.js b/src/modules/tickets/repositories/ticket.repository.js new file mode 100644 index 0000000..a6b90c1 --- /dev/null +++ b/src/modules/tickets/repositories/ticket.repository.js @@ -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, +} diff --git a/src/modules/tickets/services/cancelamento.service.js b/src/modules/tickets/services/cancelamento.service.js new file mode 100644 index 0000000..9237aa0 --- /dev/null +++ b/src/modules/tickets/services/cancelamento.service.js @@ -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. + */ \ No newline at end of file diff --git a/src/modules/tickets/services/createTickets.service.js b/src/modules/tickets/services/createTickets.service.js new file mode 100644 index 0000000..104ab57 --- /dev/null +++ b/src/modules/tickets/services/createTickets.service.js @@ -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 +} \ No newline at end of file diff --git a/src/modules/tickets/services/implantacao.service.js b/src/modules/tickets/services/implantacao.service.js new file mode 100644 index 0000000..15b83d7 --- /dev/null +++ b/src/modules/tickets/services/implantacao.service.js @@ -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 +} diff --git a/src/modules/tickets/services/mundiale.service.js b/src/modules/tickets/services/mundiale.service.js new file mode 100644 index 0000000..aab13a4 --- /dev/null +++ b/src/modules/tickets/services/mundiale.service.js @@ -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. + */ \ No newline at end of file diff --git a/src/modules/tickets/services/resolveTicketEntity.service.js b/src/modules/tickets/services/resolveTicketEntity.service.js new file mode 100644 index 0000000..3e8d730 --- /dev/null +++ b/src/modules/tickets/services/resolveTicketEntity.service.js @@ -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 } diff --git a/src/modules/tickets/services/ticketNotifications.service.js b/src/modules/tickets/services/ticketNotifications.service.js new file mode 100644 index 0000000..d695585 --- /dev/null +++ b/src/modules/tickets/services/ticketNotifications.service.js @@ -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 +} \ No newline at end of file diff --git a/src/modules/tickets/useCases/syncTickets.usecase.js b/src/modules/tickets/useCases/syncTickets.usecase.js new file mode 100644 index 0000000..3861520 --- /dev/null +++ b/src/modules/tickets/useCases/syncTickets.usecase.js @@ -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 } diff --git a/src/routes.js b/src/routes.js deleted file mode 100644 index b5697e7..0000000 --- a/src/routes.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/src/services/commentService.js b/src/services/commentService.js deleted file mode 100644 index e45c0e2..0000000 --- a/src/services/commentService.js +++ /dev/null @@ -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 }; diff --git a/src/services/hubsoftService.js b/src/services/hubsoftService.js deleted file mode 100644 index 4f63379..0000000 --- a/src/services/hubsoftService.js +++ /dev/null @@ -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} 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 -}; diff --git a/src/services/ticketService.js b/src/services/ticketService.js deleted file mode 100644 index fc1c274..0000000 --- a/src/services/ticketService.js +++ /dev/null @@ -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} 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} 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. - */ diff --git a/src/shared/infra/api/hubsoft.auth.js b/src/shared/infra/api/hubsoft.auth.js deleted file mode 100644 index 7fe1512..0000000 --- a/src/shared/infra/api/hubsoft.auth.js +++ /dev/null @@ -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; - } -}; diff --git a/src/shared/infra/api/hubsoft.config.js b/src/shared/infra/api/hubsoft.config.js deleted file mode 100644 index d1581a6..0000000 --- a/src/shared/infra/api/hubsoft.config.js +++ /dev/null @@ -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/` - } -}; \ No newline at end of file diff --git a/src/shared/model/hubsoft.model.js b/src/shared/model/hubsoft.model.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/shared/repositories/hubsoftAPI.repository.js b/src/shared/repositories/hubsoftAPI.repository.js deleted file mode 100644 index 1b61c92..0000000 --- a/src/shared/repositories/hubsoftAPI.repository.js +++ /dev/null @@ -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 -}; diff --git a/src/shared/repositories/hubsoftDB.repository.js b/src/shared/repositories/hubsoftDB.repository.js deleted file mode 100644 index 524c967..0000000 --- a/src/shared/repositories/hubsoftDB.repository.js +++ /dev/null @@ -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 - -}; \ No newline at end of file diff --git a/src/scripts/data/database.sql b/src/shared/scripts/data/database.sql similarity index 100% rename from src/scripts/data/database.sql rename to src/shared/scripts/data/database.sql diff --git a/src/scripts/data/database2.sql b/src/shared/scripts/data/database2.sql similarity index 100% rename from src/scripts/data/database2.sql rename to src/shared/scripts/data/database2.sql diff --git a/src/utils/commentSanitizer.js b/src/shared/utils/commentSanitizer.js similarity index 100% rename from src/utils/commentSanitizer.js rename to src/shared/utils/commentSanitizer.js diff --git a/src/utils/logger.js b/src/shared/utils/logger.js similarity index 98% rename from src/utils/logger.js rename to src/shared/utils/logger.js index 4233290..f33debc 100644 --- a/src/utils/logger.js +++ b/src/shared/utils/logger.js @@ -1,3 +1,5 @@ +// src/shared/utils/logger.js + const winston = require('winston'); const path = require('path'); require('winston-daily-rotate-file'); From 0b66f26f60ce499e7eb140c6181c5b80448b9658 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 6 Jan 2026 08:37:47 -0300 Subject: [PATCH 3/4] FEATURE: Adicionado /health para verificar saude da API e rotas --- src/infra/http/app.js | 47 ++++++++++++++----- src/infra/http/routes/health.routes.js | 20 ++++++++ src/infra/http/routes/index.js | 27 +++++++---- src/infra/http/routes/routeRegistry.js | 16 +++++++ .../close/controller/close.controller.js | 2 + 5 files changed, 89 insertions(+), 23 deletions(-) create mode 100644 src/infra/http/routes/health.routes.js create mode 100644 src/infra/http/routes/routeRegistry.js diff --git a/src/infra/http/app.js b/src/infra/http/app.js index 2ce9df5..34e187b 100644 --- a/src/infra/http/app.js +++ b/src/infra/http/app.js @@ -1,23 +1,44 @@ // src/infra/http/app.js const express = require('express'); -const router = require('./routes') +const routes = require('./routes'); function createApp() { - const app = express(); + const app = express(); - app.use('/api', router); // O router agora tem seu próprio middleware de JSON. + app.use(express.json()); + app.use(express.text({ type: '*/*' })); + - return app; + app.use((req, res, next) => { + let data = ''; + + req.on('data', chunk => { + data += chunk; + }); + + req.on('end', () => { + if (data && !req.body) { + req.rawBody = data; + + // tenta JSON + try { + req.body = JSON.parse(data); + } catch { + req.body = data; + } + } + + next(); + }); +}); + + + + + app.use('/api', routes); + + return app; } module.exports = createApp; - -/** - * @module app - * @description Este módulo é responsável por criar e configurar a instância do aplicativo Express. - * - * Funções: - * - `createApp()`: Uma factory function que inicializa o Express, aplica middlewares essenciais (como o `express.json` para parsear o corpo das requisições) e anexa as rotas da aplicação. - * Isso desacopla a criação do app da sua execução, facilitando testes. - */ \ No newline at end of file diff --git a/src/infra/http/routes/health.routes.js b/src/infra/http/routes/health.routes.js new file mode 100644 index 0000000..26183fe --- /dev/null +++ b/src/infra/http/routes/health.routes.js @@ -0,0 +1,20 @@ +// src/infra/http/routes/health.routes.js + +const { Router } = require('express'); +const { getRoutes } = require('./routeRegistry'); + +module.exports = () => { + const router = Router(); + + router.get('/health', (req, res) => { + res.json({ + status: 'ok', + env: process.env.NODE_ENV, + uptime: process.uptime(), + timestamp: new Date().toISOString(), + routes: getRoutes() + }); + }); + + return router; +}; diff --git a/src/infra/http/routes/index.js b/src/infra/http/routes/index.js index 2f96423..9621c57 100644 --- a/src/infra/http/routes/index.js +++ b/src/infra/http/routes/index.js @@ -1,16 +1,23 @@ +// src/infra/http/routes/index.js + 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 { registerRoute } = require('./routeRegistry'); + +const closeController = require('../../../modules/close/controller/close.controller'); +const commentController = require('../../../modules/comments/controller/glpiComment.controller'); +const healthRoutes = require('./health.routes'); const router = Router(); -router.use(express.json({ type: '*/*' })); -router.post('/webhook/close-ticket', closureController.closeTicket); +// health +router.use(healthRoutes()); +registerRoute('GET', '/api/health'); + +// webhooks +router.post('/webhook/close-ticket', closeController.closeTicket); +registerRoute('POST', '/api/webhook/close-ticket'); + router.post('/webhook/new-comment', commentController.handleGlpiComment); +registerRoute('POST', '/api/webhook/new-comment'); - - - - -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/src/infra/http/routes/routeRegistry.js b/src/infra/http/routes/routeRegistry.js new file mode 100644 index 0000000..1b119fb --- /dev/null +++ b/src/infra/http/routes/routeRegistry.js @@ -0,0 +1,16 @@ +// src/infra/http/routes/routeRegistry.js + +const routes = []; + +function registerRoute(method, path) { + routes.push({ method, path }); +} + +function getRoutes() { + return routes; +} + +module.exports = { + registerRoute, + getRoutes +}; diff --git a/src/modules/close/controller/close.controller.js b/src/modules/close/controller/close.controller.js index 0929884..53f2ccd 100644 --- a/src/modules/close/controller/close.controller.js +++ b/src/modules/close/controller/close.controller.js @@ -9,6 +9,8 @@ const { logInfo, logError } = require('../../../shared/utils/logger'); * @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) From f64dafa2e1bcb37a6059098da89243af68d48fbd Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 6 Jan 2026 11:20:57 -0300 Subject: [PATCH 4/4] =?UTF-8?q?FIX:=20Bug=20de=20duplica=C3=A7=C3=A3o=20de?= =?UTF-8?q?=20mensganes=20no=20HubSoft=20consertado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/infra/api/hubsoft.client.js | 35 ++++++++++++++++--- .../useCases/syncGlpiCommentToHub.usecase.js | 11 ++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/infra/api/hubsoft.client.js b/src/infra/api/hubsoft.client.js index d18bae3..1552c4c 100644 --- a/src/infra/api/hubsoft.client.js +++ b/src/infra/api/hubsoft.client.js @@ -14,9 +14,6 @@ async function sendHubsoftMessage(atendimentoId, mensagem) { 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}`) @@ -38,6 +35,36 @@ async function sendHubsoftMessage(atendimentoId, mensagem) { } } +/** + * Encerra um atendimento no HubSoft + */ +async function close(atendimentoId, mensagem) { +try { + const token = await getAuthToken(); + + const response = await axios.put(`${hubsoft.atendimentosUrl}${atendimentoId}`, { + "fechar_atendimento": true, + "parametros_fechamento": { + "descricao_fechamento": mensagem + } + }, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + return response.data; + } catch (error) { + logError(`Erro ao fechar atendimento ID ${atendimentoId}:`, error.response ? error.response.data : error.message); + throw error; + } +}; + + + + module.exports = { - sendHubsoftMessage + sendHubsoftMessage, + close } diff --git a/src/modules/comments/useCases/syncGlpiCommentToHub.usecase.js b/src/modules/comments/useCases/syncGlpiCommentToHub.usecase.js index 2aa4861..a96160b 100644 --- a/src/modules/comments/useCases/syncGlpiCommentToHub.usecase.js +++ b/src/modules/comments/useCases/syncGlpiCommentToHub.usecase.js @@ -2,7 +2,7 @@ const repository = require('../repositories/comment.repository') const { sanitizeGLPIComment } = require('../../../shared/utils/commentSanitizer') -const { logInfo, logError, logWarn } = require('../../../shared/utils/logger') +const { logInfo, logError, logWarning } = require('../../../shared/utils/logger') async function sync({ glpiTicketId, glpiMessageId, rawContent }) { logInfo(`[COMMENTS][GLPI->HUB] Ticket ${glpiTicketId}`) @@ -10,7 +10,7 @@ async function sync({ glpiTicketId, glpiMessageId, rawContent }) { try { const syncRecord = await repository.getSyncByGlpiId(glpiTicketId) if (!syncRecord?.hubsoft_ticket_id) { - logWarn('[COMMENTS][GLPI->HUB] Ticket sem vínculo com Hubsoft') + logWarning('[COMMENTS][GLPI->HUB] Ticket sem vínculo com Hubsoft') return } @@ -31,13 +31,18 @@ async function sync({ glpiTicketId, glpiMessageId, rawContent }) { content ) + + + await repository.insertSyncComment({ - source: 'glpi', + syncDataId: syncRecord.id, + sourceSystem: 'glpi', sourceCommentId: glpiMessageId, destinationCommentId: hubsoftMessageId, hubsoftTicketId: syncRecord.hubsoft_ticket_id, glpiTicketId, content, + author: 'glpi-user', status: 'synced' })