REFACTOR: Preparando WatchDog para nova feature

This commit is contained in:
Rafael Alves Lopes 2026-01-19 17:55:15 -03:00
parent c1f52d741f
commit ad28159185
5 changed files with 305 additions and 58 deletions

232
fluxoEntidade.md Normal file
View File

@ -0,0 +1,232 @@
# Fluxo de Resolução e Criação de Entidades (GLPI)
## Contexto
Quando um chamado de **Implantação** chega no Hub x GLPI, precisamos garantir que exista uma **Entidade** correta no GLPI para vincular o ticket.
Hoje, a resolução tenta encontrar a entidade pelo padrão de nome baseado em:
- `codigoCliente`
- `codigoServico`
- `nomeCliente`
Caso não encontre, entra a feature de **criação automática via API (GLPI v2 + OAuth)**.
> Regra importante: **criação/alteração de entidades sempre via API**, nunca via banco do GLPI.
---
## Convenções de Nome
### Entidade mãe (Cliente)
- **Formato:** `{codigoCliente} - {nomeCliente}`
- **Exemplo:** `12345 - ACME Telecom`
### Entidade filha (Serviço)
- **Formato:** `{codigoCliente} - {codigoServico} - {nomeCliente}`
- **Exemplo:** `12345 - 67890 - ACME Telecom`
> Observação: O `{nomeCliente}` é repetido na filha para manter legibilidade e facilitar busca manual no GLPI.
---
## Busca de Entidades
### Busca por Serviço (Entidade filha)
Procura por entidade que contenha:
- `{codigoCliente} - {codigoServico}` (prefixo/like)
### Busca por Cliente (Entidade mãe)
Procura por entidade que contenha:
- `{codigoCliente} -` (prefixo/like)
---
## Opção A — Criação hierárquica (Mãe + Filha quando necessário)
### Objetivo
Garantir hierarquia consistente no GLPI:
- Cliente como entidade mãe
- Serviço como entidade filha
### Fluxo (passo a passo)
1. **Tentar encontrar entidade por Serviço**
- Busca: `{codigoCliente} - {codigoServico}*`
- Se encontrou:
- ✅ usar essa entidade (filha) e finalizar
2. **Se não encontrou por Serviço: tentar encontrar entidade por Cliente**
- Busca: `{codigoCliente} -*`
- Se encontrou:
- ✅ criar **entidade filha** abaixo dessa mãe:
- Nome: `{codigoCliente} - {codigoServico} - {nomeCliente}`
- Parent: `{codigoCliente} - {nomeCliente}`
- ✅ usar a entidade criada (filha) e finalizar
3. **Se não encontrou nem por Serviço nem por Cliente**
- Criar **entidade mãe**:
- Nome: `{codigoCliente} - {nomeCliente}`
- Parent: raiz (definir entidade pai padrão, ex.: Root)
- Criar **entidade filha** abaixo da mãe:
- Nome: `{codigoCliente} - {codigoServico} - {nomeCliente}`
- Parent: `{codigoCliente} - {nomeCliente}`
- ✅ usar a entidade criada (filha) e finalizar
### Resultado final (Opção A)
- Ticket sempre fica na **entidade filha** (serviço)
- Sempre tenta manter **estrutura hierárquica** consistente
---
### Diagrama de Fluxo — Opção A (hierárquica: Mãe + Filha quando necessário)
```mermaid
flowchart TD
A[Chamado de Implantação chega do Hub] --> B[Extrair dados<br/>codigoCliente<br/>codigoServico<br/>nomeCliente]
B --> C{Existe entidade<br/>por Serviço?}
C -->|Sim| D[Usar entidade de Serviço]
D --> Z[Fim / Continua fluxo do ticket]
C -->|Não| E{Existe entidade<br/>por Cliente?}
E -->|Sim| F[Criar entidade de Serviço<br/>via API GLPI]
F --> G[Parent = Entidade Cliente<br/>Nome = codigoCliente-codigoServico-nomeCliente]
G --> Z
E -->|Não| H[Criar entidade Cliente<br/>via API GLPI]
H --> I[Parent = Contratos Ativos<br/>Nome = codigoCliente-nomeCliente]
I --> J[Criar entidade de Serviço<br/>via API GLPI]
J --> K[Parent = Entidade Cliente<br/>Nome = codigoCliente-codigoServico-nomeCliente]
K --> Z
```
## Opção B — Criação simplificada (sem forçar hierarquia)
### Objetivo
Criar o mínimo possível e evitar duplicação de entidades.
### Fluxo (passo a passo)
1. **Tentar encontrar entidade por Serviço**
- Busca: `{codigoCliente} - {codigoServico}*`
- Se encontrou:
- ✅ usar essa entidade e finalizar
2. **Se não encontrou por Serviço: tentar encontrar entidade por Cliente**
- Busca: `{codigoCliente} -*`
- Se encontrou:
- ✅ usar essa entidade mãe
- ❌ não cria entidade filha
- Finaliza
3. **Se não encontrou nem por Serviço nem por Cliente**
- Criar **uma entidade única (sem mãe)**:
- Nome: `{codigoCliente} - {codigoServico} - {nomeCliente}`
- Parent: raiz (ou padrão definido)
- ✅ usar essa entidade e finalizar
### Resultado final (Opção B)
- Ticket pode ficar:
- na entidade de serviço (se existir)
- ou na entidade de cliente (se existir)
- ou numa entidade única criada
- Não garante hierarquia “mãe → filha”
---
### Fluxo Opção B
```mermaid
flowchart TD
A[Chamado de Implantação chega do Hub] --> B[Extrair dados<br/>codigoCliente<br/>codigoServico<br/>nomeCliente]
B --> C{Existe entidade<br/>por Serviço?}
C -->|Sim| D[Usar entidade de Serviço]
D --> Z[Fim / Continua fluxo do ticket]
C -->|Não| E{Existe entidade<br/>por Cliente?}
E -->|Sim| F[Usar entidade de Cliente]
F --> Z
E -->|Não| G[Criar entidade Única<br/>via API GLPI]
G --> H[Parent = Root<br/>Nome = codigoCliente-codigoServico-nomeCliente]
H --> Z
```
## Opção C — Híbrida (Implantação cria; demais apenas resolvem)
### Objetivo
- Para **Implantação**: garantir estrutura correta criando entidades quando necessário (igual Opção A).
- Para **demais tipos de chamado**: **não criar entidades**, apenas resolver a melhor entidade existente; se não houver, usar fallback **Contratos Ativos**.
---
### Fluxo (passo a passo)
#### 1) Identificar tipo do chamado
- Se `tipo == Implantação` → executar **Fluxo A (criação hierárquica)**.
- Se `tipo != Implantação` → executar **Fluxo de Resolução sem criação**.
---
### Fluxo A (quando Implantação)
Segue exatamente a **Opção A**:
1. Buscar entidade por Serviço (`{codigoCliente} - {codigoServico}%`)
2. Se não achar, buscar por Cliente (`{codigoCliente} -%`)
3. Se achar Cliente, criar Serviço abaixo do Cliente
4. Se não achar nenhum, criar Cliente (embaixo de Contratos Ativos) e depois criar Serviço abaixo do Cliente
5. Retornar sempre a **entidade de Serviço**
---
### Fluxo de Resolução (quando NÃO é Implantação)
1. Buscar entidade por Serviço (`{codigoCliente} - {codigoServico}%`)
- Se achou: ✅ usar e finalizar
2. Se não achou, buscar entidade por Cliente (`{codigoCliente} -%`)
- Se achou: ✅ usar e finalizar
3. Se não achou nenhum dos dois:
- ✅ usar entidade **Contratos Ativos** (fallback)
- ❌ não criar nada
---
### Resultado final (Opção C)
- **Implantação**: sempre aponta para entidade de **Serviço**, criando Cliente/Serviço quando faltar.
- **Demais chamados**: nunca criam entidades; ficam na melhor existente (Serviço → Cliente → Contratos Ativos).
---
### Diagrama de Fluxo — Opção C (Híbrida)
```mermaid
flowchart TD
A[Chega chamado do Hub] --> B[Extrair dados<br/>codigoCliente<br/>codigoServico<br/>nomeCliente<br/>tipoChamado]
B --> C{Tipo é<br/>Implantação?}
%% ========== RAMO IMPLANTAÇÃO (OPÇÃO A) ==========
C -->|Sim| IA{Existe entidade<br/>por Serviço?}
IA -->|Sim| I1[Usar entidade de Serviço] --> Z[Fim / Continua fluxo do ticket]
IA -->|Não| IB{Existe entidade<br/>por Cliente?}
IB -->|Sim| I2[Criar entidade de Serviço<br/>via API GLPI<br/>Parent = Cliente] --> I3[Usar entidade criada] --> Z
IB -->|Não| I4[Criar entidade Cliente<br/>via API GLPI<br/>Parent = Contratos Ativos] --> I5[Criar entidade de Serviço<br/>via API GLPI<br/>Parent = Cliente] --> I6[Usar entidade criada] --> Z
%% ========== RAMO NÃO IMPLANTAÇÃO (RESOLVE ONLY) ==========
C -->|Não| NA{Existe entidade<br/>por Serviço?}
NA -->|Sim| N1[Usar entidade de Serviço] --> Z
NA -->|Não| NB{Existe entidade<br/>por Cliente?}
NB -->|Sim| N2[Usar entidade de Cliente] --> Z
NB -->|Não| N3[Usar entidade fallback<br/>Contratos Ativos] --> Z
```

View File

@ -3,50 +3,63 @@
const db = require('../../connections/hubglpi.pg.js');
const { logError } = require('../../../../shared/utils/logger.js');
const VALID_NOTIFICATION_TYPES = new Set(['func', 'adm']);
function resolveNotificationType(type = 'func') {
const normalized = String(type || 'func').toLowerCase();
return VALID_NOTIFICATION_TYPES.has(normalized) ? normalized : 'func';
}
function buildStatus(kind, type) {
return `${kind}_${resolveNotificationType(type)}`;
}
/**
* Verifica se a notificação foi enviada com sucesso
* Verifica se a notificaÇõÇœo ­ foi enviada com sucesso
*/
async function notificationAlreadySent(hubsoftTicketId) {
async function notificationAlreadySentFunc(hubsoftTicketId, type = 'func') {
const status = buildStatus('sent', type);
const query = `
SELECT 1
FROM watchdog_notifications
WHERE ticket_id = $1
AND status = 'SENT'
AND status = $2
LIMIT 1;
`;
try {
const { rowCount } = await db.query(query, [hubsoftTicketId]);
const { rowCount } = await db.query(query, [hubsoftTicketId, status]);
return rowCount > 0;
} catch (error) {
logError('Erro ao verificar notificação já enviada', error);
logError('Erro ao verificar notificaÇõÇœo jÇ­ enviada', error);
throw error;
}
}
/**
* Marca tickets como pendente de notificação
* Marca tickets como pendente de notificaÇõÇœo
*/
async function markNotificationsAsPending(hubsoftTicketIds) {
async function markNotificationsAsPendingFunc(hubsoftTicketIds, type = 'func') {
if (!hubsoftTicketIds || hubsoftTicketIds.length === 0) {
return;
}
const status = buildStatus('pending', type);
const query = `
INSERT INTO watchdog_notifications (ticket_id, notified_at, status)
SELECT
unnest($1::bigint[]),
NOW(),
'pending'
$2
ON CONFLICT (ticket_id)
DO UPDATE SET
notified_at = EXCLUDED.notified_at,
status = 'pending';
status = $2;
`;
try {
await db.query(query, [hubsoftTicketIds]);
await db.query(query, [hubsoftTicketIds, status]);
} catch (error) {
logError('Erro ao marcar notificações como falha', error);
logError('Erro ao marcar notificaÇõÇæes como falha', error);
throw error;
}
}
@ -55,65 +68,68 @@ async function markNotificationsAsPending(hubsoftTicketIds) {
/**
* Marca tickets como notificados com sucesso
*/
async function markNotificationsAsSent(hubsoftTicketIds) {
async function markNotificationsAsSentFunc(hubsoftTicketIds, type = 'func') {
const ids = (hubsoftTicketIds || [])
.map(id => Number(id))
.filter(Number.isFinite);
if (ids.length === 0) return;
const status = buildStatus('sent', type);
const query = `
INSERT INTO watchdog_notifications (ticket_id, notified_at, status)
SELECT
unnest($1::bigint[]),
NOW(),
'sent'
$2
ON CONFLICT (ticket_id)
DO UPDATE SET
notified_at = EXCLUDED.notified_at,
status = 'sent';
status = $2;
`;
try {
await db.query(query, [ids]);
await db.query(query, [ids, status]);
} catch (error) {
logError('[WATCHDOG][REPOSITORY] Erro ao marcar notificações como enviadas', error);
logError('[WATCHDOG][REPOSITORY] Erro ao marcar notificaÇõÇæes como enviadas', error);
throw error;
}
}
/**
* Marca tickets como falha de notificação
* Marca tickets como falha de notificaÇõÇœo
*/
async function markNotificationsAsFailed(hubsoftTicketIds) {
async function markNotificationsAsFailedFunc(hubsoftTicketIds, type = 'func') {
const ids = (hubsoftTicketIds || [])
.map(id => Number(id))
.filter(Number.isFinite);
if (ids.length === 0) return;
const status = buildStatus('failed', type);
const query = `
INSERT INTO watchdog_notifications (ticket_id, notified_at, status)
SELECT
unnest($1::bigint[]),
NOW(),
'sent'
$2
ON CONFLICT (ticket_id)
DO UPDATE SET
notified_at = EXCLUDED.notified_at,
status = 'failed';
status = $2;
`;
try {
await db.query(query, [ids]);
await db.query(query, [ids, status]);
} catch (error) {
logError('[WATCHDOG][REPOSITORY] Erro ao marcar notificações como enviadas', error);
logError('[WATCHDOG][REPOSITORY] Erro ao marcar notificaÇõÇæes como enviadas', error);
throw error;
}
}
async function getPendingTicketsForNotification() {
async function getPendingTicketsForNotificationFunc(type = 'func') {
const status = buildStatus('pending', type);
const query = `
SELECT
wn.ticket_id AS hubsoft_ticket_id,
@ -126,22 +142,22 @@ async function getPendingTicketsForNotification() {
ON wn.ticket_id = sd.hubsoft_ticket_id
INNER JOIN hubsoft_tickets ht
ON wn.ticket_id = ht.id_atendimento
WHERE wn.status = 'pending';
WHERE wn.status = $1;
`;
try {
const { rows } = await db.query(query);
const { rows } = await db.query(query, [status]);
return rows;
} catch (error) {
logError('Erro ao buscar tickets pendentes para notificação', error);
logError('Erro ao buscar tickets pendentes para notificaÇõÇœo', error);
throw error;
}
}
module.exports = {
notificationAlreadySent,
markNotificationsAsSent,
markNotificationsAsFailed,
markNotificationsAsPending,
getPendingTicketsForNotification
notificationAlreadySent: notificationAlreadySentFunc,
markNotificationsAsSent: markNotificationsAsSentFunc,
markNotificationsAsFailed: markNotificationsAsFailedFunc,
markNotificationsAsPending: markNotificationsAsPendingFunc,
getPendingTicketsForNotification: getPendingTicketsForNotificationFunc
};

View File

@ -8,9 +8,6 @@ const cancelamentoService = require('../services/cancelamento.service.js')
//const sacService = require('../services/sac.service.js') //TODO
const trocaTitularidadeService = require('../services/trocaTitularidade.service.js') //TODO
const getAuthToken = require('../../../infra/api/hubsoft.auth.js')
const ticketShared = require('../services/createTickets.service.js')
const { logInfo, logError } = require('../../../shared/utils/logger.js')

View File

@ -8,38 +8,40 @@ const model = require('../model/email.model.js')
async function runWatchdog() {
logInfo('[WATCHDOG] [JOB] Coletando chamados fechados há 31 minutos')
logInfo('[WATCHDOG] [JOB] Coletando chamados fechados hÇ­ 31 minutos')
const thresholdDate = new Date(Date.now() - 31 * 60 * 1000)
const closedTickets = await repository.getClosedTicketsSince(thresholdDate)
logInfo(`[WATCHDOG] [JOB] Encontrados ${closedTickets.length} chamados fechados`)
let notificationType = 'func'
for (const ticket of closedTickets) {
const hubGlpiTicket = await repository.checkTicketInHubGlpi(ticket.id_atendimento)
if (!hubGlpiTicket.exists) {
logInfo(`[WATCHDOG] [JOB] Chamado ${ticket.id_atendimento} não encontrado no HubGlpi. Ignorando.`)
logInfo(`[WATCHDOG] [JOB] Chamado ${ticket.id_atendimento} nao encontrado no HubGlpi. Ignorando.`)
continue
}
if (hubGlpiTicket.status === 'closed') {
logInfo(`[WATCHDOG] [JOB] Chamado ${ticket.id_atendimento} já está fechado no HubGlpi. Ignorando.`)
logInfo(`[WATCHDOG] [JOB] Chamado ${ticket.id_atendimento} ja esta fechado no HubGlpi. Ignorando.`)
continue
}
if (await repository.notificationAlreadySent(ticket.id_atendimento)) {
logInfo(`[WATCHDOG] [JOB] Notificação já enviada para o chamado ${ticket.id_atendimento}.`)
if (await repository.notificationAlreadySent(ticket.id_atendimento, notificationType)) {
logInfo(`[WATCHDOG] [JOB] NotificaÇõÇœo jÇ­ enviada para o chamado ${ticket.id_atendimento}.`)
continue
}
await repository.markNotificationsAsPending([ticket.id_atendimento])
await repository.markNotificationsAsPending([ticket.id_atendimento], notificationType)
}
const ticketsToNotify = await repository.getPendingTicketsForNotification()
logInfo(`[WATCHDOG] [JOB] ${ticketsToNotify.length} chamados pendentes para notificação.`)
const ticketsToNotify = await repository.getPendingTicketsForNotification(notificationType)
logInfo(`[WATCHDOG] [JOB] ${ticketsToNotify.length} chamados pendentes para notificaÇõÇœo.`)
if (!ticketsToNotify.length) {
logInfo('[WATCHDOG] [JOB] Nenhum chamado pendente para notificação')
logInfo('[WATCHDOG] [JOB] Nenhum chamado pendente para notificaÇõÇœo')
return
}
@ -50,13 +52,13 @@ async function runWatchdog() {
await repository.sendClosureNotifications(payload)
await repository.markNotificationsAsSent(hubsoftTicketIds)
await repository.markNotificationsAsSent(hubsoftTicketIds, notificationType)
} catch (err) {
logError('[WATCHDOG] Erro ao enviar notificações', err)
await repository.markNotificationsAsFailed(hubsoftTicketIds)
logError('[WATCHDOG] Erro ao enviar notificaÇõÇæes', err)
await repository.markNotificationsAsFailed(hubsoftTicketIds, notificationType)
}
logInfo(`[WATCHDOG] [JOB] Enviadas ${ticketsToNotify.length} notificações`)
logInfo(`[WATCHDOG] [JOB] Enviadas ${ticketsToNotify.length} notificaÇõÇæes`)
}

View File

@ -35,8 +35,8 @@ async function checkTicketInHubGlpi(hubsoftTicketId) {
/**
* Verifica se a notificação de encerramento foi enviada
*/
async function notificationAlreadySent(hubsoftTicketId) {
return wdRepository.notificationAlreadySent(hubsoftTicketId);
async function notificationAlreadySent(hubsoftTicketId, type = 'func') {
return wdRepository.notificationAlreadySent(hubsoftTicketId, type);
}
/**
@ -49,26 +49,26 @@ async function sendClosureNotifications(payload) {
/**
* Marca notificações como enviadas com sucesso
*/
async function markNotificationsAsSent(hubsoftTicketIds) {
return wdRepository.markNotificationsAsSent(hubsoftTicketIds);
async function markNotificationsAsSent(hubsoftTicketIds, type = 'func') {
return wdRepository.markNotificationsAsSent(hubsoftTicketIds, type);
}
/**
* Marca notificações como falhas
*/
async function markNotificationsAsFailed(hubsoftTicketIds, error = null) {
return wdRepository.markNotificationsAsFailed(hubsoftTicketIds, error);
async function markNotificationsAsFailed(hubsoftTicketIds, type = 'func', error = null) {
return wdRepository.markNotificationsAsFailed(hubsoftTicketIds, type, error);
}
/**
* Marca notificações como pendentes
*/
async function markNotificationsAsPending(hubsoftTicketId, error = null) {
return wdRepository.markNotificationsAsPending(hubsoftTicketId, error);
async function markNotificationsAsPending(hubsoftTicketId, type = 'func', error = null) {
return wdRepository.markNotificationsAsPending(hubsoftTicketId, type, error);
}
async function getPendingTicketsForNotification() {
return wdRepository.getPendingTicketsForNotification();
async function getPendingTicketsForNotification(type = 'func') {
return wdRepository.getPendingTicketsForNotification(type);
}
module.exports = {