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 = `
-
-
- | Campo |
- Valor |
-
-
- | 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