CLEANUP: Comentarios atualizados e Readme.md alimentado com V1.0 da documentação
This commit is contained in:
parent
0c4538355a
commit
bfa0e8acc8
43
.env
43
.env
@ -1,43 +0,0 @@
|
|||||||
# Contents of the file: /servicenow-glpi-sync/servicenow-glpi-sync/.env.example
|
|
||||||
|
|
||||||
# This is an example of the environment variables needed for the application.
|
|
||||||
|
|
||||||
# ServiceNow API Configuration
|
|
||||||
SERVICENOW_INCIDENT_URL="https://caoadev.service-now.com/api/now/table/incident?sysparm_display_value=all&sysparm_limit=20&sysparm_query=ORDERBYDESCsys_created_on&sysparm_fields=number%2state%2Copened_by%2Copened_at%2Clocation%2Cshort_description%2Cdescription%2Cvariables.caller_id%2Cvariables.location%2Cvariables.telephone_favorecido_rh%2Cvariables.numero_ramal%2Copened_by.email%2Csys_id%5Eassignment_group%3D1132c9e01bfad410e77eb9121b4bcbab"
|
|
||||||
SERVICENOW_REQUEST_URL="https://caoadev.service-now.com/api/now/table/sc_req_item?sysparm_fields=number%2Cstate%2Crequest%2Copened_by%2Copened_at%2Clocation%2Cshort_description%2Cdescription%2Cassignment_group%2Csys_id&sysparm_display_value=all&sysparm_query=assignment_group%3D1132c9e01bfad410e77eb9121b4bcbab"
|
|
||||||
SERVICENOW_TABLE_INCIDENT_URL="https://caoadev.service-now.com/api/now/table/incident"
|
|
||||||
SERVICENOW_TABLE_REQUEST_URL="https://caoadev.service-now.com/api/now/table/sc_req_item"
|
|
||||||
SERVICENOW_TABLE_JOURNAL_URL="https://caoadev.service-now.com/api/now/table/sys_journal_field"
|
|
||||||
SERVICENOW_SC_ITEM_OPTION_URL="https://caoadev.service-now.com/api/now/table/sc_item_option_mtom"
|
|
||||||
SERVICENOW_USERNAME=SOTHIS.CAOA
|
|
||||||
SERVICENOW_PASSWORD=#Sothis20
|
|
||||||
|
|
||||||
# GLPI access via DB (use DB connection instead of API)
|
|
||||||
GLPI_DB_TYPE=mysql # ou postgresql
|
|
||||||
GLPI_DB_HOST=177.73.177.32
|
|
||||||
GLPI_DB_PORT=3306
|
|
||||||
GLPI_DB_USER=snglpi
|
|
||||||
GLPI_DB_PASSWORD=j2633669
|
|
||||||
GLPI_DB_NAME=glpi_data
|
|
||||||
GLPI_DB_CHARSET=utf8mb4
|
|
||||||
|
|
||||||
|
|
||||||
# Banco SNGLPI (PostgreSQL)
|
|
||||||
SNGLPI_DB_HOST=10.0.120.75
|
|
||||||
SNGLPI_DB_PORT=5432
|
|
||||||
SNGLPI_DB_NAME=snglpi
|
|
||||||
SNGLPI_DB_USER=desenvolvimento
|
|
||||||
SNGLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
|
|
||||||
|
|
||||||
# Caminho do CSV
|
|
||||||
LOCATION_MAPPING_CSV_PATH=\\10.0.121.40\Tecnica\Controle CAOA\ENTIDADE SERVICE NOW\LOCATION_MAPPING.csv
|
|
||||||
|
|
||||||
|
|
||||||
# Logging Configuration
|
|
||||||
LOG_LEVEL=info
|
|
||||||
LOG_FILE_PATH=logs/app.log
|
|
||||||
ERROR_LOG_FILE_PATH=logs/error.log
|
|
||||||
|
|
||||||
# Other Configuration
|
|
||||||
PORT=3000
|
|
||||||
NODE_ENV=development
|
|
||||||
49
.env.development
Normal file
49
.env.development
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# =========================================
|
||||||
|
# DEVELOPMENT CONFIGURATION
|
||||||
|
# =========================================
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# ServiceNow API (Dev)
|
||||||
|
SERVICENOW_INSTANCE=https://caoadev.service-now.com
|
||||||
|
SERVICENOW_USERNAME=SOTHIS.CAOA
|
||||||
|
SERVICENOW_PASSWORD=#Sothis20
|
||||||
|
|
||||||
|
# GRUPO SOTHIS PARA BUSCA
|
||||||
|
SERVICENOW_ASSIGNMENT_GROUP="1132c9e01bfad410e77eb9121b4bcbab"
|
||||||
|
|
||||||
|
# Endpoints
|
||||||
|
SERVICENOW_INCIDENT_URL=https://caoadev.service-now.com/api/now/table/incident?sysparm_display_value=all&sysparm_limit=20
|
||||||
|
SERVICENOW_REQUEST_URL=https://caoadev.service-now.com/api/now/table/sc_req_item?sysparm_display_value=all&sysparm_limit=20
|
||||||
|
SERVICENOW_TABLE_INCIDENT_URL=https://caoadev.service-now.com/api/now/table/incident
|
||||||
|
SERVICENOW_TABLE_REQUEST_URL=https://caoadev.service-now.com/api/now/table/sc_req_item
|
||||||
|
SERVICENOW_TABLE_JOURNAL_URL=https://caoadev.service-now.com/api/now/table/sys_journal_field
|
||||||
|
SERVICENOW_SC_ITEM_OPTION_URL=https://caoadev.service-now.com/api/now/table/sc_item_option_mtom
|
||||||
|
|
||||||
|
# Usuário padrão pra criação de chamados (ServiceNow)
|
||||||
|
SERVICENOW_DEFAULT_USER=SOTHIS.CAOA
|
||||||
|
SERVICENOW_DEFAULT_USER_SYSID=b1312fda977a1250501634a6f053af98
|
||||||
|
|
||||||
|
# GLPI (banco real)
|
||||||
|
GLPI_DB_TYPE=mysql
|
||||||
|
GLPI_DB_HOST=177.73.177.32
|
||||||
|
GLPI_DB_PORT=3306
|
||||||
|
GLPI_DB_USER=snglpi
|
||||||
|
GLPI_DB_PASSWORD=j2633669
|
||||||
|
GLPI_DB_NAME=glpi_data
|
||||||
|
GLPI_DB_CHARSET=utf8mb4
|
||||||
|
|
||||||
|
# Banco intermediário (PostgreSQL)
|
||||||
|
SNGLPI_DB_HOST=localhost
|
||||||
|
SNGLPI_DB_PORT=5432
|
||||||
|
SNGLPI_DB_NAME=snglpi
|
||||||
|
SNGLPI_DB_USER=desenvolvimento
|
||||||
|
SNGLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
|
||||||
|
|
||||||
|
# CSV de mapeamento
|
||||||
|
LOCATION_MAPPING_CSV_PATH=/mnt/csv_share/OCATION_MAPPING.csv
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
LOG_RETENTION_DAYS=10
|
||||||
|
LOG_DIR=logs
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,3 +6,4 @@ config/*.json
|
|||||||
servicenow_requests.json
|
servicenow_requests.json
|
||||||
servicenow_tickets.json
|
servicenow_tickets.json
|
||||||
location_mapping.log
|
location_mapping.log
|
||||||
|
.env.production
|
||||||
|
|||||||
170
README.md
170
README.md
@ -1,80 +1,128 @@
|
|||||||
# servicenow-glpi-sync/README.md
|
# Sistema de Sincronização ServiceNow <> GLPI
|
||||||
|
|
||||||
# ServiceNow and GLPI Sync
|
## Visão Geral
|
||||||
|
|
||||||
This project is designed to facilitate the integration between ServiceNow and GLPI, allowing for the synchronization of tickets between the two platforms. It utilizes Node.js and follows the MVC architecture pattern.
|
Esta aplicação Node.js é um middleware projetado para realizar a sincronização bidirecional de tickets entre uma instância do **ServiceNow (SNOW)** e uma do **GLPI**. O objetivo é manter os tickets, seus status e comentários consistentes entre as duas plataformas, automatizando o fluxo de trabalho para as equipes de suporte.
|
||||||
|
|
||||||
## Table of Contents
|
A aplicação funciona como um job agendado (por exemplo, via cron) que executa um ciclo completo de sincronização.
|
||||||
|
|
||||||
- [Prerequisites](#prerequisites)
|
---
|
||||||
- [Installation](#installation)
|
|
||||||
- [Configuration](#configuration)
|
|
||||||
- [Running the Application](#running-the-application)
|
|
||||||
- [Endpoints](#endpoints)
|
|
||||||
- [Logging](#logging)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [License](#license)
|
|
||||||
|
|
||||||
## Prerequisites
|
## Fluxo de Execução
|
||||||
|
|
||||||
- Node.js (version 14 or higher)
|
A aplicação segue uma ordem de execução estrita para garantir a consistência dos dados. O ponto de entrada é o arquivo `src/app.js`, que orquestra a chamada dos seguintes controladores:
|
||||||
- npm (Node Package Manager)
|
|
||||||
- PostgreSQL or MySQL database
|
|
||||||
- Access to ServiceNow and GLPI APIs
|
|
||||||
|
|
||||||
## Installation
|
1. **`processErrorController` - Limpeza de Erros**
|
||||||
|
- **Objetivo**: Aumentar a resiliência do sistema.
|
||||||
|
- **Ação**: Busca por tickets que falharam em execuções anteriores (marcados com status de `error`). Ele reseta o status desses tickets para um estado anterior válido (ex: `pending_check` ou `synced`), permitindo que eles sejam reprocessados no ciclo atual.
|
||||||
|
|
||||||
1. Clone the repository:
|
2. **`processTicketsController` - Criação de Tickets**
|
||||||
|
- **Objetivo**: Trazer novos tickets do ServiceNow para o GLPI.
|
||||||
|
- **Ação**:
|
||||||
|
- **Busca no SNOW**: Invoca o `processSyncController` para buscar incidentes e requisições novas ou atualizadas no ServiceNow, utilizando uma "marca d'água" (timestamp) para otimização.
|
||||||
|
- **Criação no GLPI**: Para cada novo ticket coletado, o `glpiTicketService` é chamado para:
|
||||||
|
- Verificar se o ticket já existe no GLPI para evitar duplicatas.
|
||||||
|
- Formatar os dados (título, descrição, categoria, etc.).
|
||||||
|
- Criar o ticket correspondente no banco de dados do GLPI.
|
||||||
|
|
||||||
```
|
3. **`processCommentsController` - Sincronização de Comentários**
|
||||||
git clone https://github.com/yourusername/servicenow-glpi-sync.git
|
- **Objetivo**: Manter as conversas dos tickets sincronizadas.
|
||||||
cd servicenow-glpi-sync
|
- **Ação**: Para cada ticket ativo, realiza uma sincronização bidirecional de comentários (notas/follow-ups):
|
||||||
```
|
- **SNOW -> GLPI**: Busca novos comentários no ServiceNow, os salva no banco intermediário e os insere como follow-ups no GLPI.
|
||||||
|
- **GLPI -> SNOW**: Busca novos follow-ups no GLPI, os sanitiza (remove HTML, trata imagens) e os envia como `work_notes` para o ServiceNow.
|
||||||
|
|
||||||
2. Install the dependencies:
|
4. **`processStatusAndClosureController` - Sincronização de Status e Fechamento**
|
||||||
|
- **Objetivo**: Manter os status dos tickets alinhados e gerenciar o ciclo de vida (resolução e fechamento).
|
||||||
|
- **Ação**: Realiza uma sincronização bidirecional de status, tratando reaberturas, pausas, resoluções e fechamentos em ambas as direções.
|
||||||
|
|
||||||
```
|
---
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Copy the `.env.example` file to `.env` and fill in the required environment variables:
|
## Regras de Negócio Principais
|
||||||
|
|
||||||
```
|
A aplicação implementa várias regras de negócio para gerenciar a complexidade da sincronização:
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
### Controle de Fluxo com "Bastão" (`source_last`)
|
||||||
|
- Para evitar conflitos de atualização (race conditions), o sistema utiliza um campo `source_last` na tabela `ticket_sync`.
|
||||||
|
- Este campo funciona como um "bastão da palavra": o sistema que realiza a última atualização (SNOW ou GLPI) "segura o bastão".
|
||||||
|
- O sistema oposto só pode atualizar o ticket se ele estiver com o bastão. Isso garante que as atualizações de status e comentários ocorram de forma ordenada.
|
||||||
|
|
||||||
- Update the `.env` file with your ServiceNow and GLPI API credentials, database connection details, and any other necessary configurations.
|
### Criação e Enriquecimento de Tickets
|
||||||
|
- **Mapeamento**: Tickets do ServiceNow são mapeados para o GLPI com base em regras de categoria, prioridade e SLA definidas no `ticketGlpiModel.js`.
|
||||||
|
- **Título**: O título do ticket no GLPI é padronizado como `[TIPO] - [ENTIDADE] - [TÍTULO ORIGINAL]`.
|
||||||
|
- **Enriquecimento de Descrição**: Se uma requisição do ServiceNow chega com a descrição vazia, o sistema busca automaticamente o conteúdo de variáveis de catálogo (como `justificativa` e `telefone`) e as utiliza para preencher a descrição e os dados do ticket no GLPI.
|
||||||
|
|
||||||
## Running the Application
|
### Sincronização de Comentários
|
||||||
|
- **Lógica de 4 Casos (GLPI -> SNOW)**: Para evitar duplicatas, a sincronização de comentários do GLPI para o SNOW verifica 4 cenários:
|
||||||
|
1. **Já Sincronizado**: O comentário existe em ambos os locais. Nenhuma ação.
|
||||||
|
2. **Existe no SN, Falta no BD Local**: O registro local é criado para corrigir o estado.
|
||||||
|
3. **Existe no BD Local, Falta no SN**: O comentário é reenviado para o ServiceNow.
|
||||||
|
4. **Novo Comentário**: O comentário é enviado para o SNOW e registrado localmente.
|
||||||
|
- **Sanitização**: Comentários do GLPI passam por um processo de limpeza (`commentSanitizer.js`) que remove tags HTML e substitui imagens por um texto placeholder, garantindo que o conteúdo seja legível no ServiceNow.
|
||||||
|
|
||||||
To start the application, run:
|
### Sincronização de Status
|
||||||
|
- **Fechamento de Ticket em Espera**: Se um ticket é resolvido no GLPI, mas seu correspondente no ServiceNow está com o status "Em Espera" ou "Aguardando Atendimento", o sistema primeiro altera o status no ServiceNow para "Em Atendimento" e só então prossegue com a resolução. Isso automatiza o fluxo e evita falhas.
|
||||||
|
- **Regra "Fora do Escopo"**: Se um ticket no GLPI é solucionado com o tipo de solução "Fora do Escopo", o sistema não o resolve no ServiceNow. Em vez disso, ele adiciona uma nota de trabalho (`work_note`) explicando o motivo e fecha o registro de sincronização, tratando-o como um caso especial.
|
||||||
|
- **Reabertura**: O sistema permite a reabertura de tickets em ambas as direções, desde que o ticket não esteja permanentemente fechado.
|
||||||
|
|
||||||
|
### Ambientes (`.env`)
|
||||||
|
- A aplicação suporta múltiplos ambientes através de arquivos `.env`.
|
||||||
|
- Se a variável de ambiente `NODE_ENV` for definida como `production`, o arquivo `.env.production` será carregado.
|
||||||
|
- Caso contrário (ou se `NODE_ENV` for `development`), o arquivo `.env.development` será utilizado.
|
||||||
|
|
||||||
|
### Atualização do Mapeamento de Localidades (`update_location.py`)
|
||||||
|
- A aplicação conta com um script Python (`src/scripts/python/update_location.py`) para manter o mapeamento entre as localidades do ServiceNow e as entidades do GLPI.
|
||||||
|
- **Fonte da Verdade**: Um arquivo `.csv` localizado em um servidor de arquivos (`LOCATION_MAPPING_CSV_PATH` no `.env`) é a fonte da verdade para este mapeamento.
|
||||||
|
- **Processo**:
|
||||||
|
1. Um usuário (`sothis`) atualiza o arquivo `.csv` com as correspondências corretas entre os nomes das localidades (SNOW) e os nomes das entidades (GLPI).
|
||||||
|
2. Um `cron job` no servidor executa o script `update_location.py` periodicamente.
|
||||||
|
3. O script lê o `.csv`, busca os IDs correspondentes nos bancos de dados de ambos os sistemas e recria a tabela `location_mapping` no banco de dados intermediário (PostgreSQL).
|
||||||
|
- **Resultado**: Garante que os tickets criados no GLPI sejam sempre associados à entidade correta, com base em um mapeamento centralizado e de fácil manutenção.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estrutura do Banco de Dados Intermediário (PostgreSQL)
|
||||||
|
|
||||||
|
- **`tickets_sn`**: Armazena uma cópia local dos dados dos tickets do ServiceNow para otimizar consultas.
|
||||||
|
- **`ticket_sync`**: Tabela central que controla o estado da sincronização de cada ticket, incluindo os IDs de ambos os sistemas e o campo `source_last`.
|
||||||
|
- **`ticket_updates`**: Log de todas as atualizações (comentários, etc.), usado para evitar duplicidade e rastrear o histórico.
|
||||||
|
- **`location_mapping`**: Tabela de mapeamento entre as localizações do ServiceNow e as entidades do GLPI.
|
||||||
|
- **`sync_control`**: Armazena metadados do processo, como a marca d'água (timestamp) da última sincronização bem-sucedida.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diagrama de Fluxo (Exemplo)
|
||||||
|
|
||||||
|
### Sincronização de Resolução: GLPI -> ServiceNow
|
||||||
|
|
||||||
|
O diagrama abaixo ilustra como o sistema lida com um ticket que foi marcado como "Solucionado" no GLPI.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant App as app.js
|
||||||
|
participant StatusCtrl as processStatusAndClosureController
|
||||||
|
participant GlpiModel as TicketGlpiModel
|
||||||
|
participant SyncModel as TicketSyncModel
|
||||||
|
participant SnowService as servicenowService
|
||||||
|
participant ServiceNow as ServiceNow API
|
||||||
|
|
||||||
|
App->>StatusCtrl: Iniciar ciclo de status
|
||||||
|
StatusCtrl->>SyncModel: getTicketsToMonitor()
|
||||||
|
SyncModel-->>StatusCtrl: Lista de tickets ativos
|
||||||
|
|
||||||
|
loop Para cada ticket monitorado
|
||||||
|
StatusCtrl->>GlpiModel: getTicketStatus(glpi_id)
|
||||||
|
GlpiModel-->>StatusCtrl: Status 'Solucionado' (5)
|
||||||
|
|
||||||
|
alt Status mudou para 'Solucionado'
|
||||||
|
StatusCtrl->>GlpiModel: getTicketSolution(glpi_id)
|
||||||
|
GlpiModel-->>StatusCtrl: Detalhes da solução
|
||||||
|
|
||||||
|
StatusCtrl->>SnowService: closeTicketInServiceNow(sn_id, solucao)
|
||||||
|
SnowService->>ServiceNow: PATCH /api/now/table/incident/{sys_id} (state: 'Resolved')
|
||||||
|
ServiceNow-->>SnowService: 200 OK
|
||||||
|
SnowService-->>StatusCtrl: Sucesso
|
||||||
|
|
||||||
|
StatusCtrl->>SyncModel: updateStatus(sn_id, 'solved', 'solved')
|
||||||
|
SyncModel-->>StatusCtrl: OK
|
||||||
|
end
|
||||||
|
end
|
||||||
```
|
```
|
||||||
node server.js
|
|
||||||
```
|
|
||||||
|
|
||||||
The application will listen on the port specified in the `.env` file.
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
- `POST /tickets/sync` - Synchronize tickets from ServiceNow to GLPI.
|
|
||||||
- `GET /tickets/:id` - Retrieve details of a specific ticket.
|
|
||||||
- `POST /tickets` - Create a new ticket in GLPI based on ServiceNow data.
|
|
||||||
- `PUT /tickets/:id` - Update an existing ticket in GLPI.
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
The application uses Winston for logging. Logs are stored in the `logs` directory:
|
|
||||||
|
|
||||||
- `app.log` - Logs application events.
|
|
||||||
- `error.log` - Logs errors that occur during execution.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License. See the LICENSE file for details.
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
require('dotenv').config();
|
//config/apiConfig.js
|
||||||
|
|
||||||
const glpiDbConfig = {
|
const glpiDbConfig = {
|
||||||
client: process.env.GLPI_DB_TYPE || 'mysql',
|
client: process.env.GLPI_DB_TYPE || 'mysql',
|
||||||
@ -45,6 +45,10 @@ const snScItemOptionConfig = {
|
|||||||
baseUrl: process.env.SERVICENOW_SC_ITEM_OPTION_URL,
|
baseUrl: process.env.SERVICENOW_SC_ITEM_OPTION_URL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const assignmentGroup = {
|
||||||
|
username: process.env.SERVICENOW_ASSIGNMENT_GROUP,
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// configuração do ServiceNow e outras já existentes podem permanecer
|
// configuração do ServiceNow e outras já existentes podem permanecer
|
||||||
glpiDbConfig,
|
glpiDbConfig,
|
||||||
@ -54,5 +58,6 @@ module.exports = {
|
|||||||
snTableJournalConfig,
|
snTableJournalConfig,
|
||||||
servicenowAuthentication,
|
servicenowAuthentication,
|
||||||
snTableRequestConfig,
|
snTableRequestConfig,
|
||||||
snScItemOptionConfig
|
snScItemOptionConfig,
|
||||||
|
assignmentGroup
|
||||||
};
|
};
|
||||||
@ -1,5 +1,4 @@
|
|||||||
const { Sequelize } = require('sequelize');
|
const { Sequelize } = require('sequelize');
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
// Database connection configuration
|
// Database connection configuration
|
||||||
const dbConfig = {
|
const dbConfig = {
|
||||||
|
|||||||
36
src/app.js
36
src/app.js
@ -1,10 +1,17 @@
|
|||||||
// src/app.js
|
const path = require('path');
|
||||||
require('dotenv').config();
|
const dotenv = require('dotenv');
|
||||||
|
const nodeEnv = process.env.NODE_ENV;
|
||||||
|
let envFile = '.env.devlopment';
|
||||||
|
if (nodeEnv === 'production') {envFile = '.env.production';} else {envFile = '.env.development';}
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, '..', envFile) });
|
||||||
|
|
||||||
const { processTicketsController } = require('./controllers/processTicketsController');
|
const { processTicketsController } = require('./controllers/processTicketsController');
|
||||||
const { processCommentsController } = require('./controllers/processCommentsController');
|
const { processCommentsController } = require('./controllers/processCommentsController');
|
||||||
const { processErrorController } = require('./controllers/processErrorController');
|
const { processErrorController } = require('./controllers/processErrorController');
|
||||||
const { processStatusAndClosureController } = require('./controllers/processStatusController');
|
const { processStatusAndClosureController } = require('./controllers/processStatusController');
|
||||||
const { logInfo } = require('./utils/logger');
|
const { logInfo, logger } = require('./utils/logger');
|
||||||
|
|
||||||
|
logInfo(`🌎 Ambiente carregado: ${process.env.NODE_ENV || 'development'}`);
|
||||||
|
|
||||||
logInfo('Aplicação iniciada', {
|
logInfo('Aplicação iniciada', {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@ -13,15 +20,28 @@ logInfo('Aplicação iniciada', {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
await processErrorController(); // Reseta os erros
|
await processErrorController();
|
||||||
await processTicketsController(); // Coleta tickets do SN e cria no GLPI
|
await processTicketsController();
|
||||||
await processCommentsController(); // Sincroniza chamados Bidirecionalmente
|
await processCommentsController();
|
||||||
await processStatusAndClosureController(); // Fecha chamados Bidirecionalmente
|
await processStatusAndClosureController();
|
||||||
logInfo('🎉 Ciclo de sincronização concluído com sucesso! 🎉');
|
logInfo('🎉 Ciclo de sincronização concluído com sucesso! 🎉');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logInfo('Erro na aplicação:', error);
|
logInfo('Erro na aplicação:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file app.js
|
||||||
|
* @description
|
||||||
|
* Ponto de entrada principal da aplicação de sincronização. Este arquivo é responsável por:
|
||||||
|
* 1. Carregar as variáveis de ambiente corretas (`.env.development` ou `.env.production`) com base na variável `NODE_ENV`.
|
||||||
|
* 2. Orquestrar a execução sequencial dos controladores que compõem o ciclo de sincronização.
|
||||||
|
*
|
||||||
|
* O fluxo de execução é o seguinte:
|
||||||
|
* 1. `processErrorController`: Verifica e reseta tickets que falharam em execuções anteriores, permitindo que sejam reprocessados.
|
||||||
|
* 2. `processTicketsController`: Busca novos tickets (incidentes e requisições) do ServiceNow, os salva no banco de dados intermediário e cria os tickets correspondentes no GLPI.
|
||||||
|
* 3. `processCommentsController`: Realiza a sincronização bidirecional de comentários (notas/follow-ups) para os tickets que já estão sincronizados.
|
||||||
|
* 4. `processStatusAndClosureController`: Executa a sincronização bidirecional de status (aberto, em atendimento, resolvido, fechado) e gerencia o fechamento de tickets em ambas as plataformas.
|
||||||
|
*/
|
||||||
@ -4,8 +4,6 @@ const TicketSyncModel = require('../models/ticketSyncModel');
|
|||||||
|
|
||||||
|
|
||||||
const processCommentsController = async () => {
|
const processCommentsController = async () => {
|
||||||
|
|
||||||
// Usa getTicketsToMonitor para incluir tickets 'synced' e 'solved'.
|
|
||||||
const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor();
|
const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor();
|
||||||
|
|
||||||
for (const ticket of ticketsToMonitor){
|
for (const ticket of ticketsToMonitor){
|
||||||
@ -13,8 +11,14 @@ const processCommentsController = async () => {
|
|||||||
if(comments.length != 0){await syncCommentsSNtoGlpi(comments, ticket);}
|
if(comments.length != 0){await syncCommentsSNtoGlpi(comments, ticket);}
|
||||||
await syncCommentsGlpitoSN(ticket);
|
await syncCommentsGlpitoSN(ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
processCommentsController
|
processCommentsController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file processCommentsController.js
|
||||||
|
* @description Este controlador orquestra a sincronização bidirecional de comentários para todos os tickets ativos.
|
||||||
|
* Ele busca tickets que estão sendo monitorados e, para cada um, executa a sincronização de comentários do ServiceNow para o GLPI e vice-versa.
|
||||||
|
*/
|
||||||
@ -1,10 +1,8 @@
|
|||||||
// src/controllers/processErrorController.js
|
|
||||||
const TicketSyncModel = require('../models/ticketSyncModel');
|
const TicketSyncModel = require('../models/ticketSyncModel');
|
||||||
const { logInfo, logError, logWarning } = require('../utils/logger');
|
const { logInfo, logError, logWarning } = require('../utils/logger');
|
||||||
|
|
||||||
const processErrorController = async () => {
|
const processErrorController = async () => {
|
||||||
logInfo('Iniciando verificação de tickets com erro...');
|
logInfo('Iniciando verificação de tickets com erro...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const errorTickets = await TicketSyncModel.getTicketsInErrorState();
|
const errorTickets = await TicketSyncModel.getTicketsInErrorState();
|
||||||
|
|
||||||
@ -19,20 +17,16 @@ const processErrorController = async () => {
|
|||||||
let newSnStatus = ticket.sn_sync_status;
|
let newSnStatus = ticket.sn_sync_status;
|
||||||
let newGlpiStatus = ticket.glpi_sync_status;
|
let newGlpiStatus = ticket.glpi_sync_status;
|
||||||
|
|
||||||
// Reset 'error' na criação do GLPI para tentar de novo
|
|
||||||
if (ticket.glpi_sync_status === 'error') {
|
if (ticket.glpi_sync_status === 'error') {
|
||||||
newGlpiStatus = 'pending_check';
|
newGlpiStatus = 'pending_check';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset 'error_sync' para tentar sincronizar status/comentários de novo
|
|
||||||
if (ticket.sn_sync_status === 'error_sync') newSnStatus = 'synced';
|
if (ticket.sn_sync_status === 'error_sync') newSnStatus = 'synced';
|
||||||
if (ticket.glpi_sync_status === 'error_sync') newGlpiStatus = 'synced';
|
if (ticket.glpi_sync_status === 'error_sync') newGlpiStatus = 'synced';
|
||||||
|
|
||||||
// Reset 'error_closing' para tentar fechar de novo
|
|
||||||
if (ticket.sn_sync_status === 'error_closing') newSnStatus = 'pending_close';
|
if (ticket.sn_sync_status === 'error_closing') newSnStatus = 'pending_close';
|
||||||
if (ticket.glpi_sync_status === 'error_closing') newGlpiStatus = 'pending_close';
|
if (ticket.glpi_sync_status === 'error_closing') newGlpiStatus = 'pending_close';
|
||||||
|
|
||||||
// Atualiza o status para que o ticket reentre no fluxo normal
|
|
||||||
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, newGlpiStatus, newSnStatus);
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, newGlpiStatus, newSnStatus);
|
||||||
logInfo(`Ticket SN ID ${ticket.sn_ticket_id} resetado para reprocessamento (SN: ${newSnStatus}, GLPI: ${newGlpiStatus}).`);
|
logInfo(`Ticket SN ID ${ticket.sn_ticket_id} resetado para reprocessamento (SN: ${newSnStatus}, GLPI: ${newGlpiStatus}).`);
|
||||||
}
|
}
|
||||||
@ -45,3 +39,11 @@ const processErrorController = async () => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
processErrorController
|
processErrorController
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file processErrorController.js
|
||||||
|
* @description Este controlador é responsável por gerenciar tickets que entraram em estado de erro durante o processo de sincronização.
|
||||||
|
* Ele busca todos os tickets marcados com status de erro (ex: 'error', 'error_sync') e os reseta para um estado anterior válido
|
||||||
|
* (ex: 'pending_check', 'synced'). Isso permite que os tickets sejam automaticamente reprocessados na próxima execução do ciclo de sincronização,
|
||||||
|
* aumentando a resiliência do sistema.
|
||||||
|
*/
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
const TicketSyncModel = require('../models/ticketSyncModel');
|
const TicketSyncModel = require('../models/ticketSyncModel');
|
||||||
const TicketSnModel = require('../models/ticketSnModel');
|
const TicketSnModel = require('../models/ticketSnModel');
|
||||||
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
||||||
const TicketUpdateModel = require('../models/ticketUpdateModel');
|
|
||||||
const { logInfo, logError, logWarning } = require('../utils/logger');
|
const { logInfo, logError, logWarning } = require('../utils/logger');
|
||||||
const { stripHTML } = require('../utils/commentSanitizer');
|
const { stripHTML } = require('../utils/commentSanitizer');
|
||||||
const { closeTicketInServiceNow, updateStatusInServiceNow, addWorkNoteToServiceNow } = require('../services/servicenowService');
|
const { closeTicketInServiceNow, updateStatusInServiceNow, addWorkNoteToServiceNow } = require('../services/servicenowService');
|
||||||
@ -119,10 +118,17 @@ const processStatusAndClosureController = async () => {
|
|||||||
// Se o status for de um ticket aberto (reabertura, etc.), propaga a mudança.
|
// Se o status for de um ticket aberto (reabertura, etc.), propaga a mudança.
|
||||||
const glpiStatusAsString = TicketGlpiModel.mapGlpiStatusToString(glpiCurrentStatus);
|
const glpiStatusAsString = TicketGlpiModel.mapGlpiStatusToString(glpiCurrentStatus);
|
||||||
if (glpiStatusAsString !== snCurrentStatus) {
|
if (glpiStatusAsString !== snCurrentStatus) {
|
||||||
|
if (snCurrentStatus === 'Encerrado' || snCurrentStatus === 'Encerrado - Omitido'){
|
||||||
|
logInfo(`SN ${ticketSnDetails.ticket_number} foi Fechado. Sincronizando para GLPI...`);
|
||||||
|
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
||||||
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'closed');
|
||||||
|
}
|
||||||
|
else{
|
||||||
logInfo(`Diferença de status detectada (GLPI: ${glpiStatusAsString}, SN: ${snCurrentStatus}). Sincronizando...`);
|
logInfo(`Diferença de status detectada (GLPI: ${glpiStatusAsString}, SN: ${snCurrentStatus}). Sincronizando...`);
|
||||||
await updateStatusInServiceNow(ticket.sn_ticket_id, glpiStatusAsString);
|
await updateStatusInServiceNow(ticket.sn_ticket_id, glpiStatusAsString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Passa o bastão de volta para o SN se a ação não for de finalização
|
// Passa o bastão de volta para o SN se a ação não for de finalização
|
||||||
if (glpiCurrentStatus < 5) {
|
if (glpiCurrentStatus < 5) {
|
||||||
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW');
|
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW');
|
||||||
|
|||||||
@ -11,10 +11,8 @@ const e = require('express');
|
|||||||
* @param {('incidente'|'requisicao')} type - O tipo de ticket.
|
* @param {('incidente'|'requisicao')} type - O tipo de ticket.
|
||||||
*/
|
*/
|
||||||
const processAndSaveTickets = async (tickets, type) => {
|
const processAndSaveTickets = async (tickets, type) => {
|
||||||
// Helper para converter a data do formato 'DD/MM/YYYY HH:mm:ss' para um objeto Date.
|
|
||||||
const parseServiceNowDate = (dateString) => {
|
const parseServiceNowDate = (dateString) => {
|
||||||
if (!dateString) return null;
|
if (!dateString) return null;
|
||||||
// Verifica se já está em um formato reconhecível (como ISO 8601)
|
|
||||||
if (!dateString.includes('/')) {
|
if (!dateString.includes('/')) {
|
||||||
return new Date(dateString);
|
return new Date(dateString);
|
||||||
}
|
}
|
||||||
@ -26,18 +24,15 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
|
|
||||||
if (!tickets || tickets.length === 0) {
|
if (!tickets || tickets.length === 0) {
|
||||||
logInfo(`Nenhum ticket do tipo '${type}' para processar.`);
|
logInfo(`Nenhum ticket do tipo '${type}' para processar.`);
|
||||||
return ''; // Retorna string vazia se não houver tickets
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo(`💾 Iniciando salvamento de ${tickets.length} ${type}s no banco...`);
|
logInfo(`💾 Iniciando salvamento de ${tickets.length} ${type}s no banco...`);
|
||||||
let latestUpdateTimestamp = new Date(0); // Inicia com a data mínima (epoch)
|
let latestUpdateTimestamp = new Date(0);
|
||||||
for (const ticket of tickets) {
|
for (const ticket of tickets) {
|
||||||
try {
|
try {
|
||||||
// --- LÓGICA DE ENRIQUECIMENTO DA DESCRIÇÃO ---
|
|
||||||
// Se for uma requisição e a descrição estiver vazia, busca na sc_item_option.
|
|
||||||
if (type === 'requisicao' && (!ticket.description || ticket.description.trim() === '')) {
|
if (type === 'requisicao' && (!ticket.description || ticket.description.trim() === '')) {
|
||||||
logInfo(`Descrição vazia para requisição ${ticket.number}. Buscando em sc_item_option...`);
|
logInfo(`Descrição vazia para requisição ${ticket.number}. Buscando em sc_item_option...`);
|
||||||
// O nome 'justificativa' é um exemplo, use o nome real da sua variável.
|
|
||||||
const extraData = await fetchScItemOptionValue(ticket.sys_id, 'justificativa');
|
const extraData = await fetchScItemOptionValue(ticket.sys_id, 'justificativa');
|
||||||
const vars = {};
|
const vars = {};
|
||||||
(extraData || []).forEach(item => {
|
(extraData || []).forEach(item => {
|
||||||
@ -51,7 +46,7 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
|
|
||||||
|
|
||||||
if (vars.justification) {
|
if (vars.justification) {
|
||||||
ticket.description = vars.justification; // Atualiza o objeto do ticket em memória
|
ticket.description = vars.justification;
|
||||||
logInfo(`Descrição da requisição ${ticket.number} atualizada com valor de sc_item_option.`);
|
logInfo(`Descrição da requisição ${ticket.number} atualizada com valor de sc_item_option.`);
|
||||||
}
|
}
|
||||||
if (vars.telephone){
|
if (vars.telephone){
|
||||||
@ -59,13 +54,8 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
logInfo(`Telefone da requisição ${ticket.number} atualizado com valor de sc_item_option.`);
|
logInfo(`Telefone da requisição ${ticket.number} atualizado com valor de sc_item_option.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- FIM DA LÓGICA DE ENRIQUECIMENTO ---
|
|
||||||
|
|
||||||
// 1. Salva o ticket na tabela 'tickets_sn'
|
|
||||||
const { id: idTableTicketSn, wasInserted } = await TicketSnModel.saveTicket(ticket, type);
|
const { id: idTableTicketSn, wasInserted } = await TicketSnModel.saveTicket(ticket, type);
|
||||||
|
|
||||||
// 2. Prepara os dados para a tabela de sincronização 'ticket_sync'
|
|
||||||
// Para requisições, o status está diretamente em ticket.state
|
|
||||||
const ticketStatus = ticket.state;
|
const ticketStatus = ticket.state;
|
||||||
|
|
||||||
const dataSync = {
|
const dataSync = {
|
||||||
@ -76,31 +66,25 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
updated_at: new Date()
|
updated_at: new Date()
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Define o status inicial de sincronização
|
|
||||||
if (ticketStatus === 'Encerrado' || ticketStatus === 'Encerrado - Omitido') {
|
if (ticketStatus === 'Encerrado' || ticketStatus === 'Encerrado - Omitido') {
|
||||||
dataSync.sn_sync_status = 'closed';
|
dataSync.sn_sync_status = 'closed';
|
||||||
dataSync.glpi_sync_status = 'ignored'; // Ignora no GLPI se já veio fechado do SN
|
dataSync.glpi_sync_status = 'ignored';
|
||||||
} else {
|
} else {
|
||||||
dataSync.sn_sync_status = 'collected'; // Status que indica que foi coletado do SN
|
dataSync.sn_sync_status = 'collected';
|
||||||
dataSync.glpi_sync_status = 'pending_check'; // Status que indica que precisa ser checkado no GLPI
|
dataSync.glpi_sync_status = 'pending_check';
|
||||||
}
|
}
|
||||||
dataSync.source_last = 'SNOW'; // A última atualização veio do ServiceNow
|
dataSync.source_last = 'SNOW';
|
||||||
|
|
||||||
// 4. Cria o registro de sincronização se ele não existir
|
|
||||||
const exists = await TicketSyncModel.getIdSyncBySnId(idTableTicketSn);
|
const exists = await TicketSyncModel.getIdSyncBySnId(idTableTicketSn);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
await TicketSyncModel.createSyncRecord(dataSync);
|
await TicketSyncModel.createSyncRecord(dataSync);
|
||||||
} else if (wasInserted === false) {
|
} else if (wasInserted === false) {
|
||||||
// Se o ticket foi atualizado (não inserido), significa que uma mudança veio do SN.
|
|
||||||
// Então, definimos o SN como a última fonte da verdade.
|
|
||||||
await TicketSyncModel.updateSourceLast(idTableTicketSn, 'SNOW');
|
await TicketSyncModel.updateSourceLast(idTableTicketSn, 'SNOW');
|
||||||
logInfo(`Ticket ${ticket.number} atualizado pelo ServiceNow. 'source_last' definido como 'SNOW'.`);
|
logInfo(`Ticket ${ticket.number} atualizado pelo ServiceNow. 'source_last' definido como 'SNOW'.`);
|
||||||
} else {
|
} else {
|
||||||
logInfo(`Registro de sincronização para o ticket ${ticket.number?.value} já existe. Nenhuma ação necessária.`);
|
logInfo(`Registro de sincronização para o ticket ${ticket.number?.value} já existe. Nenhuma ação necessária.`);
|
||||||
}
|
}
|
||||||
// Se já existe, não faz nada, pois a lógica de atualização de status será em outro processo.
|
|
||||||
|
|
||||||
// Guarda o timestamp mais recente do lote para usar como a próxima marca d'água
|
|
||||||
const currentUpdateDate = parseServiceNowDate(ticket.sys_updated_on);
|
const currentUpdateDate = parseServiceNowDate(ticket.sys_updated_on);
|
||||||
if (currentUpdateDate && currentUpdateDate.getTime() > latestUpdateTimestamp.getTime()) {
|
if (currentUpdateDate && currentUpdateDate.getTime() > latestUpdateTimestamp.getTime()) {
|
||||||
latestUpdateTimestamp = currentUpdateDate;
|
latestUpdateTimestamp = currentUpdateDate;
|
||||||
@ -112,7 +96,6 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
}
|
}
|
||||||
logSync('ServiceNow', tickets.length, `${type}s`);
|
logSync('ServiceNow', tickets.length, `${type}s`);
|
||||||
|
|
||||||
// Se encontramos uma data válida, a formata para 'YYYY-MM-DD HH:MM:SS'. Senão, retorna string vazia.
|
|
||||||
return latestUpdateTimestamp.getTime() > 0
|
return latestUpdateTimestamp.getTime() > 0
|
||||||
? latestUpdateTimestamp.toISOString().slice(0, 19).replace('T', ' ')
|
? latestUpdateTimestamp.toISOString().slice(0, 19).replace('T', ' ')
|
||||||
: '';
|
: '';
|
||||||
@ -123,24 +106,18 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
*/
|
*/
|
||||||
const processSyncController = async () => {
|
const processSyncController = async () => {
|
||||||
try {
|
try {
|
||||||
// 1. Pega a marca d'água da última execução bem-sucedida
|
|
||||||
const watermark = await SyncControlModel.getWatermark('servicenow');
|
const watermark = await SyncControlModel.getWatermark('servicenow');
|
||||||
logInfo(`📡 Buscando tickets do ServiceNow atualizados desde: ${watermark}`);
|
logInfo(`📡 Buscando tickets do ServiceNow atualizados desde: ${watermark}`);
|
||||||
|
|
||||||
// Busca incidentes
|
|
||||||
|
|
||||||
const incidents = await fetchTicketsApi(watermark);
|
const incidents = await fetchTicketsApi(watermark);
|
||||||
const latestIncidentUpdate = await processAndSaveTickets(incidents, 'incidente');
|
const latestIncidentUpdate = await processAndSaveTickets(incidents, 'incidente');
|
||||||
|
|
||||||
// Busca requisições
|
|
||||||
logInfo('📡 Buscando requisições do ServiceNow...');
|
logInfo('📡 Buscando requisições do ServiceNow...');
|
||||||
const requests = await fetchRequestsApi(watermark);
|
const requests = await fetchRequestsApi(watermark);
|
||||||
const latestRequestUpdate = await processAndSaveTickets(requests, 'requisicao');
|
const latestRequestUpdate = await processAndSaveTickets(requests, 'requisicao');
|
||||||
|
|
||||||
// 2. Determina a nova marca d'água (o timestamp mais recente entre todos os tickets processados)
|
|
||||||
const newWatermark = latestIncidentUpdate > latestRequestUpdate ? latestIncidentUpdate : latestRequestUpdate;
|
const newWatermark = latestIncidentUpdate > latestRequestUpdate ? latestIncidentUpdate : latestRequestUpdate;
|
||||||
|
|
||||||
// 3. Se processamos algum ticket, atualiza a marca d'água para a próxima execução
|
|
||||||
if (newWatermark) {
|
if (newWatermark) {
|
||||||
// Adiciona 1 segundo para evitar reprocessar o último ticket em caso de timestamps iguais
|
// Adiciona 1 segundo para evitar reprocessar o último ticket em caso de timestamps iguais
|
||||||
const sixHoursInMillis = 6 * 60 * 60 * 1000;
|
const sixHoursInMillis = 6 * 60 * 60 * 1000;
|
||||||
@ -158,3 +135,18 @@ const processSyncController = async () => {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
processSyncController
|
processSyncController
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file processSyncController.js
|
||||||
|
* @description
|
||||||
|
* Este controlador é responsável por buscar novos tickets e atualizações do ServiceNow.
|
||||||
|
* Ele utiliza uma "marca d'água" (watermark) para buscar apenas os registros modificados desde a última execução,
|
||||||
|
* otimizando o processo.
|
||||||
|
*
|
||||||
|
* Funcionalidades:
|
||||||
|
* - Busca incidentes e requisições do ServiceNow com base na data da última atualização.
|
||||||
|
* - Para cada ticket, salva ou atualiza o registro no banco de dados intermediário (`tickets_sn`).
|
||||||
|
* - Cria ou atualiza o registro de controle na tabela `ticket_sync`, definindo o status inicial e a origem da atualização (`source_last`).
|
||||||
|
* - Implementa uma lógica de enriquecimento de dados para requisições com descrição vazia, buscando informações de variáveis de catálogo (`sc_item_option`).
|
||||||
|
* - Ao final, atualiza a marca d'água para a próxima execução, apenas se houverem tickets atualizados na execução vigente.
|
||||||
|
*/
|
||||||
@ -1,19 +1,20 @@
|
|||||||
// src/controllers/glpiController.js
|
|
||||||
const { syncTicketsToGlpi } = require('../services/glpiTicketService');
|
const { syncTicketsToGlpi } = require('../services/glpiTicketService');
|
||||||
const { processSyncController } = require('./processSyncController');
|
const { processSyncController } = require('./processSyncController');
|
||||||
|
|
||||||
|
|
||||||
const processTicketsController = async () => {
|
const processTicketsController = async () => {
|
||||||
|
|
||||||
// 1. Busca Incidentes e Requisições do Service Now e salva no banco intermediário
|
|
||||||
// Esta etapa prepara os dados para a sincronização com o GLPI.
|
|
||||||
await processSyncController();
|
await processSyncController();
|
||||||
|
|
||||||
//Sincroniza os tickets com o GLPI
|
|
||||||
await syncTicketsToGlpi();
|
await syncTicketsToGlpi();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
processTicketsController
|
processTicketsController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file processTicketsController.js
|
||||||
|
* @description Este controlador gerencia o fluxo inicial de criação de tickets.
|
||||||
|
* Ele primeiro invoca o `processSyncController` para buscar e salvar localmente os tickets do ServiceNow.
|
||||||
|
* Em seguida, chama o serviço `syncTicketsToGlpi` para processar esses tickets locais e criá-los no GLPI,
|
||||||
|
* estabelecendo a ligação inicial entre os sistemas.
|
||||||
|
*/
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
// src/models/locationMappingModel.js
|
|
||||||
const pool = require('../data/database');
|
const pool = require('../data/database');
|
||||||
const { logInfo, logError } = require('../utils/logger');
|
const { logInfo, logError } = require('../utils/logger');
|
||||||
|
|
||||||
class LocationMappingModel {
|
class LocationMappingModel {
|
||||||
|
|
||||||
// Buscar entity_id do GLPI baseado no location_id do ServiceNow
|
|
||||||
static async getGlpiEntityId(snLocationId) {
|
static async getGlpiEntityId(snLocationId) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -26,7 +23,6 @@ class LocationMappingModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar entity_id do GLPI baseado no location_name do ServiceNow
|
|
||||||
static async getGlpiEntityIdByName(snLocationName) {
|
static async getGlpiEntityIdByName(snLocationName) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -48,9 +44,6 @@ class LocationMappingModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Buscar entity (id e name) do GLPI baseado no location_id do ServiceNow
|
|
||||||
static async getGlpiEntity(snLocationId) {
|
static async getGlpiEntity(snLocationId) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -74,12 +67,8 @@ class LocationMappingModel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Buscar entity (id e name) do GLPI baseado no location_name do ServiceNow
|
|
||||||
static async getGlpiEntityByName(snLocationName) {
|
static async getGlpiEntityByName(snLocationName) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -105,8 +94,14 @@ class LocationMappingModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = LocationMappingModel;
|
module.exports = LocationMappingModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module LocationMappingModel
|
||||||
|
* @description Este módulo gerencia a interação com a tabela `location_mapping`, que é crucial para a correta atribuição de tickets no GLPI.
|
||||||
|
*
|
||||||
|
* Funcionalidades:
|
||||||
|
* - `getGlpiEntityId` / `getGlpiEntityIdByName`: Buscam o ID da entidade GLPI correspondente a uma localização do ServiceNow, usando o ID ou o nome da localização.
|
||||||
|
* - `getGlpiEntity` / `getGlpiEntityByName`: Buscam o objeto completo da entidade GLPI (ID e nome), que é usado para formatar o título do ticket no GLPI.
|
||||||
|
* Este mapeamento permite que tickets abertos para uma determinada localidade no ServiceNow sejam automaticamente associados à entidade correta no GLPI.
|
||||||
|
*/
|
||||||
@ -1,4 +1,3 @@
|
|||||||
// src/models/syncControlModel.js
|
|
||||||
const pool = require('../data/database');
|
const pool = require('../data/database');
|
||||||
const { logError, logInfo } = require('../utils/logger');
|
const { logError, logInfo } = require('../utils/logger');
|
||||||
|
|
||||||
@ -10,7 +9,6 @@ class SyncControlModel {
|
|||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
throw new Error(`Watermark para o sistema '${systemName}' não encontrada no banco de dados.`);
|
throw new Error(`Watermark para o sistema '${systemName}' não encontrada no banco de dados.`);
|
||||||
}
|
}
|
||||||
// Retorna no formato que o ServiceNow espera: 'YYYY-MM-DD HH:MM:SS'
|
|
||||||
return rows[0].last_watermark.toISOString().replace('T', ' ').substring(0, 19);
|
return rows[0].last_watermark.toISOString().replace('T', ' ').substring(0, 19);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `Erro ao buscar watermark para ${systemName}`);
|
logError(error, `Erro ao buscar watermark para ${systemName}`);
|
||||||
@ -35,3 +33,15 @@ class SyncControlModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = SyncControlModel;
|
module.exports = SyncControlModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module SyncControlModel
|
||||||
|
* @description Este módulo gerencia a tabela `sync_control`, que armazena metadados essenciais para o processo de sincronização,
|
||||||
|
* como as "marcas d'água" (watermarks).
|
||||||
|
*
|
||||||
|
* Funcionalidades:
|
||||||
|
* - `getWatermark(systemName)`: Obtém o timestamp do último registro processado com sucesso para um determinado sistema (ex: 'servicenow'). Isso garante que, na próxima execução, apenas os dados novos ou atualizados sejam buscados.
|
||||||
|
* - `setWatermark(systemName, watermark)`: Atualiza a marca d'água para um sistema após um ciclo de sincronização bem-sucedido.
|
||||||
|
*
|
||||||
|
* O uso de watermarks é fundamental para a eficiência do processo, evitando a re-leitura de todos os tickets a cada execução.
|
||||||
|
*/
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
// src/models/ticketGlpiModel.js
|
|
||||||
const glpiPool = require('../data/glpiDataBase');
|
const glpiPool = require('../data/glpiDataBase');
|
||||||
const { logInfo, logError } = require('../utils/logger');
|
const { logInfo, logError } = require('../utils/logger');
|
||||||
const LocationMappingModel = require('./locationMappingModel');
|
const LocationMappingModel = require('./locationMappingModel');
|
||||||
|
|
||||||
class TicketGlpiModel {
|
class TicketGlpiModel {
|
||||||
|
|
||||||
// Mapeamento de status do ServiceNow para o GLPI
|
|
||||||
static mapStatus(snState) {
|
static mapStatus(snState) {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
'Aguardando Atendimento': 1,
|
'Aguardando Atendimento': 1,
|
||||||
@ -13,12 +11,11 @@ class TicketGlpiModel {
|
|||||||
'Em Espera': 4,
|
'Em Espera': 4,
|
||||||
'Resolvido': 5,
|
'Resolvido': 5,
|
||||||
'Encerrado - Omitido': 6,
|
'Encerrado - Omitido': 6,
|
||||||
'Encerrado': 6 // Mapeia para Fechado no GLPI
|
'Encerrado': 6
|
||||||
};
|
};
|
||||||
return statusMap[snState] || 1;
|
return statusMap[snState] || 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapeamento de status do GLPI (numérico) para ServiceNow (string)
|
|
||||||
static mapGlpiStatusToString(glpiStatus) {
|
static mapGlpiStatusToString(glpiStatus) {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
1: 'Aguardando Atendimento', // Novo
|
1: 'Aguardando Atendimento', // Novo
|
||||||
@ -30,7 +27,6 @@ class TicketGlpiModel {
|
|||||||
return statusMap[glpiStatus] || 'Desconhecido';
|
return statusMap[glpiStatus] || 'Desconhecido';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapeamento de prioridade
|
|
||||||
static mapPriority(snPriority) {
|
static mapPriority(snPriority) {
|
||||||
const priorityMap = {
|
const priorityMap = {
|
||||||
'1': 5,
|
'1': 5,
|
||||||
@ -42,7 +38,6 @@ class TicketGlpiModel {
|
|||||||
return priorityMap[snPriority] || 3;
|
return priorityMap[snPriority] || 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapeamento de categoria
|
|
||||||
static mapCategory(shortDescription) {
|
static mapCategory(shortDescription) {
|
||||||
const categoryMap = {
|
const categoryMap = {
|
||||||
'Problemas com Telefonia Fixa': 5717,
|
'Problemas com Telefonia Fixa': 5717,
|
||||||
@ -53,7 +48,6 @@ class TicketGlpiModel {
|
|||||||
return categoryMap[shortDescription] || 5717;
|
return categoryMap[shortDescription] || 5717;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapeamento de SLA baseado no tipo e na descrição
|
|
||||||
static mapSlasIdTtr(ticketData) {
|
static mapSlasIdTtr(ticketData) {
|
||||||
const isRequisicao = ticketData.type === 'requisicao';
|
const isRequisicao = ticketData.type === 'requisicao';
|
||||||
const shortDesc = ticketData.short_description;
|
const shortDesc = ticketData.short_description;
|
||||||
@ -73,10 +67,9 @@ class TicketGlpiModel {
|
|||||||
if (!isRequisicao && isTelefonia) {
|
if (!isRequisicao && isTelefonia) {
|
||||||
return 34;
|
return 34;
|
||||||
}
|
}
|
||||||
return 0; // Default se nenhum critério for atendido
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formata o título do ticket
|
|
||||||
static async formatTitle(ticketData) {
|
static async formatTitle(ticketData) {
|
||||||
try {
|
try {
|
||||||
let entityData = null;
|
let entityData = null;
|
||||||
@ -92,8 +85,8 @@ class TicketGlpiModel {
|
|||||||
const tituloSn = ticketData.short_description || 'Sem título';
|
const tituloSn = ticketData.short_description || 'Sem título';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${tipo} - ${entityName} - ${tituloSn}`,
|
title: `${tipo} - ${entityName} - ${tituloSn}`, // Título formatado
|
||||||
entityId: entityData?.id || 127 // fallback para entity_id default
|
entityId: entityData?.id || 127
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, 'Erro ao formatar título do ticket');
|
logError(error, 'Erro ao formatar título do ticket');
|
||||||
@ -107,7 +100,6 @@ class TicketGlpiModel {
|
|||||||
|
|
||||||
|
|
||||||
static formatDescription(ticketData) {
|
static formatDescription(ticketData) {
|
||||||
// Função auxiliar para extrair informações usando regex
|
|
||||||
const extractInfo = (regex, description) => {
|
const extractInfo = (regex, description) => {
|
||||||
if (!description) return null;
|
if (!description) return null;
|
||||||
const match = description.match(regex);
|
const match = description.match(regex);
|
||||||
@ -123,12 +115,10 @@ class TicketGlpiModel {
|
|||||||
return extractInfo(/(\d{3,4})/i, description);
|
return extractInfo(/(\d{3,4})/i, description);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Obtém email, telefone e ramal, usando os valores do ticketData ou extraindo da descrição, se necessário.
|
|
||||||
const email = ticketData.caller_email || extractEmailFromDescription(ticketData.description) || 'N/A';
|
const email = ticketData.caller_email || extractEmailFromDescription(ticketData.description) || 'N/A';
|
||||||
const phone = ticketData.caller_phone || extractPhoneFromDescription(ticketData.description) || 'N/A';
|
const phone = ticketData.caller_phone || extractPhoneFromDescription(ticketData.description) || 'N/A';
|
||||||
const ramal = ticketData.caller_ramal || extractRamalFromDescription(ticketData.description) || 'N/A';
|
const ramal = ticketData.caller_ramal || extractRamalFromDescription(ticketData.description) || 'N/A';
|
||||||
|
|
||||||
// Formata a descrição em HTML
|
|
||||||
let htmlDescription = `
|
let htmlDescription = `
|
||||||
<table style="width:100%; border-collapse: collapse;">
|
<table style="width:100%; border-collapse: collapse;">
|
||||||
<tr style="background-color:#f2f2f2;">
|
<tr style="background-color:#f2f2f2;">
|
||||||
@ -164,23 +154,19 @@ class TicketGlpiModel {
|
|||||||
return htmlDescription;
|
return htmlDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cria um ticket no GLPI
|
|
||||||
static async createTicket(ticketData) {
|
static async createTicket(ticketData) {
|
||||||
const connection = await glpiPool.getConnection();
|
const connection = await glpiPool.getConnection();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
// Buscar entity_id com base no location_id ou location_name
|
let entityId = 127;
|
||||||
let entityId = 127; // Valor padrão (fallback)
|
|
||||||
|
|
||||||
if (ticketData.location_id) {
|
if (ticketData.location_id) {
|
||||||
entityId = await LocationMappingModel.getGlpiEntityId(ticketData.location_id) || entityId;
|
entityId = await LocationMappingModel.getGlpiEntityId(ticketData.location_id) || entityId;
|
||||||
} else if (ticketData.location_name) {
|
} else if (ticketData.location_name) {
|
||||||
entityId = await LocationMappingModel.getGlpiEntityIdByName(ticketData.location_name) || entityId;
|
entityId = await LocationMappingModel.getGlpiEntityIdByName(ticketData.location_name) || entityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extrai o email da descrição, se existir e não houver email no ticketData
|
|
||||||
let email = ticketData.caller_email;
|
let email = ticketData.caller_email;
|
||||||
if (!email && ticketData.description) {
|
if (!email && ticketData.description) {
|
||||||
const emailRegex = /[\w-\.]+@([\w-]+\.)+[\w-]{2,4}/g;
|
const emailRegex = /[\w-\.]+@([\w-]+\.)+[\w-]{2,4}/g;
|
||||||
@ -190,18 +176,13 @@ class TicketGlpiModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formata o título do ticket
|
|
||||||
const { title: formattedTitle } = await TicketGlpiModel.formatTitle(ticketData);
|
const { title: formattedTitle } = await TicketGlpiModel.formatTitle(ticketData);
|
||||||
// Formata a descrição para HTML
|
|
||||||
const formattedDescription = TicketGlpiModel.formatDescription(ticketData);
|
const formattedDescription = TicketGlpiModel.formatDescription(ticketData);
|
||||||
// Mapeia o tipo do ticket
|
|
||||||
const ticketType = ticketData.type === 'requisicao' ? 2 : 1;
|
const ticketType = ticketData.type === 'requisicao' ? 2 : 1;
|
||||||
// Determina o valor de slas_id_ttr
|
|
||||||
const slasIdTtr = TicketGlpiModel.mapSlasIdTtr(ticketData);
|
const slasIdTtr = TicketGlpiModel.mapSlasIdTtr(ticketData);
|
||||||
|
|
||||||
// Prepara os dados para o GLPI
|
|
||||||
const glpiTicketData = {
|
const glpiTicketData = {
|
||||||
entities_id: entityId, // Usar o entity_id mapeado
|
entities_id: entityId,
|
||||||
name: formattedTitle,
|
name: formattedTitle,
|
||||||
date: ticketData.opened_at || new Date(),
|
date: ticketData.opened_at || new Date(),
|
||||||
date_mod: ticketData.updated_at || new Date(),
|
date_mod: ticketData.updated_at || new Date(),
|
||||||
@ -219,7 +200,6 @@ class TicketGlpiModel {
|
|||||||
slas_id_ttr: slasIdTtr
|
slas_id_ttr: slasIdTtr
|
||||||
};
|
};
|
||||||
|
|
||||||
// Constrói a query dinamicamente e insere o ticket principal
|
|
||||||
const fields = Object.keys(glpiTicketData).join(', ');
|
const fields = Object.keys(glpiTicketData).join(', ');
|
||||||
const placeholders = Object.keys(glpiTicketData).map(() => '?').join(', ');
|
const placeholders = Object.keys(glpiTicketData).map(() => '?').join(', ');
|
||||||
const values = Object.values(glpiTicketData);
|
const values = Object.values(glpiTicketData);
|
||||||
@ -228,7 +208,6 @@ class TicketGlpiModel {
|
|||||||
const [ticketResult] = await connection.execute(ticketQuery, values);
|
const [ticketResult] = await connection.execute(ticketQuery, values);
|
||||||
const ticketId = ticketResult.insertId;
|
const ticketId = ticketResult.insertId;
|
||||||
|
|
||||||
// Adiciona o requerente
|
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT INTO glpi_tickets_users
|
`INSERT INTO glpi_tickets_users
|
||||||
(tickets_id, users_id, type, use_notification, alternative_email)
|
(tickets_id, users_id, type, use_notification, alternative_email)
|
||||||
@ -236,7 +215,6 @@ class TicketGlpiModel {
|
|||||||
[ticketId, 917]
|
[ticketId, 917]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Adiciona o grupo
|
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT INTO glpi_groups_tickets
|
`INSERT INTO glpi_groups_tickets
|
||||||
(tickets_id, groups_id, type)
|
(tickets_id, groups_id, type)
|
||||||
@ -258,7 +236,6 @@ class TicketGlpiModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifica se um ticket já existe no GLPI
|
|
||||||
static async ticketExists(snTicketNumber) {
|
static async ticketExists(snTicketNumber) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -279,7 +256,7 @@ class TicketGlpiModel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Buscar followups (notas) de um ticket no GLPI
|
|
||||||
static async getFollowupsByItemId(itemId) {
|
static async getFollowupsByItemId(itemId) {
|
||||||
const connection = await glpiPool.getConnection();
|
const connection = await glpiPool.getConnection();
|
||||||
try {
|
try {
|
||||||
@ -299,7 +276,7 @@ class TicketGlpiModel {
|
|||||||
const values = ['Ticket', items_id, 971, comment.content , 1];
|
const values = ['Ticket', items_id, 971, comment.content , 1];
|
||||||
|
|
||||||
const [result] = await connection.execute(query, values);
|
const [result] = await connection.execute(query, values);
|
||||||
return [{ id: result.insertId }]; // Retorna o ID inserido
|
return [{ id: result.insertId }];
|
||||||
}catch(error){
|
}catch(error){
|
||||||
logError(`Erro ao inserir o comentário no GLPI: ${error}`);
|
logError(`Erro ao inserir o comentário no GLPI: ${error}`);
|
||||||
} finally {
|
} finally {
|
||||||
@ -307,11 +284,9 @@ class TicketGlpiModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca tickets que foram fechados ou solucionados no GLPI
|
|
||||||
static async getClosedTickets(watermark, limit = 100) {
|
static async getClosedTickets(watermark, limit = 100) {
|
||||||
const connection = await glpiPool.getConnection();
|
const connection = await glpiPool.getConnection();
|
||||||
try {
|
try {
|
||||||
// Status 5 = Solucionado, Status 6 = Fechado
|
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
@ -332,17 +307,14 @@ class TicketGlpiModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fecha um ticket no GLPI, adicionando uma solução
|
|
||||||
static async closeTicket(ticketId, solutionContent) {
|
static async closeTicket(ticketId, solutionContent) {
|
||||||
const connection = await glpiPool.getConnection();
|
const connection = await glpiPool.getConnection();
|
||||||
try {
|
try {
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
// Adiciona a solução
|
|
||||||
const solutionQuery = `INSERT INTO glpi_itilsolutions (itemtype, items_id, content, users_id_editor, date_creation, date_mod) VALUES ('Ticket', ?, ?, ?, NOW(), NOW())`;
|
const solutionQuery = `INSERT INTO glpi_itilsolutions (itemtype, items_id, content, users_id_editor, date_creation, date_mod) VALUES ('Ticket', ?, ?, ?, NOW(), NOW())`;
|
||||||
await connection.execute(solutionQuery, [ticketId, solutionContent, 971]); // 971 = ID do usuário da integração
|
await connection.execute(solutionQuery, [ticketId, solutionContent, 971]);
|
||||||
|
|
||||||
// Atualiza o status do ticket para "Solucionado" (status 5)
|
|
||||||
const updateTicketQuery = `UPDATE glpi_tickets SET status = 5, solvedate = NOW() WHERE id = ?`;
|
const updateTicketQuery = `UPDATE glpi_tickets SET status = 5, solvedate = NOW() WHERE id = ?`;
|
||||||
await connection.execute(updateTicketQuery, [ticketId]);
|
await connection.execute(updateTicketQuery, [ticketId]);
|
||||||
|
|
||||||
@ -356,7 +328,6 @@ class TicketGlpiModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca a solução de um ticket no GLPI
|
|
||||||
static async getTicketSolution(glpiTicketId) {
|
static async getTicketSolution(glpiTicketId) {
|
||||||
const connection = await glpiPool.getConnection();
|
const connection = await glpiPool.getConnection();
|
||||||
try {
|
try {
|
||||||
@ -375,17 +346,14 @@ class TicketGlpiModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Força o fechamento de um ticket (muda status para 6)
|
|
||||||
static async forceCloseTicket(ticketId) {
|
static async forceCloseTicket(ticketId) {
|
||||||
const connection = await glpiPool.getConnection();
|
const connection = await glpiPool.getConnection();
|
||||||
try {
|
try {
|
||||||
// Atualiza o status do ticket para "Fechado" (status 6)
|
|
||||||
const updateTicketQuery = `UPDATE glpi_tickets SET status = 6, closedate = NOW() WHERE id = ?`;
|
const updateTicketQuery = `UPDATE glpi_tickets SET status = 6, closedate = NOW() WHERE id = ?`;
|
||||||
await connection.execute(updateTicketQuery, [ticketId]);
|
await connection.execute(updateTicketQuery, [ticketId]);
|
||||||
logInfo(`✅ Ticket GLPI ${ticketId} forçado para o estado Fechado (6).`);
|
logInfo(`✅ Ticket GLPI ${ticketId} forçado para o estado Fechado (6).`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `❌ Erro ao forçar fechamento do ticket ${ticketId} no GLPI`);
|
logError(error, `❌ Erro ao forçar fechamento do ticket ${ticketId} no GLPI`);
|
||||||
// Não lança o erro para não parar o fluxo principal por uma ação secundária
|
|
||||||
} finally {
|
} finally {
|
||||||
if (connection) connection.release();
|
if (connection) connection.release();
|
||||||
}
|
}
|
||||||
@ -412,7 +380,6 @@ class TicketGlpiModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca o status atual de um ticket no GLPI
|
|
||||||
static async getTicketStatus(glpiTicketId) {
|
static async getTicketStatus(glpiTicketId) {
|
||||||
const connection = await glpiPool.getConnection();
|
const connection = await glpiPool.getConnection();
|
||||||
try {
|
try {
|
||||||
@ -431,18 +398,20 @@ class TicketGlpiModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module TicketGlpiModel
|
* @module TicketGlpiModel
|
||||||
* @description Este módulo fornece métodos para gerenciar tickets no GLPI.
|
* @description Este módulo é a camada de acesso a dados para o banco de dados do GLPI.
|
||||||
* Inclui funcionalidades para mapear status, prioridade e categoria,
|
* Ele encapsula todas as interações diretas com o banco de dados MySQL do GLPI,
|
||||||
* formatar a descrição do ticket, determinar o SLA, criar tickets e
|
* oferecendo uma interface clara para criar, atualizar e consultar tickets e seus componentes.
|
||||||
* verificar se um ticket já existe no GLPI.
|
|
||||||
*
|
*
|
||||||
* O módulo usa expressões regulares para extrair informações da descrição do ticket,
|
* Principais Funcionalidades:
|
||||||
* como email, número de telefone e ramal, quando essas informações não são
|
* - **Mapeamento de Dados**: Contém funções (`mapStatus`, `mapPriority`, `mapCategory`, etc.) para traduzir os dados do formato do ServiceNow para o formato esperado pelo GLPI.
|
||||||
* fornecidas diretamente. Também formata a descrição em HTML para melhor
|
* - **Formatação de Conteúdo**: `formatTitle` e `formatDescription` preparam o título e a descrição do ticket, enriquecendo-os com informações da entidade e formatando a descrição em uma tabela HTML para melhor legibilidade.
|
||||||
* apresentação no GLPI.
|
* - **Criação de Tickets (`createTicket`)**: Orquestra a criação de um novo ticket no GLPI, incluindo a inserção na tabela principal (`glpi_tickets`), a associação de usuários e grupos.
|
||||||
|
* - **Consulta de Dados**: Oferece métodos para verificar a existência de um ticket (`ticketExists`), buscar seu status (`getTicketStatus`), soluções (`getTicketSolution`) e comentários/follow-ups (`getFollowupsByItemId`).
|
||||||
|
* - **Atualização de Tickets**: Inclui métodos para atualizar o status (`updateStatus`), adicionar comentários (`insertComment`), e fechar tickets, seja de forma padrão com uma solução (`closeTicket`) ou forçando o estado para "Fechado" (`forceCloseTicket`).
|
||||||
*
|
*
|
||||||
* O módulo interage com o banco de dados GLPI através da conexão glpiPool.
|
* Este modelo é fundamental para abstrair a complexidade do esquema de banco de dados do GLPI e garantir que as operações sejam realizadas de forma transacional e segura.
|
||||||
*/
|
*/
|
||||||
module.exports = TicketGlpiModel;
|
module.exports = TicketGlpiModel;
|
||||||
@ -1,10 +1,8 @@
|
|||||||
// src/models/ticketSnModel.js
|
|
||||||
const pool = require('../data/database');
|
const pool = require('../data/database');
|
||||||
const { logInfo, logError, logWarning } = require('../utils/logger');
|
const { logInfo, logError, logWarning } = require('../utils/logger');
|
||||||
|
|
||||||
class TicketSnModel {
|
class TicketSnModel {
|
||||||
|
|
||||||
// Salva ou atualiza um ticket no banco de dados
|
|
||||||
static async saveTicket(ticketData, typeTicket) {
|
static async saveTicket(ticketData, typeTicket) {
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO tickets_sn
|
INSERT INTO tickets_sn
|
||||||
@ -25,19 +23,14 @@ class TicketSnModel {
|
|||||||
telefone = EXCLUDED.telefone,
|
telefone = EXCLUDED.telefone,
|
||||||
opened_at = EXCLUDED.opened_at,
|
opened_at = EXCLUDED.opened_at,
|
||||||
tipo = EXCLUDED.tipo
|
tipo = EXCLUDED.tipo
|
||||||
RETURNING id, (xmax = 0) AS "wasInserted"`; // xmax=0 é um truque do PostgreSQL para saber se foi INSERT
|
RETURNING id, (xmax = 0) AS "wasInserted"`;
|
||||||
|
|
||||||
// Trata a diferença na estrutura do campo 'number' entre Incidentes e Requisições
|
|
||||||
const ticketNumber = typeof ticketData.number === 'object' ? ticketData.number?.value : ticketData.number;
|
const ticketNumber = typeof ticketData.number === 'object' ? ticketData.number?.value : ticketData.number;
|
||||||
|
|
||||||
// Mapeamento inteligente de campos baseado no tipo de ticket
|
|
||||||
let callerName, callerEmail, ramal, telefone, sysId, shortDescription, state, description, locationId, locationName, openedAt;
|
let callerName, callerEmail, ramal, telefone, sysId, shortDescription, state, description, locationId, locationName, openedAt;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (typeTicket === 'incidente') {
|
if (typeTicket === 'incidente') {
|
||||||
callerName = ticketData.caller_id?.display_value;
|
callerName = ticketData.caller_id?.display_value;
|
||||||
// O campo de email para incidentes vem como um objeto, precisamos extrair o valor.
|
|
||||||
const emailObject = ticketData['caller_id.email'] || {};
|
const emailObject = ticketData['caller_id.email'] || {};
|
||||||
callerEmail = (typeof emailObject === 'object' ? emailObject.value : emailObject) || null;
|
callerEmail = (typeof emailObject === 'object' ? emailObject.value : emailObject) || null;
|
||||||
ramal = null; // Incidentes não têm essa variável por padrão
|
ramal = null; // Incidentes não têm essa variável por padrão
|
||||||
@ -59,7 +52,6 @@ class TicketSnModel {
|
|||||||
shortDescription = ticketData.short_description || null;
|
shortDescription = ticketData.short_description || null;
|
||||||
state = ticketData.state || null;
|
state = ticketData.state || null;
|
||||||
description = ticketData.description || null;
|
description = ticketData.description || null;
|
||||||
// Para requisições, o sys_id da localização vem no final da URL do link
|
|
||||||
const locationLink = ticketData.location?.link;
|
const locationLink = ticketData.location?.link;
|
||||||
locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null;
|
locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null;
|
||||||
locationName = ticketData.location?.display_value || null;
|
locationName = ticketData.location?.display_value || null;
|
||||||
@ -89,7 +81,7 @@ class TicketSnModel {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await pool.query(query, values);
|
const result = await pool.query(query, values);
|
||||||
const wasInserted = result.rows[0].wasInserted; // Corrigido para corresponder ao alias "wasInserted"
|
const wasInserted = result.rows[0].wasInserted;
|
||||||
|
|
||||||
if (wasInserted) {
|
if (wasInserted) {
|
||||||
logInfo(`✅ NOVO ticket salvo! ID: ${result.rows[0].id}`, {
|
logInfo(`✅ NOVO ticket salvo! ID: ${result.rows[0].id}`, {
|
||||||
@ -112,7 +104,6 @@ class TicketSnModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca tickets que ainda não foram sincronizados com o GLPI
|
|
||||||
static async getPendingTickets() {
|
static async getPendingTickets() {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -149,7 +140,6 @@ class TicketSnModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca um ticket pelo número
|
|
||||||
static async findByTicketNumber(ticketNumber) {
|
static async findByTicketNumber(ticketNumber) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -166,8 +156,6 @@ class TicketSnModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Busca o status de um ticket pelo ID
|
|
||||||
static async getStatus(ticketId) {
|
static async getStatus(ticketId) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -181,9 +169,6 @@ class TicketSnModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Busca um ticket pelo ID
|
|
||||||
static async findById(ticketId) {
|
static async findById(ticketId) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -200,24 +185,20 @@ class TicketSnModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atualiza os dados de um ticket
|
|
||||||
static async updateTicket(ticketId, updateData) {
|
static async updateTicket(ticketId, updateData) {
|
||||||
try {
|
try {
|
||||||
const fields = [];
|
const fields = [];
|
||||||
const values = [];
|
const values = [];
|
||||||
let paramCount = 1;
|
let paramCount = 1;
|
||||||
|
|
||||||
// Construir dinamicamente a query baseada nos campos fornecidos
|
|
||||||
for (const [key, value] of Object.entries(updateData)) {
|
for (const [key, value] of Object.entries(updateData)) {
|
||||||
fields.push(`${key} = $${paramCount}`);
|
fields.push(`${key} = $${paramCount}`);
|
||||||
values.push(value);
|
values.push(value);
|
||||||
paramCount++;
|
paramCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adicionar updated_at
|
|
||||||
fields.push(`updated_at = NOW()`);
|
fields.push(`updated_at = NOW()`);
|
||||||
|
|
||||||
// Adicionar o ID no final dos valores
|
|
||||||
values.push(ticketId);
|
values.push(ticketId);
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
@ -247,12 +228,13 @@ class TicketSnModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @module TicketSnModel
|
* @module TicketSnModel
|
||||||
* @description Este módulo fornece métodos para interagir com a tabela de tickets do ServiceNow (tickets_sn) no banco de dados.
|
* @description Este módulo serve como a camada de acesso a dados para a tabela `tickets_sn`, que armazena uma cópia local dos dados dos tickets do ServiceNow.
|
||||||
* Inclui funcionalidades para salvar um ticket, buscar tickets pendentes para sincronização com o GLPI,
|
* Manter uma cópia local otimiza as consultas e reduz a dependência de chamadas de API repetitivas ao ServiceNow.
|
||||||
* e buscar/atualizar tickets específicos.
|
|
||||||
*
|
*
|
||||||
* A função saveTicket insere ou atualiza um ticket na tabela tickets_sn.
|
* Principais Funcionalidades:
|
||||||
* A função getPendingTickets busca tickets que ainda não foram sincronizados com o GLPI (não possuem registro em ticket_sync)
|
* - `saveTicket(ticketData, typeTicket)`: Insere um novo ticket ou atualiza um existente (UPSERT) na tabela `tickets_sn`. Ele mapeia de forma inteligente os campos de incidentes e requisições, que possuem estruturas diferentes na API do ServiceNow. Retorna se o registro foi inserido ou atualizado.
|
||||||
* e ignora os tickets com status 'Encerrado' ou 'Encerrado - Omitido'.
|
* - `getPendingTickets()`: Busca todos os tickets que estão no estado 'pending_check', indicando que foram coletados do ServiceNow mas ainda não foram processados para criação no GLPI.
|
||||||
|
* - Funções de Busca (`findByTicketNumber`, `findById`, `getStatus`): Métodos para recuperar registros de tickets ou seus status com base em diferentes identificadores.
|
||||||
|
* - `updateTicket(ticketId, updateData)`: Atualiza dinamicamente os campos de um registro de ticket existente.
|
||||||
*/
|
*/
|
||||||
module.exports = TicketSnModel;
|
module.exports = TicketSnModel;
|
||||||
@ -1,15 +1,12 @@
|
|||||||
// src/models/ticketSyncModel.js
|
|
||||||
const { query } = require('winston');
|
const { query } = require('winston');
|
||||||
const pool = require('../data/database');
|
const pool = require('../data/database');
|
||||||
const { logError } = require('../utils/logger');
|
const { logError } = require('../utils/logger');
|
||||||
|
|
||||||
class TicketSyncModel {
|
class TicketSyncModel {
|
||||||
|
|
||||||
// Cria um registro de sincronização para um ticket
|
|
||||||
static async createSyncRecord(data) {
|
static async createSyncRecord(data) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
|
||||||
const query = `INSERT INTO ticket_sync (
|
const query = `INSERT INTO ticket_sync (
|
||||||
sn_ticket_id,
|
sn_ticket_id,
|
||||||
glpi_ticket_id,
|
glpi_ticket_id,
|
||||||
@ -43,8 +40,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Buscar tickets que foram criados com sucesso para sincronizar comentários
|
|
||||||
static async getTicketsToSync(glpi_sync_status, sn_sync_status) {
|
static async getTicketsToSync(glpi_sync_status, sn_sync_status) {
|
||||||
try {
|
try {
|
||||||
const query = 'SELECT id, glpi_ticket_id, sn_ticket_id FROM ticket_sync WHERE glpi_sync_status = $1 AND sn_sync_status = $2';
|
const query = 'SELECT id, glpi_ticket_id, sn_ticket_id FROM ticket_sync WHERE glpi_sync_status = $1 AND sn_sync_status = $2';
|
||||||
@ -57,7 +52,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atualiza apenas o source_last de um ticket
|
|
||||||
static async updateSourceLast(snTicketId, source) {
|
static async updateSourceLast(snTicketId, source) {
|
||||||
try {
|
try {
|
||||||
const query = 'UPDATE ticket_sync SET source_last = $1 WHERE sn_ticket_id = $2';
|
const query = 'UPDATE ticket_sync SET source_last = $1 WHERE sn_ticket_id = $2';
|
||||||
@ -68,7 +62,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atualiza o status de sincronização de um ticket
|
|
||||||
static async updateStatus(ticketSyncId, glpiStatus, snStatus) {
|
static async updateStatus(ticketSyncId, glpiStatus, snStatus) {
|
||||||
try {
|
try {
|
||||||
const query = 'UPDATE ticket_sync SET glpi_sync_status = $1, sn_sync_status = $2 WHERE sn_ticket_id = $3';
|
const query = 'UPDATE ticket_sync SET glpi_sync_status = $1, sn_sync_status = $2 WHERE sn_ticket_id = $3';
|
||||||
@ -78,7 +71,7 @@ class TicketSyncModel {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Atualiza o status de sincronização de um ticket e o source last
|
|
||||||
static async updateStatusAndSourceLast(ticketSyncId, glpiStatus, snStatus, source_last) {
|
static async updateStatusAndSourceLast(ticketSyncId, glpiStatus, snStatus, source_last) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@ -90,7 +83,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atualiza apenas o source_last de um ticket
|
|
||||||
static async updateSourceLast(snTicketId, source) {
|
static async updateSourceLast(snTicketId, source) {
|
||||||
try {
|
try {
|
||||||
const query = 'UPDATE ticket_sync SET source_last = $1 WHERE sn_ticket_id = $2';
|
const query = 'UPDATE ticket_sync SET source_last = $1 WHERE sn_ticket_id = $2';
|
||||||
@ -101,8 +93,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static async updateStatusAndGlpiTicket(ticketSyncId, glpiStatus, snStatus, glpiTicketId) {
|
static async updateStatusAndGlpiTicket(ticketSyncId, glpiStatus, snStatus, glpiTicketId) {
|
||||||
try {
|
try {
|
||||||
const query = 'UPDATE ticket_sync SET glpi_sync_status = $1, sn_sync_status = $2, glpi_ticket_id = $4 WHERE sn_ticket_id = $3';
|
const query = 'UPDATE ticket_sync SET glpi_sync_status = $1, sn_sync_status = $2, glpi_ticket_id = $4 WHERE sn_ticket_id = $3';
|
||||||
@ -113,7 +103,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar o ID de sincronização baseado no glpi_ticket_id
|
|
||||||
static async getIdSyncByGlpiId(glpiTicketId) {
|
static async getIdSyncByGlpiId(glpiTicketId) {
|
||||||
try {
|
try {
|
||||||
const query = 'SELECT id FROM ticket_sync WHERE glpi_ticket_id = $1';
|
const query = 'SELECT id FROM ticket_sync WHERE glpi_ticket_id = $1';
|
||||||
@ -125,7 +114,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar o ID de sincronização baseado no sys_id do SN
|
|
||||||
static async getBySysId(sysId) {
|
static async getBySysId(sysId) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -140,7 +128,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar o ID de sincronização baseado no sn_ticket_id
|
|
||||||
static async getIdSyncBySnId(snTicketId) {
|
static async getIdSyncBySnId(snTicketId) {
|
||||||
try {
|
try {
|
||||||
const query = 'SELECT id FROM ticket_sync WHERE sn_ticket_id = $1';
|
const query = 'SELECT id FROM ticket_sync WHERE sn_ticket_id = $1';
|
||||||
@ -151,6 +138,7 @@ class TicketSyncModel {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getByTicketNumber(ticketNumber) {
|
static async getByTicketNumber(ticketNumber) {
|
||||||
try {
|
try {
|
||||||
const query = 'SELECT * FROM ticket_sync WHERE sn_ticket_id = $1';
|
const query = 'SELECT * FROM ticket_sync WHERE sn_ticket_id = $1';
|
||||||
@ -163,7 +151,6 @@ class TicketSyncModel {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar tickets que estão sincronizados
|
|
||||||
static async getTicketsSynced() {
|
static async getTicketsSynced() {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -184,7 +171,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar tickets que estão ativos para monitoramento (synced ou solved)
|
|
||||||
static async getTicketsToMonitor() {
|
static async getTicketsToMonitor() {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -208,7 +194,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar tickets que precisam ter o fechamento sincronizado
|
|
||||||
static async getTicketsPendingSolve() {
|
static async getTicketsPendingSolve() {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -231,7 +216,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca todos os tickets que estão em algum estado de erro
|
|
||||||
static async getTicketsInErrorState() {
|
static async getTicketsInErrorState() {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
@ -247,7 +231,6 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atualiza os timestamps de última sincronização
|
|
||||||
static async updateLastSync(snTicketId, system) {
|
static async updateLastSync(snTicketId, system) {
|
||||||
try {
|
try {
|
||||||
let fieldToUpdate;
|
let fieldToUpdate;
|
||||||
@ -256,17 +239,15 @@ class TicketSyncModel {
|
|||||||
} else if (system === 'glpi') {
|
} else if (system === 'glpi') {
|
||||||
fieldToUpdate = 'last_glpi_sync';
|
fieldToUpdate = 'last_glpi_sync';
|
||||||
} else {
|
} else {
|
||||||
return; // Não faz nada se o sistema for inválido
|
return;
|
||||||
}
|
}
|
||||||
const query = `UPDATE ticket_sync SET ${fieldToUpdate} = NOW() WHERE sn_ticket_id = $1`;
|
const query = `UPDATE ticket_sync SET ${fieldToUpdate} = NOW() WHERE sn_ticket_id = $1`;
|
||||||
await pool.query(query, [snTicketId]);
|
await pool.query(query, [snTicketId]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(`${error} ❌ Erro ao atualizar last_sync para o ID: ${snTicketId}`);
|
logError(`${error} ❌ Erro ao atualizar last_sync para o ID: ${snTicketId}`);
|
||||||
// Não lançar erro para não parar o fluxo principal
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atualiza o status de sincronização de um ticket usando o GLPI ID
|
|
||||||
static async updateStatusByGlpiId(glpiTicketId, snStatus, glpiStatus) {
|
static async updateStatusByGlpiId(glpiTicketId, snStatus, glpiStatus) {
|
||||||
try {
|
try {
|
||||||
const query = 'UPDATE ticket_sync SET sn_sync_status = $1, glpi_sync_status = $2 WHERE glpi_ticket_id = $3';
|
const query = 'UPDATE ticket_sync SET sn_sync_status = $1, glpi_sync_status = $2 WHERE glpi_ticket_id = $3';
|
||||||
@ -281,14 +262,16 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @module TicketSyncModel
|
* @module TicketSyncModel
|
||||||
* @description Este módulo fornece métodos para interagir com a tabela de sincronização de tickets (ticket_sync) no banco de dados.
|
* @description Este módulo é a camada de acesso à tabela `ticket_sync`, que atua como o cérebro do processo de sincronização.
|
||||||
* Inclui funcionalidades para buscar tickets para sincronização, atualizar o status de sincronização
|
* Ele armazena o estado de cada ticket em ambos os sistemas (GLPI e ServiceNow) e controla o fluxo de atualizações.
|
||||||
* e buscar o ID de sincronização baseado no glpi_ticket_id.
|
|
||||||
*
|
*
|
||||||
* A função getTicketsToSync busca todos os tickets que possuem IDs tanto no GLPI quanto no ServiceNow.
|
* Principais Funcionalidades:
|
||||||
* A função updateStatus atualiza os status de sincronização do GLPI e do ServiceNow para um ticket específico.
|
* - **Gerenciamento de Estado**: Métodos como `updateStatus`, `updateStatusAndSourceLast` e `updateStatusAndGlpiTicket` são usados para modificar os status de sincronização (ex: 'pending_check', 'synced', 'solved', 'closed', 'error').
|
||||||
* A função getIdSyncByGlpiId retorna o ID de sincronização baseado no glpi_ticket_id.
|
* - **Controle de Fluxo (`source_last`)**: A coluna `source_last` é gerenciada por este modelo para indicar qual sistema realizou a última modificação, prevenindo atualizações conflitantes (race conditions).
|
||||||
* Essas funcionalidades são essenciais para manter a consistência e o rastreamento dos tickets entre os dois sistemas.
|
* - **Seleção de Tickets para Processamento**: Métodos como `getTicketsToMonitor`, `getTicketsInErrorState`, e `getTicketsSynced` fornecem listas de tickets para os diferentes controladores processarem, com base em seus status atuais.
|
||||||
|
* - **Criação e Consulta**: `createSyncRecord` cria o registro inicial de um ticket, e várias funções de busca (`getBySysId`, `getIdSyncByGlpiId`, etc.) permitem localizar registros de sincronização.
|
||||||
|
*
|
||||||
|
* A tabela `ticket_sync` é a fonte da verdade para o estado da integração de cada ticket individualmente.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
// src/models/ticketUpdateModel.js
|
|
||||||
const ticketSyncModel = require('./ticketSyncModel');
|
const ticketSyncModel = require('./ticketSyncModel');
|
||||||
const pool = require('../data/database');
|
const pool = require('../data/database');
|
||||||
const { logError } = require('../utils/logger');
|
const { logError } = require('../utils/logger');
|
||||||
@ -23,7 +22,7 @@ class TicketUpdateModel {
|
|||||||
`;
|
`;
|
||||||
const values = [
|
const values = [
|
||||||
updateData.ticket_sync_id,
|
updateData.ticket_sync_id,
|
||||||
updateData.update_type, // Corrigido para usar o tipo de atualização dinâmico
|
updateData.update_type,
|
||||||
updateData.source_system,
|
updateData.source_system,
|
||||||
updateData.content,
|
updateData.content,
|
||||||
updateData.author,
|
updateData.author,
|
||||||
@ -103,4 +102,17 @@ class TicketUpdateModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TicketUpdateModel;
|
module.exports = TicketUpdateModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module TicketUpdateModel
|
||||||
|
* @description Este módulo gerencia a tabela `ticket_updates`, que serve como um log persistente de todas as atualizações (comentários, mudanças de status, etc.)
|
||||||
|
* que ocorrem em ambos os sistemas. Ele é fundamental para a sincronização de comentários e para evitar a duplicação de dados.
|
||||||
|
*
|
||||||
|
* Funcionalidades:
|
||||||
|
* - `insert(updateData)`: Insere ou atualiza (UPSERT) um registro de atualização. A cláusula `ON CONFLICT` é usada para evitar duplicatas com base no `source_id`.
|
||||||
|
* - `getBySourceId(sourceId)`: Verifica se uma atualização específica (ex: um comentário com um determinado ID) já foi registrada no banco de dados.
|
||||||
|
* - `updateDestinyId(sourceId, destinyId)`: Após uma atualização ser sincronizada com o sistema de destino, este método armazena o ID gerado no destino, completando o ciclo de mapeamento.
|
||||||
|
* - `getLatestUpdate(...)`: Busca a atualização mais recente para um determinado ticket, útil para obter notas de fechamento, por exemplo.
|
||||||
|
*/
|
||||||
@ -11,8 +11,17 @@ import pandas as pd
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
# Carrega as variáveis do .env
|
# Carrega as variáveis do .env
|
||||||
load_dotenv()
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Detecta ambiente
|
||||||
|
env = os.getenv('NODE_ENV', 'development')
|
||||||
|
env_file = f".env.{env}"
|
||||||
|
|
||||||
|
# Caminho absoluto (mesma pasta do script ou raiz do projeto)
|
||||||
|
env_path = Path(__file__).resolve().parent.parent / env_file
|
||||||
|
load_dotenv(dotenv_path=env_path)
|
||||||
|
|
||||||
|
print(f"🌎 Ambiente carregado: {env}")
|
||||||
# Configuração de logging
|
# Configuração de logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
// src/services/glpiCommentService.js
|
|
||||||
const TicketSnModel = require('../models/ticketSnModel');
|
const TicketSnModel = require('../models/ticketSnModel');
|
||||||
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
||||||
const TicketSyncModel = require('../models/ticketSyncModel');
|
const TicketSyncModel = require('../models/ticketSyncModel');
|
||||||
@ -8,11 +7,9 @@ const { existingCommentInServiceNow, syncCommentToServiceNow } = require('./serv
|
|||||||
const { sanitizeGLPIComment } = require('../utils/commentSanitizer');
|
const { sanitizeGLPIComment } = require('../utils/commentSanitizer');
|
||||||
const { log } = require('winston');
|
const { log } = require('winston');
|
||||||
|
|
||||||
// Sincroniza comentários do GLPI para o Banco de Dados local
|
|
||||||
const syncCommentsGlpitoSN = async (ticket) => {
|
const syncCommentsGlpitoSN = async (ticket) => {
|
||||||
try {
|
try {
|
||||||
logInfo('🔄 Iniciando sincronização de comments do GLPI para o Service Now...');
|
logInfo('🔄 Iniciando sincronização de comments do GLPI para o Service Now...');
|
||||||
|
|
||||||
const comments = await TicketGlpiModel.getFollowupsByItemId(ticket.glpi_ticket_id);
|
const comments = await TicketGlpiModel.getFollowupsByItemId(ticket.glpi_ticket_id);
|
||||||
await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi'); // Atualiza o timestamp
|
await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi'); // Atualiza o timestamp
|
||||||
if (comments.length === 0) {
|
if (comments.length === 0) {
|
||||||
@ -30,15 +27,11 @@ const syncCommentsGlpitoSN = async (ticket) => {
|
|||||||
const existsInSN = await existingCommentInServiceNow(ticket.sn_ticket_id, sanitized);
|
const existsInSN = await existingCommentInServiceNow(ticket.sn_ticket_id, sanitized);
|
||||||
const syncId = await TicketSyncModel.getIdSyncByGlpiId(ticket.glpi_ticket_id);
|
const syncId = await TicketSyncModel.getIdSyncByGlpiId(ticket.glpi_ticket_id);
|
||||||
|
|
||||||
// Caso 1: já sincronizado
|
|
||||||
if (existingComment && existsInSN) {
|
if (existingComment && existsInSN) {
|
||||||
logInfo(`🔁 Comentário ${comment.id} já sincronizado.`);
|
logInfo(`🔁 Comentário ${comment.id} já sincronizado.`);
|
||||||
// Não muda o source_last aqui, pois não houve nova sincronização
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Caso 2: está no SN mas não no banco
|
|
||||||
if (!existingComment && existsInSN) {
|
if (!existingComment && existsInSN) {
|
||||||
await TicketUpdateModel.insert({
|
await TicketUpdateModel.insert({
|
||||||
ticket_sync_id: syncId,
|
ticket_sync_id: syncId,
|
||||||
@ -50,11 +43,9 @@ const syncCommentsGlpitoSN = async (ticket) => {
|
|||||||
source_id: comment.id,
|
source_id: comment.id,
|
||||||
destiny_id: existsInSN.destiny_id
|
destiny_id: existsInSN.destiny_id
|
||||||
});
|
});
|
||||||
// Não muda o source_last aqui
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caso 3: existe no banco mas falta no SN
|
|
||||||
if (existingComment && !existsInSN) {
|
if (existingComment && !existsInSN) {
|
||||||
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'pending_cmt_sync');
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'pending_cmt_sync');
|
||||||
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitized);
|
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitized);
|
||||||
@ -63,7 +54,6 @@ const syncCommentsGlpitoSN = async (ticket) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caso 4: não existe em nenhum lugar → sincroniza ambos
|
|
||||||
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'pending_cmt_sync', 'pending_cmt_sync');
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'pending_cmt_sync', 'pending_cmt_sync');
|
||||||
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitized);
|
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitized);
|
||||||
await TicketUpdateModel.insert({
|
await TicketUpdateModel.insert({
|
||||||
@ -76,8 +66,6 @@ const syncCommentsGlpitoSN = async (ticket) => {
|
|||||||
source_id: comment.id,
|
source_id: comment.id,
|
||||||
destiny_id: snCommentId
|
destiny_id: snCommentId
|
||||||
});
|
});
|
||||||
// Apenas aqui, quando um comentário é efetivamente sincronizado, passamos o bastão,
|
|
||||||
// mas SOMENTE se o ticket não estiver já resolvido ou fechado.
|
|
||||||
if (ticket.sn_sync_status === 'synced' && ticket.glpi_sync_status === 'synced') {
|
if (ticket.sn_sync_status === 'synced' && ticket.glpi_sync_status === 'synced') {
|
||||||
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'GLPI');
|
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'GLPI');
|
||||||
}
|
}
|
||||||
@ -101,8 +89,7 @@ const syncCommentsSNtoGlpi = async (comments, ticket) => {
|
|||||||
for (const comment of comments) {
|
for (const comment of comments) {
|
||||||
try {
|
try {
|
||||||
const commentId = await TicketGlpiModel.insertComment(comment, ticket.glpi_ticket_id);
|
const commentId = await TicketGlpiModel.insertComment(comment, ticket.glpi_ticket_id);
|
||||||
await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi'); // Atualiza o timestamp
|
await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi');
|
||||||
// Passa o bastão para o SN, mas SOMENTE se o ticket não estiver já resolvido ou fechado.
|
|
||||||
if (ticket.sn_sync_status === 'synced' && ticket.glpi_sync_status === 'synced') {
|
if (ticket.sn_sync_status === 'synced' && ticket.glpi_sync_status === 'synced') {
|
||||||
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW');
|
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW');
|
||||||
}
|
}
|
||||||
@ -122,18 +109,20 @@ const syncCommentsSNtoGlpi = async (comments, ticket) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module glpiCommentService
|
* @module glpiCommentService
|
||||||
* @description Este módulo fornece serviços para sincronizar comentários do GLPI para o banco de dados local.
|
* @description Este serviço orquestra a sincronização de comentários (follow-ups/notas) entre GLPI e ServiceNow.
|
||||||
* A função syncCommentsGlpi busca tickets que foram criados com sucesso e sincroniza seus comentários,
|
*
|
||||||
* garantindo que apenas novos comentários sejam inseridos no banco de dados.synced
|
* Principais Funcionalidades:
|
||||||
* Utiliza os modelos TicketSnModel, TicketGlpiModel, TicketSyncModel e TicketUpdateModel para interagir com o banco de dados.
|
* - `syncCommentsGlpitoSN(ticket)`: Sincroniza comentários do GLPI para o ServiceNow. Esta é a função mais complexa, pois implementa uma lógica de 4 casos para garantir a consistência e evitar duplicatas:
|
||||||
* Atualiza o status de sincronização após cada comentário processado.
|
* 1. **Já Sincronizado**: O comentário existe no banco local e no ServiceNow. Nenhuma ação é necessária.
|
||||||
* Inclui logging detalhado para monitorar o progresso e identificar erros durante o processo de sincronização.
|
* 2. **Existe no SN, Falta no Local**: O comentário foi sincronizado em uma execução anterior que falhou. O registro local é criado para refletir a realidade.
|
||||||
|
* 3. **Existe no Local, Falta no SN**: O comentário foi registrado localmente, mas a sincronização com o SN falhou. A função reenvia o comentário para o ServiceNow.
|
||||||
|
* 4. **Não Existe em Nenhum**: Um novo comentário do GLPI. Ele é enviado para o ServiceNow e um novo registro é criado no banco local.
|
||||||
|
*
|
||||||
|
* - `syncCommentsSNtoGlpi(comments, ticket)`: Sincroniza comentários do ServiceNow para o GLPI. Este fluxo é mais simples: para cada novo comentário do SN (já filtrado e salvo localmente pelo `servicenowService`), ele o insere no GLPI.
|
||||||
|
*
|
||||||
|
* Em ambos os fluxos, o "bastão" (`source_last`) é atualizado para indicar qual sistema originou a última atualização, mantendo o controle do fluxo de status.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
// src/services/glpiService.js
|
|
||||||
const TicketSnModel = require('../models/ticketSnModel');
|
const TicketSnModel = require('../models/ticketSnModel');
|
||||||
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
||||||
const TicketUpdateModel = require('../models/ticketUpdateModel');
|
const TicketUpdateModel = require('../models/ticketUpdateModel');
|
||||||
@ -6,12 +5,9 @@ const TicketSyncModel = require('../models/ticketSyncModel');
|
|||||||
const { logInfo, logError, logWarning } = require('../utils/logger');
|
const { logInfo, logError, logWarning } = require('../utils/logger');
|
||||||
const { addWorkNoteToServiceNow } = require('./servicenowService');
|
const { addWorkNoteToServiceNow } = require('./servicenowService');
|
||||||
|
|
||||||
|
|
||||||
// Método principal que o controller vai chamar
|
|
||||||
const syncTicketsToGlpi = async () => {
|
const syncTicketsToGlpi = async () => {
|
||||||
try {
|
try {
|
||||||
logInfo('🔄 Iniciando sincronização para GLPI...');
|
logInfo('🔄 Iniciando sincronização para GLPI...');
|
||||||
// 1. Buscar tickets pendentes
|
|
||||||
const pendingTickets = await TicketSnModel.getPendingTickets();
|
const pendingTickets = await TicketSnModel.getPendingTickets();
|
||||||
|
|
||||||
if (pendingTickets.length === 0) {
|
if (pendingTickets.length === 0) {
|
||||||
@ -21,7 +17,6 @@ const syncTicketsToGlpi = async () => {
|
|||||||
|
|
||||||
logInfo(`📋 Encontrados ${pendingTickets.length} tickets para sincronizar`);
|
logInfo(`📋 Encontrados ${pendingTickets.length} tickets para sincronizar`);
|
||||||
|
|
||||||
// 2. Processar cada ticket
|
|
||||||
for (const ticket of pendingTickets) {
|
for (const ticket of pendingTickets) {
|
||||||
await processSingleTicket(ticket);
|
await processSingleTicket(ticket);
|
||||||
}
|
}
|
||||||
@ -34,42 +29,34 @@ const syncTicketsToGlpi = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Processar um ticket individual
|
|
||||||
const processSingleTicket = async (ticket) => {
|
const processSingleTicket = async (ticket) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logInfo(`🔍 Processando ticket: ${ticket.ticket_number}`);
|
logInfo(`🔍 Processando ticket: ${ticket.ticket_number}`);
|
||||||
|
|
||||||
// 1. Verifica se o ticket já possui um ID do GLPI no nosso banco de sincronização.
|
|
||||||
if (ticket.glpi_ticket_id) {
|
if (ticket.glpi_ticket_id) {
|
||||||
logInfo(`✅ Ticket ${ticket.ticket_number} já possui GLPI ID (${ticket.glpi_ticket_id}) registrado. Apenas atualizando status.`);
|
logInfo(`✅ Ticket ${ticket.ticket_number} já possui GLPI ID (${ticket.glpi_ticket_id}) registrado. Apenas atualizando status.`);
|
||||||
await TicketSyncModel.updateStatus(ticket.id, 'synced', 'synced');
|
await TicketSyncModel.updateStatus(ticket.id, 'synced', 'synced');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Se não tem ID local, verifica se o ticket existe no GLPI (buscando pelo número).
|
|
||||||
const existingTicket = await TicketGlpiModel.ticketExists(ticket.ticket_number);
|
const existingTicket = await TicketGlpiModel.ticketExists(ticket.ticket_number);
|
||||||
await TicketSyncModel.updateLastSync(ticket.id, 'glpi'); // Atualiza o timestamp
|
await TicketSyncModel.updateLastSync(ticket.id, 'glpi');
|
||||||
|
|
||||||
if (existingTicket) {
|
if (existingTicket) {
|
||||||
logInfo(`✅ Ticket ${ticket.ticket_number} encontrado no GLPI. Atualizando registro local com o ID: ${existingTicket}`);
|
logInfo(`✅ Ticket ${ticket.ticket_number} encontrado no GLPI. Atualizando registro local com o ID: ${existingTicket}`);
|
||||||
|
|
||||||
// --- NOVA VALIDAÇÃO: Verificar status do ticket existente no GLPI ---
|
|
||||||
const glpiLiveStatus = await TicketGlpiModel.getTicketStatus(existingTicket);
|
const glpiLiveStatus = await TicketGlpiModel.getTicketStatus(existingTicket);
|
||||||
if (glpiLiveStatus === 6) { // Status 6 = Fechado no GLPI
|
if (glpiLiveStatus === 6) {
|
||||||
logInfo(`Ticket GLPI ${existingTicket} já está Fechado. Marcando ticket SN ${ticket.ticket_number} como 'closed'/'ignored' e não monitorado.`);
|
logInfo(`Ticket GLPI ${existingTicket} já está Fechado. Marcando ticket SN ${ticket.ticket_number} como 'closed'/'ignored' e não monitorado.`);
|
||||||
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'ignored', 'closed', existingTicket);
|
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'ignored', 'closed', existingTicket);
|
||||||
return; // Não processa mais este ticket
|
return;
|
||||||
}
|
}
|
||||||
// --- FIM DA NOVA VALIDAÇÃO ---
|
|
||||||
|
|
||||||
// Atualiza nosso registro de sincronização com o ID do GLPI encontrado.
|
|
||||||
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', existingTicket);
|
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', existingTicket);
|
||||||
logInfo(`✅ Ticket ${ticket.ticket_number} já existe no GLPI! GLPI ID: ${existingTicket}`);
|
logInfo(`✅ Ticket ${ticket.ticket_number} já existe no GLPI! GLPI ID: ${existingTicket}`);
|
||||||
return; // Adiciona este return para evitar a criação de um novo ticket
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Converter dados para formato GLPI
|
|
||||||
const glpiData = {
|
const glpiData = {
|
||||||
ticket_number: ticket.ticket_number,
|
ticket_number: ticket.ticket_number,
|
||||||
short_description: ticket.short_description,
|
short_description: ticket.short_description,
|
||||||
@ -87,12 +74,8 @@ const processSingleTicket = async (ticket) => {
|
|||||||
updated_at: ticket.updated_at
|
updated_at: ticket.updated_at
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lógica para buscar descrição de sc_item_option se a descrição atual estiver vazia
|
|
||||||
// e for um ticket de requisição (apenas para criação de novos tickets)
|
|
||||||
if (glpiData.type === 'requisicao' && (!glpiData.description || glpiData.description.trim() === '')) {
|
if (glpiData.type === 'requisicao' && (!glpiData.description || glpiData.description.trim() === '')) {
|
||||||
logInfo(`Descrição vazia para requisição ${ticket.ticket_number}. Tentando buscar de sc_item_option.`);
|
logInfo(`Descrição vazia para requisição ${ticket.ticket_number}. Tentando buscar de sc_item_option.`);
|
||||||
// Você precisará saber o nome interno da variável de catálogo que contém a descrição.
|
|
||||||
// Por exemplo, se for 'justificativa', use 'justificativa'.
|
|
||||||
const scItemOptionDescription = await require('./servicenowService').fetchScItemOptionValue(ticket.sys_id, 'justificativa');
|
const scItemOptionDescription = await require('./servicenowService').fetchScItemOptionValue(ticket.sys_id, 'justificativa');
|
||||||
if (scItemOptionDescription) {
|
if (scItemOptionDescription) {
|
||||||
glpiData.description = scItemOptionDescription;
|
glpiData.description = scItemOptionDescription;
|
||||||
@ -102,19 +85,14 @@ const processSingleTicket = async (ticket) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Inserir no GLPI
|
|
||||||
const glpiTicketId = await TicketGlpiModel.createTicket(glpiData);
|
const glpiTicketId = await TicketGlpiModel.createTicket(glpiData);
|
||||||
await TicketSyncModel.updateLastSync(ticket.id, 'glpi'); // Atualiza o timestamp
|
await TicketSyncModel.updateLastSync(ticket.id, 'glpi');
|
||||||
|
|
||||||
// 4. Atualizar registro de sincronização
|
|
||||||
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', glpiTicketId);
|
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', glpiTicketId);
|
||||||
|
|
||||||
logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`);
|
logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `❌ Erro no ticket ${ticket.ticket_number}`);
|
logError(error, `❌ Erro no ticket ${ticket.ticket_number}`);
|
||||||
|
|
||||||
// Se temos um registro de sync, atualizar o status para error
|
|
||||||
try {
|
try {
|
||||||
await TicketSyncModel.updateStatus(ticket.id, 'error', 'collected');
|
await TicketSyncModel.updateStatus(ticket.id, 'error', 'collected');
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
@ -127,7 +105,6 @@ const checkGlpiStatusChanges = async () => {
|
|||||||
try {
|
try {
|
||||||
logInfo('📡 Verificando mudanças de status nos tickets do GLPI...');
|
logInfo('📡 Verificando mudanças de status nos tickets do GLPI...');
|
||||||
|
|
||||||
// 1. Busca todos os tickets que estão sendo monitorados
|
|
||||||
const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor();
|
const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor();
|
||||||
|
|
||||||
if (ticketsToMonitor.length === 0) {
|
if (ticketsToMonitor.length === 0) {
|
||||||
@ -136,30 +113,25 @@ const checkGlpiStatusChanges = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const ticket of ticketsToMonitor) {
|
for (const ticket of ticketsToMonitor) {
|
||||||
// 2. Para cada ticket, busca seu status real no GLPI
|
|
||||||
const glpiStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id);
|
const glpiStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id);
|
||||||
|
|
||||||
// 3. Compara o status real com o que temos no nosso banco
|
|
||||||
const currentDbStatus = ticket.glpi_sync_status;
|
const currentDbStatus = ticket.glpi_sync_status;
|
||||||
const glpiStatusAsString = TicketGlpiModel.mapGlpiStatusToString(glpiStatus).toLowerCase().replace(/ /g, '_'); // ex: 'em_atendimento'
|
const glpiStatusAsString = TicketGlpiModel.mapGlpiStatusToString(glpiStatus).toLowerCase().replace(/ /g, '_');
|
||||||
|
|
||||||
// Se o status no GLPI mudou para Solucionado (5) e nosso banco ainda não reflete isso
|
|
||||||
if (glpiStatus === 5 && currentDbStatus !== 'solved' && currentDbStatus !== 'closed') {
|
if (glpiStatus === 5 && currentDbStatus !== 'solved' && currentDbStatus !== 'closed') {
|
||||||
logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} foi Solucionado. Preparando para sincronização...`);
|
logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} foi Solucionado. Preparando para sincronização...`);
|
||||||
|
|
||||||
const solution = await TicketGlpiModel.getTicketSolution(ticket.glpi_ticket_id);
|
const solution = await TicketGlpiModel.getTicketSolution(ticket.glpi_ticket_id);
|
||||||
|
|
||||||
// REGRA "FORA DO ESCOPO"
|
|
||||||
if (solution?.solutiontypes_id === 27) {
|
if (solution?.solutiontypes_id === 27) {
|
||||||
logInfo(`REGRA DETECTADA: Ticket ${ticket.glpi_ticket_id} está fora do escopo (SolutionType ID: 27).`);
|
logInfo(`REGRA DETECTADA: Ticket ${ticket.glpi_ticket_id} está fora do escopo (SolutionType ID: 27).`);
|
||||||
const justification = solution?.content || 'Ticket marcado como "Fora do escopo" no GLPI.';
|
const justification = solution?.content || 'Ticket marcado como "Fora do escopo" no GLPI.';
|
||||||
await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket fechado no GLPI com a justificativa: ${justification}`);
|
await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket fechado no GLPI com a justificativa: ${justification}`);
|
||||||
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored');
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored');
|
||||||
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
||||||
continue; // Pula para o próximo ticket
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FLUXO NORMAL DE RESOLUÇÃO
|
|
||||||
const sanitizedContent = solution?.content ? stripHTML(solution.content) : 'Chamado solucionado sem notas.';
|
const sanitizedContent = solution?.content ? stripHTML(solution.content) : 'Chamado solucionado sem notas.';
|
||||||
await TicketUpdateModel.insert({
|
await TicketUpdateModel.insert({
|
||||||
ticket_sync_id: ticket.id,
|
ticket_sync_id: ticket.id,
|
||||||
@ -170,7 +142,6 @@ const checkGlpiStatusChanges = async () => {
|
|||||||
created_at: solution?.date_mod || new Date(),
|
created_at: solution?.date_mod || new Date(),
|
||||||
source_id: `solution-${ticket.glpi_ticket_id}`,
|
source_id: `solution-${ticket.glpi_ticket_id}`,
|
||||||
});
|
});
|
||||||
// Marca o ticket para ser resolvido no SN
|
|
||||||
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'solved', 'pending_solve', 'GLPI');
|
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'solved', 'pending_solve', 'GLPI');
|
||||||
|
|
||||||
} else if (glpiStatus === 6 && currentDbStatus !== 'closed') {
|
} else if (glpiStatus === 6 && currentDbStatus !== 'closed') {
|
||||||
@ -178,7 +149,6 @@ const checkGlpiStatusChanges = async () => {
|
|||||||
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', ticket.sn_sync_status);
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', ticket.sn_sync_status);
|
||||||
|
|
||||||
} else if (glpiStatus < 5 && glpiStatusAsString !== currentDbStatus && ticket.source_last !== 'GLPI') {
|
} else if (glpiStatus < 5 && glpiStatusAsString !== currentDbStatus && ticket.source_last !== 'GLPI') {
|
||||||
// Para outros status (reabertura, etc.), apenas passa o bastão.
|
|
||||||
logInfo(`Mudança de status (aberto) detectada no GLPI para o ticket ${ticket.glpi_ticket_id}. De '${currentDbStatus}' para '${glpiStatusAsString}'.`);
|
logInfo(`Mudança de status (aberto) detectada no GLPI para o ticket ${ticket.glpi_ticket_id}. De '${currentDbStatus}' para '${glpiStatusAsString}'.`);
|
||||||
await TicketSyncModel.updateSourceLast(ticket.sn_ticket_id, 'GLPI');
|
await TicketSyncModel.updateSourceLast(ticket.sn_ticket_id, 'GLPI');
|
||||||
}
|
}
|
||||||
@ -192,18 +162,16 @@ const closeTicketInGlpi = async (ticket) => {
|
|||||||
try {
|
try {
|
||||||
logInfo(`🔒 Iniciando fechamento do chamado GLPI ${ticket.glpi_ticket_id}...`);
|
logInfo(`🔒 Iniciando fechamento do chamado GLPI ${ticket.glpi_ticket_id}...`);
|
||||||
|
|
||||||
// Busca a mensagem de fechamento vinda do ServiceNow
|
|
||||||
const closeMessageRecord = await TicketUpdateModel.getLatestUpdate(ticket.id, 'servicenow');
|
const closeMessageRecord = await TicketUpdateModel.getLatestUpdate(ticket.id, 'servicenow');
|
||||||
const solutionContent = closeMessageRecord?.content || 'Chamado encerrado via integração com o ServiceNow.';
|
const solutionContent = closeMessageRecord?.content || 'Chamado encerrado via integração com o ServiceNow.';
|
||||||
|
|
||||||
// Chama o model para fechar o ticket no GLPI
|
|
||||||
await TicketGlpiModel.closeTicket(ticket.glpi_ticket_id, solutionContent);
|
await TicketGlpiModel.closeTicket(ticket.glpi_ticket_id, solutionContent);
|
||||||
await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi'); // Atualiza o timestamp
|
await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi');
|
||||||
|
|
||||||
logInfo(`✅ Chamado ${ticket.glpi_ticket_id} solucionado com sucesso no GLPI.`);
|
logInfo(`✅ Chamado ${ticket.glpi_ticket_id} solucionado com sucesso no GLPI.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `🚨Falha ao fechar chamado no GLPI: ${ticket.glpi_ticket_id}`);
|
logError(error, `🚨Falha ao fechar chamado no GLPI: ${ticket.glpi_ticket_id}`);
|
||||||
throw error; // Propaga o erro para o controller tratar
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -212,3 +180,15 @@ module.exports = {
|
|||||||
checkGlpiStatusChanges,
|
checkGlpiStatusChanges,
|
||||||
closeTicketInGlpi
|
closeTicketInGlpi
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module glpiTicketService
|
||||||
|
* @description Este serviço contém a lógica de negócio para a criação e atualização de tickets no GLPI.
|
||||||
|
* Ele atua como um intermediário entre os controladores e os modelos de dados.
|
||||||
|
*
|
||||||
|
* Principais Funcionalidades:
|
||||||
|
* - `syncTicketsToGlpi()`: Função principal que busca tickets pendentes no banco de dados local (previamente coletados do ServiceNow) e orquestra sua criação no GLPI.
|
||||||
|
* - `processSingleTicket(ticket)`: Processa um único ticket. Verifica se ele já existe no GLPI para evitar duplicatas. Se não existir, formata os dados (incluindo o enriquecimento da descrição para requisições) e chama o `TicketGlpiModel` para criar o ticket. Se já existir, apenas atualiza o registro de sincronização.
|
||||||
|
* - `checkGlpiStatusChanges()`: (Atualmente não utilizado no fluxo principal, sua lógica foi integrada ao `processStatusController`) Função projetada para detectar proativamente mudanças de status no GLPI.
|
||||||
|
* - `closeTicketInGlpi(ticket)`: Orquestra o processo de fechamento de um ticket no GLPI. Ele busca a nota de resolução vinda do ServiceNow e a utiliza para adicionar uma solução no GLPI antes de mudar o status para "Solucionado".
|
||||||
|
*/
|
||||||
@ -1,54 +1,47 @@
|
|||||||
// src/services/servicenowService.js
|
// src/services/servicenowService.js
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const apiConfig = require('../../config/apiConfig');
|
const apiConfig = require('../../config/apiConfig');
|
||||||
|
const { stripHTML } = require('../utils/commentSanitizer');
|
||||||
const TicketSnModel = require('../models/ticketSnModel');
|
const TicketSnModel = require('../models/ticketSnModel');
|
||||||
const { logInfo, logError, logSync } = require('../utils/logger');
|
const { logInfo, logError, logSync } = require('../utils/logger');
|
||||||
const TicketUpdateModel = require('../models/ticketUpdateModel');
|
const TicketUpdateModel = require('../models/ticketUpdateModel');
|
||||||
const TicketSyncModel = require('../models/ticketSyncModel');
|
const TicketSyncModel = require('../models/ticketSyncModel');
|
||||||
const { log } = require('winston');
|
const { log } = require('winston');
|
||||||
|
|
||||||
|
// Função para buscar incidentes do ServiceNow
|
||||||
const fetchTicketsFromServiceNow = async (watermark) => {
|
const fetchTicketsFromServiceNow = async (watermark) => {
|
||||||
try {
|
try {
|
||||||
// A watermark vem como string 'YYYY-MM-DD HH:MM:SS'. Converte para Date.
|
|
||||||
const watermarkDate = new Date(watermark);
|
const watermarkDate = new Date(watermark);
|
||||||
// Ajusta para o fuso horário correto se necessário e formata para a query do SN.
|
const watermarkBRT = new Date(watermarkDate.getTime());
|
||||||
const watermarkBRT = new Date(watermarkDate.getTime()); // Se precisar de ajuste de fuso, faça aqui.
|
|
||||||
const formattedWatermark = watermarkBRT.toISOString().replace('T', ' ').substring(0, 19);
|
const formattedWatermark = watermarkBRT.toISOString().replace('T', ' ').substring(0, 19);
|
||||||
const fields = 'number,sys_id,short_description,state,description,caller_id,caller_id.email,caller_id.phone,caller_id.mobile_phone,location,opened_at,sys_updated_on';
|
const fields = 'number,sys_id,short_description,state,description,caller_id,caller_id.email,caller_id.phone,caller_id.mobile_phone,location,opened_at,sys_updated_on';
|
||||||
const query = `assignment_group=1132c9e01bfad410e77eb9121b4bcbab^sys_updated_on>=${formattedWatermark}`;
|
const baseUrl = apiConfig.snTableIncidentConfig.baseUrl.replace(/\/$/, "");
|
||||||
const url = `${apiConfig.snTableIncidentConfig.baseUrl}?sysparm_query=${encodeURIComponent(query)}&sysparm_display_value=true&sysparm_fields=${fields}`;
|
const query = `assignment_group=${apiConfig.assignmentGroup.username}^sys_updated_on>=${formattedWatermark}`;
|
||||||
|
const url = `${baseUrl}?sysparm_query=${encodeURIComponent(query)}&sysparm_display_value=true&sysparm_fields=${fields}`;
|
||||||
const ticketsResponse = await axios.get(url, {
|
const ticketsResponse = await axios.get(url, {
|
||||||
auth: apiConfig.servicenowAuthentication.auth
|
auth: apiConfig.servicenowAuthentication.auth
|
||||||
});
|
});
|
||||||
return ticketsResponse.data.result || [];
|
return ticketsResponse.data.result || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, 'SERVICE NOW Tickets');
|
logError(error, 'SERVICE NOW Tickets');
|
||||||
throw error; // Lança o erro para que o controller não atualize a watermark
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Função para buscar requsições do ServiceNow
|
||||||
|
|
||||||
const fetchRequestsFromServiceNow = async (watermark) => {
|
const fetchRequestsFromServiceNow = async (watermark) => {
|
||||||
try {
|
try {
|
||||||
// A tabela de requisições (sc_req_item) não possui o campo 'active'. A query deve ser apenas pela data.
|
const query = `assignment_group=${apiConfig.assignmentGroup.username}^sys_updated_on>=${watermark}`;
|
||||||
// Construímos a query completa aqui, combinando o grupo de atribuição com a watermark.
|
const fields ='number,request,opened_at,short_description,sys_id,assignment_group,opened_by,opened_by.display_value,opened_by.email,description,location,state,sys_updated_on,variables';
|
||||||
const assignmentGroupSysId = '1132c9e01bfad410e77eb9121b4bcbab'; // Sys ID do grupo 'Sothis'
|
|
||||||
const query = `assignment_group=${assignmentGroupSysId}^sys_updated_on>=${watermark}`;
|
|
||||||
const fields = 'number,request,opened_at,short_description,sys_id,assignment_group,opened_by,opened_by.display_value,opened_by.email,description,location,state,sys_updated_on,variables';
|
|
||||||
// Garante que a URL base não tenha uma barra no final para evitar URL malformada.
|
|
||||||
const baseUrl = apiConfig.snTableRequestConfig.baseUrl.replace(/\/$/, "");
|
const baseUrl = apiConfig.snTableRequestConfig.baseUrl.replace(/\/$/, "");
|
||||||
//const url = `${baseUrl}?sysparm_query=${query}&sysparm_display_value=true&sysparm_fields=${fields}`;
|
|
||||||
const url = `${baseUrl}?sysparm_query=${encodeURIComponent(query)}&sysparm_display_value=true&sysparm_fields=${fields}`;
|
const url = `${baseUrl}?sysparm_query=${encodeURIComponent(query)}&sysparm_display_value=true&sysparm_fields=${fields}`;
|
||||||
|
|
||||||
|
|
||||||
const requestsResponse = await axios.get(url, {
|
const requestsResponse = await axios.get(url, {
|
||||||
auth: apiConfig.servicenowAuthentication.auth
|
auth: apiConfig.servicenowAuthentication.auth
|
||||||
});
|
});
|
||||||
return requestsResponse.data.result || [];
|
return requestsResponse.data.result || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, 'SERVICE NOW Requests');
|
logError(error, 'SERVICE NOW Requests');
|
||||||
throw error; // Lança o erro
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +179,6 @@ const existingCommentInServiceNow = async (ticketId, comment) => {
|
|||||||
|
|
||||||
// Função para buscar todos os comentários de tickets no ServiceNow a partir do ticket id no banco local
|
// Função para buscar todos os comentários de tickets no ServiceNow a partir do ticket id no banco local
|
||||||
// Deve-se atentar para a paginação da API do ServiceNow
|
// Deve-se atentar para a paginação da API do ServiceNow
|
||||||
// para fins de teste, gerar um arquivo JSON com os comentários de um ticket específico
|
|
||||||
const fetchCommentsFromServiceNow = async (ticket) => {
|
const fetchCommentsFromServiceNow = async (ticket) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@ -291,8 +283,8 @@ const fetchCommentsFromServiceNow = async (ticket) => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Adiciona no final do arquivo src/services/servicenowService.js
|
// Adiciona no final do arquivo src/services/servicenowService.js
|
||||||
const { stripHTML } = require('../utils/commentSanitizer');
|
|
||||||
const fetchAndProcessClosedTicketsFromSN = async () => {
|
const fetchAndProcessClosedTicketsFromSN = async () => {
|
||||||
try {
|
try {
|
||||||
logInfo('📡 Verificando status de tickets sincronizados no ServiceNow...');
|
logInfo('📡 Verificando status de tickets sincronizados no ServiceNow...');
|
||||||
@ -341,9 +333,7 @@ const fetchAndProcessClosedTicketsFromSN = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
//Atualiza o status (state) de um ticket no ServiceNow (incidente ou requisição).
|
||||||
* Atualiza o status (state) de um ticket no ServiceNow (incidente ou requisição).
|
|
||||||
*/
|
|
||||||
const updateStatusInServiceNow = async (snTicketId, state) => {
|
const updateStatusInServiceNow = async (snTicketId, state) => {
|
||||||
// Mapeamento de status de string para código numérico do ServiceNow
|
// Mapeamento de status de string para código numérico do ServiceNow
|
||||||
const stateMap = {
|
const stateMap = {
|
||||||
@ -446,10 +436,6 @@ const closeTicketInServiceNow = async (snTicketId, closeNotes, resolvedAt) => {
|
|||||||
work_notes: `Ticket Encerrado por Suporte Técnico Sothis: ${closeNotes}`,
|
work_notes: `Ticket Encerrado por Suporte Técnico Sothis: ${closeNotes}`,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(url);
|
|
||||||
console.log(payload);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
logError(`Tipo de ticket desconhecido: ${ticketType}`, { sysId, ticketType });
|
logError(`Tipo de ticket desconhecido: ${ticketType}`, { sysId, ticketType });
|
||||||
throw new Error(`Tipo de ticket desconhecido: ${ticketType}`);
|
throw new Error(`Tipo de ticket desconhecido: ${ticketType}`);
|
||||||
|
|||||||
@ -1,13 +1,8 @@
|
|||||||
// src/utils/commentSanitizer.js
|
|
||||||
const { logInfo, logWarning } = require('./logger');
|
const { logInfo, logWarning } = require('./logger');
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove HTML tags e converte entidades HTML - CORRIGIDO
|
|
||||||
*/
|
|
||||||
const stripHTML = (html) => {
|
const stripHTML = (html) => {
|
||||||
if (!html) return '';
|
if (!html) return '';
|
||||||
|
|
||||||
// PRIMEIRO: converte entidades HTML para caracteres normais
|
|
||||||
let cleaned = html
|
let cleaned = html
|
||||||
.replace(/</g, '<') // < para <
|
.replace(/</g, '<') // < para <
|
||||||
.replace(/>/g, '>') // > para >
|
.replace(/>/g, '>') // > para >
|
||||||
@ -18,7 +13,6 @@ const stripHTML = (html) => {
|
|||||||
.replace(/&/g, '&') // & para &
|
.replace(/&/g, '&') // & para &
|
||||||
.replace(/"/g, '"'); // " para "
|
.replace(/"/g, '"'); // " para "
|
||||||
|
|
||||||
// DEPOIS: remove tags HTML
|
|
||||||
cleaned = cleaned
|
cleaned = cleaned
|
||||||
.replace(/<br\s*\/?>/gi, '\n') // <br> para quebra de linha
|
.replace(/<br\s*\/?>/gi, '\n') // <br> para quebra de linha
|
||||||
.replace(/<p>/gi, '') // Remove <p>
|
.replace(/<p>/gi, '') // Remove <p>
|
||||||
@ -39,33 +33,24 @@ const stripHTML = (html) => {
|
|||||||
return cleaned;
|
return cleaned;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Detecta e trata imagens do GLPI
|
|
||||||
*/
|
|
||||||
const handleImages = (content) => {
|
const handleImages = (content) => {
|
||||||
const imgRegex = /<img[^>]+src="([^"]+)"[^>]*>/gi;
|
const imgRegex = /<img[^>]+src="([^"]+)"[^>]*>/gi;
|
||||||
const hasImages = imgRegex.test(content);
|
const hasImages = imgRegex.test(content);
|
||||||
|
|
||||||
if (hasImages) {
|
if (hasImages) {
|
||||||
logWarning('📷 Imagem detectada no comentário GLPI');
|
logWarning('📷 Imagem detectada no comentário GLPI');
|
||||||
// Remove toda a tag de imagem mas mantém o texto around
|
|
||||||
return content.replace(/<a[^>]*>.*?<img[^>]*>.*?<\/a>/gi, '[IMAGEM ANEXA NO GLPI]');
|
return content.replace(/<a[^>]*>.*?<img[^>]*>.*?<\/a>/gi, '[IMAGEM ANEXA NO GLPI]');
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Limpa metadados JSON do GLPI
|
|
||||||
*/
|
|
||||||
const cleanJSONMetadata = (content) => {
|
const cleanJSONMetadata = (content) => {
|
||||||
if (typeof content === 'string' && content.includes('{id=') && content.includes('content=')) {
|
if (typeof content === 'string' && content.includes('{id=') && content.includes('content=')) {
|
||||||
try {
|
try {
|
||||||
// Extrai o conteúdo do campo content= do objeto
|
|
||||||
const contentMatch = content.match(/content=([^,]+),/);
|
const contentMatch = content.match(/content=([^,]+),/);
|
||||||
if (contentMatch && contentMatch[1]) {
|
if (contentMatch && contentMatch[1]) {
|
||||||
let extracted = contentMatch[1].trim();
|
let extracted = contentMatch[1].trim();
|
||||||
// Remove aspas se existirem
|
|
||||||
extracted = extracted.replace(/^['"]|['"]$/g, '');
|
extracted = extracted.replace(/^['"]|['"]$/g, '');
|
||||||
return extracted;
|
return extracted;
|
||||||
}
|
}
|
||||||
@ -76,27 +61,20 @@ const cleanJSONMetadata = (content) => {
|
|||||||
return content;
|
return content;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Função principal de sanitização
|
|
||||||
*/
|
|
||||||
const sanitizeGLPIComment = (commentObj) => {
|
const sanitizeGLPIComment = (commentObj) => {
|
||||||
if (!commentObj || typeof commentObj !== 'object') {
|
if (!commentObj || typeof commentObj !== 'object') {
|
||||||
logWarning('Comentário inválido recebido', { comment: commentObj });
|
logWarning('Comentário inválido recebido', { comment: commentObj });
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pega o conteúdo do objeto comment
|
|
||||||
let content = commentObj.content || '';
|
let content = commentObj.content || '';
|
||||||
|
|
||||||
// Se for string vazia, retorna vazio
|
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
// Aplica as limpezas em sequência
|
|
||||||
content = cleanJSONMetadata(content.toString());
|
content = cleanJSONMetadata(content.toString());
|
||||||
content = handleImages(content);
|
content = handleImages(content);
|
||||||
content = stripHTML(content);
|
content = stripHTML(content);
|
||||||
|
|
||||||
// Log para debugging
|
|
||||||
if (content !== commentObj.content) {
|
if (content !== commentObj.content) {
|
||||||
logInfo('🔧 Comentário sanitizado', {
|
logInfo('🔧 Comentário sanitizado', {
|
||||||
original: commentObj.content.substring(0, 100) + '...',
|
original: commentObj.content.substring(0, 100) + '...',
|
||||||
@ -108,17 +86,16 @@ const sanitizeGLPIComment = (commentObj) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Esse módulo fornece funções para sanitizar comentários importados do GLPI.
|
* @module commentSanitizer
|
||||||
* Ele remove tags HTML, trata imagens e limpa metadados JSON específicos do GLPI.
|
* @description Este módulo utilitário é especializado em limpar e formatar o conteúdo dos comentários (follow-ups) vindos do GLPI.
|
||||||
* O objetivo é garantir que os comentários sejam legíveis e seguros para exibição.
|
* 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 principais:
|
* Funções:
|
||||||
* - sanitizeGLPIComment: Função principal que aplica todas as sanitizações.
|
* - `sanitizeGLPIComment(commentObj)`: A função principal que orquestra o processo de limpeza.
|
||||||
* - stripHTML: Remove tags HTML e converte algumas entidades para texto simples.
|
* - `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: Detecta imagens e substitui por um marcador de imagem.
|
* - `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: Remove metadados JSON do GLPI, extraindo apenas o conteúdo relevante.
|
* - `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.
|
||||||
*
|
|
||||||
* Logs são gerados para ajudar no debugging e monitoramento do processo de sanitização.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -127,6 +104,3 @@ module.exports = {
|
|||||||
handleImages,
|
handleImages,
|
||||||
cleanJSONMetadata
|
cleanJSONMetadata
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user