FEAT/DOC: Feature finalizada - Branch Finalizada
- A aplicação escuta webhooks do GLPI para eventos de fechamento de tickets. - Ao receber um webhook para um ticket "Mundiale", o serviço fecha o atendimento correspondente na API do HubSoft e atualiza o status no banco de dados local. - Regra de negócio: Caso o status de sincronia seja (`status_sync = 'processing_close'`). O middleware irá se resguarda para condições de corrida causadas por webhooks duplicados do GLPI, garantindo que um ticket seja processado para fechamento apenas uma vez. - Documentação do projeto adicionado ao Readme.md
This commit is contained in:
parent
f8095c80e5
commit
6acd5369da
@ -39,9 +39,9 @@ HUBGLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
|
||||
# BANCO DE DADOS FINAL - GLPI (MySQL - Desenvolvimento)
|
||||
# ==============================================================================
|
||||
GLPI_DB_TYPE=mysql
|
||||
GLPI_DB_HOST=177.73.177.32
|
||||
GLPI_DB_HOST=177.73.177.44
|
||||
GLPI_DB_PORT=3306
|
||||
GLPI_DB_USER=snglpi
|
||||
GLPI_DB_PASSWORD=j2633669
|
||||
GLPI_DB_USER=desenvolvimento
|
||||
GLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
|
||||
GLPI_DB_NAME=glpi_data
|
||||
GLPI_DB_CHARSET=utf8mb4
|
||||
|
||||
162
README.md
Normal file
162
README.md
Normal file
@ -0,0 +1,162 @@
|
||||
# Serviço de Integração HubSoft <> GLPI
|
||||
|
||||
   
|
||||
|
||||
Este serviço realiza a integração e sincronização de tickets entre as plataformas **HubSoft** e **GLPI**, garantindo que as informações fluam de maneira consistente e automatizada entre os dois sistemas.
|
||||
|
||||
## ✨ Funcionalidades Principais
|
||||
|
||||
- **Sincronização Bidirecional Parcial**:
|
||||
- **Criação**: Tickets abertos no HubSoft são automaticamente criados no GLPI.
|
||||
- **Fechamento**: Tickets "Mundiale" fechados no GLPI disparam o fechamento do atendimento correspondente no HubSoft.
|
||||
- **Processamento Assíncrono**: Utiliza um cron job para processar a criação de tickets em segundo plano, sem impactar a performance da API.
|
||||
- **Banco de Dados Intermediário**: Usa um banco de dados PostgreSQL para gerenciar o estado da sincronização, garantindo resiliência e rastreabilidade.
|
||||
- **Mecanismo de Trava (Locking)**: Previne condições de corrida e processamento duplicado de webhooks.
|
||||
- **Configuração Flexível**: Todas as chaves de API e conexões de banco de dados são gerenciadas por variáveis de ambiente.
|
||||
- **Logging Detalhado**: Registra todas as operações e erros em arquivos de log para fácil depuração.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Arquitetura e Fluxo de Dados
|
||||
|
||||
A aplicação opera com dois fluxos principais:
|
||||
|
||||
### 1. Fluxo de Criação de Tickets (Cron Job)
|
||||
|
||||
Este fluxo roda a cada 5 minutos para sincronizar novos atendimentos do HubSoft para o GLPI.
|
||||
|
||||
```
|
||||
┌──────────────────┐ ┌───────────────────┐ ┌──────────────────┐ ┌──────────┐
|
||||
│ DB do HubSoft ├─────►│ Serviço (Cron) ├─────►│ DB Intermediário ├─────►│ API GLPI │
|
||||
└──────────────────┘ └───────────────────┘ └──────────────────┘ └──────────┘
|
||||
(Lê) (Processa dados) (Salva & Trava) (Cria Ticket)
|
||||
```
|
||||
|
||||
1. O **Cron Job** é disparado.
|
||||
2. O serviço consulta o banco de dados do **HubSoft** em busca de novos atendimentos.
|
||||
3. Os dados são processados e salvos no **Banco de Dados Intermediário** com o status `pending_create`.
|
||||
4. O serviço lê os tickets pendentes, formata os dados e os envia para a **API do GLPI** para criar o ticket.
|
||||
5. Após a criação, o status no banco intermediário é atualizado para `created_glpi`.
|
||||
|
||||
### 2. Fluxo de Fechamento de Tickets (Webhook)
|
||||
|
||||
Este fluxo é iniciado por um evento de fechamento de ticket no GLPI.
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────────┐ ┌────────────────┐ ┌──────────────────┐
|
||||
│ GLPI Webhook ├─────►│ Serviço (API) ├─────►│ API do HubSoft ├─────►│ DB Intermediário│
|
||||
└──────────────┘ └──────────────────┘ └────────────────┘ └──────────────────┘
|
||||
(Dispara) (Valida e Trava) (Fecha Atendimento) (Atualiza Status)
|
||||
```
|
||||
|
||||
1. O **GLPI** dispara um webhook quando um ticket é fechado.
|
||||
2. A API do serviço recebe a requisição e verifica se o ticket é elegível (contém "Mundiale" no título).
|
||||
3. O serviço obtém uma **trava** no banco de dados intermediário, mudando o status para `processing_close` para evitar processamento duplicado.
|
||||
4. Uma chamada é feita para a **API do HubSoft** para fechar o atendimento correspondente.
|
||||
5. Após o sucesso, o status no banco intermediário é atualizado para `closed_glpi`.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Instalação e Execução
|
||||
|
||||
### Pré-requisitos
|
||||
|
||||
- Node.js (versão 18.x ou superior)
|
||||
- NPM
|
||||
- Acesso a dois bancos de dados PostgreSQL (um para o HubSoft e outro para a aplicação).
|
||||
|
||||
### 1. Clone o Repositório
|
||||
|
||||
```bash
|
||||
git clone <url-do-seu-repositorio>
|
||||
cd <nome-do-repositorio>
|
||||
```
|
||||
|
||||
### 2. Instale as Dependências
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Configure as Variáveis de Ambiente
|
||||
|
||||
Crie um arquivo `.env` na raiz do projeto, copiando o exemplo de `.env.example`.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Preencha o arquivo `.env` com as credenciais corretas:
|
||||
|
||||
```ini
|
||||
# Configurações do Servidor
|
||||
PORT=3000
|
||||
|
||||
# Banco de Dados da Aplicação (HubGLPI)
|
||||
HUBGLPI_DB_HOST=localhost
|
||||
HUBGLPI_DB_PORT=5432
|
||||
HUBGLPI_DB_USER=postgres
|
||||
HUBGLPI_DB_PASSWORD=sua_senha
|
||||
HUBGLPI_DB_NAME=hubglpi
|
||||
|
||||
# Banco de Dados do HubSoft (Acesso de Leitura)
|
||||
HUBSOFT_DB_HOST=ip_do_banco_hubsoft
|
||||
HUBSOFT_DB_PORT=5432
|
||||
HUBSOFT_DB_USER=usuario_leitura
|
||||
HUBSOFT_DB_PASSWORD=senha_leitura
|
||||
HUBSOFT_DB_NAME=hubsoft
|
||||
|
||||
# API do GLPI
|
||||
GLPI_API_URL=https://seu-glpi.com/apirest.php
|
||||
GLPI_APP_TOKEN=seu_app_token
|
||||
GLPI_USER_TOKEN=seu_user_token
|
||||
|
||||
# API do HubSoft
|
||||
HUBSOFT_API_URL=https://seu-hubsoft.com/api/v1
|
||||
HUBSOFT_API_TOKEN=seu_token_hubsoft
|
||||
```
|
||||
|
||||
### 4. Inicie a Aplicação
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
O servidor será iniciado, e o cron job começará a ser executado em segundo plano.
|
||||
|
||||
---
|
||||
|
||||
## 📂 Estrutura do Projeto
|
||||
|
||||
```
|
||||
src/
|
||||
├── app.js # Configuração da instância do Express.
|
||||
├── server.js # Ponto de entrada: inicia o servidor e o cron job.
|
||||
├── routes.js # Definição das rotas da API.
|
||||
│
|
||||
├── config/ # Arquivos de configuração.
|
||||
│ ├── apiConfig.js # Configuração das APIs externas.
|
||||
│ ├── dbConfig.js # Configuração dos bancos de dados.
|
||||
│ └── envLoader.js # Carregador de variáveis de ambiente.
|
||||
│
|
||||
├── controller/ # Camada que lida com requisições HTTP.
|
||||
│ ├── ClosureController.js # Controller para o webhook de fechamento.
|
||||
│ └── processController.js # Controller para o fluxo de criação via cron.
|
||||
│
|
||||
├── data/ # Configuração da conexão com os bancos.
|
||||
│ ├── hubglpiDataBase.js # Pool de conexão para o DB da aplicação.
|
||||
│ └── hubsoftDataBase.js # Pool de conexão para o DB do HubSoft.
|
||||
│
|
||||
├── model/ # Camada de acesso a dados.
|
||||
│ ├── glpiModel.js # Funções para interagir com a API do GLPI.
|
||||
│ ├── hubglpiModel.js # Funções para interagir com o DB da aplicação.
|
||||
│ └── hubsoftModel.js # Funções para interagir com o DB do HubSoft.
|
||||
│
|
||||
├── services/ # Camada de lógica de negócio.
|
||||
│ ├── hubsoftService.js # Lógica para interagir com a API do HubSoft.
|
||||
│ └── ticketService.js # Lógica de negócio para o fechamento de tickets.
|
||||
│
|
||||
└── utils/ # Funções utilitárias.
|
||||
├── commentSanitizer.js# Limpeza de texto e HTML.
|
||||
└── logger.js # Configuração do sistema de logs (Winston).
|
||||
```
|
||||
3637
logs/app.log
Normal file
3637
logs/app.log
Normal file
File diff suppressed because it is too large
Load Diff
113
logs/error.log
Normal file
113
logs/error.log
Normal file
@ -0,0 +1,113 @@
|
||||
{"level":"error","message":"AggregateError - Erro ao obter ID de sync_data","timestamp":"2025-11-06 16:13:35"}
|
||||
{"level":"error","message":"AggregateError - Erro ao obter ID de sync_data","timestamp":"2025-11-06 16:14:44"}
|
||||
{"level":"error","message":"AggregateError - Erro ao obter ID de sync_data","timestamp":"2025-11-06 16:15:03"}
|
||||
{"level":"error","message":"AggregateError - Erro ao obter ID de sync_data","timestamp":"2025-11-06 16:15:40"}
|
||||
{"level":"error","message":"AggregateError - Erro ao obter ID de sync_data","timestamp":"2025-11-06 16:15:41"}
|
||||
{"level":"error","message":" - Erro ao processar atendimentos: AggregateError","timestamp":"2025-11-06 16:17:18"}
|
||||
{"level":"error","message":" - Erro ao processar atendimentos: AggregateError","timestamp":"2025-11-06 16:17:54"}
|
||||
{"level":"error","message":" - Erro ao processar atendimentos: AggregateError","timestamp":"2025-11-06 16:18:42"}
|
||||
{"level":"error","message":"AggregateError - [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.","timestamp":"2025-11-06 16:34:35"}
|
||||
{"level":"error","message":"AggregateError - [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.","timestamp":"2025-11-06 16:34:57"}
|
||||
{"level":"error","message":"AggregateError - [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.","timestamp":"2025-11-06 16:36:29"}
|
||||
{"level":"error","message":"AggregateError - [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.","timestamp":"2025-11-06 16:43:44"}
|
||||
{"level":"error","message":"AggregateError - [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.","timestamp":"2025-11-06 16:52:42"}
|
||||
{"level":"error","message":"AggregateError - [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.","timestamp":"2025-11-06 16:56:57"}
|
||||
{"level":"error","message":"AggregateError - Erro ao buscar atendimentos no banco de dados HubSoft.","timestamp":"2025-11-06 17:00:17"}
|
||||
{"level":"error","message":"AggregateError - Erro ao buscar atendimentos no banco de dados HubSoft.","timestamp":"2025-11-06 17:00:59"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:49"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:50"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:50"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:50"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:50"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:50"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:50"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:50"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:50"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:50"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:51"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:51"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:51"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:51"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:51"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:05:51"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:10"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:11"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:12"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:12"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:12"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:12"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:06:12"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:11:17"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de cliente: Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'ORDER BY ASC' at line 1","timestamp":"2025-11-06 17:11:57"}
|
||||
{"level":"error","message":" - Erro ao buscar entidade por código de serviço: Error: Access denied for user 'snglpi'@'177.73.177.3' (using password: YES)","timestamp":"2025-11-06 17:25:25"}
|
||||
{"level":"error","message":"AggregateError - Erro ao obter ID de sync_data","timestamp":"2025-11-07 15:10:48"}
|
||||
{"level":"error","message":"AggregateError - Erro ao obter ID de sync_data","timestamp":"2025-11-07 15:17:18"}
|
||||
{"level":"error","message":"AggregateError - Erro ao obter ID de sync_data","timestamp":"2025-11-07 15:17:18"}
|
||||
{"level":"error","message":"AggregateError - Erro ao obter ID de sync_data","timestamp":"2025-11-07 15:17:18"}
|
||||
{"level":"error","message":"AggregateError - Erro ao obter ID de sync_data","timestamp":"2025-11-07 15:20:57"}
|
||||
{"level":"error","message":"Error: connect ETIMEDOUT 177.73.177.69:9432 - Erro ao buscar atendimentos no banco de dados HubSoft.","timestamp":"2025-11-07 15:33:48"}
|
||||
{"level":"error","message":"Error: connect ETIMEDOUT 177.73.177.69:9432 - [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.","timestamp":"2025-11-07 15:33:48"}
|
||||
{"level":"error","message":" - Falha ao atualizar status de sync_data para o glpi ticket ID 34335.","timestamp":"2025-11-10 11:44:23"}
|
||||
{"level":"error","message":" - Falha ao fechar atendimento no HubSoft para o ticket ID 34335. Resposta: {\"status\":\"error\",\"msg\":\"Atendimento não identificado.\"}","timestamp":"2025-11-10 11:55:09"}
|
||||
{"level":"error","message":" - Erro ao obter ID de sync_data, error: syntax error at or near \"FROM\"","timestamp":"2025-11-10 12:15:35"}
|
||||
{"level":"error","message":" - Erro ao fechar atendimento no HubSoft para o ticket ID 34335: ReferenceError: updateFechaAtendimento is not defined","timestamp":"2025-11-10 12:23:41"}
|
||||
{"level":"error","message":"Error: Client network socket disconnected before secure TLS connection was established - Erro ao obter token de autenticação:","timestamp":"2025-11-10 15:08:34"}
|
||||
{"level":"error","message":"Client network socket disconnected before secure TLS connection was established - Erro ao fechar atendimento ID 2233:","timestamp":"2025-11-10 15:08:34"}
|
||||
{"level":"error","message":" - Erro ao fechar atendimento no HubSoft para o ticket ID 34336: Error: Client network socket disconnected before secure TLS connection was established","timestamp":"2025-11-10 15:08:34"}
|
||||
{"level":"error","message":"Error: Client network socket disconnected before secure TLS connection was established - Erro ao obter token de autenticação:","timestamp":"2025-11-10 15:08:34"}
|
||||
{"level":"error","message":"Client network socket disconnected before secure TLS connection was established - Erro ao fechar atendimento ID 2233:","timestamp":"2025-11-10 15:08:34"}
|
||||
{"level":"error","message":" - Erro ao fechar atendimento no HubSoft para o ticket ID 34336: Error: Client network socket disconnected before secure TLS connection was established","timestamp":"2025-11-10 15:08:34"}
|
||||
{"level":"error","message":" - Erro ao obter ID de sync_data, Error: Connection terminated unexpectedly","timestamp":"2025-11-10 15:09:25"}
|
||||
{"level":"error","message":" - Erro ao obter ID de sync_data, Error: Connection terminated unexpectedly","timestamp":"2025-11-10 15:09:49"}
|
||||
{"level":"error","message":" - Requisição para fechar ticket recebida com corpo inválido ou ausente.","timestamp":"2025-11-10 15:16:36"}
|
||||
{"level":"error","message":" - Erro no processo de fechamento do ticket GLPI ID 34336: Falha ao fechar atendimento no HubSoft: {\"status\":\"error\",\"msg\":\"Atendimento já finalizado\"}","timestamp":"2025-11-10 15:36:43"}
|
||||
{"level":"error","message":"Error: Client network socket disconnected before secure TLS connection was established - Erro ao obter token de autenticação:","timestamp":"2025-11-10 15:56:33"}
|
||||
{"level":"error","message":"Client network socket disconnected before secure TLS connection was established - Erro ao fechar atendimento ID 2239:","timestamp":"2025-11-10 15:56:33"}
|
||||
{"level":"error","message":" - Erro no processo de fechamento do ticket GLPI ID 34342: Client network socket disconnected before secure TLS connection was established","timestamp":"2025-11-10 15:56:33"}
|
||||
{"level":"error","message":" - Erro ao obter ID de sync_data, Error: Connection terminated unexpectedly","timestamp":"2025-11-10 15:58:23"}
|
||||
140
node_modules/.package-lock.json
generated
vendored
140
node_modules/.package-lock.json
generated
vendored
@ -59,6 +59,12 @@
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
@ -68,6 +74,17 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
@ -184,6 +201,18 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||
@ -240,6 +269,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@ -335,6 +373,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@ -421,6 +474,63 @@
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -518,6 +628,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@ -761,6 +886,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cron": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
|
||||
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@ -963,6 +1097,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
|
||||
142
package-lock.json
generated
142
package-lock.json
generated
@ -9,9 +9,11 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"mysql2": "^3.15.2",
|
||||
"node-cron": "^4.2.1",
|
||||
"pg": "^8.16.3",
|
||||
"qs": "^6.14.0",
|
||||
"winston": "^3.18.3"
|
||||
@ -72,6 +74,12 @@
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
@ -81,6 +89,17 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
@ -197,6 +216,18 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||
@ -253,6 +284,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@ -348,6 +388,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@ -434,6 +489,63 @@
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -531,6 +643,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@ -774,6 +901,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cron": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
|
||||
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@ -976,6 +1112,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
|
||||
@ -9,9 +9,11 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"mysql2": "^3.15.2",
|
||||
"node-cron": "^4.2.1",
|
||||
"pg": "^8.16.3",
|
||||
"qs": "^6.14.0",
|
||||
"winston": "^3.18.3"
|
||||
|
||||
@ -11,3 +11,12 @@ function createApp() {
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
@ -6,8 +6,6 @@ function loadEnv() {
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
|
||||
const envPath = path.resolve(process.cwd(), `.env.${nodeEnv}`);
|
||||
|
||||
console.log(`Carregando variáveis de ambiente de: ${envPath}`);
|
||||
dotenv.config({ path: envPath });
|
||||
}
|
||||
|
||||
|
||||
48
src/controller/ClosureController.js
Normal file
48
src/controller/ClosureController.js
Normal file
@ -0,0 +1,48 @@
|
||||
const ticketService = 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) => {
|
||||
try {
|
||||
let rawData = '';
|
||||
const bodyRequest = req.body;
|
||||
|
||||
req.on('data', chunk => {
|
||||
rawData += chunk;
|
||||
});
|
||||
|
||||
req.on('end', async () => {
|
||||
let bodyRequest;
|
||||
try {
|
||||
bodyRequest = JSON.parse(rawData);
|
||||
} catch (err) {
|
||||
logError('Erro ao parsear JSON:', err);
|
||||
bodyRequest = {};
|
||||
}
|
||||
|
||||
const ticketId = bodyRequest.item.items_id;
|
||||
logInfo(`Ticket ${ticketId} acionado para encerramento.`);
|
||||
const closingTicket = await ticketService.fechaTicket(bodyRequest);
|
||||
res.status(200).json(closingTicket);
|
||||
});
|
||||
|
||||
} catch (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.
|
||||
*/
|
||||
@ -249,34 +249,42 @@ const processAtendimento = async (ticketData) => {
|
||||
// ================================================================================
|
||||
|
||||
const processaAtendimentos = async (skipHubSoft = false) => {
|
||||
try {
|
||||
let atendimentosDB = [];
|
||||
|
||||
if (!skipHubSoft) {
|
||||
// Esta parte depende da VPN
|
||||
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 {
|
||||
// Processa o ticket do HubSoft
|
||||
const ticketData = await processTicketFromHubSoft(atendimento);
|
||||
// Salva no banco intermediário (hubglpi)
|
||||
await saveTicketToHubGlpi(ticketData);
|
||||
}
|
||||
catch (error) {
|
||||
} 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)');
|
||||
}
|
||||
|
||||
// Coletando atendimentos do banco intermediário (hubglpi)
|
||||
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 {
|
||||
@ -288,11 +296,8 @@ const processaAtendimentos = async (skipHubSoft = false) => {
|
||||
logError(`Erro ao processar atendimento ${atendimento.id_atendimento}:, ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError(`Erro ao processar atendimentos: ${error}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processaAtendimentos,
|
||||
@ -300,3 +305,15 @@ module.exports = {
|
||||
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.
|
||||
*/
|
||||
@ -1,31 +0,0 @@
|
||||
const ticketService = require('../services/ticketService.js');
|
||||
|
||||
const closeTicket = async (req, res) => {
|
||||
try {
|
||||
let rawData = '';
|
||||
|
||||
req.on('data', chunk => {
|
||||
rawData += chunk;
|
||||
});
|
||||
|
||||
req.on('end', async () => {
|
||||
let bodyRequest;
|
||||
try {
|
||||
bodyRequest = JSON.parse(rawData);
|
||||
} catch (err) {
|
||||
console.error('Erro ao parsear JSON:', err);
|
||||
bodyRequest = {};
|
||||
}
|
||||
|
||||
console.log('Fechando ticket com os dados:', bodyRequest);
|
||||
const closingTicket = await ticketService.fechaTicket(bodyRequest);
|
||||
res.status(200).json(closingTicket);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = { closeTicket };
|
||||
@ -1,16 +1,11 @@
|
||||
const hubsoftController = require('./controller/processController.js');
|
||||
const { logError, logInfo } = require('./utils/logger.js');
|
||||
const loadEnv = require('./config/envLoader');
|
||||
|
||||
// Carrega as variáveis de ambiente
|
||||
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'
|
||||
});
|
||||
|
||||
// Inicia o processamento dos atendimentos
|
||||
hubsoftController.processaAtendimentos();
|
||||
@ -66,7 +66,7 @@ class GlpiModel {
|
||||
|
||||
static async selectEntityIdCodCliente(id) {
|
||||
|
||||
const query = `SELECT id FROM glpi_entities WHERE name LIKE ? LIMIT 1 ORDER BY ASC;`;
|
||||
const query = `SELECT id FROM glpi_entities WHERE name LIKE ? LIMIT 1;`;
|
||||
const values = [`${id} -%`];
|
||||
|
||||
try {
|
||||
|
||||
@ -129,25 +129,31 @@ class HubglpiModel {
|
||||
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);
|
||||
logError(`Erro ao obter ID de sync_data, ${err}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static async get_idSyncByGlpiID(glpi_ticket_id) {
|
||||
static async getIdByGlpiID(glpi_ticket_id) {
|
||||
const query = `
|
||||
SELECT id FROM sync_data
|
||||
SELECT
|
||||
id,
|
||||
glpi_ticket_id glpiId,
|
||||
hubsoft_ticket_id hubsoftId
|
||||
FROM sync_data
|
||||
WHERE glpi_ticket_id = $1;
|
||||
`;
|
||||
const values = [glpi_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;
|
||||
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
|
||||
}
|
||||
catch (err) {
|
||||
logError('Erro ao obter ID de sync_data', err);
|
||||
return null; // Retorna null se não encontrar
|
||||
} catch (err) {
|
||||
logError(`Erro ao obter ID de sync_data, ${err}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
@ -185,40 +191,65 @@ class HubglpiModel {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
static async updateClosingTicket(syncId, closeMessage) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN'); // inicia transação
|
||||
|
||||
static async update_syncaDataError(sync_error_message, id_atendimento) {
|
||||
const query = `
|
||||
// 1. Atualiza sync_data para 'closed'
|
||||
const querySync = `
|
||||
UPDATE sync_data
|
||||
set sync_error_message = $1,
|
||||
status_sync = 'sync_error',
|
||||
last_sync_attempt = $2
|
||||
WHERE hubsoft_ticket_id = $3
|
||||
SET status_sync = 'closed',
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
`;
|
||||
const values = [
|
||||
sync_error_message,
|
||||
new Date(),
|
||||
id_atendimento
|
||||
];
|
||||
const resSync = await client.query(querySync, [syncId]);
|
||||
const updatedSyncData = resSync.rows[0];
|
||||
|
||||
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}`);
|
||||
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(id_atendimento, status_sync, source_last) {
|
||||
|
||||
|
||||
|
||||
static async updateSyncDataStatus( ticketId, status_sync, source_last){
|
||||
const query = `
|
||||
UPDATE sync_data
|
||||
SET status_sync = $1,
|
||||
source_last = $2
|
||||
WHERE hubsoft_ticket_id = $3
|
||||
WHERE id = $3
|
||||
RETURNING *;
|
||||
`;
|
||||
const values = [status_sync, source_last, id_atendimento];
|
||||
const values = [status_sync, source_last, ticketId];
|
||||
|
||||
try {
|
||||
const res = await pool.query(query, values);
|
||||
@ -229,5 +260,61 @@ class HubglpiModel {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
@ -1,29 +1,52 @@
|
||||
const dbConfig = require('../config/dbConfig.js');
|
||||
const { logError, logInfo } = require('../utils/logger');
|
||||
|
||||
const { Pool } = require('pg');
|
||||
const pool = new Pool({
|
||||
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.');
|
||||
});
|
||||
|
||||
const getAtendimentosFromDB = async () => {
|
||||
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;';
|
||||
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 validateAtendimentoStatus = (id_atendimento) => {
|
||||
const query = `SELECT id_atendimento_status FROM atendimento WHERE id_atendimento = ${id_atendimento};`;
|
||||
return pool.query(query);
|
||||
}
|
||||
|
||||
const validateMensagensByAtendimento = (id_atendimento) => {
|
||||
const query = `SELECT id_atendimento_mensagem, id_atendimento, mensagem, data_cadastro FROM atendimento_mensagem WHERE id_atendimento = ${id_atendimento} ;`
|
||||
return pool.query(query);
|
||||
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 updateFechaAtendimento = async (id_atendimento, closingMessage) => {
|
||||
@ -31,13 +54,15 @@ const updateFechaAtendimento = async (id_atendimento, closingMessage) => {
|
||||
UPDATE atendimento
|
||||
SET descricao_fechamento = $1,
|
||||
data_fechamento = $2,
|
||||
status_atendimento = $3
|
||||
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
|
||||
|
||||
const values = [closingMessage, new Date(), 'Solucionado', id_atendimento];
|
||||
// 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];
|
||||
@ -48,5 +73,8 @@ const updateFechaAtendimento = async (id_atendimento, closingMessage) => {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAtendimentosFromDB, validateAtendimentoStatus, validateMensagensByAtendimento
|
||||
getAtendimentosFromDB,
|
||||
validateAtendimentoStatus,
|
||||
validateMensagensByAtendimento,
|
||||
updateFechaAtendimento // Exportando a função
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
const { Router } = require('express');
|
||||
const ticketController = require('./controller/ticketController.js');
|
||||
const ticketController = require('./controller/ClosureController.js');
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
||||
@ -1,13 +1,36 @@
|
||||
const dotenv = require('dotenv');
|
||||
const createApp = require('./app.js');
|
||||
const loadEnv = require('./config/envLoader');
|
||||
loadEnv();
|
||||
|
||||
const cron = require('node-cron');
|
||||
const createApp = require('./app.js');
|
||||
const { processaAtendimentos } = require('./controller/processController.js');
|
||||
const { logInfo, logError } = require('./utils/logger.js');
|
||||
|
||||
const app = createApp();
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`✅ Servidor rodando na porta ${PORT}`);
|
||||
logInfo(`🚀 Servidor HTTP iniciado e ouvindo na porta ${PORT}`);
|
||||
|
||||
// Agenda a tarefa para rodar a cada 5 minutos.
|
||||
logInfo('⏰ Agendando cron job para processar atendimentos a cada 5 minutos.');
|
||||
cron.schedule('*/5 * * * *', async () => {
|
||||
logInfo('CRON: Iniciando processamento de atendimentos...');
|
||||
try {
|
||||
await processaAtendimentos(true);
|
||||
logInfo('CRON: Processamento de atendimentos concluído com sucesso.');
|
||||
} catch (error) {
|
||||
logError('CRON: Erro durante o processamento de atendimentos.', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
@ -10,7 +10,6 @@ const getAuthToken = async () => {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
console.log('Token de autenticação obtido com sucesso.');
|
||||
return response.data.access_token;
|
||||
} catch (error) {
|
||||
logError('Erro ao obter token de autenticação:', error);
|
||||
|
||||
@ -1,122 +1,144 @@
|
||||
// HUBXGLPI/src/services/ticketService.js
|
||||
|
||||
const { log } = require("winston");
|
||||
const { get_idSyncByGlpiID } = require("../model/hubglpiModel.js");
|
||||
const { updateSyncDataStatus } = require("../model/syncDataModel.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');
|
||||
|
||||
// {
|
||||
// "item": {
|
||||
// "id": 27779,
|
||||
// "itemtype": "Ticket",
|
||||
// "items_id": 34213,
|
||||
// "content": "<p>Closing Task<\/p>",
|
||||
// "user": {
|
||||
// "id": 917,
|
||||
// "name": "rafael.lopes"
|
||||
// },
|
||||
// "user_editor": null
|
||||
// },
|
||||
// "event": "new",
|
||||
// "parent_item": {
|
||||
// "id": 34213,
|
||||
// "name": "TESTE",
|
||||
// "content": "<p>teste<\/p>",
|
||||
// "is_deleted": false,
|
||||
// "urgency": 3,
|
||||
// "impact": 3,
|
||||
// "priority": 3,
|
||||
// "actiontime": 0,
|
||||
// "date_creation": "2025-11-03T16:26:12-03:00",
|
||||
// "date_mod": "2025-11-05T10:44:03-03:00",
|
||||
// "date": "2025-11-03T16:26:12-03:00",
|
||||
// "type": 1,
|
||||
// "external_id": "",
|
||||
// "status": {
|
||||
// "id": 5,
|
||||
// "name": "Solucionado"
|
||||
// },
|
||||
// "category": {
|
||||
// "id": 5814,
|
||||
// "name": "HubSoft"
|
||||
// },
|
||||
// "location": null,
|
||||
// "request_type": {
|
||||
// "id": 1,
|
||||
// "name": "Helpdesk"
|
||||
// },
|
||||
// "entity": {
|
||||
// "id": 0,
|
||||
// "name": "Contratos Ativos",
|
||||
// "completename": "Contratos Ativos"
|
||||
// },
|
||||
// "team": []
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* 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<object|null>} Um objeto com os IDs { hubsoftId, glpiId, syncId } ou null se não for aplicável.
|
||||
*/
|
||||
|
||||
const updateSyncStatus = async (ticketId, ticketTitle) => {
|
||||
const handleMundialeTicket = async (glpiTicketId, ticketTitle) => {
|
||||
try {
|
||||
//verificar se titulo contem "Mundiale"
|
||||
if (ticketTitle.includes("Mundiale")) {
|
||||
const exists = await get_idSyncByGlpiID(ticketId);
|
||||
if (exists) {
|
||||
log.info(
|
||||
`-> Atualizando banco de dados para o glpi ticket ID ${ticketId} que será fechado.`
|
||||
);
|
||||
const updateResult = await updateSyncDataStatus(
|
||||
ticketId,
|
||||
"pending_close",
|
||||
"glpi"
|
||||
);
|
||||
if (!ticketTitle.includes("Mundiale")) {
|
||||
logInfo(`Ticket ID ${glpiTicketId} não é da Mundiale. Ignorando fechamento.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (updateResult) {
|
||||
log.info(
|
||||
`Status de sync_data atualizado para fechamento do glpi ticket ID ${ticketId}.`
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
log.error(
|
||||
`Falha ao atualizar status de sync_data para o glpi ticket ID ${ticketId}.`
|
||||
);
|
||||
return false;
|
||||
const syncRecord = await getIdByGlpiID(glpiTicketId);
|
||||
if (!syncRecord) {
|
||||
logInfo(`Nenhum registro de sincronização encontrado para o ticket GLPI ID ${glpiTicketId}.`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info(
|
||||
`Ticket ID ${ticketId} não relacionado à Mundiale. Ignorando fechamento.`
|
||||
);
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
const fechaTicket = async (bodyRequest) => {
|
||||
const ticketId = bodyRequest.item.items_id;
|
||||
const ticketTitle = bodyRequest.parent_item.name;
|
||||
const closingMessage = bodyRequest.item.content || "Fechamento automático do ticket.";
|
||||
|
||||
try {
|
||||
const updateStatus = await updateSyncStatus(ticketId, ticketTitle);
|
||||
if (updateStatus) {
|
||||
log.info(`-> Iniciando fechamento do atendimento no HubSoft para o ticket ID ${ticketId}.`);
|
||||
const closeResponse = await closeAtendimento(ticketId, closingMessage);
|
||||
|
||||
if (closeResponse.status === "success") {
|
||||
log.info(`Atendimento no HubSoft fechado com sucesso para o ticket ID ${ticketId}.`);
|
||||
const dbHubsoftClose = await updateFechaAtendimento(ticketId, closingMessage);
|
||||
if (dbHubsoftClose) {
|
||||
log.info(`Banco de dados atualizado com sucesso para o fechamento do atendimento ID ${ticketId}.`);
|
||||
return closeResponse;
|
||||
} else {
|
||||
log.error(`Falha ao atualizar o banco de dados para o fechamento do atendimento ID ${ticketId}.`);
|
||||
return closeResponse;
|
||||
}
|
||||
} else {
|
||||
log.error(`Falha ao fechar atendimento no HubSoft para o ticket ID ${ticketId}. Resposta: ${JSON.stringify(closeResponse)}`);
|
||||
}
|
||||
// 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) {
|
||||
log.error(`Erro ao fechar atendimento no HubSoft para o ticket ID ${ticketId}: ${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<object>} 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.
|
||||
*/
|
||||
|
||||
106
src/utils/commentSanitizer.js
Normal file
106
src/utils/commentSanitizer.js
Normal file
@ -0,0 +1,106 @@
|
||||
const { logInfo, logWarning } = require('./logger');
|
||||
|
||||
const stripHTML = (html) => {
|
||||
if (!html) return '';
|
||||
|
||||
let cleaned = html
|
||||
.replace(/</g, '<') // < para <
|
||||
.replace(/>/g, '>') // > para >
|
||||
.replace(/&/g, '&') // & para &
|
||||
.replace(/ /g, ' ') // para espaço
|
||||
.replace(/</g, '<') // < para <
|
||||
.replace(/>/g, '>') // > para >
|
||||
.replace(/&/g, '&') // & para &
|
||||
.replace(/"/g, '"'); // " para "
|
||||
|
||||
cleaned = cleaned
|
||||
.replace(/<br\s*\/?>/gi, '\n') // <br> para quebra de linha
|
||||
.replace(/<p>/gi, '') // Remove <p>
|
||||
.replace(/<\/p>/gi, '\n\n') // </p> para duas quebras
|
||||
.replace(/<strong>/gi, '**') // <strong> para **
|
||||
.replace(/<\/strong>/gi, '**') // </strong> para **
|
||||
.replace(/<em>/gi, '*') // <em> para *
|
||||
.replace(/<\/em>/gi, '*') // </em> para *
|
||||
.replace(/<blockquote>/gi, '> ') // <blockquote> para citação
|
||||
.replace(/<\/blockquote>/gi, '') // </blockquote>
|
||||
.replace(/<div[^>]*>/gi, '') // Remove <div> com qualquer atributo
|
||||
.replace(/<\/div>/gi, '\n') // </div> para quebra
|
||||
.replace(/<[^>]*>/g, '') // Remove todas outras tags HTML
|
||||
.replace(/\n\s*\n\s*\n/g, '\n\n') // Remove múltiplas quebras
|
||||
.replace(/^\s+|\s+$/g, '') // Remove espaços no início/fim
|
||||
.trim();
|
||||
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
const handleImages = (content) => {
|
||||
const imgRegex = /<img[^>]+src="([^"]+)"[^>]*>/gi;
|
||||
const hasImages = imgRegex.test(content);
|
||||
|
||||
if (hasImages) {
|
||||
logWarning('📷 Imagem detectada no comentário GLPI');
|
||||
return content.replace(/<a[^>]*>.*?<img[^>]*>.*?<\/a>/gi, '[IMAGEM ANEXA NO GLPI]');
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const cleanJSONMetadata = (content) => {
|
||||
if (typeof content === 'string' && content.includes('{id=') && content.includes('content=')) {
|
||||
try {
|
||||
const contentMatch = content.match(/content=([^,]+),/);
|
||||
if (contentMatch && contentMatch[1]) {
|
||||
let extracted = contentMatch[1].trim();
|
||||
extracted = extracted.replace(/^['"]|['"]$/g, '');
|
||||
return extracted;
|
||||
}
|
||||
} catch (error) {
|
||||
logWarning('Não foi possível extrair conteúdo do metadado GLPI', { content });
|
||||
}
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
const sanitizeGLPIComment = (commentObj) => {
|
||||
if (!commentObj || typeof commentObj !== 'object') {
|
||||
logWarning('Comentário inválido recebido', { comment: commentObj });
|
||||
return '';
|
||||
}
|
||||
|
||||
let content = commentObj.content || '';
|
||||
|
||||
if (!content) return '';
|
||||
|
||||
content = cleanJSONMetadata(content.toString());
|
||||
content = handleImages(content);
|
||||
content = stripHTML(content);
|
||||
|
||||
if (content !== commentObj.content) {
|
||||
logInfo('🔧 Comentário sanitizado', {
|
||||
original: commentObj.content.substring(0, 100) + '...',
|
||||
cleaned: content.substring(0, 100) + '...'
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
/**
|
||||
* @module commentSanitizer
|
||||
* @description Este módulo utilitário é especializado em limpar e formatar o conteúdo dos comentários (follow-ups) vindos do GLPI.
|
||||
* O GLPI armazena comentários em formato HTML, muitas vezes com metadados e formatações que não são desejáveis no ServiceNow.
|
||||
* O objetivo é converter esse conteúdo em texto puro e legível.
|
||||
*
|
||||
* Funções:
|
||||
* - `sanitizeGLPIComment(commentObj)`: A função principal que orquestra o processo de limpeza.
|
||||
* - `stripHTML(html)`: Remove a maioria das tags HTML, convertendo algumas (como `<br>`, `<p>`, `<strong>`) em marcações de texto simples (quebras de linha, asteriscos).
|
||||
* - `handleImages(content)`: Detecta a presença de imagens nos comentários do GLPI e as substitui por um texto placeholder, como "[IMAGEM ANEXA NO GLPI]", já que as imagens não podem ser transferidas diretamente.
|
||||
* - `cleanJSONMetadata(content)`: Remove metadados em formato de string JSON que o GLPI às vezes insere no campo de conteúdo, extraindo apenas o texto real do comentário.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
sanitizeGLPIComment,
|
||||
stripHTML,
|
||||
handleImages,
|
||||
cleanJSONMetadata
|
||||
};
|
||||
@ -40,8 +40,13 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(({ timestamp, level, message, stack }) => {
|
||||
return `${timestamp} [${level}]: ${stack || message}`;
|
||||
winston.format.printf((info) => {
|
||||
const { timestamp, level, message, stack, ...meta } = info;
|
||||
let logMessage = `${timestamp} [${level}]: ${stack || message}`;
|
||||
if (Object.keys(meta).length) {
|
||||
logMessage += ` ${JSON.stringify(meta, null, 2)}`;
|
||||
}
|
||||
return logMessage;
|
||||
})
|
||||
)
|
||||
}));
|
||||
@ -64,19 +69,10 @@ const logWarning = (message, meta = {}) => {
|
||||
logger.warn(message, meta);
|
||||
};
|
||||
|
||||
// Log de sincronização específico
|
||||
const logSync = (service, count, type) => {
|
||||
logger.info(`SYNC: ${service} - ${count} ${type} sincronizados`, {
|
||||
service,
|
||||
count,
|
||||
type
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
logger,
|
||||
logError,
|
||||
logInfo,
|
||||
logWarning,
|
||||
logSync
|
||||
logWarning
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user