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:
Rafael Alves Lopes 2025-11-10 17:01:33 -03:00
parent f8095c80e5
commit 6acd5369da
22 changed files with 4723 additions and 230 deletions

View File

@ -39,9 +39,9 @@ HUBGLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
# BANCO DE DADOS FINAL - GLPI (MySQL - Desenvolvimento) # BANCO DE DADOS FINAL - GLPI (MySQL - Desenvolvimento)
# ============================================================================== # ==============================================================================
GLPI_DB_TYPE=mysql GLPI_DB_TYPE=mysql
GLPI_DB_HOST=177.73.177.32 GLPI_DB_HOST=177.73.177.44
GLPI_DB_PORT=3306 GLPI_DB_PORT=3306
GLPI_DB_USER=snglpi GLPI_DB_USER=desenvolvimento
GLPI_DB_PASSWORD=j2633669 GLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
GLPI_DB_NAME=glpi_data GLPI_DB_NAME=glpi_data
GLPI_DB_CHARSET=utf8mb4 GLPI_DB_CHARSET=utf8mb4

162
README.md Normal file
View File

@ -0,0 +1,162 @@
# Serviço de Integração HubSoft <> GLPI
![Node.js](https://img.shields.io/badge/Node.js-18.x-green) ![Express.js](https://img.shields.io/badge/Express.js-4.x-blue) ![PostgreSQL](https://img.shields.io/badge/Database-PostgreSQL-blue) ![License](https://img.shields.io/badge/License-MIT-yellow.svg)
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

File diff suppressed because it is too large Load Diff

113
logs/error.log Normal file
View 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
View File

@ -59,6 +59,12 @@
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT" "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": { "node_modules/aws-ssl-profiles": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
@ -68,6 +74,17 @@
"node": ">= 6.0.0" "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": { "node_modules/body-parser": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@ -184,6 +201,18 @@
"node": ">=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": { "node_modules/content-disposition": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", "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": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -335,6 +373,21 @@
"node": ">= 0.4" "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": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -421,6 +474,63 @@
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
"license": "MIT" "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": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -518,6 +628,21 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -761,6 +886,15 @@
"node": ">= 0.6" "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": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@ -963,6 +1097,12 @@
"node": ">= 0.10" "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": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",

142
package-lock.json generated
View File

@ -9,9 +9,11 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.13.2",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.1.0", "express": "^5.1.0",
"mysql2": "^3.15.2", "mysql2": "^3.15.2",
"node-cron": "^4.2.1",
"pg": "^8.16.3", "pg": "^8.16.3",
"qs": "^6.14.0", "qs": "^6.14.0",
"winston": "^3.18.3" "winston": "^3.18.3"
@ -72,6 +74,12 @@
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT" "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": { "node_modules/aws-ssl-profiles": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
@ -81,6 +89,17 @@
"node": ">= 6.0.0" "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": { "node_modules/body-parser": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@ -197,6 +216,18 @@
"node": ">=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": { "node_modules/content-disposition": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", "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": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -348,6 +388,21 @@
"node": ">= 0.4" "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": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -434,6 +489,63 @@
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
"license": "MIT" "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": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -531,6 +643,21 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -774,6 +901,15 @@
"node": ">= 0.6" "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": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@ -976,6 +1112,12 @@
"node": ">= 0.10" "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": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",

View File

@ -9,9 +9,11 @@
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"axios": "^1.13.2",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.1.0", "express": "^5.1.0",
"mysql2": "^3.15.2", "mysql2": "^3.15.2",
"node-cron": "^4.2.1",
"pg": "^8.16.3", "pg": "^8.16.3",
"qs": "^6.14.0", "qs": "^6.14.0",
"winston": "^3.18.3" "winston": "^3.18.3"

View File

@ -10,4 +10,13 @@ function createApp() {
return app; return app;
} }
module.exports = 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.
*/

View File

@ -6,8 +6,6 @@ function loadEnv() {
const nodeEnv = process.env.NODE_ENV || 'development'; const nodeEnv = process.env.NODE_ENV || 'development';
const envPath = path.resolve(process.cwd(), `.env.${nodeEnv}`); const envPath = path.resolve(process.cwd(), `.env.${nodeEnv}`);
console.log(`Carregando variáveis de ambiente de: ${envPath}`);
dotenv.config({ path: envPath }); dotenv.config({ path: envPath });
} }

View 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.
*/

View File

@ -249,34 +249,42 @@ const processAtendimento = async (ticketData) => {
// ================================================================================ // ================================================================================
const processaAtendimentos = async (skipHubSoft = false) => { const processaAtendimentos = async (skipHubSoft = false) => {
try { let atendimentosDB = [];
let atendimentosDB = [];
if (!skipHubSoft) { if (!skipHubSoft) {
// Esta parte depende da VPN try {
logInfo('Buscando atendimentos do HubSoft...'); logInfo('Buscando atendimentos do HubSoft...');
atendimentosDB = await hubsoftModel.getAtendimentosFromDB(); atendimentosDB = await hubsoftModel.getAtendimentosFromDB();
logInfo(`Total de atendimentos obtidos do HubSoft: ${atendimentosDB.length}`); logInfo(`Total de atendimentos obtidos do HubSoft: ${atendimentosDB.length}`);
for (const atendimento of atendimentosDB) { for (const atendimento of atendimentosDB) {
try { try {
// Processa o ticket do HubSoft
const ticketData = await processTicketFromHubSoft(atendimento); const ticketData = await processTicketFromHubSoft(atendimento);
// Salva no banco intermediário (hubglpi)
await saveTicketToHubGlpi(ticketData); await saveTicketToHubGlpi(ticketData);
} } catch (error) {
catch (error) {
logError(`Erro ao processar atendimento ${atendimento.id_atendimento}:`, error); logError(`Erro ao processar atendimento ${atendimento.id_atendimento}:`, error);
} }
} }
} catch (error) {
} else { 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);
logInfo('Pulando a busca de atendimentos do HubSoft (skipHubSoft = true)'); // 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(); 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) { for (const atendimento of atendimentosDB) {
try { try {
@ -288,15 +296,24 @@ const processaAtendimentos = async (skipHubSoft = false) => {
logError(`Erro ao processar atendimento ${atendimento.id_atendimento}:, ${error}`); logError(`Erro ao processar atendimento ${atendimento.id_atendimento}:, ${error}`);
} }
} }
} catch (error) {
logError(`Erro ao processar atendimentos: ${error}`);
} }
}; }
module.exports = { module.exports = {
processaAtendimentos, processaAtendimentos,
processAtendimento, processAtendimento,
formatTicketDataForGlpi, formatTicketDataForGlpi,
formatDescription 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.
*/

View File

@ -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 };

View File

@ -1,16 +1,11 @@
const hubsoftController = require('./controller/processController.js');
const { logError, logInfo } = require('./utils/logger.js');
const loadEnv = require('./config/envLoader'); const loadEnv = require('./config/envLoader');
// Carrega as variáveis de ambiente
loadEnv(); loadEnv();
const hubsoftController = require('./controller/processController.js');
const { logInfo } = require('./utils/logger.js');
logInfo('Aplicação iniciada', { logInfo('Aplicação iniciada', {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV || 'development' environment: process.env.NODE_ENV || 'development'
}); });
// Inicia o processamento dos atendimentos
hubsoftController.processaAtendimentos(); hubsoftController.processaAtendimentos();

View File

@ -66,7 +66,7 @@ class GlpiModel {
static async selectEntityIdCodCliente(id) { 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} -%`]; const values = [`${id} -%`];
try { try {

View File

@ -129,25 +129,31 @@ class HubglpiModel {
logInfo('ID de sync_data obtido com sucesso:', res.rows[0]); logInfo('ID de sync_data obtido com sucesso:', res.rows[0]);
return res.rows[0] ? res.rows[0].id : null; return res.rows[0] ? res.rows[0].id : null;
} catch (err) { } catch (err) {
logError('Erro ao obter ID de sync_data', err); logError(`Erro ao obter ID de sync_data, ${err}`);
throw err; throw err;
} }
} }
static async get_idSyncByGlpiID(glpi_ticket_id) { static async getIdByGlpiID(glpi_ticket_id) {
const query = ` 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; WHERE glpi_ticket_id = $1;
`; `;
const values = [glpi_ticket_id]; const values = [glpi_ticket_id];
try { try {
const res = await pool.query(query, values); const { rows } = await pool.query(query, values);
logInfo('ID de sync_data obtido com sucesso:', res.rows[0]); if (rows.length > 0) {
return res.rows[0] ? res.rows[0].id : null; 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; throw err;
} }
@ -185,21 +191,87 @@ class HubglpiModel {
throw err; 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) { // 1. Atualiza sync_data para 'closed'
const querySync = `
UPDATE sync_data
SET status_sync = 'closed',
updated_at = NOW()
WHERE id = $1
RETURNING *;
`;
const resSync = await client.query(querySync, [syncId]);
const updatedSyncData = resSync.rows[0];
if (!updatedSyncData) {
throw new Error(`Nenhum registro sync_data encontrado com o id ${syncId} para atualizar.`);
}
// 2. Atualiza hubsoft_tickets com os dados de fechamento
const queryHubsoft = `
UPDATE hubsoft_tickets
SET updated_at = NOW(),
data_fechamento = NOW(),
descricao_fechamento = $2,
status_atendimento = 'Resolvido'
WHERE id_atendimento = $1
RETURNING *;
`;
const resHubsoft = await client.query(queryHubsoft, [updatedSyncData.hubsoft_ticket_id, closeMessage]);
await client.query('COMMIT'); // confirma transação
return {
sync_data: updatedSyncData,
hubsoft_tickets: resHubsoft.rows[0]
};
} catch (err) {
await client.query('ROLLBACK'); // desfaz se der erro
logError(`Erro ao atualizar ticket: ${err}`);
throw err;
} finally {
client.release();
}
}
static async updateSyncDataStatus( ticketId, status_sync, source_last){
const query = `
UPDATE sync_data
SET status_sync = $1,
source_last = $2
WHERE id = $3
RETURNING *;
`;
const values = [status_sync, source_last, ticketId];
try {
const res = await pool.query(query, values);
return res.rows[0];
} catch (err) {
logError(`Erro ao atualizar status_sync na tabela sync_data: ${err}`);
throw err;
}
}
static async updateSyncaDataError(sync_error_message, id_atendimento) {
const query = ` const query = `
UPDATE sync_data UPDATE sync_data
set sync_error_message = $1, set sync_error_message = $1,
status_sync = 'sync_error', status_sync = 'sync_error',
last_sync_attempt = $2 last_sync_attempt = NOW()
WHERE hubsoft_ticket_id = $3 WHERE id = $2
RETURNING *; RETURNING *;
`; `;
const values = [
sync_error_message, const values = [sync_error_message, id_atendimento];
new Date(),
id_atendimento
];
try { try {
const res = await pool.query(query, values); const res = await pool.query(query, values);
@ -210,24 +282,39 @@ class HubglpiModel {
} }
} }
static async updateSyncDataStatus(id_atendimento, status_sync, source_last) { static async lockTicketForClosing(syncId) {
const query = ` const query = `
UPDATE sync_data UPDATE sync_data
SET status_sync = $1, SET status_sync = 'processing_close',
source_last = $2 updated_at = NOW()
WHERE hubsoft_ticket_id = $3 WHERE id = $1 AND status_sync NOT IN ('processing_close', 'closed_glpi')
RETURNING *; RETURNING *;
`; `;
const values = [status_sync, source_last, id_atendimento]; const values = [syncId];
try { try {
const res = await pool.query(query, values); const res = await pool.query(query, values);
return res.rows[0]; // 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) { } catch (err) {
logError(`Erro ao atualizar status_sync na tabela sync_data: ${err}`); logError(`Erro ao tentar travar o ticket para fechamento (syncId: ${syncId}): ${err}`);
throw err; throw err;
} }
} }
} }
module.exports = HubglpiModel; 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.
*/

View File

@ -1,29 +1,52 @@
const dbConfig = require('../config/dbConfig.js'); const dbConfig = require('../config/dbConfig.js');
const { logError, logInfo } = require('../utils/logger');
const { Pool } = require('pg'); const hubsoftDbConfig = {
const pool = new Pool({
host: dbConfig.hubsoft.databaseHost, host: dbConfig.hubsoft.databaseHost,
port: dbConfig.hubsoft.databasePort, port: dbConfig.hubsoft.databasePort,
database: dbConfig.hubsoft.databaseName, database: dbConfig.hubsoft.databaseName,
user: dbConfig.hubsoft.databaseUser, user: dbConfig.hubsoft.databaseUser,
password: dbConfig.hubsoft.databasePassword 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 () => { pool.on('error', (err) => {
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;'; logError('Erro na conexão com o banco de dados HubSoft', err);
});
const { rows } = await pool.query(query);
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; return rows;
} }
const validateAtendimentoStatus = (id_atendimento) => { const validateMensagensByAtendimento = async (id_atendimento) => {
const query = `SELECT id_atendimento_status FROM atendimento WHERE id_atendimento = ${id_atendimento};`; // Corrigido para prevenir SQL Injection
return pool.query(query); 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);
const validateMensagensByAtendimento = (id_atendimento) => { return rows;
const query = `SELECT id_atendimento_mensagem, id_atendimento, mensagem, data_cadastro FROM atendimento_mensagem WHERE id_atendimento = ${id_atendimento} ;`
return pool.query(query);
} }
const updateFechaAtendimento = async (id_atendimento, closingMessage) => { const updateFechaAtendimento = async (id_atendimento, closingMessage) => {
@ -31,13 +54,15 @@ const updateFechaAtendimento = async (id_atendimento, closingMessage) => {
UPDATE atendimento UPDATE atendimento
SET descricao_fechamento = $1, SET descricao_fechamento = $1,
data_fechamento = $2, data_fechamento = $2,
status_atendimento = $3 id_atendimento_status = $3 -- Assumindo que o status é um ID numérico
WHERE id_atendimento = $4; WHERE id_atendimento = $4;
`; `;
// Precisamos descobrir qual é o status de "Fechado" no Hubsoft // 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 { try {
const res = await pool.query(query, values); const res = await pool.query(query, values);
return res.rows[0]; return res.rows[0];
@ -48,5 +73,8 @@ const updateFechaAtendimento = async (id_atendimento, closingMessage) => {
} }
module.exports = { module.exports = {
getAtendimentosFromDB, validateAtendimentoStatus, validateMensagensByAtendimento getAtendimentosFromDB,
validateAtendimentoStatus,
validateMensagensByAtendimento,
updateFechaAtendimento // Exportando a função
}; };

View File

@ -1,5 +1,5 @@
const { Router } = require('express'); const { Router } = require('express');
const ticketController = require('./controller/ticketController.js'); const ticketController = require('./controller/ClosureController.js');
const router = Router(); const router = Router();

View File

@ -1,13 +1,36 @@
const dotenv = require('dotenv');
const createApp = require('./app.js');
const loadEnv = require('./config/envLoader'); const loadEnv = require('./config/envLoader');
loadEnv(); 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 app = createApp();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
app.listen(PORT, () => { 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.
*/

View File

@ -10,7 +10,6 @@ const getAuthToken = async () => {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
} }
}); });
console.log('Token de autenticação obtido com sucesso.');
return response.data.access_token; return response.data.access_token;
} catch (error) { } catch (error) {
logError('Erro ao obter token de autenticação:', error); logError('Erro ao obter token de autenticação:', error);

View File

@ -1,122 +1,144 @@
// HUBXGLPI/src/services/ticketService.js // HUBXGLPI/src/services/ticketService.js
const { log } = require("winston");
const { get_idSyncByGlpiID } = require("../model/hubglpiModel.js"); const { logError, logInfo, logWarning} = require('../utils/logger');
const { updateSyncDataStatus } = require("../model/syncDataModel.js"); const { getIdByGlpiID, updateClosingTicket, updateSyncaDataError, lockTicketForClosing } = require("../model/hubglpiModel.js");
const { sanitizeGLPIComment } = require('../utils/commentSanitizer.js');
const { closeAtendimento } = require("./hubsoftService.js"); const { closeAtendimento } = require("./hubsoftService.js");
const { hubsoft } = require('../config/apiConfig.js');
const e = require('express');
// { /**
// "item": { * Verifica se um ticket é da "Mundiale" e atualiza seu status para 'pending_close'.
// "id": 27779, * @param {number} glpiTicketId - O ID do ticket no GLPI.
// "itemtype": "Ticket", * @param {string} ticketTitle - O título do ticket.
// "items_id": 34213, * @returns {Promise<object|null>} Um objeto com os IDs { hubsoftId, glpiId, syncId } ou null se não for aplicável.
// "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": []
// }
// }
const updateSyncStatus = async (ticketId, 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 (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;
}
}
} else {
log.info(
`Ticket ID ${ticketId} não relacionado à Mundiale. Ignorando fechamento.`
);
}
} catch (error) {}
};
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.";
const handleMundialeTicket = async (glpiTicketId, ticketTitle) => {
try { try {
const updateStatus = await updateSyncStatus(ticketId, ticketTitle); if (!ticketTitle.includes("Mundiale")) {
if (updateStatus) { logInfo(`Ticket ID ${glpiTicketId} não é da Mundiale. Ignorando fechamento.`);
log.info(`-> Iniciando fechamento do atendimento no HubSoft para o ticket ID ${ticketId}.`); return null;
const closeResponse = await closeAtendimento(ticketId, closingMessage); }
if (closeResponse.status === "success") { const syncRecord = await getIdByGlpiID(glpiTicketId);
log.info(`Atendimento no HubSoft fechado com sucesso para o ticket ID ${ticketId}.`); if (!syncRecord) {
const dbHubsoftClose = await updateFechaAtendimento(ticketId, closingMessage); logInfo(`Nenhum registro de sincronização encontrado para o ticket GLPI ID ${glpiTicketId}.`);
if (dbHubsoftClose) { return null;
log.info(`Banco de dados atualizado com sucesso para o fechamento do atendimento ID ${ticketId}.`); }
return closeResponse;
} else { logInfo(`-> Tentando obter trava para fechar o ticket GLPI ID ${glpiTicketId}.`);
log.error(`Falha ao atualizar o banco de dados para o fechamento do atendimento ID ${ticketId}.`); const lockedTicket = await lockTicketForClosing(syncRecord.id);
return closeResponse;
} if (lockedTicket) {
} else { logInfo(`Trava obtida com sucesso. Status atualizado para 'processing_close' para o ticket GLPI ID ${glpiTicketId}.`);
log.error(`Falha ao fechar atendimento no HubSoft para o ticket ID ${ticketId}. Resposta: ${JSON.stringify(closeResponse)}`); return {
} hubsoftId: lockedTicket.hubsoft_ticket_id,
glpiId: glpiTicketId,
syncId: lockedTicket.id,
};
} else {
// Se não conseguimos a trava, é porque outro processo já está tratando dele.
// Isso não é um erro, apenas uma requisição duplicada.
logWarning(`Não foi possível obter a trava para o ticket GLPI ID ${glpiTicketId}. Provavelmente já está sendo processado.`);
return null;
} }
} catch (error) { } 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.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 está fechado.
* - `updateLocalDatabaseOnClose(...)`: Atualiza o status do ticket no banco de dados local após o fechamento bem-sucedido.
*/

View File

@ -0,0 +1,106 @@
const { logInfo, logWarning } = require('./logger');
const stripHTML = (html) => {
if (!html) return '';
let cleaned = html
.replace(/&#60;/g, '<') // &#60; para <
.replace(/&#62;/g, '>') // &#62; para >
.replace(/&#38;/g, '&') // &#38; para &
.replace(/&nbsp;/g, ' ') // &nbsp; para espaço
.replace(/&lt;/g, '<') // &lt; para <
.replace(/&gt;/g, '>') // &gt; para >
.replace(/&amp;/g, '&') // &amp; para &
.replace(/&quot;/g, '"'); // &quot; 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]", 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
};

View File

@ -40,8 +40,13 @@ if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({ logger.add(new winston.transports.Console({
format: winston.format.combine( format: winston.format.combine(
winston.format.colorize(), winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, stack }) => { winston.format.printf((info) => {
return `${timestamp} [${level}]: ${stack || message}`; 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); 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 = { module.exports = {
logger, logger,
logError, logError,
logInfo, logInfo,
logWarning, logWarning
logSync
}; };