CLEANUP: Comentarios atualizados e Readme.md alimentado com V1.0 da documentação

This commit is contained in:
Rafael Alves Lopes 2025-10-29 14:20:21 -03:00
parent 0c4538355a
commit bfa0e8acc8
24 changed files with 398 additions and 425 deletions

43
.env
View File

@ -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
View 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
View File

@ -6,3 +6,4 @@ config/*.json
servicenow_requests.json
servicenow_tickets.json
location_mapping.log
.env.production

170
README.md
View File

@ -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)
- npm (Node Package Manager)
- PostgreSQL or MySQL database
- Access to ServiceNow and GLPI APIs
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:
## 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.
```
git clone https://github.com/yourusername/servicenow-glpi-sync.git
cd servicenow-glpi-sync
```
3. **`processCommentsController` - Sincronização de Comentários**
- **Objetivo**: Manter as conversas dos tickets sincronizadas.
- **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
```
cp .env.example .env
```
A aplicação implementa várias regras de negócio para gerenciar a complexidade da sincronização:
## 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.

View File

@ -1,4 +1,4 @@
require('dotenv').config();
//config/apiConfig.js
const glpiDbConfig = {
client: process.env.GLPI_DB_TYPE || 'mysql',
@ -45,6 +45,10 @@ const snScItemOptionConfig = {
baseUrl: process.env.SERVICENOW_SC_ITEM_OPTION_URL,
}
const assignmentGroup = {
username: process.env.SERVICENOW_ASSIGNMENT_GROUP,
}
module.exports = {
// configuração do ServiceNow e outras já existentes podem permanecer
glpiDbConfig,
@ -54,5 +58,6 @@ module.exports = {
snTableJournalConfig,
servicenowAuthentication,
snTableRequestConfig,
snScItemOptionConfig
snScItemOptionConfig,
assignmentGroup
};

View File

@ -1,5 +1,4 @@
const { Sequelize } = require('sequelize');
require('dotenv').config();
// Database connection configuration
const dbConfig = {

View File

@ -1,10 +1,17 @@
// src/app.js
require('dotenv').config();
const path = require('path');
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 { processCommentsController } = require('./controllers/processCommentsController');
const { processErrorController } = require('./controllers/processErrorController');
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', {
timestamp: new Date().toISOString(),
@ -13,15 +20,28 @@ logInfo('Aplicação iniciada', {
async function main() {
try {
await processErrorController(); // Reseta os erros
await processTicketsController(); // Coleta tickets do SN e cria no GLPI
await processCommentsController(); // Sincroniza chamados Bidirecionalmente
await processStatusAndClosureController(); // Fecha chamados Bidirecionalmente
await processErrorController();
await processTicketsController();
await processCommentsController();
await processStatusAndClosureController();
logInfo('🎉 Ciclo de sincronização concluído com sucesso! 🎉');
} catch (error) {
logInfo('Erro na aplicação:', error);
}
}
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 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.
*/

View File

@ -4,8 +4,6 @@ const TicketSyncModel = require('../models/ticketSyncModel');
const processCommentsController = async () => {
// Usa getTicketsToMonitor para incluir tickets 'synced' e 'solved'.
const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor();
for (const ticket of ticketsToMonitor){
@ -13,8 +11,14 @@ const processCommentsController = async () => {
if(comments.length != 0){await syncCommentsSNtoGlpi(comments, ticket);}
await syncCommentsGlpitoSN(ticket);
}
}
module.exports = {
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.
*/

View File

@ -1,10 +1,8 @@
// src/controllers/processErrorController.js
const TicketSyncModel = require('../models/ticketSyncModel');
const { logInfo, logError, logWarning } = require('../utils/logger');
const processErrorController = async () => {
logInfo('Iniciando verificação de tickets com erro...');
try {
const errorTickets = await TicketSyncModel.getTicketsInErrorState();
@ -19,20 +17,16 @@ const processErrorController = async () => {
let newSnStatus = ticket.sn_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') {
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.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.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);
logInfo(`Ticket SN ID ${ticket.sn_ticket_id} resetado para reprocessamento (SN: ${newSnStatus}, GLPI: ${newGlpiStatus}).`);
}
@ -45,3 +39,11 @@ const processErrorController = async () => {
module.exports = {
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.
*/

View File

@ -1,7 +1,6 @@
const TicketSyncModel = require('../models/ticketSyncModel');
const TicketSnModel = require('../models/ticketSnModel');
const TicketGlpiModel = require('../models/ticketGlpiModel');
const TicketUpdateModel = require('../models/ticketUpdateModel');
const { logInfo, logError, logWarning } = require('../utils/logger');
const { stripHTML } = require('../utils/commentSanitizer');
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.
const glpiStatusAsString = TicketGlpiModel.mapGlpiStatusToString(glpiCurrentStatus);
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...`);
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
if (glpiCurrentStatus < 5) {
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW');

View File

@ -11,10 +11,8 @@ const e = require('express');
* @param {('incidente'|'requisicao')} type - O tipo de ticket.
*/
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) => {
if (!dateString) return null;
// Verifica se já está em um formato reconhecível (como ISO 8601)
if (!dateString.includes('/')) {
return new Date(dateString);
}
@ -26,18 +24,15 @@ const processAndSaveTickets = async (tickets, type) => {
if (!tickets || tickets.length === 0) {
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...`);
let latestUpdateTimestamp = new Date(0); // Inicia com a data mínima (epoch)
let latestUpdateTimestamp = new Date(0);
for (const ticket of tickets) {
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() === '')) {
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 vars = {};
(extraData || []).forEach(item => {
@ -51,7 +46,7 @@ const processAndSaveTickets = async (tickets, type) => {
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.`);
}
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.`);
}
}
// --- FIM DA LÓGICA DE ENRIQUECIMENTO ---
// 1. Salva o ticket na tabela 'tickets_sn'
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 dataSync = {
@ -76,31 +66,25 @@ const processAndSaveTickets = async (tickets, type) => {
updated_at: new Date()
};
// 3. Define o status inicial de sincronização
if (ticketStatus === 'Encerrado' || ticketStatus === 'Encerrado - Omitido') {
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 {
dataSync.sn_sync_status = 'collected'; // Status que indica que foi coletado do SN
dataSync.glpi_sync_status = 'pending_check'; // Status que indica que precisa ser checkado no GLPI
dataSync.sn_sync_status = 'collected';
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);
if (!exists) {
await TicketSyncModel.createSyncRecord(dataSync);
} 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');
logInfo(`Ticket ${ticket.number} atualizado pelo ServiceNow. 'source_last' definido como 'SNOW'.`);
} else {
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);
if (currentUpdateDate && currentUpdateDate.getTime() > latestUpdateTimestamp.getTime()) {
latestUpdateTimestamp = currentUpdateDate;
@ -112,7 +96,6 @@ const processAndSaveTickets = async (tickets, type) => {
}
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
? latestUpdateTimestamp.toISOString().slice(0, 19).replace('T', ' ')
: '';
@ -123,24 +106,18 @@ const processAndSaveTickets = async (tickets, type) => {
*/
const processSyncController = async () => {
try {
// 1. Pega a marca d'água da última execução bem-sucedida
const watermark = await SyncControlModel.getWatermark('servicenow');
logInfo(`📡 Buscando tickets do ServiceNow atualizados desde: ${watermark}`);
// Busca incidentes
const incidents = await fetchTicketsApi(watermark);
const latestIncidentUpdate = await processAndSaveTickets(incidents, 'incidente');
// Busca requisições
logInfo('📡 Buscando requisições do ServiceNow...');
const requests = await fetchRequestsApi(watermark);
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;
// 3. Se processamos algum ticket, atualiza a marca d'água para a próxima execução
if (newWatermark) {
// Adiciona 1 segundo para evitar reprocessar o último ticket em caso de timestamps iguais
const sixHoursInMillis = 6 * 60 * 60 * 1000;
@ -158,3 +135,18 @@ const processSyncController = async () => {
module.exports = {
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.
*/

View File

@ -1,19 +1,20 @@
// src/controllers/glpiController.js
const { syncTicketsToGlpi } = require('../services/glpiTicketService');
const { processSyncController } = require('./processSyncController');
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();
//Sincroniza os tickets com o GLPI
await syncTicketsToGlpi();
}
module.exports = {
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.
*/

View File

@ -1,10 +1,7 @@
// src/models/locationMappingModel.js
const pool = require('../data/database');
const { logInfo, logError } = require('../utils/logger');
class LocationMappingModel {
// Buscar entity_id do GLPI baseado no location_id do ServiceNow
static async getGlpiEntityId(snLocationId) {
try {
const query = `
@ -26,7 +23,6 @@ class LocationMappingModel {
}
}
// Buscar entity_id do GLPI baseado no location_name do ServiceNow
static async getGlpiEntityIdByName(snLocationName) {
try {
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) {
try {
const query = `
@ -74,12 +67,8 @@ class LocationMappingModel {
return null;
}
}
// Buscar entity (id e name) do GLPI baseado no location_name do ServiceNow
static async getGlpiEntityByName(snLocationName) {
try {
const query = `
@ -105,8 +94,14 @@ class 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.
*/

View File

@ -1,4 +1,3 @@
// src/models/syncControlModel.js
const pool = require('../data/database');
const { logError, logInfo } = require('../utils/logger');
@ -10,7 +9,6 @@ class SyncControlModel {
if (rows.length === 0) {
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);
} catch (error) {
logError(error, `Erro ao buscar watermark para ${systemName}`);
@ -35,3 +33,15 @@ class 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.
*/

View File

@ -1,11 +1,9 @@
// src/models/ticketGlpiModel.js
const glpiPool = require('../data/glpiDataBase');
const { logInfo, logError } = require('../utils/logger');
const LocationMappingModel = require('./locationMappingModel');
class TicketGlpiModel {
// Mapeamento de status do ServiceNow para o GLPI
static mapStatus(snState) {
const statusMap = {
'Aguardando Atendimento': 1,
@ -13,12 +11,11 @@ class TicketGlpiModel {
'Em Espera': 4,
'Resolvido': 5,
'Encerrado - Omitido': 6,
'Encerrado': 6 // Mapeia para Fechado no GLPI
'Encerrado': 6
};
return statusMap[snState] || 1;
}
// Mapeamento de status do GLPI (numérico) para ServiceNow (string)
static mapGlpiStatusToString(glpiStatus) {
const statusMap = {
1: 'Aguardando Atendimento', // Novo
@ -30,7 +27,6 @@ class TicketGlpiModel {
return statusMap[glpiStatus] || 'Desconhecido';
}
// Mapeamento de prioridade
static mapPriority(snPriority) {
const priorityMap = {
'1': 5,
@ -42,7 +38,6 @@ class TicketGlpiModel {
return priorityMap[snPriority] || 3;
}
// Mapeamento de categoria
static mapCategory(shortDescription) {
const categoryMap = {
'Problemas com Telefonia Fixa': 5717,
@ -53,7 +48,6 @@ class TicketGlpiModel {
return categoryMap[shortDescription] || 5717;
}
// Mapeamento de SLA baseado no tipo e na descrição
static mapSlasIdTtr(ticketData) {
const isRequisicao = ticketData.type === 'requisicao';
const shortDesc = ticketData.short_description;
@ -73,10 +67,9 @@ class TicketGlpiModel {
if (!isRequisicao && isTelefonia) {
return 34;
}
return 0; // Default se nenhum critério for atendido
return 0;
}
// Formata o título do ticket
static async formatTitle(ticketData) {
try {
let entityData = null;
@ -92,8 +85,8 @@ class TicketGlpiModel {
const tituloSn = ticketData.short_description || 'Sem título';
return {
title: `${tipo} - ${entityName} - ${tituloSn}`,
entityId: entityData?.id || 127 // fallback para entity_id default
title: `${tipo} - ${entityName} - ${tituloSn}`, // Título formatado
entityId: entityData?.id || 127
};
} catch (error) {
logError(error, 'Erro ao formatar título do ticket');
@ -107,7 +100,6 @@ class TicketGlpiModel {
static formatDescription(ticketData) {
// Função auxiliar para extrair informações usando regex
const extractInfo = (regex, description) => {
if (!description) return null;
const match = description.match(regex);
@ -123,12 +115,10 @@ class TicketGlpiModel {
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 phone = ticketData.caller_phone || extractPhoneFromDescription(ticketData.description) || 'N/A';
const ramal = ticketData.caller_ramal || extractRamalFromDescription(ticketData.description) || 'N/A';
// Formata a descrição em HTML
let htmlDescription = `
<table style="width:100%; border-collapse: collapse;">
<tr style="background-color:#f2f2f2;">
@ -164,23 +154,19 @@ class TicketGlpiModel {
return htmlDescription;
}
// Cria um ticket no GLPI
static async createTicket(ticketData) {
const connection = await glpiPool.getConnection();
try {
await connection.beginTransaction();
// Buscar entity_id com base no location_id ou location_name
let entityId = 127; // Valor padrão (fallback)
let entityId = 127;
if (ticketData.location_id) {
entityId = await LocationMappingModel.getGlpiEntityId(ticketData.location_id) || entityId;
} else if (ticketData.location_name) {
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;
if (!email && ticketData.description) {
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);
// Formata a descrição para HTML
const formattedDescription = TicketGlpiModel.formatDescription(ticketData);
// Mapeia o tipo do ticket
const ticketType = ticketData.type === 'requisicao' ? 2 : 1;
// Determina o valor de slas_id_ttr
const slasIdTtr = TicketGlpiModel.mapSlasIdTtr(ticketData);
// Prepara os dados para o GLPI
const glpiTicketData = {
entities_id: entityId, // Usar o entity_id mapeado
entities_id: entityId,
name: formattedTitle,
date: ticketData.opened_at || new Date(),
date_mod: ticketData.updated_at || new Date(),
@ -219,7 +200,6 @@ class TicketGlpiModel {
slas_id_ttr: slasIdTtr
};
// Constrói a query dinamicamente e insere o ticket principal
const fields = Object.keys(glpiTicketData).join(', ');
const placeholders = Object.keys(glpiTicketData).map(() => '?').join(', ');
const values = Object.values(glpiTicketData);
@ -228,7 +208,6 @@ class TicketGlpiModel {
const [ticketResult] = await connection.execute(ticketQuery, values);
const ticketId = ticketResult.insertId;
// Adiciona o requerente
await connection.execute(
`INSERT INTO glpi_tickets_users
(tickets_id, users_id, type, use_notification, alternative_email)
@ -236,7 +215,6 @@ class TicketGlpiModel {
[ticketId, 917]
);
// Adiciona o grupo
await connection.execute(
`INSERT INTO glpi_groups_tickets
(tickets_id, groups_id, type)
@ -258,7 +236,6 @@ class TicketGlpiModel {
}
}
// Verifica se um ticket já existe no GLPI
static async ticketExists(snTicketNumber) {
try {
const query = `
@ -279,7 +256,7 @@ class TicketGlpiModel {
return null;
}
}
// Buscar followups (notas) de um ticket no GLPI
static async getFollowupsByItemId(itemId) {
const connection = await glpiPool.getConnection();
try {
@ -299,7 +276,7 @@ class TicketGlpiModel {
const values = ['Ticket', items_id, 971, comment.content , 1];
const [result] = await connection.execute(query, values);
return [{ id: result.insertId }]; // Retorna o ID inserido
return [{ id: result.insertId }];
}catch(error){
logError(`Erro ao inserir o comentário no GLPI: ${error}`);
} finally {
@ -307,11 +284,9 @@ class TicketGlpiModel {
}
}
// Busca tickets que foram fechados ou solucionados no GLPI
static async getClosedTickets(watermark, limit = 100) {
const connection = await glpiPool.getConnection();
try {
// Status 5 = Solucionado, Status 6 = Fechado
const query = `
SELECT
t.id,
@ -332,17 +307,14 @@ class TicketGlpiModel {
}
}
// Fecha um ticket no GLPI, adicionando uma solução
static async closeTicket(ticketId, solutionContent) {
const connection = await glpiPool.getConnection();
try {
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())`;
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 = ?`;
await connection.execute(updateTicketQuery, [ticketId]);
@ -356,7 +328,6 @@ class TicketGlpiModel {
}
}
// Busca a solução de um ticket no GLPI
static async getTicketSolution(glpiTicketId) {
const connection = await glpiPool.getConnection();
try {
@ -375,17 +346,14 @@ class TicketGlpiModel {
}
}
// Força o fechamento de um ticket (muda status para 6)
static async forceCloseTicket(ticketId) {
const connection = await glpiPool.getConnection();
try {
// Atualiza o status do ticket para "Fechado" (status 6)
const updateTicketQuery = `UPDATE glpi_tickets SET status = 6, closedate = NOW() WHERE id = ?`;
await connection.execute(updateTicketQuery, [ticketId]);
logInfo(`✅ Ticket GLPI ${ticketId} forçado para o estado Fechado (6).`);
} catch (error) {
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 {
if (connection) connection.release();
}
@ -412,7 +380,6 @@ class TicketGlpiModel {
}
}
// Busca o status atual de um ticket no GLPI
static async getTicketStatus(glpiTicketId) {
const connection = await glpiPool.getConnection();
try {
@ -431,18 +398,20 @@ class TicketGlpiModel {
}
}
/**
* @module TicketGlpiModel
* @description Este módulo fornece métodos para gerenciar tickets no GLPI.
* Inclui funcionalidades para mapear status, prioridade e categoria,
* formatar a descrição do ticket, determinar o SLA, criar tickets e
* verificar se um ticket existe no GLPI.
* @description Este módulo é a camada de acesso a dados para o banco de dados do GLPI.
* Ele encapsula todas as interações diretas com o banco de dados MySQL do GLPI,
* oferecendo uma interface clara para criar, atualizar e consultar tickets e seus componentes.
*
* O módulo usa expressões regulares para extrair informações da descrição do ticket,
* como email, número de telefone e ramal, quando essas informações não são
* fornecidas diretamente. Também formata a descrição em HTML para melhor
* apresentação no GLPI.
* Principais Funcionalidades:
* - **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.
* - **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.
* - **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;

View File

@ -1,10 +1,8 @@
// src/models/ticketSnModel.js
const pool = require('../data/database');
const { logInfo, logError, logWarning } = require('../utils/logger');
class TicketSnModel {
// Salva ou atualiza um ticket no banco de dados
static async saveTicket(ticketData, typeTicket) {
const query = `
INSERT INTO tickets_sn
@ -25,19 +23,14 @@ class TicketSnModel {
telefone = EXCLUDED.telefone,
opened_at = EXCLUDED.opened_at,
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;
// Mapeamento inteligente de campos baseado no tipo de ticket
let callerName, callerEmail, ramal, telefone, sysId, shortDescription, state, description, locationId, locationName, openedAt;
if (typeTicket === 'incidente') {
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'] || {};
callerEmail = (typeof emailObject === 'object' ? emailObject.value : emailObject) || null;
ramal = null; // Incidentes não têm essa variável por padrão
@ -59,7 +52,6 @@ class TicketSnModel {
shortDescription = ticketData.short_description || null;
state = ticketData.state || 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;
locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null;
locationName = ticketData.location?.display_value || null;
@ -89,7 +81,7 @@ class TicketSnModel {
try {
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) {
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() {
try {
const query = `
@ -149,7 +140,6 @@ class TicketSnModel {
}
}
// Busca um ticket pelo número
static async findByTicketNumber(ticketNumber) {
try {
const query = `
@ -166,8 +156,6 @@ class TicketSnModel {
}
}
// Busca o status de um ticket pelo ID
static async getStatus(ticketId) {
try {
const query = `
@ -181,9 +169,6 @@ class TicketSnModel {
}
}
// Busca um ticket pelo ID
static async findById(ticketId) {
try {
const query = `
@ -200,24 +185,20 @@ class TicketSnModel {
}
}
// Atualiza os dados de um ticket
static async updateTicket(ticketId, updateData) {
try {
const fields = [];
const values = [];
let paramCount = 1;
// Construir dinamicamente a query baseada nos campos fornecidos
for (const [key, value] of Object.entries(updateData)) {
fields.push(`${key} = $${paramCount}`);
values.push(value);
paramCount++;
}
// Adicionar updated_at
fields.push(`updated_at = NOW()`);
// Adicionar o ID no final dos valores
values.push(ticketId);
const query = `
@ -247,12 +228,13 @@ class 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.
* Inclui funcionalidades para salvar um ticket, buscar tickets pendentes para sincronização com o GLPI,
* e buscar/atualizar tickets específicos.
* @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.
* Manter uma cópia local otimiza as consultas e reduz a dependência de chamadas de API repetitivas ao ServiceNow.
*
* A função saveTicket insere ou atualiza um ticket na tabela tickets_sn.
* A função getPendingTickets busca tickets que ainda não foram sincronizados com o GLPI (não possuem registro em ticket_sync)
* e ignora os tickets com status 'Encerrado' ou 'Encerrado - Omitido'.
* Principais Funcionalidades:
* - `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.
* - `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;

View File

@ -1,15 +1,12 @@
// src/models/ticketSyncModel.js
const { query } = require('winston');
const pool = require('../data/database');
const { logError } = require('../utils/logger');
class TicketSyncModel {
// Cria um registro de sincronização para um ticket
static async createSyncRecord(data) {
try {
const query = `INSERT INTO ticket_sync (
sn_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) {
try {
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) {
try {
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) {
try {
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;
}
}
// Atualiza o status de sincronização de um ticket e o source last
static async updateStatusAndSourceLast(ticketSyncId, glpiStatus, snStatus, source_last) {
try {
@ -90,7 +83,6 @@ class TicketSyncModel {
}
}
// Atualiza apenas o source_last de um ticket
static async updateSourceLast(snTicketId, source) {
try {
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) {
try {
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) {
try {
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) {
try {
const query = `
@ -140,7 +128,6 @@ class TicketSyncModel {
}
}
// Buscar o ID de sincronização baseado no sn_ticket_id
static async getIdSyncBySnId(snTicketId) {
try {
const query = 'SELECT id FROM ticket_sync WHERE sn_ticket_id = $1';
@ -151,6 +138,7 @@ class TicketSyncModel {
throw error;
}
}
static async getByTicketNumber(ticketNumber) {
try {
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() {
try {
const query = `
@ -184,7 +171,6 @@ class TicketSyncModel {
}
}
// Buscar tickets que estão ativos para monitoramento (synced ou solved)
static async getTicketsToMonitor() {
try {
const query = `
@ -208,7 +194,6 @@ class TicketSyncModel {
}
}
// Buscar tickets que precisam ter o fechamento sincronizado
static async getTicketsPendingSolve() {
try {
const query = `
@ -231,7 +216,6 @@ class TicketSyncModel {
}
}
// Busca todos os tickets que estão em algum estado de erro
static async getTicketsInErrorState() {
try {
const query = `
@ -247,7 +231,6 @@ class TicketSyncModel {
}
}
// Atualiza os timestamps de última sincronização
static async updateLastSync(snTicketId, system) {
try {
let fieldToUpdate;
@ -256,17 +239,15 @@ class TicketSyncModel {
} else if (system === 'glpi') {
fieldToUpdate = 'last_glpi_sync';
} 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`;
await pool.query(query, [snTicketId]);
} catch (error) {
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) {
try {
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
* @description Este módulo fornece métodos para interagir com a tabela de sincronização de tickets (ticket_sync) no banco de dados.
* Inclui funcionalidades para buscar tickets para sincronização, atualizar o status de sincronização
* e buscar o ID de sincronização baseado no glpi_ticket_id.
* @description Este módulo é a camada de acesso à tabela `ticket_sync`, que atua como o cérebro do processo de sincronização.
* Ele armazena o estado de cada ticket em ambos os sistemas (GLPI e ServiceNow) e controla o fluxo de atualizações.
*
* A função getTicketsToSync busca todos os tickets que possuem IDs tanto no GLPI quanto no ServiceNow.
* A função updateStatus atualiza os status de sincronização do GLPI e do ServiceNow para um ticket específico.
* A função getIdSyncByGlpiId retorna o ID de sincronização baseado no glpi_ticket_id.
* Essas funcionalidades são essenciais para manter a consistência e o rastreamento dos tickets entre os dois sistemas.
* Principais Funcionalidades:
* - **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').
* - **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).
* - **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.
*/

View File

@ -1,4 +1,3 @@
// src/models/ticketUpdateModel.js
const ticketSyncModel = require('./ticketSyncModel');
const pool = require('../data/database');
const { logError } = require('../utils/logger');
@ -23,7 +22,7 @@ class TicketUpdateModel {
`;
const values = [
updateData.ticket_sync_id,
updateData.update_type, // Corrigido para usar o tipo de atualização dinâmico
updateData.update_type,
updateData.source_system,
updateData.content,
updateData.author,
@ -103,4 +102,17 @@ class 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) 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.
*/

View File

@ -11,8 +11,17 @@ import pandas as pd
from datetime import timedelta
# 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
logging.basicConfig(
level=logging.INFO,

View File

@ -1,4 +1,3 @@
// src/services/glpiCommentService.js
const TicketSnModel = require('../models/ticketSnModel');
const TicketGlpiModel = require('../models/ticketGlpiModel');
const TicketSyncModel = require('../models/ticketSyncModel');
@ -8,11 +7,9 @@ const { existingCommentInServiceNow, syncCommentToServiceNow } = require('./serv
const { sanitizeGLPIComment } = require('../utils/commentSanitizer');
const { log } = require('winston');
// Sincroniza comentários do GLPI para o Banco de Dados local
const syncCommentsGlpitoSN = async (ticket) => {
try {
logInfo('🔄 Iniciando sincronização de comments do GLPI para o Service Now...');
const comments = await TicketGlpiModel.getFollowupsByItemId(ticket.glpi_ticket_id);
await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi'); // Atualiza o timestamp
if (comments.length === 0) {
@ -30,15 +27,11 @@ const syncCommentsGlpitoSN = async (ticket) => {
const existsInSN = await existingCommentInServiceNow(ticket.sn_ticket_id, sanitized);
const syncId = await TicketSyncModel.getIdSyncByGlpiId(ticket.glpi_ticket_id);
// Caso 1: já sincronizado
if (existingComment && existsInSN) {
logInfo(`🔁 Comentário ${comment.id} já sincronizado.`);
// Não muda o source_last aqui, pois não houve nova sincronização
continue;
}
// Caso 2: está no SN mas não no banco
if (!existingComment && existsInSN) {
await TicketUpdateModel.insert({
ticket_sync_id: syncId,
@ -50,11 +43,9 @@ const syncCommentsGlpitoSN = async (ticket) => {
source_id: comment.id,
destiny_id: existsInSN.destiny_id
});
// Não muda o source_last aqui
continue;
}
// Caso 3: existe no banco mas falta no SN
if (existingComment && !existsInSN) {
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'pending_cmt_sync');
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitized);
@ -63,7 +54,6 @@ const syncCommentsGlpitoSN = async (ticket) => {
continue;
}
// Caso 4: não existe em nenhum lugar → sincroniza ambos
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'pending_cmt_sync', 'pending_cmt_sync');
const snCommentId = await syncCommentToServiceNow(ticket.sn_ticket_id, sanitized);
await TicketUpdateModel.insert({
@ -76,8 +66,6 @@ const syncCommentsGlpitoSN = async (ticket) => {
source_id: comment.id,
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') {
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'GLPI');
}
@ -101,8 +89,7 @@ const syncCommentsSNtoGlpi = async (comments, ticket) => {
for (const comment of comments) {
try {
const commentId = await TicketGlpiModel.insertComment(comment, ticket.glpi_ticket_id);
await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi'); // Atualiza o timestamp
// Passa o bastão para o SN, mas SOMENTE se o ticket não estiver já resolvido ou fechado.
await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi');
if (ticket.sn_sync_status === 'synced' && ticket.glpi_sync_status === 'synced') {
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW');
}
@ -122,18 +109,20 @@ const syncCommentsSNtoGlpi = async (comments, ticket) => {
}
};
/**
* @module glpiCommentService
* @description Este módulo fornece serviços para sincronizar comentários do GLPI para o banco de dados local.
* 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
* Utiliza os modelos TicketSnModel, TicketGlpiModel, TicketSyncModel e TicketUpdateModel para interagir com o banco de dados.
* Atualiza o status de sincronização após cada comentário processado.
* Inclui logging detalhado para monitorar o progresso e identificar erros durante o processo de sincronização.
* @description Este serviço orquestra a sincronização de comentários (follow-ups/notas) entre GLPI e ServiceNow.
*
* Principais Funcionalidades:
* - `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:
* 1. ** Sincronizado**: O comentário existe no banco local e no ServiceNow. Nenhuma ação é necessária.
* 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 ( 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 = {

View File

@ -1,4 +1,3 @@
// src/services/glpiService.js
const TicketSnModel = require('../models/ticketSnModel');
const TicketGlpiModel = require('../models/ticketGlpiModel');
const TicketUpdateModel = require('../models/ticketUpdateModel');
@ -6,12 +5,9 @@ const TicketSyncModel = require('../models/ticketSyncModel');
const { logInfo, logError, logWarning } = require('../utils/logger');
const { addWorkNoteToServiceNow } = require('./servicenowService');
// Método principal que o controller vai chamar
const syncTicketsToGlpi = async () => {
try {
logInfo('🔄 Iniciando sincronização para GLPI...');
// 1. Buscar tickets pendentes
const pendingTickets = await TicketSnModel.getPendingTickets();
if (pendingTickets.length === 0) {
@ -21,7 +17,6 @@ const syncTicketsToGlpi = async () => {
logInfo(`📋 Encontrados ${pendingTickets.length} tickets para sincronizar`);
// 2. Processar cada ticket
for (const ticket of pendingTickets) {
await processSingleTicket(ticket);
}
@ -34,42 +29,34 @@ const syncTicketsToGlpi = async () => {
}
};
// Processar um ticket individual
const processSingleTicket = async (ticket) => {
try {
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) {
logInfo(`✅ Ticket ${ticket.ticket_number} já possui GLPI ID (${ticket.glpi_ticket_id}) registrado. Apenas atualizando status.`);
await TicketSyncModel.updateStatus(ticket.id, 'synced', 'synced');
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);
await TicketSyncModel.updateLastSync(ticket.id, 'glpi'); // Atualiza o timestamp
await TicketSyncModel.updateLastSync(ticket.id, 'glpi');
if (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);
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.`);
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);
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 = {
ticket_number: ticket.ticket_number,
short_description: ticket.short_description,
@ -87,12 +74,8 @@ const processSingleTicket = async (ticket) => {
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() === '')) {
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');
if (scItemOptionDescription) {
glpiData.description = scItemOptionDescription;
@ -102,19 +85,14 @@ const processSingleTicket = async (ticket) => {
}
}
// 3. Inserir no GLPI
const glpiTicketId = await TicketGlpiModel.createTicket(glpiData);
await TicketSyncModel.updateLastSync(ticket.id, 'glpi'); // Atualiza o timestamp
// 4. Atualizar registro de sincronização
await TicketSyncModel.updateLastSync(ticket.id, 'glpi');
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', glpiTicketId);
logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`);
} catch (error) {
logError(error, `❌ Erro no ticket ${ticket.ticket_number}`);
// Se temos um registro de sync, atualizar o status para error
try {
await TicketSyncModel.updateStatus(ticket.id, 'error', 'collected');
} catch (innerError) {
@ -127,7 +105,6 @@ const checkGlpiStatusChanges = async () => {
try {
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();
if (ticketsToMonitor.length === 0) {
@ -136,30 +113,25 @@ const checkGlpiStatusChanges = async () => {
}
for (const ticket of ticketsToMonitor) {
// 2. Para cada ticket, busca seu status real no GLPI
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 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') {
logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} foi Solucionado. Preparando para sincronização...`);
const solution = await TicketGlpiModel.getTicketSolution(ticket.glpi_ticket_id);
// REGRA "FORA DO ESCOPO"
if (solution?.solutiontypes_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.';
await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket fechado no GLPI com a justificativa: ${justification}`);
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored');
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.';
await TicketUpdateModel.insert({
ticket_sync_id: ticket.id,
@ -170,7 +142,6 @@ const checkGlpiStatusChanges = async () => {
created_at: solution?.date_mod || new Date(),
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');
} 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);
} 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}'.`);
await TicketSyncModel.updateSourceLast(ticket.sn_ticket_id, 'GLPI');
}
@ -192,18 +162,16 @@ const closeTicketInGlpi = async (ticket) => {
try {
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 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 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.`);
} catch (error) {
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,
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 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 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".
*/

View File

@ -1,54 +1,47 @@
// src/services/servicenowService.js
const axios = require('axios');
const apiConfig = require('../../config/apiConfig');
const { stripHTML } = require('../utils/commentSanitizer');
const TicketSnModel = require('../models/ticketSnModel');
const { logInfo, logError, logSync } = require('../utils/logger');
const TicketUpdateModel = require('../models/ticketUpdateModel');
const TicketSyncModel = require('../models/ticketSyncModel');
const { log } = require('winston');
// Função para buscar incidentes do ServiceNow
const fetchTicketsFromServiceNow = async (watermark) => {
try {
// A watermark vem como string 'YYYY-MM-DD HH:MM:SS'. Converte para Date.
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()); // Se precisar de ajuste de fuso, faça aqui.
const watermarkBRT = new Date(watermarkDate.getTime());
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 query = `assignment_group=1132c9e01bfad410e77eb9121b4bcbab^sys_updated_on>=${formattedWatermark}`;
const url = `${apiConfig.snTableIncidentConfig.baseUrl}?sysparm_query=${encodeURIComponent(query)}&sysparm_display_value=true&sysparm_fields=${fields}`;
const baseUrl = apiConfig.snTableIncidentConfig.baseUrl.replace(/\/$/, "");
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, {
auth: apiConfig.servicenowAuthentication.auth
});
return ticketsResponse.data.result || [];
} catch (error) {
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) => {
try {
// A tabela de requisições (sc_req_item) não possui o campo 'active'. A query deve ser apenas pela data.
// Construímos a query completa aqui, combinando o grupo de atribuição com a watermark.
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 query = `assignment_group=${apiConfig.assignmentGroup.username}^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';
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 requestsResponse = await axios.get(url, {
auth: apiConfig.servicenowAuthentication.auth
});
return requestsResponse.data.result || [];
} catch (error) {
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
// 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) => {
try {
@ -291,8 +283,8 @@ const fetchCommentsFromServiceNow = async (ticket) => {
return [];
}
};
// Adiciona no final do arquivo src/services/servicenowService.js
const { stripHTML } = require('../utils/commentSanitizer');
const fetchAndProcessClosedTicketsFromSN = async () => {
try {
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) => {
// Mapeamento de status de string para código numérico do ServiceNow
const stateMap = {
@ -446,10 +436,6 @@ const closeTicketInServiceNow = async (snTicketId, closeNotes, resolvedAt) => {
work_notes: `Ticket Encerrado por Suporte Técnico Sothis: ${closeNotes}`,
};
console.log(url);
console.log(payload);
} else {
logError(`Tipo de ticket desconhecido: ${ticketType}`, { sysId, ticketType });
throw new Error(`Tipo de ticket desconhecido: ${ticketType}`);

View File

@ -1,13 +1,8 @@
// src/utils/commentSanitizer.js
const { logInfo, logWarning } = require('./logger');
/**
* Remove HTML tags e converte entidades HTML - CORRIGIDO
*/
const stripHTML = (html) => {
if (!html) return '';
// PRIMEIRO: converte entidades HTML para caracteres normais
let cleaned = html
.replace(/&#60;/g, '<') // &#60; para <
.replace(/&#62;/g, '>') // &#62; para >
@ -18,7 +13,6 @@ const stripHTML = (html) => {
.replace(/&amp;/g, '&') // &amp; para &
.replace(/&quot;/g, '"'); // &quot; para "
// DEPOIS: remove tags HTML
cleaned = cleaned
.replace(/<br\s*\/?>/gi, '\n') // <br> para quebra de linha
.replace(/<p>/gi, '') // Remove <p>
@ -39,33 +33,24 @@ const stripHTML = (html) => {
return cleaned;
};
/**
* Detecta e trata imagens do GLPI
*/
const handleImages = (content) => {
const imgRegex = /<img[^>]+src="([^"]+)"[^>]*>/gi;
const hasImages = imgRegex.test(content);
if (hasImages) {
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;
};
/**
* Limpa metadados JSON do GLPI
*/
const cleanJSONMetadata = (content) => {
if (typeof content === 'string' && content.includes('{id=') && content.includes('content=')) {
try {
// Extrai o conteúdo do campo content= do objeto
const contentMatch = content.match(/content=([^,]+),/);
if (contentMatch && contentMatch[1]) {
let extracted = contentMatch[1].trim();
// Remove aspas se existirem
extracted = extracted.replace(/^['"]|['"]$/g, '');
return extracted;
}
@ -76,27 +61,20 @@ const cleanJSONMetadata = (content) => {
return content;
};
/**
* Função principal de sanitização
*/
const sanitizeGLPIComment = (commentObj) => {
if (!commentObj || typeof commentObj !== 'object') {
logWarning('Comentário inválido recebido', { comment: commentObj });
return '';
}
// Pega o conteúdo do objeto comment
let content = commentObj.content || '';
// Se for string vazia, retorna vazio
if (!content) return '';
// Aplica as limpezas em sequência
content = cleanJSONMetadata(content.toString());
content = handleImages(content);
content = stripHTML(content);
// Log para debugging
if (content !== commentObj.content) {
logInfo('🔧 Comentário sanitizado', {
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.
* Ele remove tags HTML, trata imagens e limpa metadados JSON específicos do GLPI.
* O objetivo é garantir que os comentários sejam legíveis e seguros para exibição.
* @module commentSanitizer
* @description Este módulo utilitário é especializado em limpar e formatar o conteúdo dos comentários (follow-ups) vindos do GLPI.
* O GLPI armazena comentários em formato HTML, muitas vezes com metadados e formatações que não são desejáveis no ServiceNow.
* O objetivo é converter esse conteúdo em texto puro e legível.
*
* Funções principais:
* - sanitizeGLPIComment: Função principal que aplica todas as sanitizações.
* - stripHTML: Remove tags HTML e converte algumas entidades para texto simples.
* - handleImages: Detecta imagens e substitui por um marcador de imagem.
* - cleanJSONMetadata: Remove metadados JSON do GLPI, extraindo apenas o conteúdo relevante.
*
* Logs são gerados para ajudar no debugging e monitoramento do processo de sanitização.
* Funções:
* - `sanitizeGLPIComment(commentObj)`: A função principal que orquestra o processo de limpeza.
* - `stripHTML(html)`: Remove a maioria das tags HTML, convertendo algumas (como `<br>`, `<p>`, `<strong>`) em marcações de texto simples (quebras de linha, asteriscos).
* - `handleImages(content)`: Detecta a presença de imagens nos comentários do GLPI e as substitui por um texto placeholder, como "[IMAGEM ANEXA NO GLPI]", que as imagens não podem ser transferidas diretamente.
* - `cleanJSONMetadata(content)`: Remove metadados em formato de string JSON que o GLPI às vezes insere no campo de conteúdo, extraindo apenas o texto real do comentário.
*/
module.exports = {
@ -127,6 +104,3 @@ module.exports = {
handleImages,
cleanJSONMetadata
};