FIX : corrige bugs silenciosos
- normaliza comentarios e logs (ASCII) - corrige campo no createSyncRecord (glpi_ticket_id) - remove metodo duplicado updateSourceLast - corrige uso de ID errado na atualizacao de status/comentarios - atualiza o README
This commit is contained in:
parent
019837acab
commit
e84647a2ba
@ -4,7 +4,7 @@
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
# Frequência de execução do cron job (formato cron). Padrão: '* * * * *' (a cada 1 minutos)
|
# Frequencia de execucao do cron job (formato cron). Padrao: '* * * * *' (a cada 1 minutos)
|
||||||
CRON_SCHEDULE='* * * * *'
|
CRON_SCHEDULE='* * * * *'
|
||||||
|
|
||||||
# ServiceNow API (Dev)
|
# ServiceNow API (Dev)
|
||||||
@ -23,9 +23,10 @@ SERVICENOW_TABLE_REQUEST_URL=https://caoadev.service-now.com/api/now/table/sc_re
|
|||||||
SERVICENOW_TABLE_JOURNAL_URL=https://caoadev.service-now.com/api/now/table/sys_journal_field
|
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_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)
|
# Usuario padrao pra criacao de chamados (ServiceNow)
|
||||||
SERVICENOW_DEFAULT_USER=SOTHIS.CAOA
|
SERVICENOW_DEFAULT_USER=SOTHIS.CAOA
|
||||||
SERVICENOW_DEFAULT_USER_SYSID=b1312fda977a1250501634a6f053af98
|
SERVICENOW_DEFAULT_USER_SYSID=b1312fda977a1250501634a6f053af98
|
||||||
|
SERVICENOW_RESOLVED_BY_SYSID=b131d6ed1b2ba510c5e163923b4bcb77
|
||||||
|
|
||||||
# GLPI (banco real)
|
# GLPI (banco real)
|
||||||
GLPI_DB_TYPE=mysql
|
GLPI_DB_TYPE=mysql
|
||||||
@ -36,10 +37,10 @@ GLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
|
|||||||
GLPI_DB_NAME=glpi_data
|
GLPI_DB_NAME=glpi_data
|
||||||
GLPI_DB_CHARSET=utf8mb4
|
GLPI_DB_CHARSET=utf8mb4
|
||||||
|
|
||||||
# ID do usuário padrão do GLPI para criação de tickets e comentários
|
# ID do usuario padrao do GLPI para criacao de tickets e comentarios
|
||||||
GLPI_DEFAULT_USER_ID=11111
|
GLPI_DEFAULT_USER_ID=11111
|
||||||
|
|
||||||
# Banco intermediário (PostgreSQL)
|
# Banco intermediario (PostgreSQL)
|
||||||
SNGLPI_DB_HOST=10.0.120.75
|
SNGLPI_DB_HOST=10.0.120.75
|
||||||
SNGLPI_DB_PORT=5432
|
SNGLPI_DB_PORT=5432
|
||||||
SNGLPI_DB_NAME=snglpi-development
|
SNGLPI_DB_NAME=snglpi-development
|
||||||
|
|||||||
32
.env.example
32
.env.example
@ -1,31 +1,39 @@
|
|||||||
# ===================================================================
|
# ===================================================================
|
||||||
# EXEMPLO DE ARQUIVO DE CONFIGURAÇÃO DE AMBIENTE
|
# EXEMPLO DE ARQUIVO DE CONFIGURACAO DE AMBIENTE
|
||||||
#
|
#
|
||||||
# Copie este arquivo para .env.development e .env.production
|
# Copie este arquivo para .env.development e .env.production
|
||||||
# e preencha com os valores corretos para cada ambiente.
|
# e preencha com os valores corretos para cada ambiente.
|
||||||
# Não adicione senhas ou chaves secretas diretamente neste arquivo.
|
# Nao adicione senhas ou chaves secretas diretamente neste arquivo.
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
# --- Configuração Geral da Aplicação ---
|
# --- Configuracao Geral da Aplicacao ---
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
# Frequência de execução do cron job (formato cron). Padrão: '* * * * *' (a cada 1 minutos)
|
# Frequencia de execucao do cron job (formato cron). Padrao: '* * * * *' (a cada 1 minutos)
|
||||||
CRON_SCHEDULE='* * * * *'
|
CRON_SCHEDULE='* * * * *'
|
||||||
|
|
||||||
# --- Configuração da API do ServiceNow ---
|
# --- Configuracao da API do ServiceNow ---
|
||||||
# URL base da sua instância do ServiceNow
|
# URL base da sua instancia do ServiceNow
|
||||||
SERVICENOW_INSTANCE=https://your-instance.service-now.com
|
SERVICENOW_INSTANCE=https://your-instance.service-now.com
|
||||||
SERVICENOW_USERNAME=
|
SERVICENOW_USERNAME=
|
||||||
SERVICENOW_PASSWORD=
|
SERVICENOW_PASSWORD=
|
||||||
|
|
||||||
# Sys_id do grupo de atribuição no ServiceNow que será monitorado para novos tickets
|
# Sys_id do grupo de atribuicao no ServiceNow que sera monitorado para novos tickets
|
||||||
SERVICENOW_ASSIGNMENT_GROUP=
|
SERVICENOW_ASSIGNMENT_GROUP=
|
||||||
|
|
||||||
# Sys_id do usuário padrão no ServiceNow que será usado pela integração para criar/atualizar tickets
|
# Sys_id do usuario padrao no ServiceNow que sera usado pela integracao para criar/atualizar tickets
|
||||||
SERVICENOW_DEFAULT_USER_SYSID=
|
SERVICENOW_DEFAULT_USER_SYSID=
|
||||||
|
SERVICENOW_DEFAULT_USER=
|
||||||
|
SERVICENOW_RESOLVED_BY_SYSID=
|
||||||
|
|
||||||
# --- Configuração do Banco de Dados do GLPI (MySQL) ---
|
# Endpoints de tabela usados pela aplicacao
|
||||||
|
SERVICENOW_TABLE_INCIDENT_URL=
|
||||||
|
SERVICENOW_TABLE_REQUEST_URL=
|
||||||
|
SERVICENOW_TABLE_JOURNAL_URL=
|
||||||
|
SERVICENOW_SC_ITEM_OPTION_URL=
|
||||||
|
|
||||||
|
# --- Configuracao do Banco de Dados do GLPI (MySQL) ---
|
||||||
GLPI_DB_TYPE=mysql
|
GLPI_DB_TYPE=mysql
|
||||||
GLPI_DB_HOST=localhost
|
GLPI_DB_HOST=localhost
|
||||||
GLPI_DB_PORT=3306
|
GLPI_DB_PORT=3306
|
||||||
@ -34,17 +42,17 @@ GLPI_DB_PASSWORD=
|
|||||||
GLPI_DB_NAME=glpi
|
GLPI_DB_NAME=glpi
|
||||||
GLPI_DB_CHARSET=utf8mb4
|
GLPI_DB_CHARSET=utf8mb4
|
||||||
|
|
||||||
# ID do usuário padrão do GLPI para criação de tickets e comentários
|
# ID do usuario padrao do GLPI para criacao de tickets e comentarios
|
||||||
GLPI_DEFAULT_USER_ID=1118
|
GLPI_DEFAULT_USER_ID=1118
|
||||||
|
|
||||||
# --- Configuração do Banco de Dados Intermediário (PostgreSQL) ---
|
# --- Configuracao do Banco de Dados Intermediario (PostgreSQL) ---
|
||||||
SNGLPI_DB_HOST=localhost
|
SNGLPI_DB_HOST=localhost
|
||||||
SNGLPI_DB_PORT=5432
|
SNGLPI_DB_PORT=5432
|
||||||
SNGLPI_DB_NAME=snglpi_sync
|
SNGLPI_DB_NAME=snglpi_sync
|
||||||
SNGLPI_DB_USER=
|
SNGLPI_DB_USER=
|
||||||
SNGLPI_DB_PASSWORD=
|
SNGLPI_DB_PASSWORD=
|
||||||
|
|
||||||
# --- Configuração de Logs ---
|
# --- Configuracao de Logs ---
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
# --- Mapeamento de Localidades (Opcional) ---
|
# --- Mapeamento de Localidades (Opcional) ---
|
||||||
|
|||||||
321
README.md
321
README.md
@ -1,206 +1,217 @@
|
|||||||
# Sistema de Sincronização ServiceNow <> GLPI
|
# Sistema de Sincronizacao ServiceNow <> GLPI
|
||||||
|
|
||||||
## Visão Geral
|
Middleware em Node.js para sincronizacao bidirecional de tickets, comentarios e status entre ServiceNow e GLPI, com banco intermediario PostgreSQL e acesso direto ao banco MySQL/MariaDB do GLPI.
|
||||||
|
|
||||||
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.
|
## O que o projeto faz hoje
|
||||||
|
|
||||||
A aplicação funciona como um job agendado (por exemplo, via cron) que executa um ciclo completo de sincronização.
|
- Busca incidentes e requisicoes no ServiceNow por watermark (`sync_control`).
|
||||||
|
- Salva/atualiza tickets no banco intermediario (`tickets_sn`).
|
||||||
|
- Cria tickets no GLPI para registros pendentes (`ticket_sync.glpi_sync_status = 'pending_check'`).
|
||||||
|
- Sincroniza comentarios em duas direcoes:
|
||||||
|
- ServiceNow -> GLPI
|
||||||
|
- GLPI -> ServiceNow
|
||||||
|
- Sincroniza status em duas direcoes com controle de origem (`source_last`: `SNOW`/`GLPI`).
|
||||||
|
- Trata regras de negocio especificas de fechamento/reabertura.
|
||||||
|
- Reprocessa tickets em estado de erro no inicio de cada ciclo.
|
||||||
|
- Executa em job agendado (`node-cron`) com protecao contra concorrencia local (`isCronRunning`).
|
||||||
|
|
||||||
---
|
## Arquitetura
|
||||||
|
|
||||||
## Como Executar a Aplicação
|
### Componentes
|
||||||
|
|
||||||
Esta seção contém as instruções essenciais para configurar e rodar a aplicação em diferentes ambientes.
|
- `cron.js`: agenda e dispara o ciclo de sincronizacao.
|
||||||
|
- `src/app.js`: orquestra o ciclo principal.
|
||||||
|
- `src/controllers/*`: fluxo de tickets, comentarios, status e recuperacao de erro.
|
||||||
|
- `src/services/*`: integracoes com ServiceNow e GLPI.
|
||||||
|
- `src/models/*`: acesso a dados (PostgreSQL e GLPI MySQL).
|
||||||
|
- `src/data/*`: pools de conexao (`pg` e `mysql2`).
|
||||||
|
- `src/scripts/python/update_location_mapping.py`: atualiza tabela `location_mapping` a partir de CSV.
|
||||||
|
|
||||||
### 1. Pré-requisitos
|
### Bancos envolvidos
|
||||||
|
|
||||||
|
- PostgreSQL (`snglpi`): estado da integracao.
|
||||||
|
- MySQL/MariaDB (GLPI): criacao/consulta/atualizacao de tickets e followups.
|
||||||
|
|
||||||
|
## Fluxo do ciclo
|
||||||
|
|
||||||
|
Executado em ordem pelo `main()`:
|
||||||
|
|
||||||
|
1. `processErrorController`
|
||||||
|
2. `processTicketsController`
|
||||||
|
3. `processCommentsController`
|
||||||
|
4. `processStatusAndClosureController`
|
||||||
|
|
||||||
|
## Regras de negocio importantes
|
||||||
|
|
||||||
|
- Controle de precedencia por `source_last` para reduzir conflito de atualizacao.
|
||||||
|
- Ticket resolvido no GLPI com `solutiontypes_id = 27` (fora do escopo):
|
||||||
|
- Nao resolve no SN.
|
||||||
|
- Adiciona `work_note` no SN.
|
||||||
|
- Marca sincronizacao como encerrada/ignorada.
|
||||||
|
- Se GLPI resolver e SN estiver em `Em Espera` ou `Aguardando Atendimento`, integracao forca `Em Atendimento` antes de resolver no SN.
|
||||||
|
- Comentarios GLPI sao sanitizados (HTML/imagens/metadados) antes de envio ao SN.
|
||||||
|
- Watermark de coleta no ServiceNow usa margem de seguranca de 6 horas para tras (`newWatermark - 6h`).
|
||||||
|
- Motivo: reduzir risco de perda de eventos em casos de atraso de replicacao, diferenca de timezone e clock skew entre sistemas.
|
||||||
|
- Efeito colateral esperado: releitura de uma janela recente e maior chance de reprocessamento controlado (idempotencia pelo banco local).
|
||||||
|
|
||||||
|
## Regras de negocio detalhadas
|
||||||
|
|
||||||
|
1. Controle de origem (`source_last`)
|
||||||
|
- `SNOW` ou `GLPI` define quem teve a ultima escrita valida para o ticket.
|
||||||
|
- Evita corrida de atualizacao de status/comentario entre os dois sistemas.
|
||||||
|
|
||||||
|
2. Status GLPI -> ServiceNow
|
||||||
|
- Solucao no GLPI dispara tentativa de resolucao no SN.
|
||||||
|
- Se SN estiver em status bloqueante (`Em Espera` ou `Aguardando Atendimento`), a integracao seta `Em Atendimento` antes da resolucao.
|
||||||
|
- Regra fora do escopo (`solutiontypes_id = 27`) fecha fluxo local sem resolver no SN e registra `work_note`.
|
||||||
|
|
||||||
|
3. Status ServiceNow -> GLPI
|
||||||
|
- Mudancas de estado no SN sao refletidas no GLPI respeitando o estado final de fechado.
|
||||||
|
- Fechamento permanente no GLPI bloqueia reabertura automatica por sincronizacao.
|
||||||
|
|
||||||
|
4. Comentarios GLPI -> SN
|
||||||
|
- Fluxo com verificacao de existencia local/remota para evitar duplicatas.
|
||||||
|
- Conteudo passa por sanitizacao para remover HTML, normalizar texto e tratar imagens.
|
||||||
|
|
||||||
|
5. Comentarios SN -> GLPI
|
||||||
|
- Comentarios novos detectados no SN sao persistidos no banco intermediario e enviados ao GLPI.
|
||||||
|
- IDs de origem/destino ficam registrados em `ticket_updates` para rastreabilidade.
|
||||||
|
|
||||||
|
6. Reprocessamento de erro
|
||||||
|
- Inicio de cada ciclo tenta resetar estados de erro para recolocar tickets no fluxo automatico.
|
||||||
|
|
||||||
|
## Pre-requisitos
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- NPM
|
||||||
|
- PostgreSQL
|
||||||
|
- MySQL/MariaDB com base do GLPI acessivel
|
||||||
|
- Python 3.10+ (para o mapeador de localidades)
|
||||||
|
|
||||||
|
## Instalacao
|
||||||
|
|
||||||
- **Node.js**: Certifique-se de ter o Node.js (versão 18 ou superior) instalado.
|
|
||||||
- **Dependências**: Na raiz do projeto, execute o comando abaixo para instalar todas as dependências necessárias:
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
- **Arquivos de Ambiente**: Crie os arquivos `.env.development` e `.env.production` na raiz do projeto, baseando-se no exemplo `.env.example` (se houver) e preenchendo com as credenciais e URLs corretas para cada ambiente.
|
|
||||||
|
|
||||||
### 2. Executando com Scripts NPM
|
### Dependencias Python
|
||||||
|
|
||||||
### 2. Configuração do Ambiente Python
|
|
||||||
|
|
||||||
A aplicação utiliza um script Python para sincronizar o mapeamento de localidades.
|
|
||||||
|
|
||||||
#### a) Dependências Python
|
|
||||||
|
|
||||||
Navegue até a pasta do script e instale as dependências listadas no `requirements.txt`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd src/scripts/python
|
cd src/scripts/python
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
#### b) Montagem do Compartilhamento de Arquivos (CIFS/Samba)
|
## Configuracao de ambiente
|
||||||
|
|
||||||
O script Python lê um arquivo `.csv` de um compartilhamento de rede. No servidor de produção, é necessário montar este compartilhamento para que o caminho definido em `LOCATION_MAPPING_CSV_PATH` seja acessível.
|
Crie `.env.development` e `.env.production` com base em `.env.example` e inclua tambem as variaveis usadas no codigo:
|
||||||
|
|
||||||
Adicione a seguinte linha ao seu arquivo `/etc/fstab` para montar o compartilhamento automaticamente durante o boot do sistema. **Ajuste o IP, credenciais e caminhos conforme necessário.**
|
### ServiceNow
|
||||||
|
|
||||||
```fstab
|
- `SERVICENOW_USERNAME`
|
||||||
//10.0.121.40/Tecnica/Controle\040CAOA/ENTIDADE\040SERVICE\040NOW /mnt/csv_share cifs username=<seu_usuario>,password=<sua_senha>,uid=1000,gid=1000,iocharset=utf8 0 0
|
- `SERVICENOW_PASSWORD`
|
||||||
```
|
- `SERVICENOW_ASSIGNMENT_GROUP`
|
||||||
|
- `SERVICENOW_TABLE_INCIDENT_URL`
|
||||||
|
- `SERVICENOW_TABLE_REQUEST_URL`
|
||||||
|
- `SERVICENOW_TABLE_JOURNAL_URL`
|
||||||
|
- `SERVICENOW_SC_ITEM_OPTION_URL`
|
||||||
|
|
||||||
Após editar o `/etc/fstab`, execute `sudo mount -a` para montar o compartilhamento imediatamente.
|
### GLPI (MySQL/MariaDB)
|
||||||
|
|
||||||
Os comandos abaixo utilizam a variável `NODE_ENV` para carregar o arquivo de ambiente (`.env`) correto.
|
- `GLPI_DB_HOST`
|
||||||
|
- `GLPI_DB_PORT`
|
||||||
|
- `GLPI_DB_USER`
|
||||||
|
- `GLPI_DB_PASSWORD`
|
||||||
|
- `GLPI_DB_NAME`
|
||||||
|
- `GLPI_DB_CHARSET`
|
||||||
|
- `GLPI_DEFAULT_USER_ID`
|
||||||
|
|
||||||
|
### Banco intermediario (PostgreSQL)
|
||||||
|
|
||||||
|
- `SNGLPI_DB_HOST`
|
||||||
|
- `SNGLPI_DB_PORT`
|
||||||
|
- `SNGLPI_DB_NAME`
|
||||||
|
- `SNGLPI_DB_USER`
|
||||||
|
- `SNGLPI_DB_PASSWORD`
|
||||||
|
|
||||||
|
### Agendamento e mapeamento
|
||||||
|
|
||||||
|
- `CRON_SCHEDULE` (ex.: `*/5 * * * *`)
|
||||||
|
- `LOCATION_MAPPING_CSV_PATH` (arquivo CSV para mapeamento SN -> GLPI)
|
||||||
|
|
||||||
|
## Execucao
|
||||||
|
|
||||||
|
### Desenvolvimento
|
||||||
|
|
||||||
#### Ambiente de Desenvolvimento
|
|
||||||
Para rodar em modo de desenvolvimento com reinício automático a cada alteração de arquivo (usando `nodemon`):
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Ambiente de Produção
|
### Producao
|
||||||
Para rodar a aplicação em modo de produção (usando `node`):
|
|
||||||
```bash
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
ou o comando explícito:
|
|
||||||
```bash
|
```bash
|
||||||
npm run start:prod
|
npm run start:prod
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Executando com PM2
|
## Execucao com PM2
|
||||||
|
|
||||||
O PM2 é o gerenciador de processos recomendado para ambientes de produção. Ele garante que a aplicação reinicie automaticamente em caso de falhas e facilita o gerenciamento.
|
Subir os dois processos (sync + mapeador):
|
||||||
|
|
||||||
- **Para iniciar em modo de produção:**
|
|
||||||
```bash
|
```bash
|
||||||
pm2 start ecosystem.config.js --env production
|
pm2 start ecosystem.config.js --env production
|
||||||
```
|
```
|
||||||
- **Para iniciar em modo de desenvolvimento:**
|
|
||||||
|
Comandos uteis:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pm2 start ecosystem.config.js --env development
|
pm2 list
|
||||||
```
|
pm2 logs sn-glpi-sync-cron
|
||||||
- **Comandos úteis do PM2:**
|
pm2 logs sn-glpi-location-mapper
|
||||||
```bash
|
pm2 restart sn-glpi-sync-cron
|
||||||
pm2 list # Lista todos os processos
|
|
||||||
pm2 logs sn-glpi-sync-cron # Exibe os logs em tempo real
|
|
||||||
pm2 restart sn-glpi-sync-cron # Reinicia a aplicação
|
|
||||||
pm2 stop sn-glpi-sync-cron # Para a aplicação
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Fluxo de Execução
|
## Script de mapeamento de localidades
|
||||||
|
|
||||||
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:
|
Arquivo: `src/scripts/python/update_location_mapping.py`
|
||||||
|
|
||||||
1. **`processErrorController` - Limpeza de Erros**
|
- Carrega `NODE_ENV` e respectivo `.env.*`.
|
||||||
- **Objetivo**: Aumentar a resiliência do sistema.
|
- Monitora alteracoes recentes no CSV (`LOCATION_MAPPING_CSV_PATH`).
|
||||||
- **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.
|
- Recria os dados da tabela `location_mapping` com base no CSV e IDs validos em ambos os bancos.
|
||||||
|
- Roda em loop continuo (ideal via PM2 como processo separado).
|
||||||
|
|
||||||
2. **`processTicketsController` - Criação de Tickets**
|
## Estrutura do banco intermediario
|
||||||
- **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**
|
Definicao base em `src/scripts/database/scriptBD.sql`:
|
||||||
- **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.
|
|
||||||
|
|
||||||
4. **`processStatusAndClosureController` - Sincronização de Status e Fechamento**
|
- `tickets_sn`
|
||||||
- **Objetivo**: Manter os status dos tickets alinhados e gerenciar o ciclo de vida (resolução e fechamento).
|
- `ticket_sync`
|
||||||
- **Ação**: Realiza uma sincronização bidirecional de status, tratando reaberturas, pausas, resoluções e fechamentos em ambas as direções.
|
- `ticket_updates`
|
||||||
|
- `location_mapping`
|
||||||
|
- `sync_control`
|
||||||
|
|
||||||
---
|
## Limitacoes conhecidas (estado atual)
|
||||||
|
|
||||||
## Regras de Negócio Principais
|
- Nao ha suite de testes automatizados (`npm test` e placeholder).
|
||||||
|
- Ha regras hardcoded que deveriam vir de configuracao (ex.: `resolved_by` em fechamento no SN).
|
||||||
|
- Parte do fluxo depende de acesso direto ao banco do GLPI (acoplamento operacional alto).
|
||||||
|
- Existe logica sensivel de timezone/watermark que merece revisao para evitar reprocesso/perda de evento.
|
||||||
|
|
||||||
A aplicação implementa várias regras de negócio para gerenciar a complexidade da sincronização:
|
## Proximas melhorias recomendadas
|
||||||
|
|
||||||
### Controle de Fluxo com "Bastão" (`source_last`)
|
- Cobertura de testes (unitario e integracao com mocks de API/DB).
|
||||||
- Para evitar conflitos de atualização (race conditions), o sistema utiliza um campo `source_last` na tabela `ticket_sync`.
|
- Externalizar IDs e regras hardcoded para variaveis de ambiente.
|
||||||
- Este campo funciona como um "bastão da palavra": o sistema que realiza a última atualização (SNOW ou GLPI) "segura o bastão".
|
- Revisar estrategia de watermark/timezone e idempotencia.
|
||||||
- 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.
|
- Criar healthcheck/observabilidade (metricas de ciclo, falhas por etapa, tickets processados).
|
||||||
|
- Adicionar validacoes de configuracao na inicializacao (falha rapida quando faltar env obrigatoria).
|
||||||
|
|
||||||
### Criação e Enriquecimento de Tickets
|
## Arquivos principais
|
||||||
- **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.
|
|
||||||
|
|
||||||
### Sincronização de Comentários
|
- `cron.js`
|
||||||
- **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:
|
- `src/app.js`
|
||||||
1. **Já Sincronizado**: O comentário existe em ambos os locais. Nenhuma ação.
|
- `src/controllers/processTicketsController.js`
|
||||||
2. **Existe no SN, Falta no BD Local**: O registro local é criado para corrigir o estado.
|
- `src/controllers/processCommentsController.js`
|
||||||
3. **Existe no BD Local, Falta no SN**: O comentário é reenviado para o ServiceNow.
|
- `src/controllers/processStatusController.js`
|
||||||
4. **Novo Comentário**: O comentário é enviado para o SNOW e registrado localmente.
|
- `src/controllers/processErrorController.js`
|
||||||
- **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.
|
- `src/services/servicenowService.js`
|
||||||
|
- `src/services/glpiTicketService.js`
|
||||||
### Sincronização de Status
|
- `src/services/glpiCommentService.js`
|
||||||
- **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.
|
- `src/scripts/python/update_location_mapping.py`
|
||||||
- **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
|
|
||||||
```
|
|
||||||
|
|||||||
@ -3,10 +3,9 @@ const TicketSyncModel = require('../models/ticketSyncModel');
|
|||||||
const SyncControlModel = require('../models/syncControlModel');
|
const SyncControlModel = require('../models/syncControlModel');
|
||||||
const { fetchTicketsFromServiceNow: fetchTicketsApi, fetchRequestsFromServiceNow: fetchRequestsApi, fetchScItemOptionValue } = require('../services/servicenowService');
|
const { fetchTicketsFromServiceNow: fetchTicketsApi, fetchRequestsFromServiceNow: fetchRequestsApi, fetchScItemOptionValue } = require('../services/servicenowService');
|
||||||
const { logInfo, logError, logSync } = require('../utils/logger');
|
const { logInfo, logError, logSync } = require('../utils/logger');
|
||||||
const e = require('express');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processa e salva um lote de tickets (incidentes ou requisições).
|
* Processa e salva um lote de tickets (incidentes ou requisicoes).
|
||||||
* @param {Array} tickets - A lista de tickets da API do ServiceNow.
|
* @param {Array} tickets - A lista de tickets da API do ServiceNow.
|
||||||
* @param {('incidente'|'requisicao')} type - O tipo de ticket.
|
* @param {('incidente'|'requisicao')} type - O tipo de ticket.
|
||||||
*/
|
*/
|
||||||
@ -18,7 +17,7 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
}
|
}
|
||||||
const parts = dateString.match(/(\d{2})\/(\d{2})\/(\d{4}) (\d{2}):(\d{2}):(\d{2})/);
|
const parts = dateString.match(/(\d{2})\/(\d{2})\/(\d{4}) (\d{2}):(\d{2}):(\d{2})/);
|
||||||
if (!parts) return null; // Retorna nulo se o formato for inesperado
|
if (!parts) return null; // Retorna nulo se o formato for inesperado
|
||||||
// Formato: new Date(ano, mês-1, dia, hora, minuto, segundo)
|
// Formato: new Date(ano, mes-1, dia, hora, minuto, segundo)
|
||||||
return new Date(parts[3], parts[2] - 1, parts[1], parts[4], parts[5], parts[6]);
|
return new Date(parts[3], parts[2] - 1, parts[1], parts[4], parts[5], parts[6]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -27,12 +26,12 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo(`💾 Iniciando salvamento de ${tickets.length} ${type}s no banco...`);
|
logInfo(`INFO: Iniciando salvamento de ${tickets.length} ${type}s no banco...`);
|
||||||
let latestUpdateTimestamp = new Date(0);
|
let latestUpdateTimestamp = new Date(0);
|
||||||
for (const ticket of tickets) {
|
for (const ticket of tickets) {
|
||||||
try {
|
try {
|
||||||
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(`Descricao vazia para requisicao ${ticket.number}. Buscando em sc_item_option...`);
|
||||||
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 => {
|
||||||
@ -47,11 +46,11 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
|
|
||||||
if (vars.justification) {
|
if (vars.justification) {
|
||||||
ticket.description = vars.justification;
|
ticket.description = vars.justification;
|
||||||
logInfo(`Descrição da requisição ${ticket.number} atualizada com valor de sc_item_option.`);
|
logInfo(`Descricao da requisicao ${ticket.number} atualizada com valor de sc_item_option.`);
|
||||||
}
|
}
|
||||||
if (vars.telephone){
|
if (vars.telephone){
|
||||||
ticket.telefone = vars.telephone;
|
ticket.telefone = vars.telephone;
|
||||||
logInfo(`Telefone da requisição ${ticket.number} atualizado com valor de sc_item_option.`);
|
logInfo(`Telefone da requisicao ${ticket.number} atualizado com valor de sc_item_option.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +81,7 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
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 sincronizacao para o ticket ${ticket.number?.value} ja existe. Nenhuma acao necessaria.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUpdateDate = parseServiceNowDate(ticket.sys_updated_on);
|
const currentUpdateDate = parseServiceNowDate(ticket.sys_updated_on);
|
||||||
@ -102,33 +101,33 @@ const processAndSaveTickets = async (tickets, type) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller para buscar tickets e requisições do ServiceNow e salvá-los localmente.
|
* Controller para buscar tickets e requisicoes do ServiceNow e salva-los localmente.
|
||||||
*/
|
*/
|
||||||
const processSyncController = async () => {
|
const processSyncController = async () => {
|
||||||
try {
|
try {
|
||||||
const watermark = await SyncControlModel.getWatermark('servicenow');
|
const watermark = await SyncControlModel.getWatermark('servicenow');
|
||||||
logInfo(`📡 Buscando tickets do ServiceNow atualizados desde: ${watermark}`);
|
logInfo(`INFO: Buscando tickets do ServiceNow atualizados desde: ${watermark}`);
|
||||||
|
|
||||||
const incidents = await fetchTicketsApi(watermark);
|
const incidents = await fetchTicketsApi(watermark);
|
||||||
const latestIncidentUpdate = await processAndSaveTickets(incidents, 'incidente');
|
const latestIncidentUpdate = await processAndSaveTickets(incidents, 'incidente');
|
||||||
|
|
||||||
logInfo('📡 Buscando requisições do ServiceNow...');
|
logInfo('INFO: Buscando requisicoes do ServiceNow...');
|
||||||
const requests = await fetchRequestsApi(watermark);
|
const requests = await fetchRequestsApi(watermark);
|
||||||
const latestRequestUpdate = await processAndSaveTickets(requests, 'requisicao');
|
const latestRequestUpdate = await processAndSaveTickets(requests, 'requisicao');
|
||||||
|
|
||||||
const newWatermark = latestIncidentUpdate > latestRequestUpdate ? latestIncidentUpdate : latestRequestUpdate;
|
const newWatermark = latestIncidentUpdate > latestRequestUpdate ? latestIncidentUpdate : latestRequestUpdate;
|
||||||
|
|
||||||
if (newWatermark) {
|
if (newWatermark) {
|
||||||
// Adiciona 1 segundo para evitar reprocessar o último ticket em caso de timestamps iguais
|
// Mantem margem de seguranca de 6 horas para evitar perda de eventos por atraso de replicacao, timezone ou clock skew entre sistemas.
|
||||||
const sixHoursInMillis = 6 * 60 * 60 * 1000;
|
const sixHoursInMillis = 6 * 60 * 60 * 1000;
|
||||||
const nextWatermark = new Date(new Date(newWatermark).getTime() - sixHoursInMillis);
|
const nextWatermark = new Date(new Date(newWatermark).getTime() - sixHoursInMillis);
|
||||||
await SyncControlModel.setWatermark('servicenow', nextWatermark);
|
await SyncControlModel.setWatermark('servicenow', nextWatermark);
|
||||||
} else {
|
} else {
|
||||||
logInfo("Nenhum ticket novo ou atualizado encontrado. A marca d'água não foi alterada.");
|
logInfo("Nenhum ticket novo ou atualizado encontrado. A marca d'agua nao foi alterada.");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, 'Erro no processo de sincronização de tickets do ServiceNow.');
|
logError(error, 'Erro no processo de sincronizacao de tickets do ServiceNow.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -139,14 +138,14 @@ module.exports = {
|
|||||||
/**
|
/**
|
||||||
* @file processSyncController.js
|
* @file processSyncController.js
|
||||||
* @description
|
* @description
|
||||||
* Este controlador é responsável por buscar novos tickets e atualizações do ServiceNow.
|
* Este controlador e responsavel por buscar novos tickets e atualizacoes do ServiceNow.
|
||||||
* Ele utiliza uma "marca d'água" (watermark) para buscar apenas os registros modificados desde a última execução,
|
* Ele utiliza uma "marca d'agua" (watermark) para buscar apenas os registros modificados desde a ultima execucao,
|
||||||
* otimizando o processo.
|
* otimizando o processo.
|
||||||
*
|
*
|
||||||
* Funcionalidades:
|
* Funcionalidades:
|
||||||
* - Busca incidentes e requisições do ServiceNow com base na data da última atualização.
|
* - Busca incidentes e requisicoes do ServiceNow com base na data da ultima atualizacao.
|
||||||
* - Para cada ticket, salva ou atualiza o registro no banco de dados intermediário (`tickets_sn`).
|
* - Para cada ticket, salva ou atualiza o registro no banco de dados intermediario (`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`).
|
* - Cria ou atualiza o registro de controle na tabela `ticket_sync`, definindo o status inicial e a origem da atualizacao (`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`).
|
* - Implementa uma logica de enriquecimento de dados para requisicoes com descricao vazia, buscando informacoes de variaveis de catalogo (`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.
|
* - Ao final, atualiza a marca d'agua para a proxima execucao, apenas se houverem tickets atualizados na execucao vigente.
|
||||||
*/
|
*/
|
||||||
@ -21,7 +21,7 @@ class TicketSyncModel {
|
|||||||
|
|
||||||
const values = [
|
const values = [
|
||||||
data.sn_ticket_id,
|
data.sn_ticket_id,
|
||||||
data.glpi_ticket,
|
data.glpi_ticket_id,
|
||||||
data.last_sn_sync,
|
data.last_sn_sync,
|
||||||
data.last_glpi_sync,
|
data.last_glpi_sync,
|
||||||
data.created_at,
|
data.created_at,
|
||||||
@ -35,7 +35,7 @@ class TicketSyncModel {
|
|||||||
await pool.query(query, values);
|
await pool.query(query, values);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '❌ Erro ao criar registro de sincronização');
|
logError(error, 'ERRO: Erro ao criar registro de sincronizacao');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,7 +47,7 @@ class TicketSyncModel {
|
|||||||
const { rows } = await pool.query(query, values);
|
const { rows } = await pool.query(query, values);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '❌ Erro ao buscar tickets para sincronizar');
|
logError(error, 'ERRO: Erro ao buscar tickets para sincronizar');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,7 +57,7 @@ class TicketSyncModel {
|
|||||||
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';
|
||||||
await pool.query(query, [source, snTicketId]);
|
await pool.query(query, [source, snTicketId]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `❌ Erro ao atualizar source_last para o ticket SN ID: ${snTicketId}`);
|
logError(error, `ERRO: Erro ao atualizar source_last para o ticket SN ID: ${snTicketId}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ class TicketSyncModel {
|
|||||||
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';
|
||||||
await pool.query(query, [glpiStatus, snStatus, ticketSyncId]);
|
await pool.query(query, [glpiStatus, snStatus, ticketSyncId]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(`${error} ❌ Erro ao atualizar status de sincronização para o ID: ${ticketSyncId}`);
|
logError(`${error} ERRO: Erro ao atualizar status de sincronizacao para o ID: ${ticketSyncId}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,17 +78,7 @@ class TicketSyncModel {
|
|||||||
const query = 'UPDATE ticket_sync SET glpi_sync_status = $1, sn_sync_status= $2, source_last = $4 WHERE sn_ticket_id = $3';
|
const query = 'UPDATE ticket_sync SET glpi_sync_status = $1, sn_sync_status= $2, source_last = $4 WHERE sn_ticket_id = $3';
|
||||||
await pool.query(query, [glpiStatus, snStatus, ticketSyncId, source_last]);
|
await pool.query(query, [glpiStatus, snStatus, ticketSyncId, source_last]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(`${error} ❌ Erro ao atualizar status de sincronização para o ID: ${ticketSyncId}`);
|
logError(`${error} ERRO: Erro ao atualizar status de sincronizacao para o ID: ${ticketSyncId}`);
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async updateSourceLast(snTicketId, source) {
|
|
||||||
try {
|
|
||||||
const query = 'UPDATE ticket_sync SET source_last = $1 WHERE sn_ticket_id = $2';
|
|
||||||
await pool.query(query, [source, snTicketId]);
|
|
||||||
} catch (error) {
|
|
||||||
logError(error, `❌ Erro ao atualizar source_last para o ticket SN ID: ${snTicketId}`);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,7 +88,7 @@ class TicketSyncModel {
|
|||||||
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';
|
||||||
await pool.query(query, [glpiStatus, snStatus, ticketSyncId, glpiTicketId]);
|
await pool.query(query, [glpiStatus, snStatus, ticketSyncId, glpiTicketId]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `❌ Erro ao atualizar status de sincronização para o ID: ${ticketSyncId}`);
|
logError(error, `ERRO: Erro ao atualizar status de sincronizacao para o ID: ${ticketSyncId}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,7 +99,7 @@ class TicketSyncModel {
|
|||||||
const { rows } = await pool.query(query, [glpiTicketId]);
|
const { rows } = await pool.query(query, [glpiTicketId]);
|
||||||
return rows[0] ? rows[0].id : null;
|
return rows[0] ? rows[0].id : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `❌ Erro ao buscar ID de sincronização por GLPI ID: ${glpiTicketId}`);
|
logError(error, `ERRO: Erro ao buscar ID de sincronizacao por GLPI ID: ${glpiTicketId}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,7 +113,7 @@ class TicketSyncModel {
|
|||||||
const { rows } = await pool.query(query, [sysId]);
|
const { rows } = await pool.query(query, [sysId]);
|
||||||
return rows[0] || null;
|
return rows[0] || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `❌ Erro ao buscar registro de sincronização por sys_id: ${sysId}`);
|
logError(error, `ERRO: Erro ao buscar registro de sincronizacao por sys_id: ${sysId}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,7 +124,7 @@ class TicketSyncModel {
|
|||||||
const { rows } = await pool.query(query, [snTicketId]);
|
const { rows } = await pool.query(query, [snTicketId]);
|
||||||
return rows[0] ? rows[0].id : null;
|
return rows[0] ? rows[0].id : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `❌ Erro ao buscar ID de sincronização por SN ID: ${snTicketId}`);
|
logError(error, `ERRO: Erro ao buscar ID de sincronizacao por SN ID: ${snTicketId}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,7 +136,7 @@ class TicketSyncModel {
|
|||||||
return rows[0] || null;
|
return rows[0] || null;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(`${error}, ❌ Erro ao buscar ticket ${ticketNumber}}` )
|
logError(`${error}, ERRO: Erro ao buscar ticket ${ticketNumber}}` )
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -166,7 +156,7 @@ class TicketSyncModel {
|
|||||||
const { rows } = await pool.query(query);
|
const { rows } = await pool.query(query);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '❌ Erro ao buscar tickets para sincronizar');
|
logError(error, 'ERRO: Erro ao buscar tickets para sincronizar');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,7 +179,7 @@ class TicketSyncModel {
|
|||||||
const { rows } = await pool.query(query);
|
const { rows } = await pool.query(query);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '❌ Erro ao buscar tickets para monitorar');
|
logError(error, 'ERRO: Erro ao buscar tickets para monitorar');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,7 +201,7 @@ class TicketSyncModel {
|
|||||||
const { rows } = await pool.query(query);
|
const { rows } = await pool.query(query);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '❌ Erro ao buscar tickets pendentes de fechamento');
|
logError(error, 'ERRO: Erro ao buscar tickets pendentes de fechamento');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,7 +216,7 @@ class TicketSyncModel {
|
|||||||
const { rows } = await pool.query(query);
|
const { rows } = await pool.query(query);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '❌ Erro ao buscar tickets em estado de erro');
|
logError(error, 'ERRO: Erro ao buscar tickets em estado de erro');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -244,7 +234,7 @@ class TicketSyncModel {
|
|||||||
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: Erro ao atualizar last_sync para o ID: ${snTicketId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +243,7 @@ class TicketSyncModel {
|
|||||||
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';
|
||||||
await pool.query(query, [snStatus, glpiStatus, glpiTicketId]);
|
await pool.query(query, [snStatus, glpiStatus, glpiTicketId]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(`${error} ❌ Erro ao atualizar status de sincronização para o GLPI ID: ${glpiTicketId}`);
|
logError(`${error} ERRO: Erro ao atualizar status de sincronizacao para o GLPI ID: ${glpiTicketId}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,16 +252,16 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @module TicketSyncModel
|
* @module TicketSyncModel
|
||||||
* @description Este módulo é a camada de acesso à tabela `ticket_sync`, que atua como o cérebro do processo de sincronização.
|
* @description Este modulo e a camada de acesso a tabela `ticket_sync`, que atua como o cerebro do processo de sincronizacao.
|
||||||
* Ele armazena o estado de cada ticket em ambos os sistemas (GLPI e ServiceNow) e controla o fluxo de atualizações.
|
* Ele armazena o estado de cada ticket em ambos os sistemas (GLPI e ServiceNow) e controla o fluxo de atualizacoes.
|
||||||
*
|
*
|
||||||
* Principais Funcionalidades:
|
* 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').
|
* - **Gerenciamento de Estado**: Metodos como `updateStatus`, `updateStatusAndSourceLast` e `updateStatusAndGlpiTicket` sao usados para modificar os status de sincronizacao (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).
|
* - **Controle de Fluxo (`source_last`)**: A coluna `source_last` e gerenciada por este modelo para indicar qual sistema realizou a ultima modificacao, prevenindo atualizacoes 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.
|
* - **Selecao de Tickets para Processamento**: Metodos 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.
|
* - **Criacao e Consulta**: `createSyncRecord` cria o registro inicial de um ticket, e varias funcoes de busca (`getBySysId`, `getIdSyncByGlpiId`, etc.) permitem localizar registros de sincronizacao.
|
||||||
*
|
*
|
||||||
* A tabela `ticket_sync` é a fonte da verdade para o estado da integração de cada ticket individualmente.
|
* A tabela `ticket_sync` e a fonte da verdade para o estado da integracao de cada ticket individualmente.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,16 +9,16 @@ const { log } = require('winston');
|
|||||||
|
|
||||||
const syncCommentsGlpitoSN = async (ticket) => {
|
const syncCommentsGlpitoSN = async (ticket) => {
|
||||||
try {
|
try {
|
||||||
logInfo('🔄 Iniciando sincronização de comments do GLPI para o Service Now...');
|
logInfo('INFO: Iniciando sincronizacao de comments do GLPI para o ServiceNow...');
|
||||||
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) {
|
||||||
logInfo(`Nenhum comentário encontrado no GLPI para o ticket GLPI ID: ${ticket.glpi_ticket_id}`);
|
logInfo(`Nenhum comentario encontrado no GLPI para o ticket GLPI ID: ${ticket.glpi_ticket_id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo(`📝 ${comments.length} comentários encontrados (GLPI ID: ${ticket.glpi_ticket_id})`);
|
logInfo(`INFO: ${comments.length} comentarios encontrados (GLPI ID: ${ticket.glpi_ticket_id})`);
|
||||||
await TicketSyncModel.updateStatus(ticket.glpi_ticket_id, 'comment_collected', 'pending_cmt_check');
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'comment_collected', 'pending_cmt_check');
|
||||||
|
|
||||||
for (const comment of comments) {
|
for (const comment of comments) {
|
||||||
try {
|
try {
|
||||||
@ -28,7 +28,7 @@ const syncCommentsGlpitoSN = async (ticket) => {
|
|||||||
const syncId = await TicketSyncModel.getIdSyncByGlpiId(ticket.glpi_ticket_id);
|
const syncId = await TicketSyncModel.getIdSyncByGlpiId(ticket.glpi_ticket_id);
|
||||||
|
|
||||||
if (existingComment && existsInSN) {
|
if (existingComment && existsInSN) {
|
||||||
logInfo(`🔁 Comentário ${comment.id} já sincronizado.`);
|
logInfo(`INFO: Comentario ${comment.id} ja sincronizado.`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,20 +71,20 @@ const syncCommentsGlpitoSN = async (ticket) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError(`Erro ao sincronizar comentário ${comment.id}: ${err}`);
|
logError(`Erro ao sincronizar comentario ${comment.id}: ${err}`);
|
||||||
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'error', 'error');
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'error', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo('✅ Sincronização de comentários GLPI → SN concluída!');
|
logInfo('OK: Sincronizacao de comentarios GLPI -> SN concluida!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(`Erro geral na sincronização: ${error}`);
|
logError(`Erro geral na sincronizacao: ${error}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const syncCommentsSNtoGlpi = async (comments, ticket) => {
|
const syncCommentsSNtoGlpi = async (comments, ticket) => {
|
||||||
logInfo('🔄 Iniciando sincronização de comments do ServiceNow para o GLPI...');
|
logInfo('INFO: Iniciando sincronizacao de comments do ServiceNow para o GLPI...');
|
||||||
|
|
||||||
for (const comment of comments) {
|
for (const comment of comments) {
|
||||||
try {
|
try {
|
||||||
@ -95,10 +95,10 @@ const syncCommentsSNtoGlpi = async (comments, ticket) => {
|
|||||||
}
|
}
|
||||||
await TicketUpdateModel.updateDestinyId(comment.source_id, commentId[0].id);
|
await TicketUpdateModel.updateDestinyId(comment.source_id, commentId[0].id);
|
||||||
|
|
||||||
logInfo(`✅ Comentário inserido com sucesso no GLPI para o chamado: ${ticket.glpi_ticket_id}`);
|
logInfo(`OK: Comentario inserido com sucesso no GLPI para o chamado: ${ticket.glpi_ticket_id}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(`⚠️ Erro ao processar comentário do ticket ${ticket.sn_ticket_id}: ${error}`);
|
logError(`ERRO: Erro ao processar comentario do ticket ${ticket.sn_ticket_id}: ${error}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'error');
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'error');
|
||||||
@ -111,18 +111,18 @@ const syncCommentsSNtoGlpi = async (comments, ticket) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @module glpiCommentService
|
* @module glpiCommentService
|
||||||
* @description Este serviço orquestra a sincronização de comentários (follow-ups/notas) entre GLPI e ServiceNow.
|
* @description Este servico orquestra a sincronizacao de comentarios (follow-ups/notas) entre GLPI e ServiceNow.
|
||||||
*
|
*
|
||||||
* Principais Funcionalidades:
|
* 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:
|
* - `syncCommentsGlpitoSN(ticket)`: Sincroniza comentarios do GLPI para o ServiceNow. Esta e a funcao mais complexa, pois implementa uma logica de 4 casos para garantir a consistencia e evitar duplicatas:
|
||||||
* 1. **Já Sincronizado**: O comentário existe no banco local e no ServiceNow. Nenhuma ação é necessária.
|
* 1. **Ja Sincronizado**: O comentario existe no banco local e no ServiceNow. Nenhuma acao e necessaria.
|
||||||
* 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.
|
* 2. **Existe no SN, Falta no Local**: O comentario foi sincronizado em uma execucao anterior que falhou. O registro local e 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.
|
* 3. **Existe no Local, Falta no SN**: O comentario foi registrado localmente, mas a sincronizacao com o SN falhou. A funcao reenvia o comentario 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.
|
* 4. **Nao Existe em Nenhum**: Um novo comentario do GLPI. Ele e enviado para o ServiceNow e um novo registro e 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.
|
* - `syncCommentsSNtoGlpi(comments, ticket)`: Sincroniza comentarios do ServiceNow para o GLPI. Este fluxo e mais simples: para cada novo comentario do SN (ja 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.
|
* Em ambos os fluxos, o "bastao" (`source_last`) e atualizado para indicar qual sistema originou a ultima atualizacao, mantendo o controle do fluxo de status.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@ -8,6 +8,8 @@ const TicketUpdateModel = require('../models/ticketUpdateModel');
|
|||||||
const TicketSyncModel = require('../models/ticketSyncModel');
|
const TicketSyncModel = require('../models/ticketSyncModel');
|
||||||
const { log } = require('winston');
|
const { log } = require('winston');
|
||||||
|
|
||||||
|
const resolvedBySysId = process.env.SERVICENOW_RESOLVED_BY_SYSID || 'b131d6ed1b2ba510c5e163923b4bcb77';
|
||||||
|
|
||||||
const fetchTicketsFromServiceNow = async (watermark) => {
|
const fetchTicketsFromServiceNow = async (watermark) => {
|
||||||
try {
|
try {
|
||||||
const watermarkDate = new Date(watermark);
|
const watermarkDate = new Date(watermark);
|
||||||
@ -47,12 +49,12 @@ const getTicketSysId = async (ticketId) => {
|
|||||||
try {
|
try {
|
||||||
const ticket = await TicketSnModel.findById(ticketId);
|
const ticket = await TicketSnModel.findById(ticketId);
|
||||||
if (!ticket) {
|
if (!ticket) {
|
||||||
logError(`Ticket com ID local ${ticketId} não encontrado.`, 'getTicketSysId');
|
logError(`Ticket com ID local ${ticketId} nao encontrado.`, 'getTicketSysId');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return ticket.sys_id;
|
return ticket.sys_id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '🚨Falha ao obter sys_id do ticket');
|
logError(error, 'ERRO: Falha ao obter sys_id do ticket');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,14 +63,14 @@ const getTicketTypeById = async (ticketId) => {
|
|||||||
try {
|
try {
|
||||||
const ticket = await TicketSnModel.findById(ticketId);
|
const ticket = await TicketSnModel.findById(ticketId);
|
||||||
if (!ticket) {
|
if (!ticket) {
|
||||||
logError(`Ticket com ID local ${ticketId} não encontrado.`, 'getTicketTypeById');
|
logError(`Ticket com ID local ${ticketId} nao encontrado.`, 'getTicketTypeById');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const ticketType = ticket.tipo;
|
const ticketType = ticket.tipo;
|
||||||
logInfo(`⚠️ Tipo do ticket ${ticketId} - Tipo: ${ticketType}`, { ticketId, ticketType });
|
logInfo(`INFO: Tipo do ticket ${ticketId} - Tipo: ${ticketType}`, { ticketId, ticketType });
|
||||||
return ticketType;
|
return ticketType;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '🚨Falha ao obter tipo do ticket');
|
logError(error, 'ERRO: Falha ao obter tipo do ticket');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,15 +87,15 @@ const getCommentId = async (sysId, comment) => {
|
|||||||
const data = response.data;
|
const data = response.data;
|
||||||
const comments = data.result;
|
const comments = data.result;
|
||||||
|
|
||||||
// Procurar o comentário exato na lista retornada
|
// Procurar o comentario exato na lista retornada
|
||||||
for (const item of comments) {
|
for (const item of comments) {
|
||||||
if (item.value && item.value.includes(comment)) {
|
if (item.value && item.value.includes(comment)) {
|
||||||
return item.sys_id; // Retorna o sys_id do comentário encontrado
|
return item.sys_id; // Retorna o sys_id do comentario encontrado
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '🚨Falha ao obter ID do comentário no ServiceNow');
|
logError(error, 'ERRO: Falha ao obter ID do comentario no ServiceNow');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,34 +126,34 @@ const createCommentInServiceNow = async (snId, sysId, comment, ticketType) => {
|
|||||||
|
|
||||||
// Verificar se retorna status 200 para sucesso
|
// Verificar se retorna status 200 para sucesso
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
logError(`❌ Falha ao criar comentário. Status: ${response.status}`, { sysId, ticketType });
|
logError(`ERRO: Falha ao criar comentario. Status: ${response.status}`, { sysId, ticketType });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// retornar id do comentário criado
|
// retornar id do comentario criado
|
||||||
const commentId = await getCommentId(sysId, comment);
|
const commentId = await getCommentId(sysId, comment);
|
||||||
|
|
||||||
if (!commentId) {
|
if (!commentId) {
|
||||||
logError('❌ Comentário criado, mas não foi possível obter o ID do comentário', { sysId });
|
logError('ERRO: Comentario criado, mas nao foi possivel obter o ID do comentario', { sysId });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo(`✅ Comentário criado com sucesso no ServiceNow. Comment ID: ${commentId}`, { sysId, ticketType });
|
logInfo(`OK: Comentario criado com sucesso no ServiceNow. Comment ID: ${commentId}`, { sysId, ticketType });
|
||||||
|
|
||||||
return commentId;
|
return commentId;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '🚨Falha ao criar comentário no ServiceNow');
|
logError(error, 'ERRO: Falha ao criar comentario no ServiceNow');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncCommentToServiceNow = async (ticketId, comment) => {
|
const syncCommentToServiceNow = async (ticketId, comment) => {
|
||||||
logInfo(`Iniciando sincronização do comentário para o ticket SN Ticket: ${ticketId}`);
|
logInfo(`Iniciando sincronizacao do comentario para o ticket SN Ticket: ${ticketId}`);
|
||||||
const snSysId = await getTicketSysId(ticketId);
|
const snSysId = await getTicketSysId(ticketId);
|
||||||
const ticketType = await getTicketTypeById(ticketId);
|
const ticketType = await getTicketTypeById(ticketId);
|
||||||
|
|
||||||
if (!snSysId) {
|
if (!snSysId) {
|
||||||
logError(`❌ sys_id do ServiceNow não encontrado para o ticket SN Ticket: ${ticketId}`);
|
logError(`ERRO: sys_id do ServiceNow nao encontrado para o ticket SN Ticket: ${ticketId}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const snCommentId = await createCommentInServiceNow(ticketId,snSysId, comment, ticketType);
|
const snCommentId = await createCommentInServiceNow(ticketId,snSysId, comment, ticketType);
|
||||||
@ -165,7 +167,7 @@ const existingCommentInServiceNow = async (ticketId, comment) => {
|
|||||||
return commentExists;
|
return commentExists;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '🚨Falha ao verificar existência do comentário no ServiceNow');
|
logError(error, 'ERRO: Falha ao verificar existencia do comentario no ServiceNow');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,7 +175,7 @@ const existingCommentInServiceNow = async (ticketId, comment) => {
|
|||||||
const fetchCommentsFromServiceNow = async (ticket) => {
|
const fetchCommentsFromServiceNow = async (ticket) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
logInfo(`Iniciando busca de comentários do ServiceNow para o ticket SN Ticket: ${ticket.glpi_ticket_id}`);
|
logInfo(`Iniciando busca de comentarios do ServiceNow para o ticket SN Ticket: ${ticket.glpi_ticket_id}`);
|
||||||
|
|
||||||
const sys_id = ticket.sys_id;
|
const sys_id = ticket.sys_id;
|
||||||
|
|
||||||
@ -192,7 +194,7 @@ const fetchCommentsFromServiceNow = async (ticket) => {
|
|||||||
sysparm_limit: pageSize,
|
sysparm_limit: pageSize,
|
||||||
sysparm_offset: offset
|
sysparm_offset: offset
|
||||||
};
|
};
|
||||||
logInfo(`Buscando página ${page} de comentários do ticket SN Ticket: ${ticket.glpi_ticket_id}`);
|
logInfo(`Buscando pagina ${page} de comentarios do ticket SN Ticket: ${ticket.glpi_ticket_id}`);
|
||||||
const response = await axios.get(apiConfig.snTableJournalConfig.baseUrl, {
|
const response = await axios.get(apiConfig.snTableJournalConfig.baseUrl, {
|
||||||
auth: apiConfig.servicenowAuthentication.auth,
|
auth: apiConfig.servicenowAuthentication.auth,
|
||||||
params
|
params
|
||||||
@ -203,33 +205,33 @@ const fetchCommentsFromServiceNow = async (ticket) => {
|
|||||||
|
|
||||||
|
|
||||||
if (!Array.isArray(comments) || comments.length === 0) {
|
if (!Array.isArray(comments) || comments.length === 0) {
|
||||||
break; // não há mais comentários
|
break; // nao ha mais comentarios
|
||||||
}
|
}
|
||||||
|
|
||||||
allComments.push(...comments);
|
allComments.push(...comments);
|
||||||
|
|
||||||
if (comments.length < pageSize) {
|
if (comments.length < pageSize) {
|
||||||
break; // última página
|
break; // ultima pagina
|
||||||
}
|
}
|
||||||
|
|
||||||
// avançar para próxima página
|
// avancar para proxima pagina
|
||||||
offset += pageSize;
|
offset += pageSize;
|
||||||
|
|
||||||
// proteção extra para evitar loop infinito em caso de comportamento inesperado da API
|
// protecao extra para evitar loop infinito em caso de comportamento inesperado da API
|
||||||
if (page >= internalMaxPages) {
|
if (page >= internalMaxPages) {
|
||||||
logError(new Error('Max pages reached while paginating comments'), { ticketId, page });
|
logError(new Error('Max pages reached while paginating comments'), { ticketId, page });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traz apenas os tickets que não são do sys_created_by: "SOTHIS.CAOA"
|
// Traz apenas os tickets que nao sao do sys_created_by: "SOTHIS.CAOA"
|
||||||
const filteredComments = allComments.filter(comment => comment.sys_created_by !== 'SOTHIS.CAOA');
|
const filteredComments = allComments.filter(comment => comment.sys_created_by !== 'SOTHIS.CAOA');
|
||||||
if (filteredComments.length === 0) {
|
if (filteredComments.length === 0) {
|
||||||
logInfo(`Nenhum comentário encontrado no Service Now para o ticket GLPI ID: ${ticket.glpi_ticket_id}`);
|
logInfo(`Nenhum comentario encontrado no Service Now para o ticket GLPI ID: ${ticket.glpi_ticket_id}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo(`Encontrado ${filteredComments.length} comentários para o ticket SN Ticket: ${ticket.glpi_ticket_id}`)
|
logInfo(`Encontrado ${filteredComments.length} comentarios para o ticket SN Ticket: ${ticket.glpi_ticket_id}`)
|
||||||
|
|
||||||
|
|
||||||
const commentsInserted = [];
|
const commentsInserted = [];
|
||||||
@ -238,7 +240,7 @@ const fetchCommentsFromServiceNow = async (ticket) => {
|
|||||||
const existingComment = await TicketUpdateModel.getBySourceId(comment.sys_id);
|
const existingComment = await TicketUpdateModel.getBySourceId(comment.sys_id);
|
||||||
|
|
||||||
if (existingComment) {
|
if (existingComment) {
|
||||||
logInfo(`Comentário: ${comment.sys_id} já existe no banco de dados`, { step: 3 });
|
logInfo(`Comentario: ${comment.sys_id} ja existe no banco de dados`, { step: 3 });
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
@ -259,9 +261,9 @@ const fetchCommentsFromServiceNow = async (ticket) => {
|
|||||||
|
|
||||||
if (commentInserted.success) {
|
if (commentInserted.success) {
|
||||||
commentsInserted.push(commentData);
|
commentsInserted.push(commentData);
|
||||||
logInfo(`✅ Comentário ID ${comment.sys_id} do ${commentData.source_system} inserido no banco!`, { step: 4 });
|
logInfo(`OK: Comentario ID ${comment.sys_id} do ${commentData.source_system} inserido no banco!`, { step: 4 });
|
||||||
} else {
|
} else {
|
||||||
logError(`❌ Erro ao inserir comentário ID ${comment.sys_id} no banco: ${commentInserted.error}`, { step: 4 });
|
logError(`ERRO: Erro ao inserir comentario ID ${comment.sys_id} no banco: ${commentInserted.error}`, { step: 4 });
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -270,16 +272,16 @@ const fetchCommentsFromServiceNow = async (ticket) => {
|
|||||||
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '🚨Falha ao buscar comentários do ServiceNow');
|
logError(error, 'ERRO: Falha ao buscar comentarios do ServiceNow');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchAndProcessClosedTicketsFromSN = async () => {
|
const fetchAndProcessClosedTicketsFromSN = async () => {
|
||||||
try {
|
try {
|
||||||
logInfo('📡 Verificando status de tickets sincronizados no ServiceNow...');
|
logInfo('INFO: Verificando status de tickets sincronizados no ServiceNow...');
|
||||||
|
|
||||||
// 1. Busca apenas os tickets que estão abertos e sincronizados no nosso banco.
|
// 1. Busca apenas os tickets que estao abertos e sincronizados no nosso banco.
|
||||||
const syncedTickets = await TicketSyncModel.getTicketsSynced();
|
const syncedTickets = await TicketSyncModel.getTicketsSynced();
|
||||||
|
|
||||||
if (syncedTickets.length === 0) {
|
if (syncedTickets.length === 0) {
|
||||||
@ -297,12 +299,12 @@ const fetchAndProcessClosedTicketsFromSN = async () => {
|
|||||||
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'solved', 'SNOW');
|
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'solved', 'SNOW');
|
||||||
|
|
||||||
} else if (ticketSnDetails && ticketSnDetails.status === 'Encerrado - Omitido') { // 'Encerrado - Omitido' mapeia para 'Closed' (7)
|
} else if (ticketSnDetails && ticketSnDetails.status === 'Encerrado - Omitido') { // 'Encerrado - Omitido' mapeia para 'Closed' (7)
|
||||||
logInfo(`Ticket SN ${ticketSnDetails.ticket_number} foi Fechado permanentemente. Marcando para sincronização de fechamento...`);
|
logInfo(`Ticket SN ${ticketSnDetails.ticket_number} foi Fechado permanentemente. Marcando para sincronizacao de fechamento...`);
|
||||||
|
|
||||||
// Sanitiza o conteúdo da nota de fechamento antes de salvar.
|
// Sanitiza o conteudo da nota de fechamento antes de salvar.
|
||||||
const sanitizedContent = stripHTML(ticketSnDetails.close_notes || 'Chamado encerrado sem notas.');
|
const sanitizedContent = stripHTML(ticketSnDetails.close_notes || 'Chamado encerrado sem notas.');
|
||||||
|
|
||||||
// Salva a nota de fechamento no nosso banco intermediário.
|
// Salva a nota de fechamento no nosso banco intermediario.
|
||||||
await TicketUpdateModel.insert({
|
await TicketUpdateModel.insert({
|
||||||
ticket_sync_id: ticket.id,
|
ticket_sync_id: ticket.id,
|
||||||
source_system: 'servicenow',
|
source_system: 'servicenow',
|
||||||
@ -319,12 +321,12 @@ const fetchAndProcessClosedTicketsFromSN = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, '🚨Falha ao buscar chamados encerrados do ServiceNow');
|
logError(error, 'ERRO: Falha ao buscar chamados encerrados do ServiceNow');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 codigo numerico do ServiceNow
|
||||||
const stateMap = {
|
const stateMap = {
|
||||||
'Aguardando Atendimento': '1', // New
|
'Aguardando Atendimento': '1', // New
|
||||||
'Em Atendimento': '2', // In Progress
|
'Em Atendimento': '2', // In Progress
|
||||||
@ -333,13 +335,13 @@ const updateStatusInServiceNow = async (snTicketId, state) => {
|
|||||||
'Encerrado - Omitido': '7' // Closed
|
'Encerrado - Omitido': '7' // Closed
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateCode = stateMap[state] || state; // Usa o código mapeado ou o valor original se não encontrar
|
const stateCode = stateMap[state] || state; // Usa o codigo mapeado ou o valor original se nao encontrar
|
||||||
|
|
||||||
let sysId = null; // Declarar sysId fora do try para estar disponível no catch
|
let sysId = null; // Declarar sysId fora do try para estar disponivel no catch
|
||||||
try {
|
try {
|
||||||
const ticketSn = await TicketSnModel.findById(snTicketId);
|
const ticketSn = await TicketSnModel.findById(snTicketId);
|
||||||
if (!ticketSn) {
|
if (!ticketSn) {
|
||||||
logError(`Ticket SN com ID ${snTicketId} não encontrado para atualização de status.`, 'updateStatusInServiceNow');
|
logError(`Ticket SN com ID ${snTicketId} nao encontrado para atualizacao de status.`, 'updateStatusInServiceNow');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,7 +356,7 @@ const updateStatusInServiceNow = async (snTicketId, state) => {
|
|||||||
payload = { state: stateCode, incident_state: stateCode }; // Envia state e incident_state
|
payload = { state: stateCode, incident_state: stateCode }; // Envia state e incident_state
|
||||||
} else if (ticketType === 'requisicao') {
|
} else if (ticketType === 'requisicao') {
|
||||||
url = `${apiConfig.snTableRequestConfig.baseUrl}/${sysId}`;
|
url = `${apiConfig.snTableRequestConfig.baseUrl}/${sysId}`;
|
||||||
payload = { state: stateCode }; // Requisições podem ter um fluxo diferente
|
payload = { state: stateCode }; // Requisicoes podem ter um fluxo diferente
|
||||||
} 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}`);
|
||||||
@ -366,15 +368,15 @@ const updateStatusInServiceNow = async (snTicketId, state) => {
|
|||||||
await TicketSyncModel.updateLastSync(snTicketId, 'servicenow'); // Atualiza o timestamp
|
await TicketSyncModel.updateLastSync(snTicketId, 'servicenow'); // Atualiza o timestamp
|
||||||
|
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
logError(`❌ Falha ao atualizar status. Status: ${response.status} - ${response.statusText}`, { sysId, ticketType, state: stateCode });
|
logError(`ERRO: Falha ao atualizar status. Status: ${response.status} - ${response.statusText}`, { sysId, ticketType, state: stateCode });
|
||||||
throw new Error(`Falha ao atualizar status no ServiceNow: ${response.status} - ${response.statusText}`);
|
throw new Error(`Falha ao atualizar status no ServiceNow: ${response.status} - ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo(`✅ Status do ticket atualizado com sucesso no ServiceNow.`, { sysId, ticketType, state: stateCode });
|
logInfo(`OK: Status do ticket atualizado com sucesso no ServiceNow.`, { sysId, ticketType, state: stateCode });
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `🚨 Falha ao atualizar status no ServiceNow para o ticket com sys_id: ${sysId}`);
|
logError(error, `ERRO: Falha ao atualizar status no ServiceNow para o ticket com sys_id: ${sysId}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -385,7 +387,7 @@ const closeTicketInServiceNow = async (snTicketId, closeNotes, resolvedAt) => {
|
|||||||
try {
|
try {
|
||||||
const ticketSn = await TicketSnModel.findById(snTicketId);
|
const ticketSn = await TicketSnModel.findById(snTicketId);
|
||||||
if (!ticketSn) {
|
if (!ticketSn) {
|
||||||
logError(`Ticket SN com ID ${snTicketId} não encontrado para fechamento.`, 'closeTicketInServiceNow');
|
logError(`Ticket SN com ID ${snTicketId} nao encontrado para fechamento.`, 'closeTicketInServiceNow');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,9 +396,6 @@ const closeTicketInServiceNow = async (snTicketId, closeNotes, resolvedAt) => {
|
|||||||
let url = ''; // Declarar url fora do if
|
let url = ''; // Declarar url fora do if
|
||||||
let payload = {};
|
let payload = {};
|
||||||
|
|
||||||
// O ID do usuário que está resolvendo o chamado. Pegue o sys_id do usuário da integração no SN.
|
|
||||||
// O nome do usuário que está resolvendo o chamado.
|
|
||||||
const resolvedBySysId = 'b131d6ed1b2ba510c5e163923b4bcb77'; // sys_id do usuário que resolveu
|
|
||||||
|
|
||||||
if (ticketType === 'incidente') {
|
if (ticketType === 'incidente') {
|
||||||
url = `${apiConfig.snTableIncidentConfig.baseUrl}/${sysId}`;
|
url = `${apiConfig.snTableIncidentConfig.baseUrl}/${sysId}`;
|
||||||
@ -407,7 +406,7 @@ const closeTicketInServiceNow = async (snTicketId, closeNotes, resolvedAt) => {
|
|||||||
resolved_at: new Date(resolvedAt).toISOString().slice(0, 19).replace('T', ' '),
|
resolved_at: new Date(resolvedAt).toISOString().slice(0, 19).replace('T', ' '),
|
||||||
close_code: 'Closed/Resolved by caller',
|
close_code: 'Closed/Resolved by caller',
|
||||||
close_notes: closeNotes,
|
close_notes: closeNotes,
|
||||||
work_notes: `Ticket Encerrado por Suporte Técnico Sothis: ${closeNotes}`
|
work_notes: `Ticket Encerrado por Suporte Tecnico Sothis: ${closeNotes}`
|
||||||
};
|
};
|
||||||
|
|
||||||
} else if (ticketType === 'requisicao') {
|
} else if (ticketType === 'requisicao') {
|
||||||
@ -418,7 +417,7 @@ const closeTicketInServiceNow = async (snTicketId, closeNotes, resolvedAt) => {
|
|||||||
resolved_by: resolvedBySysId,
|
resolved_by: resolvedBySysId,
|
||||||
resolved_at: new Date(resolvedAt).toISOString().slice(0, 19).replace('T', ' '),
|
resolved_at: new Date(resolvedAt).toISOString().slice(0, 19).replace('T', ' '),
|
||||||
closeNotes: closeNotes,
|
closeNotes: closeNotes,
|
||||||
work_notes: `Ticket Encerrado por Suporte Técnico Sothis: ${closeNotes}`,
|
work_notes: `Ticket Encerrado por Suporte Tecnico Sothis: ${closeNotes}`,
|
||||||
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -435,15 +434,15 @@ const closeTicketInServiceNow = async (snTicketId, closeNotes, resolvedAt) => {
|
|||||||
await TicketSyncModel.updateLastSync(snTicketId, 'servicenow'); // Corrigido para usar snTicketId
|
await TicketSyncModel.updateLastSync(snTicketId, 'servicenow'); // Corrigido para usar snTicketId
|
||||||
|
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
logError(`❌ Falha ao resolver ticket. Status: ${response.status} - ${response.statusText}`, { sysId, ticketType });
|
logError(`ERRO: Falha ao resolver ticket. Status: ${response.status} - ${response.statusText}`, { sysId, ticketType });
|
||||||
throw new Error(`Falha ao fechar ticket no ServiceNow: ${response.status} - ${response.statusText}`);
|
throw new Error(`Falha ao fechar ticket no ServiceNow: ${response.status} - ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
logInfo(`✅ Ticket resolvido com sucesso no ServiceNow.`, { sysId, ticketType });
|
logInfo(`OK: Ticket resolvido com sucesso no ServiceNow.`, { sysId, ticketType });
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `🚨 Falha ao fechar ticket no ServiceNow (sysId: ${sysId})`);
|
logError(error, `ERRO: Falha ao fechar ticket no ServiceNow (sysId: ${sysId})`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -453,7 +452,7 @@ const addWorkNoteToServiceNow = async (snTicketId, workNote) => {
|
|||||||
try {
|
try {
|
||||||
const ticketSn = await TicketSnModel.findById(snTicketId);
|
const ticketSn = await TicketSnModel.findById(snTicketId);
|
||||||
if (!ticketSn) {
|
if (!ticketSn) {
|
||||||
logError(`Ticket SN com ID ${snTicketId} não encontrado para adicionar work note.`, 'addWorkNoteToServiceNow');
|
logError(`Ticket SN com ID ${snTicketId} nao encontrado para adicionar work note.`, 'addWorkNoteToServiceNow');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,20 +476,20 @@ const addWorkNoteToServiceNow = async (snTicketId, workNote) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await TicketSyncModel.updateLastSync(snTicketId, 'servicenow');
|
await TicketSyncModel.updateLastSync(snTicketId, 'servicenow');
|
||||||
logInfo(`✅ Work note adicionada com sucesso ao ticket SN ${sysId}.`);
|
logInfo(`OK: Work note adicionada com sucesso ao ticket SN ${sysId}.`);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `🚨 Falha ao adicionar work note ao ticket SN (sysId: ${sysId})`);
|
logError(error, `ERRO: Falha ao adicionar work note ao ticket SN (sysId: ${sysId})`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca o valor de uma variável de catálogo (sc_item_option) para um Request Item (RITM) específico.
|
* Busca o valor de uma variavel de catalogo (sc_item_option) para um Request Item (RITM) especifico.
|
||||||
* @param {string} ritmSysId - O sys_id do sc_req_item (RITM).
|
* @param {string} ritmSysId - O sys_id do sc_req_item (RITM).
|
||||||
* @param {string} variableName - O nome interno (name) da variável de catálogo.
|
* @param {string} variableName - O nome interno (name) da variavel de catalogo.
|
||||||
* @returns {Promise<string|null>} O valor da variável ou null se não encontrada/erro.
|
* @returns {Promise<string|null>} O valor da variavel ou null se nao encontrada/erro.
|
||||||
*/
|
*/
|
||||||
const fetchScItemOptionValue = async (ritmSysId, variableName) => {
|
const fetchScItemOptionValue = async (ritmSysId, variableName) => {
|
||||||
try {
|
try {
|
||||||
@ -505,7 +504,7 @@ const fetchScItemOptionValue = async (ritmSysId, variableName) => {
|
|||||||
const result = response.data.result;
|
const result = response.data.result;
|
||||||
return (result && result.length > 0) ? result : null;
|
return (result && result.length > 0) ? result : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, `🚨 Falha ao buscar valor da variável '${variableName}' para RITM ${ritmSysId}`);
|
logError(error, `ERRO: Falha ao buscar valor da variavel '${variableName}' para RITM ${ritmSysId}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -529,18 +528,18 @@ module.exports = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @module servicenowService
|
* @module servicenowService
|
||||||
* @description Este serviço encapsula todas as interações com a API REST do ServiceNow.
|
* @description Este servico encapsula todas as interacoes com a API REST do ServiceNow.
|
||||||
* Ele é responsável por buscar, criar e atualizar dados na plataforma ServiceNow.
|
* Ele e responsavel por buscar, criar e atualizar dados na plataforma ServiceNow.
|
||||||
*
|
*
|
||||||
* Principais Funcionalidades:
|
* Principais Funcionalidades:
|
||||||
* - **Busca de Tickets**: `fetchTicketsFromServiceNow` e `fetchRequestsFromServiceNow` buscam incidentes e requisições, respectivamente, utilizando uma marca d'água para obter apenas os registros novos ou atualizados.
|
* - **Busca de Tickets**: `fetchTicketsFromServiceNow` e `fetchRequestsFromServiceNow` buscam incidentes e requisicoes, respectivamente, utilizando uma marca d'agua para obter apenas os registros novos ou atualizados.
|
||||||
* - **Busca de Comentários (`fetchCommentsFromServiceNow`)**: Busca todos os comentários (journal entries) de um ticket, com lógica de paginação para lidar com grandes volumes de dados. Ele também filtra os comentários gerados pelo próprio sistema de integração para evitar loops.
|
* - **Busca de Comentarios (`fetchCommentsFromServiceNow`)**: Busca todos os comentarios (journal entries) de um ticket, com logica de paginacao para lidar com grandes volumes de dados. Ele tambem filtra os comentarios gerados pelo proprio sistema de integracao para evitar loops.
|
||||||
* - **Criação de Comentários**: `createCommentInServiceNow` e a função wrapper `syncCommentToServiceNow` adicionam comentários (work notes/comments) a um ticket existente no ServiceNow.
|
* - **Criacao de Comentarios**: `createCommentInServiceNow` e a funcao wrapper `syncCommentToServiceNow` adicionam comentarios (work notes/comments) a um ticket existente no ServiceNow.
|
||||||
* - **Atualização de Status**: `updateStatusInServiceNow` modifica o estado de um ticket (ex: para 'Em Atendimento').
|
* - **Atualizacao de Status**: `updateStatusInServiceNow` modifica o estado de um ticket (ex: para 'Em Atendimento').
|
||||||
* - **Fechamento de Tickets (`closeTicketInServiceNow`)**: Altera o estado de um ticket para "Resolvido", preenchendo os campos obrigatórios como notas de fechamento e data de resolução.
|
* - **Fechamento de Tickets (`closeTicketInServiceNow`)**: Altera o estado de um ticket para "Resolvido", preenchendo os campos obrigatorios como notas de fechamento e data de resolucao.
|
||||||
* - **Adição de Notas (`addWorkNoteToServiceNow`)**: Adiciona uma nota de trabalho a um ticket sem alterar seu status, útil para registrar informações ou avisos.
|
* - **Adicao de Notas (`addWorkNoteToServiceNow`)**: Adiciona uma nota de trabalho a um ticket sem alterar seu status, util para registrar informacoes ou avisos.
|
||||||
* - **Busca de Variáveis de Catálogo (`fetchScItemOptionValue`)**: Função especializada para buscar valores de variáveis em itens de requisição (RITMs), usada para enriquecer a descrição de tickets.
|
* - **Busca de Variaveis de Catalogo (`fetchScItemOptionValue`)**: Funcao especializada para buscar valores de variaveis em itens de requisicao (RITMs), usada para enriquecer a descricao de tickets.
|
||||||
*
|
*
|
||||||
* Todas as funções de interação com a API são envolvidas em blocos try-catch com logging detalhado para facilitar o monitoramento e a depuração.
|
* Todas as funcoes de interacao com a API sao envolvidas em blocos try-catch com logging detalhado para facilitar o monitoramento e a depuracao.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user