From 50d5b77261a9e589660712b3992c9c268f90992c Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 10 Mar 2026 12:53:33 -0300 Subject: [PATCH] HOTFIX: Corrgidigo bug em prod --- docs/RODAMP.MD | 165 +++++++++++++++++++++ src/controllers/processStatusController.js | 49 ++++-- 2 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 docs/RODAMP.MD diff --git a/docs/RODAMP.MD b/docs/RODAMP.MD new file mode 100644 index 0000000..2d36af8 --- /dev/null +++ b/docs/RODAMP.MD @@ -0,0 +1,165 @@ +# Roadmap de Evolucao - SN <-> GLPI + +## Visao Geral +Objetivo: elevar confiabilidade, observabilidade e arquitetura da integracao sem interromper operacao. + +Principios: +- migracao incremental (sem Big Bang) +- manter compatibilidade com fluxo atual +- cada fase precisa ter criterio claro de aceite + +## Fases (ordem recomendada) + +### Fase 0 - Baseline e seguranca operacional (1 semana) +Objetivo: criar base para evolucao segura. + +Entregas: +- checklist oficial de deploy/rollback em producao +- backup e restore testado para banco intermediario +- playbook de incidentes (falha SN, falha GLPI, falha DB) +- padrao de versionamento e convencao de commits + +Criterios de aceite: +- rollback executado com sucesso em ambiente de teste +- documentacao revisada e aprovada + +Risco: baixo + +--- + +### Fase 1 - Logs estruturados e rastreabilidade (1-2 semanas) +Objetivo: saber exatamente onde e por que falhou. + +Entregas: +- padrao de log com contexto por camada e direcao: + - `[USECASE][GLPI>SN]` + - `[USECASE][SN>GLPI]` + - `[SERVICE]`, `[REPOSITORY]`, `[INTEGRATION]` +- `correlation_id` por ciclo e por ticket +- campos padrao nos logs: `ticket_number`, `sn_ticket_id`, `glpi_ticket_id`, `source_last`, `step` +- metricas minimas por ciclo: processados, sucesso, erro, duracao + +Criterios de aceite: +- conseguir rastrear 1 ticket de ponta a ponta por `correlation_id` +- reduzir tempo de diagnostico de erro em pelo menos 50% + +Risco: baixo + +--- + +### Fase 2 - Estabilizacao final de fluxos (1 semana) +Objetivo: fechar lacunas de negocio antes de refatoracao pesada. + +Entregas: +- bateria manual oficial para `incidente` e `requisicao` +- validacao de regras finais: + - resolucao GLPI -> SN + - reabertura SN -> GLPI + - fechamento permanente GLPI nao reabre + - fora de escopo nao reabre +- hardening de idempotencia em comentarios/tarefas + +Criterios de aceite: +- 100% do checklist de fluxos principais aprovado em DEV + +Risco: baixo + +--- + +### Fase 3 - Modularizacao progressiva (monolito modular, estilo clean) (3-5 semanas) +Objetivo: organizar o sistema em modulos com fronteiras claras. + +Entregas: +- estrutura em camadas: + - `domain` (regras e entidades) + - `application` (casos de uso) + - `infrastructure` (DB, APIs, logger) + - `interfaces` (cron/controllers) +- definicao de portas/adapters para SN, GLPI e repositorios +- migracao por vertical (ticket -> comment -> status) + +Criterios de aceite: +- novos casos de uso entram sem acoplar controller direto a SQL/API +- arquitetura documentada em diagrama simples + +Risco: medio + +--- + +### Fase 4 - GLPI via API (2-4 semanas) +Objetivo: reduzir dependencia de escrita direta no banco GLPI. + +Entregas: +- cliente GLPI API para: + - criar ticket + - inserir comentario + - inserir tarefa + - atualizar status +- fallback controlado para SQL apenas enquanto necessario +- comparativo de comportamento API x SQL em DEV + +Criterios de aceite: +- ao menos 1 fluxo completo operando via API com sucesso +- sem regressao de regras de negocio + +Risco: medio + +--- + +### Fase 5 - Sincronizacao de imagens/anexos (3-6 semanas) +Objetivo: suportar anexos dos dois lados. + +Entregas: +- modelagem de rastreio de anexos (source_id, destiny_id, hash, status) +- pipeline SN -> GLPI e GLPI -> SN: + - listar anexos + - baixar binario + - validar tipo/tamanho + - upload no destino +- deduplicacao por hash +- retries com backoff + +Criterios de aceite: +- anexos de teste sobem nos dois sentidos com rastreabilidade +- sem duplicacao de anexos no reprocessamento + +Risco: alto + +--- + +### Fase 6 - Qualidade e teste automatizado (2-4 semanas, em paralelo) +Objetivo: diminuir risco de regressao. + +Entregas: +- testes unitarios de mapeamento/status/regras +- testes de integracao com mocks de SN e GLPI +- smoke test automatizado por ciclo + +Criterios de aceite: +- cobertura minima em regras criticas +- pipeline bloqueando merge em caso de regressao + +Risco: medio + +## Backlog Tecnico (prioridade) +P1: +- logs estruturados + correlation_id +- checklist oficial de deploy/rollback +- bateria de testes de `requisicao` + +P2: +- modularizacao por casos de uso +- GLPI API para comentarios/tarefas + +P3: +- anexos/imagens bidirecional +- observabilidade avancada (dashboards/alertas) + +## Definicao de Pronto (DoD) por fase +- codigo revisado +- documentacao atualizada +- evidencias de teste anexadas +- plano de rollback definido + +## Observacao +Este roadmap prioriza estabilidade de operacao antes de expansao funcional. A transicao para arquitetura modular deve ser gradual e guiada por casos de uso reais. diff --git a/src/controllers/processStatusController.js b/src/controllers/processStatusController.js index 7141b63..419ec1e 100644 --- a/src/controllers/processStatusController.js +++ b/src/controllers/processStatusController.js @@ -7,6 +7,31 @@ const { closeTicketInServiceNow, updateStatusInServiceNow, addWorkNoteToServiceN const { addServiceNowClosureNoteToGlpi } = require('../services/glpiTicketService'); const outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27); +const snCloseRetryMax = Number(process.env.SN_CLOSE_RETRY_MAX || 2); +const snCloseRetryDelayMs = Number(process.env.SN_CLOSE_RETRY_DELAY_MS || 1500); + +const waitMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const attemptCloseInServiceNowWithRetry = async (ticketSnDetails, closeNotes, resolvedAt) => { + for (let attempt = 1; attempt <= snCloseRetryMax + 1; attempt += 1) { + const closeOk = await closeTicketInServiceNow(ticketSnDetails.id, closeNotes, resolvedAt); + if (closeOk) { + const liveStatus = await fetchTicketLiveStatusFromServiceNow(ticketSnDetails.sys_id, ticketSnDetails.tipo); + if (liveStatus === 'Resolvido' || liveStatus === 'Encerrado' || liveStatus === 'Encerrado - Omitido') { + return { ok: true, liveStatus }; + } + logWarning(`Fechamento SN tentou atualizar, mas status ao vivo ficou '${liveStatus}'. Tentando novamente.`); + } else { + logWarning(`Tentativa ${attempt} de fechamento no SN falhou para ${ticketSnDetails.ticket_number}.`); + } + + if (attempt <= snCloseRetryMax) { + await waitMs(snCloseRetryDelayMs); + } + } + + return { ok: false, liveStatus: null }; +}; const processStatusAndClosureController = async () => { logInfo('Iniciando processamento de status e fechamento de tickets...'); @@ -61,14 +86,9 @@ const processStatusAndClosureController = async () => { } // Regra de precedencia de finalizacao: - // quando GLPI ja esta finalizado e SN ainda nao, prioriza GLPI->SN - // apenas se o bastao ainda estiver com GLPI. - // Se o bastao estiver com SNOW, permitimos reabertura SN->GLPI. - if ( - glpiIsFinalState && - !snIsFinalState && - (!glpiWasFinalLocally || ticket.source_last === 'GLPI') - ) { + // quando GLPI ja esta finalizado e SN ainda nao, sempre prioriza GLPI->SN + // para evitar reabertura indevida no GLPI. + if (glpiIsFinalState && !snIsFinalState) { effectiveSourceLast = 'GLPI'; } @@ -113,8 +133,8 @@ const processStatusAndClosureController = async () => { const glpiCurrentStatus = glpiLiveStatus; // Reutiliza o status que já buscamos if (glpiCurrentStatus !== null) { if (glpiCurrentStatus === 5) { // GLPI foi para 'Solucionado' - // Apenas processa se o SN não estiver já resolvido/fechado - if (ticket.sn_sync_status !== 'solved' && ticket.sn_sync_status !== 'closed') { + // Apenas processa se o SN nao estiver resolvido/fechado de fato + if (!snIsFinalState) { logInfo(`GLPI ${ticket.glpi_ticket_id} foi Solucionado. Sincronizando para SN...`); const solution = await TicketGlpiModel.getTicketSolution(ticket.glpi_ticket_id); @@ -136,8 +156,13 @@ const processStatusAndClosureController = async () => { // FLUXO NORMAL DE RESOLUÇÃO const closeNotes = solution?.content ? stripHTML(solution.content) : 'Resolvido via integração 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'); + const closeResult = await attemptCloseInServiceNowWithRetry(ticketSnDetails, closeNotes, resolvedAt); + if (closeResult.ok) { + await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'solved', 'solved', 'GLPI'); + } else { + await TicketSyncModel.updateSourceLast(ticket.sn_ticket_id, 'GLPI'); + logWarning(`Falha ao confirmar fechamento no SN para ${ticketSnDetails.ticket_number}. Mantendo ticket para nova tentativa.`); + } } } } else if (glpiCurrentStatus === 6) {