FIX/FEATURE: Ajustes de fluxo SN<->GLPI e documentação operacional
- feito ajuste do fluxo GLPI->SN para tratar status 5 (resolve no SN) e status 6 (aviso definitivo e retirada de monitoramento) - feito ajuste de reabertura SN->GLPI quando ticket estiver solved no GLPI - feito ajuste de data/hora de comments/worknotes para evitar inversão de data - feito ajuste de vínculo Ticket Externo no SN com validação de persistência e retry idempotente - feito ajuste de filtro de journals para ignorar SOTHIS.CAOA e admin.caoa por configuração - feito inclusão de monitor dedicado de encerramento definitivo (processGlpiClosureController) - feito atualização de flags de controle (ENABLE_STATUS_SYNC, ENABLE_GLPI_CLOSE_CRON, ENABLE_GLPI_WEBHOOK) - feito atualização da documentação (README, docs/regrasdenegocio.md, docs/fluxo.md, docs/casosdeuso.md, docs/checklist.md)
This commit is contained in:
parent
a7f9f7f714
commit
35b4a1fef9
@ -12,6 +12,9 @@ PORT=3000
|
|||||||
|
|
||||||
# Frequencia de execucao do cron job (formato cron). Padrao: '* * * * *' (a cada 1 minutos)
|
# Frequencia de execucao do cron job (formato cron). Padrao: '* * * * *' (a cada 1 minutos)
|
||||||
CRON_SCHEDULE='* * * * *'
|
CRON_SCHEDULE='* * * * *'
|
||||||
|
ENABLE_STATUS_SYNC=false
|
||||||
|
ENABLE_GLPI_WEBHOOK=true
|
||||||
|
ENABLE_GLPI_CLOSE_CRON=true
|
||||||
|
|
||||||
# --- Configuracao da API do ServiceNow ---
|
# --- Configuracao da API do ServiceNow ---
|
||||||
# URL base da sua instancia do ServiceNow
|
# URL base da sua instancia do ServiceNow
|
||||||
@ -26,12 +29,14 @@ SERVICENOW_ASSIGNMENT_GROUP=
|
|||||||
SERVICENOW_DEFAULT_USER_SYSID=
|
SERVICENOW_DEFAULT_USER_SYSID=
|
||||||
SERVICENOW_DEFAULT_USER=
|
SERVICENOW_DEFAULT_USER=
|
||||||
SERVICENOW_RESOLVED_BY_SYSID=
|
SERVICENOW_RESOLVED_BY_SYSID=
|
||||||
|
SERVICENOW_IGNORE_USERS_JOURNAL=SOTHIS.CAOA,admin.caoa
|
||||||
|
|
||||||
# Endpoints de tabela usados pela aplicacao
|
# Endpoints de tabela usados pela aplicacao
|
||||||
SERVICENOW_TABLE_INCIDENT_URL=
|
SERVICENOW_TABLE_INCIDENT_URL=
|
||||||
SERVICENOW_TABLE_REQUEST_URL=
|
SERVICENOW_TABLE_REQUEST_URL=
|
||||||
SERVICENOW_TABLE_JOURNAL_URL=
|
SERVICENOW_TABLE_JOURNAL_URL=
|
||||||
SERVICENOW_SC_ITEM_OPTION_URL=
|
SERVICENOW_SC_ITEM_OPTION_URL=
|
||||||
|
SERVICENOW_EXTERNAL_TICKET_FIELD=u_external_ticket
|
||||||
|
|
||||||
# --- Configuracao do Banco de Dados do GLPI (MySQL) ---
|
# --- Configuracao do Banco de Dados do GLPI (MySQL) ---
|
||||||
GLPI_DB_TYPE=mysql
|
GLPI_DB_TYPE=mysql
|
||||||
|
|||||||
271
README.md
271
README.md
@ -1,251 +1,84 @@
|
|||||||
# Sistema de Sincronizacao ServiceNow <> GLPI
|
# Sistema de Integracao ServiceNow <-> GLPI
|
||||||
|
|
||||||
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.
|
Middleware Node.js para integrar chamados entre ServiceNow e GLPI, usando PostgreSQL como banco intermediario e acesso ao banco do GLPI.
|
||||||
|
|
||||||
## O que o projeto faz hoje
|
## Estado atual
|
||||||
|
|
||||||
- Busca incidentes e requisicoes no ServiceNow por watermark (`sync_control`).
|
1. Coleta de tickets SN por watermark.
|
||||||
- Salva/atualiza tickets no banco intermediario (`tickets_sn`).
|
2. Criacao/vinculo de tickets no GLPI.
|
||||||
- Cria tickets no GLPI para registros pendentes (`ticket_sync.glpi_sync_status = 'pending_check'`).
|
3. Sincronizacao de comentarios e tasks em duas direcoes.
|
||||||
- Sincroniza comentarios em duas direcoes:
|
4. Preenchimento do campo "Ticket Externo" no SN com o ID GLPI.
|
||||||
- ServiceNow -> GLPI
|
5. Monitor GLPI para `status=5` (resolver SN) e `status=6` (encerrar definitivamente no SN).
|
||||||
- GLPI -> ServiceNow
|
6. Flags para desligar sincronizacao legada de status.
|
||||||
- No fluxo ServiceNow -> GLPI:
|
|
||||||
- `comments` viram comentarios (followups) no GLPI.
|
|
||||||
- `work_notes` viram tarefas no GLPI.
|
|
||||||
- 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
|
|
||||||
|
|
||||||
### Componentes
|
|
||||||
|
|
||||||
- `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.
|
|
||||||
|
|
||||||
### Bancos envolvidos
|
|
||||||
|
|
||||||
- PostgreSQL (`snglpi`): estado da integracao.
|
|
||||||
- MySQL/MariaDB (GLPI): criacao/consulta/atualizacao de tickets e followups.
|
|
||||||
|
|
||||||
## Fluxo do ciclo
|
## Fluxo do ciclo
|
||||||
|
|
||||||
Executado em ordem pelo `main()`:
|
Executado pelo `main()`:
|
||||||
|
|
||||||
1. `processErrorController`
|
1. `processErrorController`
|
||||||
2. `processTicketsController`
|
2. `processTicketsController`
|
||||||
3. `processCommentsController`
|
3. `processCommentsController`
|
||||||
4. `processStatusAndClosureController`
|
4. `processGlpiClosureController` (quando `ENABLE_GLPI_CLOSE_CRON=true`)
|
||||||
|
5. `processStatusAndClosureController` apenas quando `ENABLE_STATUS_SYNC=true`
|
||||||
|
|
||||||
## Regras de negocio importantes
|
## Documentacao detalhada
|
||||||
|
|
||||||
- Controle de precedencia por `source_last` para reduzir conflito de atualizacao.
|
1. Fluxo tecnico: [docs/fluxo.md](docs/fluxo.md)
|
||||||
- Ao detectar divergencia de status, o bastao nao e invertido para `GLPI` quando a ultima origem valida ja e `SNOW`.
|
2. Regras de negocio completas: [docs/regrasdenegocio.md](docs/regrasdenegocio.md)
|
||||||
- Encerramento/Resolucao vindo do ServiceNow:
|
3. Casos de uso: [docs/casosdeuso.md](docs/casosdeuso.md)
|
||||||
- Nao fecha ticket no GLPI automaticamente.
|
4. Plano e checklist de rollout: [docs/checklist.md](docs/checklist.md)
|
||||||
- Insere nota formatada no GLPI.
|
|
||||||
- Marca sync como `closed/closed` para remover do monitoramento.
|
|
||||||
- Ticket resolvido no GLPI com `solutiontypes_id = GLPI_OOS_SOLUTION_TYPE_ID` (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
|
## Variaveis de ambiente importantes
|
||||||
|
|
||||||
1. Controle de origem (`source_last`)
|
### Feature flags
|
||||||
- `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
|
1. `ENABLE_STATUS_SYNC`
|
||||||
- Solucao no GLPI dispara tentativa de resolucao no SN.
|
- `true`: liga fluxo legado de status bidirecional
|
||||||
- Se SN estiver em status bloqueante (`Em Espera` ou `Aguardando Atendimento`), a integracao seta `Em Atendimento` antes da resolucao.
|
- `false`: desliga fluxo legado de status
|
||||||
- Regra fora do escopo (`solutiontypes_id = GLPI_OOS_SOLUTION_TYPE_ID`) fecha fluxo local sem resolver no SN e registra `work_note`.
|
2. `ENABLE_GLPI_CLOSE_CRON`
|
||||||
|
- `true`: liga monitor de fechamento definitivo GLPI=6
|
||||||
3. Status ServiceNow -> GLPI
|
- `false`: desliga monitor de fechamento definitivo
|
||||||
- Mudancas de estado no SN sao refletidas no GLPI para tickets ativos.
|
|
||||||
- Para `Resolvido`, `Encerrado` e `Encerrado - Omitido` no SN:
|
|
||||||
- GLPI recebe nota de encerramento/resolucao.
|
|
||||||
- Integracao remove ticket do monitoramento (`closed/closed`), sem fechar ticket no GLPI.
|
|
||||||
- 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.
|
|
||||||
- `work_notes` do SN sao persistidas como `update_type = task` e enviadas como tarefa no GLPI.
|
|
||||||
- Campo `author` no banco intermediario usa `sys_created_by` do SN.
|
|
||||||
- Conteudo enviado ao GLPI e formatado com card visual e autor no cabecalho.
|
|
||||||
- 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
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dependencias Python
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd src/scripts/python
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuracao de ambiente
|
|
||||||
|
|
||||||
Crie `.env.development` e `.env.production` com base em `.env.example` e inclua tambem as variaveis usadas no codigo:
|
|
||||||
|
|
||||||
### ServiceNow
|
### ServiceNow
|
||||||
|
|
||||||
- `SERVICENOW_USERNAME`
|
1. `SERVICENOW_TABLE_INCIDENT_URL`
|
||||||
- `SERVICENOW_PASSWORD`
|
2. `SERVICENOW_TABLE_REQUEST_URL`
|
||||||
- `SERVICENOW_ASSIGNMENT_GROUP`
|
3. `SERVICENOW_TABLE_JOURNAL_URL`
|
||||||
- `SERVICENOW_TABLE_INCIDENT_URL`
|
4. `SERVICENOW_SC_ITEM_OPTION_URL`
|
||||||
- `SERVICENOW_TABLE_REQUEST_URL`
|
5. `SERVICENOW_DEFAULT_USER`
|
||||||
- `SERVICENOW_TABLE_JOURNAL_URL`
|
6. `SERVICENOW_IGNORE_DEFAULT_USER_JOURNAL`
|
||||||
- `SERVICENOW_SC_ITEM_OPTION_URL`
|
7. `SERVICENOW_EXTERNAL_TICKET_FIELD` (default: `u_external_ticket`)
|
||||||
- `SERVICENOW_DEFAULT_USER`
|
|
||||||
- `SERVICENOW_RESOLVED_BY_SYSID`
|
|
||||||
- `SERVICENOW_IGNORE_DEFAULT_USER_JOURNAL` (`true` por padrao; use `false` para testes locais)
|
|
||||||
|
|
||||||
### GLPI (MySQL/MariaDB)
|
### GLPI e banco intermediario
|
||||||
|
|
||||||
- `GLPI_DB_HOST`
|
1. `GLPI_DB_*`
|
||||||
- `GLPI_DB_PORT`
|
2. `SNGLPI_DB_*`
|
||||||
- `GLPI_DB_USER`
|
3. `GLPI_OOS_SOLUTION_TYPE_ID`
|
||||||
- `GLPI_DB_PASSWORD`
|
|
||||||
- `GLPI_DB_NAME`
|
|
||||||
- `GLPI_DB_CHARSET`
|
|
||||||
- `GLPI_DEFAULT_USER_ID`
|
|
||||||
- `GLPI_DEFAULT_GROUP_ID` (padrao `30`)
|
|
||||||
- `GLPI_DEFAULT_ENTITY_ID` (padrao `127`)
|
|
||||||
- `GLPI_OOS_SOLUTION_TYPE_ID` (padrao `27`)
|
|
||||||
|
|
||||||
### Banco intermediario (PostgreSQL)
|
## Desenvolvimento
|
||||||
|
|
||||||
- `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
|
|
||||||
|
|
||||||
|
1. Instalar dependencias:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
2. Criar env:
|
||||||
|
```bash
|
||||||
|
copy .env.example .env.development
|
||||||
|
```
|
||||||
|
3. Executar:
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Producao
|
## Producao
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run start:prod
|
|
||||||
```
|
|
||||||
|
|
||||||
## Execucao com PM2
|
|
||||||
|
|
||||||
Subir os dois processos (sync + mapeador):
|
|
||||||
|
|
||||||
|
1. PM2:
|
||||||
```bash
|
```bash
|
||||||
pm2 start ecosystem.config.js --env production
|
pm2 start ecosystem.config.js --env production
|
||||||
```
|
```
|
||||||
|
|
||||||
Comandos uteis:
|
## Observacoes
|
||||||
|
|
||||||
```bash
|
1. O projeto ainda nao tem suite automatizada robusta.
|
||||||
pm2 list
|
2. O rollout recomendado e por fases com feature flags.
|
||||||
pm2 logs sn-glpi-sync-cron
|
3. Nao remover schema legado antes de estabilizar o fluxo novo.
|
||||||
pm2 logs sn-glpi-location-mapper
|
|
||||||
pm2 restart sn-glpi-sync-cron
|
|
||||||
```
|
|
||||||
|
|
||||||
## Script de mapeamento de localidades
|
|
||||||
|
|
||||||
Arquivo: `src/scripts/python/update_location_mapping.py`
|
|
||||||
|
|
||||||
- Carrega `NODE_ENV` e respectivo `.env.*`.
|
|
||||||
- Monitora alteracoes recentes no CSV (`LOCATION_MAPPING_CSV_PATH`).
|
|
||||||
- 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).
|
|
||||||
|
|
||||||
## Como os dados chegam do ServiceNow
|
|
||||||
|
|
||||||
- Incidentes: consulta na tabela definida por `SERVICENOW_TABLE_INCIDENT_URL`, filtrando por `assignment_group` e `sys_updated_on >= watermark`.
|
|
||||||
- Requisicoes: consulta na tabela definida por `SERVICENOW_TABLE_REQUEST_URL` com o mesmo filtro.
|
|
||||||
- Comentarios: consulta na tabela definida por `SERVICENOW_TABLE_JOURNAL_URL` por `element_id` e `element IN (comments, work_notes)`, com paginacao (`limit/offset`).
|
|
||||||
- Enriquecimento de requisicoes: consulta em `SERVICENOW_SC_ITEM_OPTION_URL` para capturar variaveis como justificativa e telefone.
|
|
||||||
- Campos principais persistidos localmente:
|
|
||||||
- Ticket: `number`, `sys_id`, `short_description`, `state`, `description`, `caller/opened_by`, `location`, `opened_at`, `sys_updated_on`.
|
|
||||||
- Atualizacoes: `sys_id`, `element`, `value`, `sys_created_on`, `sys_created_by`.
|
|
||||||
|
|
||||||
## Estrutura do banco intermediario
|
|
||||||
|
|
||||||
Definicao base em `src/scripts/database/scriptBD.sql`:
|
|
||||||
|
|
||||||
- `tickets_sn`
|
|
||||||
- `ticket_sync`
|
|
||||||
- `ticket_updates`
|
|
||||||
- `location_mapping`
|
|
||||||
- `sync_control`
|
|
||||||
|
|
||||||
Arquivo auxiliar de manutencao/reset (com comandos destrutivos):
|
|
||||||
|
|
||||||
- `src/scripts/database/maintenance_reset.sql`
|
|
||||||
|
|
||||||
## Limitacoes conhecidas (estado atual)
|
|
||||||
|
|
||||||
- Nao ha suite de testes automatizados (`npm test` e placeholder).
|
|
||||||
- Parte do fluxo depende de acesso direto ao banco do GLPI (acoplamento operacional alto). (Utilizar API em nova versão)
|
|
||||||
- Existe logica sensivel de timezone/watermark que merece revisao para evitar reprocesso/perda de evento.
|
|
||||||
|
|
||||||
## Proximas melhorias recomendadas
|
|
||||||
|
|
||||||
- Cobertura de testes (unitario e integracao com mocks de API/DB).
|
|
||||||
- Externalizar IDs e regras hardcoded para variaveis de ambiente.
|
|
||||||
- Revisar estrategia de watermark/timezone e idempotencia.
|
|
||||||
- Criar healthcheck/observabilidade (metricas de ciclo, falhas por etapa, tickets processados).
|
|
||||||
- Adicionar validacoes de configuracao na inicializacao (falha rapida quando faltar env obrigatoria).
|
|
||||||
- Alterar uso de banco do GLPI para a API do GLPI.
|
|
||||||
|
|
||||||
## Arquivos principais
|
|
||||||
|
|
||||||
- `cron.js`
|
|
||||||
- `src/app.js`
|
|
||||||
- `src/controllers/processTicketsController.js`
|
|
||||||
- `src/controllers/processCommentsController.js`
|
|
||||||
- `src/controllers/processStatusController.js`
|
|
||||||
- `src/controllers/processErrorController.js`
|
|
||||||
- `src/services/servicenowService.js`
|
|
||||||
- `src/services/glpiTicketService.js`
|
|
||||||
- `src/services/glpiCommentService.js`
|
|
||||||
- `src/scripts/python/update_location_mapping.py`
|
|
||||||
|
|||||||
153
docs/casosdeuso.md
Normal file
153
docs/casosdeuso.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# Casos de Uso (Formato RPG)
|
||||||
|
|
||||||
|
## Capitulo 1 - Abertura do chamado
|
||||||
|
|
||||||
|
No reino da CAOA, um usuario abre um chamado no ServiceNow.
|
||||||
|
O oraculo da integracao observa o novo registro e grava no banco intermediario.
|
||||||
|
Sem demora, o chamado atravessa o portal SN -> GLPI.
|
||||||
|
No GLPI, o ticket nasce com seu vinculo e recebe seu identificador.
|
||||||
|
De volta ao ServiceNow, o campo "Ticket Externo" e preenchido com o ID GLPI.
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
1. Chamado criado no GLPI.
|
||||||
|
2. `ticket_sync` em `synced/synced`.
|
||||||
|
3. Ticket Externo preenchido no SN.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capitulo 2 - O chamado fora do escopo
|
||||||
|
|
||||||
|
Um atendente da Sothis analisa o chamado e percebe:
|
||||||
|
"Problema relacionado a Rede e nao Telefonia".
|
||||||
|
|
||||||
|
Ele registra essa justificativa na nota de solucao do GLPI (fora do escopo).
|
||||||
|
A integracao reconhece o tipo de solucao configurado para fora de escopo.
|
||||||
|
Entao envia a justificativa para o ServiceNow como nota.
|
||||||
|
Nao encerra o chamado no SN nesse passo de negocio de fora de escopo.
|
||||||
|
O chamado sai do monitoramento automatico da integracao.
|
||||||
|
|
||||||
|
Estado final esperado no banco intermediario:
|
||||||
|
|
||||||
|
1. `glpi_sync_status = 'closed'`
|
||||||
|
2. `sn_sync_status = 'ignored'`
|
||||||
|
|
||||||
|
Interpretacao operacional:
|
||||||
|
|
||||||
|
1. Chamado nao sera mais tratado pela fila automatica da integracao.
|
||||||
|
2. Evita retrabalho e loops de sincronizacao.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capitulo 3 - Encerramento definitivo
|
||||||
|
|
||||||
|
Quando um chamado chega ao encerramento definitivo no GLPI (`status=6`),
|
||||||
|
o cron de fechamento detecta o estado final e executa:
|
||||||
|
|
||||||
|
1. Publica no SN a mensagem:
|
||||||
|
- "Chamado encerrado definitivamente. Reabertura deste chamado nao sera atendida. Caso necessario, abra um novo chamado."
|
||||||
|
2. Nao altera o status do chamado no SN.
|
||||||
|
3. Marca `ticket_sync` como `closed/closed` para retirar do monitoramento.
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
1. Nao fica chamado fechado no GLPI e aberto no SN.
|
||||||
|
2. Nao ocorre tentativa de reabertura automatica para GLPI `6`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capitulo 4 - Comunicacao entre os reinos (comentarios)
|
||||||
|
|
||||||
|
No salao da CAOA (ServiceNow), o usuario escreve:
|
||||||
|
"Equipe Sothis, conseguimos reproduzir o erro as 14h10."
|
||||||
|
|
||||||
|
A mensagem atravessa o portal SN -> GLPI e aparece como followup.
|
||||||
|
Do outro lado, no castelo da Sothis (GLPI), o atendente responde:
|
||||||
|
"Recebido. Estamos validando o circuito."
|
||||||
|
|
||||||
|
A resposta volta pelo portal GLPI -> SN como comentario.
|
||||||
|
Os escribas da integracao registram origem e destino para evitar duplicidade.
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
1. Comentario SN vira followup GLPI.
|
||||||
|
2. Followup GLPI vira comentario SN.
|
||||||
|
3. Sem mensagens duplicadas no vai-e-volta.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capitulo 5 - As tarefas e os segredos do reino
|
||||||
|
|
||||||
|
No reino da CAOA, toda `work_note` do SN e uma ordem de batalha.
|
||||||
|
Essas ordens sao enviadas ao GLPI como tarefas visiveis para o time tecnico.
|
||||||
|
|
||||||
|
No reino da Sothis, tarefas internas do GLPI sao consideradas secretas.
|
||||||
|
Elas nao atravessam o portal para o SN.
|
||||||
|
Somente followups/comentarios publicos participam da comunicacao entre reinos.
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
1. Toda `work_note` SN vira task GLPI.
|
||||||
|
2. Task criada no GLPI nao volta para SN.
|
||||||
|
3. O SN recebe somente comentarios/followups do GLPI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capitulo 6 - A solucao no reino Sothis (GLPI)
|
||||||
|
|
||||||
|
Quando o atendente Sothis conclui o trabalho tecnico no GLPI,
|
||||||
|
ele registra a solucao e coloca o chamado em **Solucionado** (`status=5`).
|
||||||
|
|
||||||
|
O cron da integracao observa esse estado e realiza:
|
||||||
|
|
||||||
|
1. Busca a nota de solucao do GLPI.
|
||||||
|
2. Resolve o chamado correspondente no SN.
|
||||||
|
3. Atualiza o estado local para `solved/solved`.
|
||||||
|
|
||||||
|
Se o chamado no SN estiver em estado que bloqueia encerramento
|
||||||
|
(como espera/aguardando), a integracao primeiro ajusta para em atendimento
|
||||||
|
e depois aplica a resolucao.
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
1. GLPI solucionado reflete SN resolvido.
|
||||||
|
2. A nota de resolucao do GLPI vira base do fechamento no SN.
|
||||||
|
3. Chamado continua monitorado ate o encerramento definitivo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capitulo 7 - A solucao vinda do reino CAOA (SN)
|
||||||
|
|
||||||
|
Quando a CAOA marca o chamado como resolvido no SN,
|
||||||
|
a integracao **nao fecha o chamado no GLPI automaticamente**.
|
||||||
|
|
||||||
|
Em vez disso:
|
||||||
|
|
||||||
|
1. Uma nota informativa e adicionada no GLPI.
|
||||||
|
2. O chamado sai do monitoramento automatico de status.
|
||||||
|
|
||||||
|
Essa regra evita conflito de fluxo e protege o time tecnico de mudancas
|
||||||
|
de estado inesperadas do outro reino.
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
1. GLPI recebe contexto da resolucao do SN.
|
||||||
|
2. Nao ocorre fechamento forcado no GLPI por esse evento.
|
||||||
|
3. Fluxo fica estavel para evitar reaberturas em cadeia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Capitulo 8 - Regras de ouro para os guardioes dos dois reinos
|
||||||
|
|
||||||
|
1. Comentario e diplomacia entre reinos: sempre pode cruzar o portal.
|
||||||
|
2. Task do SN e ordem operacional para Sothis: cruza para o GLPI.
|
||||||
|
3. Task interna do GLPI e sigilo tecnico: nao cruza para SN.
|
||||||
|
4. GLPI `5` resolve SN.
|
||||||
|
5. GLPI `6` encerra monitoramento e publica aviso definitivo no SN.
|
||||||
|
6. Reabertura automatica:
|
||||||
|
- permitida se GLPI estiver `5`
|
||||||
|
- bloqueada se GLPI estiver `6`
|
||||||
|
7. Fora de escopo:
|
||||||
|
- justifica no SN
|
||||||
|
- remove do monitoramento
|
||||||
|
- estado final `closed/ignored`.
|
||||||
56
docs/fluxo.md
Normal file
56
docs/fluxo.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Fluxo Operacional
|
||||||
|
|
||||||
|
## 1. Fluxo do ciclo (`main`)
|
||||||
|
|
||||||
|
1. `processErrorController`
|
||||||
|
2. `processTicketsController`
|
||||||
|
3. `processCommentsController`
|
||||||
|
4. `processGlpiClosureController` (se `ENABLE_GLPI_CLOSE_CRON=true`)
|
||||||
|
5. `processStatusAndClosureController` apenas se `ENABLE_STATUS_SYNC=true`
|
||||||
|
|
||||||
|
## 2. Fluxo SN -> GLPI
|
||||||
|
|
||||||
|
1. Coleta incidentes/requisicoes no SN por watermark.
|
||||||
|
2. UPSERT em `tickets_sn`.
|
||||||
|
3. Cria/atualiza `ticket_sync` com estados iniciais.
|
||||||
|
4. Cria ticket no GLPI quando `pending_check`.
|
||||||
|
5. Grava `glpi_ticket_id` em `ticket_sync`.
|
||||||
|
6. Atualiza campo "Ticket Externo" no SN com ID GLPI.
|
||||||
|
7. Se falhar atualizar "Ticket Externo", agenda retry em `ticket_updates`.
|
||||||
|
|
||||||
|
## 3. Fluxo de comentarios/tasks
|
||||||
|
|
||||||
|
### 3.1 SN -> GLPI
|
||||||
|
|
||||||
|
1. Busca `comments` e `work_notes` no SN.
|
||||||
|
2. Persiste em `ticket_updates` (idempotente por `source_id`).
|
||||||
|
3. Envia:
|
||||||
|
- `comment` -> followup GLPI
|
||||||
|
- `task` -> task GLPI
|
||||||
|
|
||||||
|
### 3.2 GLPI -> SN
|
||||||
|
|
||||||
|
1. Busca followups GLPI.
|
||||||
|
2. Sanitiza conteudo HTML/imagens.
|
||||||
|
3. Envia comentario para SN evitando duplicidade.
|
||||||
|
4. Atualiza `destiny_id` para rastrear espelhamento.
|
||||||
|
|
||||||
|
## 4. Resolucao e fechamento GLPI -> SN (monitor cron)
|
||||||
|
|
||||||
|
1. Monitor consulta tickets ativos no `ticket_sync`.
|
||||||
|
2. Busca status real no GLPI.
|
||||||
|
3. Se status GLPI = `5`:
|
||||||
|
- busca nota de solucao
|
||||||
|
- se SN estiver em espera, move para em atendimento
|
||||||
|
- resolve SN
|
||||||
|
- marca `ticket_sync` como `solved/solved`
|
||||||
|
4. Se status GLPI = `6`:
|
||||||
|
- adiciona comentario no SN:
|
||||||
|
- "Chamado encerrado definitivamente. Reabertura deste chamado nao sera atendida. Caso necessario, abra um novo chamado."
|
||||||
|
- nao altera status do chamado no SN
|
||||||
|
- marca `ticket_sync` como `closed/closed` para retirar do monitoramento
|
||||||
|
|
||||||
|
## 5. Reabertura
|
||||||
|
|
||||||
|
1. Reabertura automatica permitida apenas para tickets em GLPI `5`.
|
||||||
|
2. Se GLPI `6`, reabertura automatica bloqueada.
|
||||||
211
docs/regrasdenegocio.md
Normal file
211
docs/regrasdenegocio.md
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
# Regras de Negocio Completas
|
||||||
|
|
||||||
|
## 1. Escopo funcional da integracao
|
||||||
|
|
||||||
|
1. A origem oficial de novos chamados e o ServiceNow (SN).
|
||||||
|
2. O GLPI recebe chamados criados no SN e passa a participar do atendimento.
|
||||||
|
3. A integracao cobre:
|
||||||
|
- criacao/vinculo de chamados SN -> GLPI
|
||||||
|
- sincronizacao de comentarios (SN <-> GLPI)
|
||||||
|
- sincronizacao de tasks (SN -> GLPI via `work_notes`)
|
||||||
|
- fechamento definitivo GLPI -> SN (status GLPI = 6)
|
||||||
|
4. Sincronizacao legada bidirecional de status existe, mas deve ficar desligada no modelo novo (por flag).
|
||||||
|
|
||||||
|
## 2. Governanca e responsabilidade entre sistemas
|
||||||
|
|
||||||
|
1. SN e a origem da demanda.
|
||||||
|
2. GLPI e o sistema tecnico de atendimento e encerramento final.
|
||||||
|
3. Banco intermediario (PostgreSQL) e a fonte de controle operacional da integracao.
|
||||||
|
|
||||||
|
## 3. Orquestracao do ciclo
|
||||||
|
|
||||||
|
1. O ciclo roda por `cron`.
|
||||||
|
2. Ordem de execucao:
|
||||||
|
- `processErrorController`
|
||||||
|
- `processTicketsController`
|
||||||
|
- `processCommentsController`
|
||||||
|
- `processGlpiClosureController` (se habilitado)
|
||||||
|
- `processStatusAndClosureController` (somente legado, se habilitado)
|
||||||
|
3. Ha protecao para evitar execucao concorrente do mesmo ciclo no mesmo processo.
|
||||||
|
|
||||||
|
## 4. Feature flags e comportamento
|
||||||
|
|
||||||
|
1. `ENABLE_STATUS_SYNC`
|
||||||
|
- `true`: executa sincronizacao legada de status bidirecional.
|
||||||
|
- `false`: desliga fluxo legado de status.
|
||||||
|
2. `ENABLE_GLPI_CLOSE_CRON`
|
||||||
|
- `true`: executa monitor de fechamento definitivo GLPI=6.
|
||||||
|
- `false`: desliga monitor.
|
||||||
|
3. `ENABLE_GLPI_WEBHOOK`
|
||||||
|
- reservado para rollout por evento (planejado), sem obrigatoriedade no fluxo atual.
|
||||||
|
|
||||||
|
## 5. Regras de coleta de tickets no ServiceNow
|
||||||
|
|
||||||
|
1. Busca incidentes e requisicoes por `assignment_group` e `sys_updated_on >= watermark`.
|
||||||
|
2. Watermark vem de `sync_control`.
|
||||||
|
3. Novo watermark usa maior `sys_updated_on` encontrado no ciclo.
|
||||||
|
4. Watermark salvo aplica margem de seguranca de 6 horas para tras.
|
||||||
|
5. Se nao houver tickets novos/atualizados, watermark nao e alterado.
|
||||||
|
|
||||||
|
## 6. Regras de persistencia local (`tickets_sn`)
|
||||||
|
|
||||||
|
1. UPSERT por `ticket_number`.
|
||||||
|
2. `sys_id` e unico.
|
||||||
|
3. Campos de incidente e requisicao tem mapeamento diferente.
|
||||||
|
4. Para requisicao, pode haver enriquecimento por variaveis de catalogo:
|
||||||
|
- justificativa
|
||||||
|
- telefone
|
||||||
|
5. `updated_at` e atualizado automaticamente (trigger no banco).
|
||||||
|
|
||||||
|
## 7. Regras de criacao de estado de sincronizacao (`ticket_sync`)
|
||||||
|
|
||||||
|
1. Novo ticket entra com `sn_ticket_id` vinculado.
|
||||||
|
2. Se status SN vier final (`Encerrado` ou `Encerrado - Omitido`):
|
||||||
|
- `sn_sync_status = closed`
|
||||||
|
- `glpi_sync_status = ignored`
|
||||||
|
3. Se status SN vier aberto:
|
||||||
|
- `sn_sync_status = collected`
|
||||||
|
- `glpi_sync_status = pending_check`
|
||||||
|
4. `source_last` existe no schema e legado, mas deve ser deprecado para decisao de status no modelo novo.
|
||||||
|
|
||||||
|
## 8. Regras de criacao/vinculo no GLPI
|
||||||
|
|
||||||
|
1. Apenas tickets com `glpi_sync_status = pending_check` entram na criacao/vinculo.
|
||||||
|
2. Se `glpi_ticket_id` ja existir no banco intermediario:
|
||||||
|
- manter `synced/synced`
|
||||||
|
3. Se nao houver `glpi_ticket_id` local:
|
||||||
|
- procurar ticket no GLPI por numero SN no titulo/conteudo
|
||||||
|
4. Se encontrar ticket no GLPI:
|
||||||
|
- vincular `glpi_ticket_id`
|
||||||
|
- se status GLPI=6, marcar `ignored/closed` e nao monitorar
|
||||||
|
- caso contrario, marcar `synced/synced`
|
||||||
|
5. Se nao encontrar:
|
||||||
|
- criar ticket no GLPI
|
||||||
|
- vincular e marcar `synced/synced`
|
||||||
|
6. Erro de criacao:
|
||||||
|
- marcar estado de erro para reprocessamento.
|
||||||
|
|
||||||
|
## 9. Regras de formatacao ao criar ticket no GLPI
|
||||||
|
|
||||||
|
1. Titulo segue padrao: tipo + entidade + short_description.
|
||||||
|
2. Entidade vem de `location_mapping`; fallback para entidade padrao.
|
||||||
|
3. Descricao e montada em HTML com dados de solicitante.
|
||||||
|
4. Requisicao sem justificativa/descricao pode usar valores default configurados.
|
||||||
|
5. Prioridade, categoria e SLA usam mapeamentos predefinidos.
|
||||||
|
|
||||||
|
## 10. Regra do campo "Ticket Externo" no SN
|
||||||
|
|
||||||
|
1. Ao criar/vincular ticket no GLPI, escrever ID GLPI no SN.
|
||||||
|
2. Campo usado e configuravel por `SERVICENOW_EXTERNAL_TICKET_FIELD` (default `u_external_ticket`).
|
||||||
|
3. Se a escrita falhar, nao recriar ticket no GLPI.
|
||||||
|
4. Falha gera pendencia de retry idempotente em `ticket_updates` (`update_type=external_link`).
|
||||||
|
5. Retry processa pendencias com `destiny_id IS NULL`.
|
||||||
|
6. Ao sucesso, `destiny_id` da pendencia vira `done`.
|
||||||
|
|
||||||
|
## 11. Regras de sincronizacao de comentarios SN -> GLPI
|
||||||
|
|
||||||
|
1. Busca journal entries `comments` e `work_notes` do SN.
|
||||||
|
2. Ordena cronologicamente antes de enviar.
|
||||||
|
3. Idempotencia por `ticket_updates.source_id` (sys_id do journal).
|
||||||
|
4. `comments` viram followup no GLPI.
|
||||||
|
5. `work_notes` viram task no GLPI.
|
||||||
|
6. Conteudo enviado para GLPI e formatado com card visual e autor.
|
||||||
|
7. Em falha de envio, registra estado de erro para novo ciclo.
|
||||||
|
|
||||||
|
## 12. Regras de sincronizacao de comentarios GLPI -> SN
|
||||||
|
|
||||||
|
1. Busca followups do GLPI ignorando usuario padrao tecnico da integracao.
|
||||||
|
2. Sanitiza HTML, metadados e imagens.
|
||||||
|
3. Evita duplicidade com verificacao local e remota.
|
||||||
|
4. Atualiza mapeamento origem/destino em `ticket_updates.destiny_id`.
|
||||||
|
5. Ao final, evita deixar ticket preso em estado transitorio de comentario.
|
||||||
|
|
||||||
|
## 13. Regras de filtro de usuario padrao no SN (ambiente de teste)
|
||||||
|
|
||||||
|
1. `SERVICENOW_DEFAULT_USER` define usuario da integracao.
|
||||||
|
2. `SERVICENOW_IGNORE_DEFAULT_USER_JOURNAL`:
|
||||||
|
- `true` (padrao): ignora journals desse usuario para evitar loop.
|
||||||
|
- `false`: nao ignora (util para dev com usuario unico).
|
||||||
|
|
||||||
|
## 14. Regras de fechamento e resolucao
|
||||||
|
|
||||||
|
### 14.1 Fluxo novo (prioritario)
|
||||||
|
|
||||||
|
1. Fechamento definitivo GLPI `status=6` e refletido no SN via monitor cron.
|
||||||
|
2. Ao detectar GLPI=6:
|
||||||
|
- enviar comentario no SN:
|
||||||
|
- "Chamado encerrado definitivamente. Reabertura deste chamado nao sera atendida. Caso necessario, abra um novo chamado."
|
||||||
|
- nao alterar status do chamado no SN
|
||||||
|
- marcar `ticket_sync` como `closed/closed` para retirar do monitoramento
|
||||||
|
3. Aviso de encerramento definitivo no SN e idempotente por `source_id` em `ticket_updates`.
|
||||||
|
|
||||||
|
### 14.2 Fluxo legado de status (quando habilitado)
|
||||||
|
|
||||||
|
1. Pode propagar estado entre SN e GLPI.
|
||||||
|
2. Usa regras de precedencia historicas com `source_last`.
|
||||||
|
3. Deve ser considerado transitorio e desligavel por flag.
|
||||||
|
|
||||||
|
## 15. Regras de fora do escopo (Sothis)
|
||||||
|
|
||||||
|
1. Quando solucao no GLPI usa `solutiontypes_id = GLPI_OOS_SOLUTION_TYPE_ID`:
|
||||||
|
- envia justificativa ao SN em nota
|
||||||
|
- nao segue fluxo normal de atendimento
|
||||||
|
- estado final esperado no intermediario:
|
||||||
|
- `glpi_sync_status = closed`
|
||||||
|
- `sn_sync_status = ignored`
|
||||||
|
2. Chamado sai do monitoramento automatico.
|
||||||
|
|
||||||
|
## 16. Regras de estados monitorados vs terminais
|
||||||
|
|
||||||
|
### 16.1 Monitorados
|
||||||
|
|
||||||
|
1. `pending_check`
|
||||||
|
2. `synced`
|
||||||
|
3. `solved`
|
||||||
|
4. `error` e variantes de erro para reprocesso
|
||||||
|
|
||||||
|
### 16.2 Terminais
|
||||||
|
|
||||||
|
1. `closed`
|
||||||
|
2. `ignored`
|
||||||
|
|
||||||
|
Regra geral: `closed` e `ignored` nao participam das rotinas normais de sincronizacao.
|
||||||
|
|
||||||
|
## 17. Regra de reabertura
|
||||||
|
|
||||||
|
1. Reabertura automatica permitida apenas quando GLPI estiver em `5` (solved).
|
||||||
|
2. Se GLPI estiver em `6` (fechado definitivo), reabertura automatica nao e permitida.
|
||||||
|
|
||||||
|
## 18. Regras de idempotencia e rastreabilidade
|
||||||
|
|
||||||
|
1. `ticket_updates.source_id` e unico.
|
||||||
|
2. Toda sincronizacao relevante registra origem e destino quando possivel.
|
||||||
|
3. `ticket_sync.last_sn_sync` e `last_glpi_sync` registram ultima interacao por sistema.
|
||||||
|
|
||||||
|
## 19. Regras de erro e reprocessamento
|
||||||
|
|
||||||
|
1. Inicio de ciclo executa reset de estados de erro processaveis.
|
||||||
|
2. Erro em etapa nao deve causar duplicidade de criacao.
|
||||||
|
3. Tickets em erro voltam para fila valida de processamento conforme tipo do erro.
|
||||||
|
|
||||||
|
## 20. Regras de monitoramento operacional
|
||||||
|
|
||||||
|
1. Logs devem permitir rastrear:
|
||||||
|
- criacao de ticket
|
||||||
|
- sincronizacao de comentario/task
|
||||||
|
- fechamento definitivo
|
||||||
|
- falhas e retries
|
||||||
|
2. Rollout deve ser faseado por flag e com possibilidade de rollback.
|
||||||
|
|
||||||
|
## 21. Regras de schema (situacao atual)
|
||||||
|
|
||||||
|
1. Nao remover colunas legadas neste momento.
|
||||||
|
2. Nao criar tabela nova para eventos nesta fase.
|
||||||
|
3. Reaproveitar `ticket_updates` para pendencias e idempotencia.
|
||||||
|
|
||||||
|
## 22. Criterios de sucesso de negocio
|
||||||
|
|
||||||
|
1. Ticket fechado definitivamente no GLPI nao permanece aberto no SN.
|
||||||
|
2. Comentarios e tasks nao duplicam.
|
||||||
|
3. Tickets fora do escopo saem de monitoramento com estado final correto.
|
||||||
|
4. Integracao nao entra em loop por status intermediario.
|
||||||
51
src/app.js
51
src/app.js
@ -2,50 +2,53 @@ const path = require('path');
|
|||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
const nodeEnv = process.env.NODE_ENV;
|
const nodeEnv = process.env.NODE_ENV;
|
||||||
let envFile = '.env.development';
|
let envFile = '.env.development';
|
||||||
if (nodeEnv === 'production') {envFile = '.env.production';} else {envFile = '.env.development';}
|
if (nodeEnv === 'production') {
|
||||||
|
envFile = '.env.production';
|
||||||
|
} else {
|
||||||
|
envFile = '.env.development';
|
||||||
|
}
|
||||||
dotenv.config({ path: path.resolve(__dirname, '..', envFile) });
|
dotenv.config({ path: path.resolve(__dirname, '..', envFile) });
|
||||||
|
|
||||||
const { processTicketsController } = require('./controllers/processTicketsController');
|
const { processTicketsController } = require('./controllers/processTicketsController');
|
||||||
const { processCommentsController } = require('./controllers/processCommentsController');
|
const { processCommentsController } = require('./controllers/processCommentsController');
|
||||||
const { processErrorController } = require('./controllers/processErrorController');
|
const { processErrorController } = require('./controllers/processErrorController');
|
||||||
const { processStatusAndClosureController } = require('./controllers/processStatusController');
|
const { processStatusAndClosureController } = require('./controllers/processStatusController');
|
||||||
const { logInfo, logger } = require('./utils/logger');
|
const { processGlpiClosureController } = require('./controllers/processGlpiClosureController');
|
||||||
|
const { logInfo } = require('./utils/logger');
|
||||||
|
|
||||||
logInfo(`🌎 Ambiente carregado: ${process.env.NODE_ENV || 'development'}`);
|
logInfo(`Ambiente carregado: ${process.env.NODE_ENV || 'development'}`);
|
||||||
|
|
||||||
logInfo('Aplicação iniciada', {
|
logInfo('Aplicacao iniciada', {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
environment: process.env.NODE_ENV || 'development'
|
environment: process.env.NODE_ENV || 'development'
|
||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
|
const statusSyncEnabled = process.env.ENABLE_STATUS_SYNC === 'true';
|
||||||
|
const glpiCloseCronEnabled = process.env.ENABLE_GLPI_CLOSE_CRON !== 'false';
|
||||||
|
|
||||||
await processErrorController();
|
await processErrorController();
|
||||||
await processTicketsController();
|
await processTicketsController();
|
||||||
await processCommentsController();
|
await processCommentsController();
|
||||||
await processStatusAndClosureController();
|
|
||||||
logInfo('🎉 Ciclo de sincronização concluído com sucesso! 🎉');
|
if (statusSyncEnabled) {
|
||||||
|
await processStatusAndClosureController();
|
||||||
|
} else {
|
||||||
|
logInfo('Sincronizacao legada de status desabilitada (ENABLE_STATUS_SYNC=false).');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glpiCloseCronEnabled) {
|
||||||
|
await processGlpiClosureController();
|
||||||
|
} else {
|
||||||
|
logInfo('Monitor de encerramento definitivo GLPI desabilitado (ENABLE_GLPI_CLOSE_CRON=false).');
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo('Ciclo de sincronizacao concluido com sucesso.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logInfo('Erro na aplicação:', error);
|
logInfo('Erro na aplicacao:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A chamada main() foi removida daqui.
|
|
||||||
// Agora, o app.js apenas define e exporta a função main.
|
|
||||||
// A responsabilidade de EXECUTAR a função main é do cron.js,
|
|
||||||
// que a importará e a chamará dentro do agendamento.
|
|
||||||
module.exports = { main };
|
module.exports = { main };
|
||||||
|
|
||||||
/**
|
|
||||||
* @file app.js
|
|
||||||
* @description
|
|
||||||
* Ponto de entrada principal da aplicação de sincronização. Este arquivo é responsável por:
|
|
||||||
* 1. Carregar as variáveis de ambiente corretas (`.env.development` ou `.env.production`) com base na variável `NODE_ENV`.
|
|
||||||
* 2. Orquestrar a execução sequencial dos controladores que compõem o ciclo de sincronização.
|
|
||||||
*
|
|
||||||
* O fluxo de execução é o seguinte:
|
|
||||||
* 1. `processErrorController`: Verifica e reseta tickets que falharam em execuções anteriores, permitindo que sejam reprocessados.
|
|
||||||
* 2. `processTicketsController`: Busca novos tickets (incidentes e requisições) do ServiceNow, os salva no banco de dados intermediário e cria os tickets correspondentes no GLPI.
|
|
||||||
* 3. `processCommentsController`: Realiza a sincronização bidirecional de comentários (notas/follow-ups) para os tickets que já estão sincronizados.
|
|
||||||
* 4. `processStatusAndClosureController`: Executa a sincronização bidirecional de status (aberto, em atendimento, resolvido, fechado) e gerencia o fechamento de tickets em ambas as plataformas.
|
|
||||||
*/
|
|
||||||
@ -1,30 +1,50 @@
|
|||||||
const {fetchCommentsFromServiceNow } = require('../services/servicenowService');
|
const { fetchCommentsFromServiceNow, fetchTicketLiveStatusFromServiceNow } = require('../services/servicenowService');
|
||||||
const { syncCommentsGlpitoSN, syncCommentsSNtoGlpi } = require('../services/glpiCommentService');
|
const { syncCommentsGlpitoSN, syncCommentsSNtoGlpi } = require('../services/glpiCommentService');
|
||||||
const TicketSyncModel = require('../models/ticketSyncModel');
|
const TicketSyncModel = require('../models/ticketSyncModel');
|
||||||
|
const TicketSnModel = require('../models/ticketSnModel');
|
||||||
|
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
||||||
const { logInfo } = require('../utils/logger');
|
const { logInfo } = require('../utils/logger');
|
||||||
|
|
||||||
|
const ensureReopenFromSNWhenGlpiSolved = async (ticket) => {
|
||||||
|
if (ticket.glpi_sync_status !== 'solved') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticketSn = await TicketSnModel.findById(ticket.sn_ticket_id);
|
||||||
|
if (!ticketSn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveSnStatus = await fetchTicketLiveStatusFromServiceNow(ticketSn.sys_id, ticketSn.tipo);
|
||||||
|
const snIsFinal = liveSnStatus === 'Resolvido' || liveSnStatus === 'Encerrado' || liveSnStatus === 'Encerrado - Omitido';
|
||||||
|
if (snIsFinal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const glpiStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id);
|
||||||
|
if (glpiStatus === 5) {
|
||||||
|
await TicketGlpiModel.updateStatus(ticket.glpi_ticket_id, 'Em Atendimento');
|
||||||
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'synced');
|
||||||
|
logInfo(`Reabertura detectada no SN para ticket ${ticket.sn_ticket_id}. GLPI ${ticket.glpi_ticket_id} retornou para Em Atendimento.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const processCommentsController = async () => {
|
const processCommentsController = async () => {
|
||||||
const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor();
|
const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor();
|
||||||
logInfo(`📋Tickets em monitoramento: ${ticketsToMonitor.length}`);
|
logInfo(`Tickets em monitoramento: ${ticketsToMonitor.length}`);
|
||||||
|
|
||||||
|
for (const ticket of ticketsToMonitor) {
|
||||||
|
await ensureReopenFromSNWhenGlpiSolved(ticket);
|
||||||
|
|
||||||
for (const ticket of ticketsToMonitor){
|
|
||||||
// Sincroniza comentários do ServiceNow para o GLPI
|
|
||||||
const comments = await fetchCommentsFromServiceNow(ticket);
|
const comments = await fetchCommentsFromServiceNow(ticket);
|
||||||
// Se houver comentários novos no ServiceNow, sincroniza para o GLPI
|
if (comments.length !== 0) {
|
||||||
if(comments.length != 0){await syncCommentsSNtoGlpi(comments, ticket);}
|
await syncCommentsSNtoGlpi(comments, ticket);
|
||||||
// Sincroniza comentários do GLPI para o ServiceNow
|
}
|
||||||
|
|
||||||
await syncCommentsGlpitoSN(ticket);
|
await syncCommentsGlpitoSN(ticket);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
processCommentsController
|
processCommentsController
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @file processCommentsController.js
|
|
||||||
* @description Este controlador orquestra a sincronização bidirecional de comentários para todos os tickets ativos.
|
|
||||||
* Ele busca tickets que estão sendo monitorados e, para cada um, executa a sincronização de comentários do ServiceNow para o GLPI e vice-versa.
|
|
||||||
*/
|
|
||||||
93
src/controllers/processGlpiClosureController.js
Normal file
93
src/controllers/processGlpiClosureController.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
const TicketSyncModel = require('../models/ticketSyncModel');
|
||||||
|
const TicketGlpiModel = require('../models/ticketGlpiModel');
|
||||||
|
const TicketUpdateModel = require('../models/ticketUpdateModel');
|
||||||
|
const TicketSnModel = require('../models/ticketSnModel');
|
||||||
|
const { addCommentToServiceNow, addWorkNoteToServiceNow, updateStatusInServiceNow, fetchTicketLiveStatusFromServiceNow, closeTicketInServiceNow } = require('../services/servicenowService');
|
||||||
|
const { stripHTML } = require('../utils/commentSanitizer');
|
||||||
|
const { logInfo, logError } = require('../utils/logger');
|
||||||
|
|
||||||
|
const DEFINITIVE_CLOSE_NOTICE = 'Chamado encerrado definitivamente. Reabertura deste chamado nao sera atendida. Caso necessario, abra um novo chamado.';
|
||||||
|
const outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27);
|
||||||
|
|
||||||
|
const processGlpiClosureController = async () => {
|
||||||
|
try {
|
||||||
|
const tickets = await TicketSyncModel.getTicketsForClosureMonitor();
|
||||||
|
if (!tickets.length) {
|
||||||
|
logInfo('Nenhum ticket ativo para monitoramento de fechamento definitivo no GLPI.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ticket of tickets) {
|
||||||
|
try {
|
||||||
|
const glpiStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id);
|
||||||
|
if (glpiStatus !== 5 && glpiStatus !== 6) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticketSn = await TicketSnModel.findById(ticket.sn_ticket_id);
|
||||||
|
if (!ticketSn) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveSnStatus = await fetchTicketLiveStatusFromServiceNow(ticketSn.sys_id, ticketSn.tipo);
|
||||||
|
if (glpiStatus === 5) {
|
||||||
|
if (ticket.sn_sync_status === 'solved') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const solution = await TicketGlpiModel.getTicketSolution(ticket.glpi_ticket_id);
|
||||||
|
if (solution?.solutiontypes_id === outOfScopeSolutionTypeId) {
|
||||||
|
const justification = solution?.content ? stripHTML(solution.content) : 'Ticket marcado como fora do escopo.';
|
||||||
|
await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket nao sera mais exibido para equipe Sothis, com a justificativa: ${justification}`);
|
||||||
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored');
|
||||||
|
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (liveSnStatus === 'Em Espera' || liveSnStatus === 'Aguardando Atendimento') {
|
||||||
|
await updateStatusInServiceNow(ticket.sn_ticket_id, 'Em Atendimento');
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeNotes = solution?.content ? stripHTML(solution.content) : 'Resolvido via integracao GLPI.';
|
||||||
|
const resolvedAt = solution?.date_mod || new Date();
|
||||||
|
await closeTicketInServiceNow(ticket.sn_ticket_id, closeNotes, resolvedAt);
|
||||||
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'solved', 'solved');
|
||||||
|
logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} solucionado e refletido no SN.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceId = `glpi_closed_notice:${ticket.glpi_ticket_id}`;
|
||||||
|
const alreadyNotified = await TicketUpdateModel.getBySourceId(sourceId);
|
||||||
|
if (!alreadyNotified) {
|
||||||
|
const commentSent = await addCommentToServiceNow(ticket.sn_ticket_id, DEFINITIVE_CLOSE_NOTICE);
|
||||||
|
if (commentSent) {
|
||||||
|
await TicketUpdateModel.insert({
|
||||||
|
ticket_sync_id: ticket.id,
|
||||||
|
update_type: 'close_notice',
|
||||||
|
source_system: 'glpi',
|
||||||
|
content: DEFINITIVE_CLOSE_NOTICE,
|
||||||
|
author: 'integration',
|
||||||
|
created_at: new Date(),
|
||||||
|
updated_at: new Date(),
|
||||||
|
source_id: sourceId,
|
||||||
|
destiny_id: 'done'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regra nova: nao altera status no SN ao fechar definitivamente no GLPI.
|
||||||
|
// Apenas registra comentario e remove do monitoramento local.
|
||||||
|
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'closed');
|
||||||
|
logInfo(`Ticket GLPI ${ticket.glpi_ticket_id} encerrado definitivamente e refletido no SN.`);
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, `Falha ao processar fechamento definitivo GLPI para ticket ${ticket.glpi_ticket_id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'Erro geral no monitoramento de fechamento definitivo GLPI');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
processGlpiClosureController
|
||||||
|
};
|
||||||
@ -1,10 +1,12 @@
|
|||||||
const { syncTicketsToGlpi } = require('../services/glpiTicketService');
|
const { syncTicketsToGlpi } = require('../services/glpiTicketService');
|
||||||
const { processSyncController } = require('./processSyncController');
|
const { processSyncController } = require('./processSyncController');
|
||||||
|
const { processPendingExternalTicketLinks } = require('../services/servicenowService');
|
||||||
|
|
||||||
|
|
||||||
const processTicketsController = async () => {
|
const processTicketsController = async () => {
|
||||||
await processSyncController();
|
await processSyncController();
|
||||||
await syncTicketsToGlpi();
|
await syncTicketsToGlpi();
|
||||||
|
await processPendingExternalTicketLinks();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@ -112,6 +112,7 @@ class TicketSnModel {
|
|||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
|
tsync.id AS ticket_sync_id,
|
||||||
ts.id,
|
ts.id,
|
||||||
ts.ticket_number,
|
ts.ticket_number,
|
||||||
ts.short_description,
|
ts.short_description,
|
||||||
|
|||||||
@ -183,6 +183,29 @@ class TicketSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getTicketsForClosureMonitor() {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
tsync.id,
|
||||||
|
tsync.glpi_ticket_id,
|
||||||
|
tsync.sn_ticket_id,
|
||||||
|
tsync.source_last,
|
||||||
|
tsync.sn_sync_status,
|
||||||
|
tsync.glpi_sync_status
|
||||||
|
FROM ticket_sync tsync
|
||||||
|
WHERE tsync.glpi_ticket_id IS NOT NULL
|
||||||
|
AND tsync.glpi_sync_status NOT IN ('closed', 'ignored')
|
||||||
|
AND tsync.sn_sync_status NOT IN ('closed', 'ignored')
|
||||||
|
`;
|
||||||
|
const { rows } = await pool.query(query);
|
||||||
|
return rows;
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'ERRO: Falha ao buscar tickets para monitoramento de fechamento GLPI');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async getTicketsPendingSolve() {
|
static async getTicketsPendingSolve() {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
|
|||||||
@ -101,6 +101,23 @@ class TicketUpdateModel {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getPendingBySourcePrefix(sourcePrefix) {
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT *
|
||||||
|
FROM ticket_updates
|
||||||
|
WHERE source_id LIKE $1
|
||||||
|
AND destiny_id IS NULL
|
||||||
|
ORDER BY created_at ASC NULLS FIRST, id ASC
|
||||||
|
`;
|
||||||
|
const { rows } = await pool.query(query, [`${sourcePrefix}%`]);
|
||||||
|
return rows;
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, `Erro ao buscar updates pendentes por prefixo: ${sourcePrefix}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TicketUpdateModel;
|
module.exports = TicketUpdateModel;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ const TicketGlpiModel = require('../models/ticketGlpiModel');
|
|||||||
const TicketUpdateModel = require('../models/ticketUpdateModel');
|
const TicketUpdateModel = require('../models/ticketUpdateModel');
|
||||||
const TicketSyncModel = require('../models/ticketSyncModel');
|
const TicketSyncModel = require('../models/ticketSyncModel');
|
||||||
const { logInfo, logError } = require('../utils/logger');
|
const { logInfo, logError } = require('../utils/logger');
|
||||||
const { addWorkNoteToServiceNow } = require('./servicenowService');
|
const { addWorkNoteToServiceNow, updateExternalTicketInServiceNow, queueExternalTicketLinkRetry } = require('./servicenowService');
|
||||||
const { stripHTML } = require('../utils/commentSanitizer');
|
const { stripHTML } = require('../utils/commentSanitizer');
|
||||||
|
|
||||||
const outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27);
|
const outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27);
|
||||||
@ -40,6 +40,8 @@ const processSingleTicket = async (ticket) => {
|
|||||||
if (ticket.glpi_ticket_id) {
|
if (ticket.glpi_ticket_id) {
|
||||||
logInfo(`✅ Ticket ${ticket.ticket_number} já possui GLPI ID (${ticket.glpi_ticket_id}) registrado. Apenas atualizando status.`);
|
logInfo(`✅ Ticket ${ticket.ticket_number} já possui GLPI ID (${ticket.glpi_ticket_id}) registrado. Apenas atualizando status.`);
|
||||||
await TicketSyncModel.updateStatus(ticket.id, 'synced', 'synced');
|
await TicketSyncModel.updateStatus(ticket.id, 'synced', 'synced');
|
||||||
|
const linkedOk = await updateExternalTicketInServiceNow(ticket.id, ticket.glpi_ticket_id);
|
||||||
|
await queueExternalTicketLinkRetry(ticket.ticket_sync_id, ticket.id, ticket.glpi_ticket_id, linkedOk);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +58,8 @@ const processSingleTicket = async (ticket) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', existingTicket);
|
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', existingTicket);
|
||||||
|
const existingLinkOk = await updateExternalTicketInServiceNow(ticket.id, existingTicket);
|
||||||
|
await queueExternalTicketLinkRetry(ticket.ticket_sync_id, ticket.id, existingTicket, existingLinkOk);
|
||||||
logInfo(`✅ Ticket ${ticket.ticket_number} já existe no GLPI! GLPI ID: ${existingTicket}`);
|
logInfo(`✅ Ticket ${ticket.ticket_number} já existe no GLPI! GLPI ID: ${existingTicket}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -80,6 +84,8 @@ const processSingleTicket = async (ticket) => {
|
|||||||
const glpiTicketId = await TicketGlpiModel.createTicket(glpiData);
|
const glpiTicketId = await TicketGlpiModel.createTicket(glpiData);
|
||||||
await TicketSyncModel.updateLastSync(ticket.id, 'glpi');
|
await TicketSyncModel.updateLastSync(ticket.id, 'glpi');
|
||||||
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', glpiTicketId);
|
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', glpiTicketId);
|
||||||
|
const externalLinkOk = await updateExternalTicketInServiceNow(ticket.id, glpiTicketId);
|
||||||
|
await queueExternalTicketLinkRetry(ticket.ticket_sync_id, ticket.id, glpiTicketId, externalLinkOk);
|
||||||
|
|
||||||
logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`);
|
logInfo(`✅ Ticket ${ticket.ticket_number} sincronizado! GLPI ID: ${glpiTicketId}`);
|
||||||
|
|
||||||
|
|||||||
@ -10,15 +10,37 @@ const TicketSyncModel = require('../models/ticketSyncModel');
|
|||||||
const resolvedBySysId = process.env.SERVICENOW_RESOLVED_BY_SYSID || 'b131d6ed1b2ba510c5e163923b4bcb77';
|
const resolvedBySysId = process.env.SERVICENOW_RESOLVED_BY_SYSID || 'b131d6ed1b2ba510c5e163923b4bcb77';
|
||||||
const servicenowDefaultUser = process.env.SERVICENOW_DEFAULT_USER || 'SOTHIS.CAOA';
|
const servicenowDefaultUser = process.env.SERVICENOW_DEFAULT_USER || 'SOTHIS.CAOA';
|
||||||
const shouldIgnoreDefaultUserJournal = process.env.SERVICENOW_IGNORE_DEFAULT_USER_JOURNAL !== 'false';
|
const shouldIgnoreDefaultUserJournal = process.env.SERVICENOW_IGNORE_DEFAULT_USER_JOURNAL !== 'false';
|
||||||
|
const externalTicketField = process.env.SERVICENOW_EXTERNAL_TICKET_FIELD || 'u_external_tickets';
|
||||||
|
const ignoredJournalUsers = (
|
||||||
|
process.env.SERVICENOW_IGNORE_USERS_JOURNAL ||
|
||||||
|
`${servicenowDefaultUser},admin.caoa`
|
||||||
|
)
|
||||||
|
.split(',')
|
||||||
|
.map((u) => String(u || '').trim().toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const normalizeServiceNowDateTime = (value) => {
|
const normalizeServiceNowDateTime = (value) => {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const asString = String(value).trim();
|
const asString = String(value).trim();
|
||||||
const m = asString.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})/);
|
|
||||||
if (m) return `${m[1]} ${m[2]}`;
|
// Formato ISO/local: YYYY-MM-DD HH:mm:ss
|
||||||
|
const ymd = asString.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/);
|
||||||
|
if (ymd) {
|
||||||
|
return `${ymd[1]}-${ymd[2]}-${ymd[3]} ${ymd[4]}:${ymd[5]}:${ymd[6]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formato SN comum com barra: DD/MM/YYYY HH:mm:ss
|
||||||
|
const dmy = asString.match(/^(\d{2})\/(\d{2})\/(\d{4})[ T](\d{2}):(\d{2}):(\d{2})/);
|
||||||
|
if (dmy) {
|
||||||
|
return `${dmy[3]}-${dmy[2]}-${dmy[1]} ${dmy[4]}:${dmy[5]}:${dmy[6]}`;
|
||||||
|
}
|
||||||
|
|
||||||
const parsed = new Date(asString);
|
const parsed = new Date(asString);
|
||||||
if (Number.isNaN(parsed.getTime())) return null;
|
if (Number.isNaN(parsed.getTime())) return null;
|
||||||
return parsed.toISOString().slice(0, 19).replace('T', ' ');
|
|
||||||
|
// Fallback preservando timestamp local sem converter para UTC.
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())} ${pad(parsed.getHours())}:${pad(parsed.getMinutes())}:${pad(parsed.getSeconds())}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchTicketsFromServiceNow = async (watermark) => {
|
const fetchTicketsFromServiceNow = async (watermark) => {
|
||||||
@ -265,11 +287,13 @@ const fetchCommentsFromServiceNow = async (ticket) => {
|
|||||||
if (!shouldIgnoreDefaultUserJournal) {
|
if (!shouldIgnoreDefaultUserJournal) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const createdBy = String(entry.sys_created_by || '').trim().toLowerCase();
|
||||||
|
const ignoredUser = ignoredJournalUsers.includes(createdBy);
|
||||||
if (entry.element === 'comments') {
|
if (entry.element === 'comments') {
|
||||||
return entry.sys_created_by !== servicenowDefaultUser;
|
return !ignoredUser;
|
||||||
}
|
}
|
||||||
if (entry.element === 'work_notes') {
|
if (entry.element === 'work_notes') {
|
||||||
return entry.sys_created_by !== servicenowDefaultUser;
|
return !ignoredUser;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@ -548,6 +572,152 @@ const addWorkNoteToServiceNow = async (snTicketId, workNote) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addCommentToServiceNow = async (snTicketId, comment) => {
|
||||||
|
let sysId = null;
|
||||||
|
try {
|
||||||
|
const ticketSn = await TicketSnModel.findById(snTicketId);
|
||||||
|
if (!ticketSn) {
|
||||||
|
logError(`Ticket SN com ID ${snTicketId} nao encontrado para adicionar comentario.`, 'addCommentToServiceNow');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
sysId = ticketSn.sys_id;
|
||||||
|
const ticketType = ticketSn.tipo;
|
||||||
|
let url = '';
|
||||||
|
|
||||||
|
if (ticketType === 'incidente') {
|
||||||
|
url = `${apiConfig.snTableIncidentConfig.baseUrl}/${sysId}`;
|
||||||
|
} else if (ticketType === 'requisicao') {
|
||||||
|
url = `${apiConfig.snTableRequestConfig.baseUrl}/${sysId}`;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Tipo de ticket desconhecido: ${ticketType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = { comments: comment };
|
||||||
|
const response = await axios.patch(url, payload, {
|
||||||
|
auth: apiConfig.servicenowAuthentication.auth,
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
await TicketSyncModel.updateLastSync(snTicketId, 'servicenow');
|
||||||
|
if (response.status !== 200) {
|
||||||
|
logError(`Falha ao adicionar comentario no SN. Status: ${response.status}`, { snTicketId, sysId });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo(`OK: Comentario adicionado com sucesso ao ticket SN ${sysId}.`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, `ERRO: Falha ao adicionar comentario ao ticket SN (sysId: ${sysId})`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateExternalTicketInServiceNow = async (snTicketId, glpiTicketId) => {
|
||||||
|
let sysId = null;
|
||||||
|
try {
|
||||||
|
const ticketSn = await TicketSnModel.findById(snTicketId);
|
||||||
|
if (!ticketSn) {
|
||||||
|
logError(`Ticket SN com ID ${snTicketId} nao encontrado para atualizar Ticket Externo.`, 'updateExternalTicketInServiceNow');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sysId = ticketSn.sys_id;
|
||||||
|
const ticketType = ticketSn.tipo;
|
||||||
|
let url = '';
|
||||||
|
if (ticketType === 'incidente') {
|
||||||
|
url = `${apiConfig.snTableIncidentConfig.baseUrl}/${sysId}`;
|
||||||
|
} else if (ticketType === 'requisicao') {
|
||||||
|
url = `${apiConfig.snTableRequestConfig.baseUrl}/${sysId}`;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Tipo de ticket desconhecido: ${ticketType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = { [externalTicketField]: String(glpiTicketId) };
|
||||||
|
const response = await axios.patch(url, payload, {
|
||||||
|
auth: apiConfig.servicenowAuthentication.auth,
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
await TicketSyncModel.updateLastSync(snTicketId, 'servicenow');
|
||||||
|
if (response.status !== 200) {
|
||||||
|
logError(`Falha ao atualizar Ticket Externo no SN. Status: ${response.status}`, { snTicketId, glpiTicketId, externalTicketField });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validacao de leitura: alguns campos customizados podem retornar 200 e nao persistir por ACL/schema.
|
||||||
|
const verifyResp = await axios.get(`${url}?sysparm_fields=${externalTicketField}&sysparm_display_value=true`, {
|
||||||
|
auth: apiConfig.servicenowAuthentication.auth,
|
||||||
|
headers: { 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
const result = verifyResp?.data?.result || {};
|
||||||
|
const rawField = result[externalTicketField];
|
||||||
|
const persistedValue = typeof rawField === 'object'
|
||||||
|
? (rawField.value || rawField.display_value || '')
|
||||||
|
: (rawField || '');
|
||||||
|
|
||||||
|
if (String(persistedValue) !== String(glpiTicketId)) {
|
||||||
|
logError(`Ticket Externo nao persistiu no SN (campo ${externalTicketField}).`, {
|
||||||
|
snTicketId,
|
||||||
|
sysId,
|
||||||
|
expected: String(glpiTicketId),
|
||||||
|
got: String(persistedValue)
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo(`OK: Ticket Externo confirmado no SN (${externalTicketField}=${glpiTicketId}).`, { snTicketId, sysId });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, `ERRO: Falha ao atualizar Ticket Externo no SN (sysId: ${sysId})`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const queueExternalTicketLinkRetry = async (ticketSyncId, snTicketId, glpiTicketId, shouldMarkDone = false) => {
|
||||||
|
const sourceId = `external_link:${snTicketId}:${glpiTicketId}`;
|
||||||
|
const payload = JSON.stringify({ sn_ticket_id: snTicketId, glpi_ticket_id: glpiTicketId, field: externalTicketField });
|
||||||
|
await TicketUpdateModel.insert({
|
||||||
|
ticket_sync_id: ticketSyncId,
|
||||||
|
update_type: 'external_link',
|
||||||
|
source_system: 'sync',
|
||||||
|
content: payload,
|
||||||
|
author: 'integration',
|
||||||
|
created_at: new Date(),
|
||||||
|
updated_at: new Date(),
|
||||||
|
source_id: sourceId,
|
||||||
|
destiny_id: shouldMarkDone ? 'done' : null
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const processPendingExternalTicketLinks = async () => {
|
||||||
|
try {
|
||||||
|
const pendings = await TicketUpdateModel.getPendingBySourcePrefix('external_link:');
|
||||||
|
if (!pendings.length) return;
|
||||||
|
|
||||||
|
for (const pending of pendings) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(pending.content || '{}');
|
||||||
|
const snTicketId = payload.sn_ticket_id;
|
||||||
|
const glpiTicketId = payload.glpi_ticket_id;
|
||||||
|
if (!snTicketId || !glpiTicketId) {
|
||||||
|
logError(`Payload invalido para retry de Ticket Externo. update_id=${pending.id}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = await updateExternalTicketInServiceNow(snTicketId, glpiTicketId);
|
||||||
|
if (ok) {
|
||||||
|
await TicketUpdateModel.updateDestinyId(pending.source_id, 'done');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, `Falha ao processar retry de Ticket Externo. update_id=${pending.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'ERRO: Falha geral no retry de Ticket Externo');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca o valor de uma variavel de catalogo (sc_item_option) para um Request Item (RITM) especifico.
|
* 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).
|
||||||
@ -602,7 +772,11 @@ module.exports = {
|
|||||||
closeTicketInServiceNow,
|
closeTicketInServiceNow,
|
||||||
updateStatusInServiceNow,
|
updateStatusInServiceNow,
|
||||||
addWorkNoteToServiceNow,
|
addWorkNoteToServiceNow,
|
||||||
fetchScItemOptionValue
|
addCommentToServiceNow,
|
||||||
|
fetchScItemOptionValue,
|
||||||
|
updateExternalTicketInServiceNow,
|
||||||
|
queueExternalTicketLinkRetry,
|
||||||
|
processPendingExternalTicketLinks
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user