Merge branch 'feature/snow-worknotes-glpi-task-review'
This commit is contained in:
commit
b2cbeb66e5
165
docs/RODAMP.MD
Normal file
165
docs/RODAMP.MD
Normal 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.
|
||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user