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 = {