diff --git a/fluxoEntidade.md b/fluxoEntidade.md
new file mode 100644
index 0000000..5c6fc7f
--- /dev/null
+++ b/fluxoEntidade.md
@@ -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
codigoCliente
codigoServico
nomeCliente]
+
+ B --> C{Existe entidade
por Serviço?}
+ C -->|Sim| D[Usar entidade de Serviço]
+ D --> Z[Fim / Continua fluxo do ticket]
+
+ C -->|Não| E{Existe entidade
por Cliente?}
+
+ E -->|Sim| F[Criar entidade de Serviço
via API GLPI]
+ F --> G[Parent = Entidade Cliente
Nome = codigoCliente-codigoServico-nomeCliente]
+ G --> Z
+
+ E -->|Não| H[Criar entidade Cliente
via API GLPI]
+ H --> I[Parent = Contratos Ativos
Nome = codigoCliente-nomeCliente]
+
+ I --> J[Criar entidade de Serviço
via API GLPI]
+ J --> K[Parent = Entidade Cliente
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
codigoCliente
codigoServico
nomeCliente]
+
+ B --> C{Existe entidade
por Serviço?}
+ C -->|Sim| D[Usar entidade de Serviço]
+ D --> Z[Fim / Continua fluxo do ticket]
+
+ C -->|Não| E{Existe entidade
por Cliente?}
+
+ E -->|Sim| F[Usar entidade de Cliente]
+ F --> Z
+
+ E -->|Não| G[Criar entidade Única
via API GLPI]
+ G --> H[Parent = Root
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
codigoCliente
codigoServico
nomeCliente
tipoChamado]
+
+ B --> C{Tipo é
Implantação?}
+
+ %% ========== RAMO IMPLANTAÇÃO (OPÇÃO A) ==========
+ C -->|Sim| IA{Existe entidade
por Serviço?}
+ IA -->|Sim| I1[Usar entidade de Serviço] --> Z[Fim / Continua fluxo do ticket]
+
+ IA -->|Não| IB{Existe entidade
por Cliente?}
+ IB -->|Sim| I2[Criar entidade de Serviço
via API GLPI
Parent = Cliente] --> I3[Usar entidade criada] --> Z
+
+ IB -->|Não| I4[Criar entidade Cliente
via API GLPI
Parent = Contratos Ativos] --> I5[Criar entidade de Serviço
via API GLPI
Parent = Cliente] --> I6[Usar entidade criada] --> Z
+
+ %% ========== RAMO NÃO IMPLANTAÇÃO (RESOLVE ONLY) ==========
+ C -->|Não| NA{Existe entidade
por Serviço?}
+ NA -->|Sim| N1[Usar entidade de Serviço] --> Z
+
+ NA -->|Não| NB{Existe entidade
por Cliente?}
+ NB -->|Sim| N2[Usar entidade de Cliente] --> Z
+
+ NB -->|Não| N3[Usar entidade fallback
Contratos Ativos] --> Z
+```
\ No newline at end of file
diff --git a/src/infra/db/repositories/hubglpi/watchdog.repository.js b/src/infra/db/repositories/hubglpi/watchdog.repository.js
index 700ad30..145afad 100644
--- a/src/infra/db/repositories/hubglpi/watchdog.repository.js
+++ b/src/infra/db/repositories/hubglpi/watchdog.repository.js
@@ -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 já foi enviada com sucesso
+ * Verifica se a notificaÇõÇœo jÇ 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
};
diff --git a/src/modules/tickets/useCases/syncTickets.usecase.js b/src/modules/tickets/useCases/syncTickets.usecase.js
index ba71824..f59b713 100644
--- a/src/modules/tickets/useCases/syncTickets.usecase.js
+++ b/src/modules/tickets/useCases/syncTickets.usecase.js
@@ -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')
diff --git a/src/modules/watchdog/job/job.js b/src/modules/watchdog/job/job.js
index 92d95f9..6dbc7e5 100644
--- a/src/modules/watchdog/job/job.js
+++ b/src/modules/watchdog/job/job.js
@@ -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`)
}
@@ -64,4 +66,4 @@ runWatchdog().catch((error) => {
logError('[WATCHDOG] [JOB] Erro ao executar o job do Watchdog', error)
})
-module.exports = { runWatchdog}
\ No newline at end of file
+module.exports = { runWatchdog}
diff --git a/src/modules/watchdog/repository/watchdog.repository.js b/src/modules/watchdog/repository/watchdog.repository.js
index 7a78998..b26f78e 100644
--- a/src/modules/watchdog/repository/watchdog.repository.js
+++ b/src/modules/watchdog/repository/watchdog.repository.js
@@ -35,8 +35,8 @@ async function checkTicketInHubGlpi(hubsoftTicketId) {
/**
* Verifica se a notificação de encerramento já 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 = {