HOTFIX: Corrgidigo bug em prod

This commit is contained in:
Rafael Lopes 2026-03-10 12:53:33 -03:00
parent a7f9f7f714
commit 50d5b77261
2 changed files with 202 additions and 12 deletions

165
docs/RODAMP.MD Normal file
View File

@ -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.

View File

@ -7,6 +7,31 @@ const { closeTicketInServiceNow, updateStatusInServiceNow, addWorkNoteToServiceN
const { addServiceNowClosureNoteToGlpi } = require('../services/glpiTicketService'); const { addServiceNowClosureNoteToGlpi } = require('../services/glpiTicketService');
const outOfScopeSolutionTypeId = Number(process.env.GLPI_OOS_SOLUTION_TYPE_ID || 27); 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 () => { const processStatusAndClosureController = async () => {
logInfo('Iniciando processamento de status e fechamento de tickets...'); logInfo('Iniciando processamento de status e fechamento de tickets...');
@ -61,14 +86,9 @@ const processStatusAndClosureController = async () => {
} }
// Regra de precedencia de finalizacao: // Regra de precedencia de finalizacao:
// quando GLPI ja esta finalizado e SN ainda nao, prioriza GLPI->SN // quando GLPI ja esta finalizado e SN ainda nao, sempre prioriza GLPI->SN
// apenas se o bastao ainda estiver com GLPI. // para evitar reabertura indevida no GLPI.
// Se o bastao estiver com SNOW, permitimos reabertura SN->GLPI. if (glpiIsFinalState && !snIsFinalState) {
if (
glpiIsFinalState &&
!snIsFinalState &&
(!glpiWasFinalLocally || ticket.source_last === 'GLPI')
) {
effectiveSourceLast = 'GLPI'; effectiveSourceLast = 'GLPI';
} }
@ -113,8 +133,8 @@ const processStatusAndClosureController = async () => {
const glpiCurrentStatus = glpiLiveStatus; // Reutiliza o status que já buscamos const glpiCurrentStatus = glpiLiveStatus; // Reutiliza o status que já buscamos
if (glpiCurrentStatus !== null) { if (glpiCurrentStatus !== null) {
if (glpiCurrentStatus === 5) { // GLPI foi para 'Solucionado' if (glpiCurrentStatus === 5) { // GLPI foi para 'Solucionado'
// Apenas processa se o SN não estiver já resolvido/fechado // Apenas processa se o SN nao estiver resolvido/fechado de fato
if (ticket.sn_sync_status !== 'solved' && ticket.sn_sync_status !== 'closed') { if (!snIsFinalState) {
logInfo(`GLPI ${ticket.glpi_ticket_id} foi Solucionado. Sincronizando para SN...`); logInfo(`GLPI ${ticket.glpi_ticket_id} foi Solucionado. Sincronizando para SN...`);
const solution = await TicketGlpiModel.getTicketSolution(ticket.glpi_ticket_id); const solution = await TicketGlpiModel.getTicketSolution(ticket.glpi_ticket_id);
@ -136,8 +156,13 @@ const processStatusAndClosureController = async () => {
// FLUXO NORMAL DE RESOLUÇÃO // FLUXO NORMAL DE RESOLUÇÃO
const closeNotes = solution?.content ? stripHTML(solution.content) : 'Resolvido via integração GLPI.'; const closeNotes = solution?.content ? stripHTML(solution.content) : 'Resolvido via integração GLPI.';
const resolvedAt = solution?.date_mod || new Date(); const resolvedAt = solution?.date_mod || new Date();
await closeTicketInServiceNow(ticket.sn_ticket_id, closeNotes, resolvedAt); const closeResult = await attemptCloseInServiceNowWithRetry(ticketSnDetails, closeNotes, resolvedAt);
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'solved', 'solved'); 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) { } else if (glpiCurrentStatus === 6) {