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