feat: complete reservation lifecycle

This commit is contained in:
2026-08-04 16:30:11 +01:00
parent 077015f17a
commit e667d05bc1
24 changed files with 1096 additions and 46 deletions

View File

@@ -48,6 +48,10 @@ A defesa ocorre em duas camadas:
Uma criação ou aprovação deve bloquear as linhas dos ativos, voltar a validar dentro da transação e tolerar corrida através da restrição da base de dados.
`AvailabilityService::availableAssets()` aplica as mesmas condições em SQL para alimentar catálogo e formulário sem carregar todo o inventário em memória. O período consultado é propagado até à criação da requisição; a validação transacional continua a ser a garantia aplicacional final.
Pré-reservas ficam bloqueantes em `held` enquanto aguardam decisão. `ReservationService` centraliza criação, cancelamento e expiração: todas as transições bloqueiam a requisição, atualizam os itens bloqueantes e criam um `AuditLog`. Um job único procura prazos vencidos a cada cinco minutos. A restrição GiST continua a proteger contra corridas entre estes ciclos de vida.
## Fluxo de dados Snipe-IT
1. Um job consulta a API paginada com token de leitura.

View File

@@ -2,7 +2,7 @@
## ADR-001 — Monólito modular Laravel/Inertia
**Data:** 2026-08-04
**Data:** 2026-08-04
**Estado:** aceite
Usar Laravel 13 com React/TypeScript através de Inertia no mesmo projeto. Mantém autenticação, autorização, filas e transações numa única unidade operacional, sem impedir separação por domínio.
@@ -48,3 +48,10 @@ Textos funcionais são escritos em português europeu. Nomes de código, eventos
**Estado:** aceite
Consumir `/api/v1/hardware` por `offset`/`limit`, com páginas configuráveis até ao máximo oficial de 500 registos. Cada ativo visto recebe o ID da execução em `last_seen_sync_run_id`; a desativação de ausentes só ocorre depois de todas as páginas terminarem com sucesso. Upserts de páginas já válidas podem permanecer após uma falha, permitindo convergência na execução seguinte sem manter uma transação aberta durante chamadas HTTP. Repetir apenas falhas de ligação, HTTP 429 e HTTP 5xx. Usar lock distribuído, job único e token exclusivo de leitura.
## ADR-008 — Pré-reserva bloqueante com expiração auditada
**Data:** 2026-08-04
**Estado:** aceite
Uma requisição submetida cria itens em `held` e bloqueia o período durante 48 horas, até decisão, cancelamento ou expiração. O próprio requisitante pode cancelar apenas enquanto a máquina de estados admite a transição. Um job único executado a cada cinco minutos expira pedidos `pending_approval` ou `awaiting_information` com prazo vencido. Cancelamento e expiração usam bloqueio transacional da requisição, libertam todos os itens bloqueantes e produzem eventos de auditoria; intervalos que se tocam continuam permitidos pela semântica `[início, fim)`.

View File

@@ -1,7 +1,7 @@
# Estado do projeto e handoff
**Atualizado em:** 4 de agosto de 2026
**Fase:** Fases 1 e 2 concluídas; primeiro fluxo vertical da Fase 3 concluído
**Fase:** Fases 1, 2 e 3 concluídas
**Último agente:** Codex
## Resultado disponível
@@ -21,26 +21,31 @@
- Cliente Snipe-IT autenticado com paginação, timeout, retry seletivo e validação de respostas.
- Sincronização convergente de ativos com histórico, hash de conteúdo, lock, job único e desativação apenas após execução completa.
- Comando `snipe:sync-assets`, execução horária e painel de diagnóstico exclusivo para administradores.
- Pesquisa do catálogo por início/fim que exclui atribuições atuais, reservas bloqueantes e bloqueios administrativos.
- Propagação do período consultado do catálogo para o formulário e consulta atualizável de equipamentos livres.
- Cancelamento pelo próprio requisitante, com validação de estado, libertação transacional dos itens e auditoria.
- Expiração de pré-reservas vencidas através do comando `reservations:expire` e job único agendado a cada cinco minutos.
- Calendário mensal pessoal, isolado por utilizador e com navegação entre meses.
## Limites conhecidos
- A integração está implementada e testada contra respostas simuladas; falta configurar e validar URL, versão e token da instância institucional. Até lá, o catálogo utiliza dados fictícios do seeder.
- Microsoft Entra ID ainda não está implementado; as variáveis de ambiente já estão definidas.
- Aprovação, rejeição, entrega e devolução têm tabelas preparadas, mas ainda não possuem ações ou interface.
- As notificações e jobs de expiração ainda não foram implementados.
- As notificações ainda não foram implementadas.
- As políticas de autorização finas ainda não foram ligadas às rotas; nesta fase todas as rotas funcionais exigem utilizador autenticado, verificado e ativo.
- A disponibilidade apresentada no catálogo ainda não recebe um período; a validação real ocorre ao submeter.
- O calendário atual é a vista mensal pessoal do MVP; vistas diária, semanal, global e por equipamento/categoria/localização ficam para as áreas operacional e administrativa.
## Próxima tarefa recomendada
Completar a **Fase 3 — motor de reservas**:
Implementar a **Fase 4 — aprovações**:
1. abrir `app/Domain/Reservations/Services/AvailabilityService.php` e `ReservationService.php`;
2. acrescentar pesquisa do catálogo por período e apresentar disponibilidade real;
3. implementar cancelamento pelo requisitante com transição explícita e auditoria;
4. criar expiração automática de pré-reservas pendentes e libertação dos itens;
5. adicionar uma vista de calendário pessoal;
6. testar limites temporais, cancelamento, expiração e autorização;
1. criar `app/Domain/Approvals/Services/ApprovalService.php` e policies para o perfil aprovador;
2. criar fila e detalhe de requisições pendentes com filtros por data, categoria e requisitante;
3. implementar aprovação com nova validação transacional de disponibilidade e promoção dos itens para `reserved`;
4. implementar rejeição e pedido de esclarecimentos, com justificação e registo em `approval_actions`/auditoria;
5. permitir substituição explícita por equipamento equivalente sem contornar a deteção de conflitos;
6. emitir notificações assíncronas ao requisitante e ao aprovador;
7. atualizar este documento e o changelog.
## Testes da última sessão
@@ -48,19 +53,20 @@ Completar a **Fase 3 — motor de reservas**:
Validação concluída em 4 de agosto de 2026:
- `docker compose exec -T app composer test`: passou;
- Laravel Pint: 97 ficheiros sem desvios;
- PHPStan/Larastan nível 7: 75 ficheiros, zero erros;
- Pest: 53 testes passaram, 201 asserções e 2 testes de registo ignorados por a funcionalidade estar deliberadamente desativada;
- Laravel Pint: 102 ficheiros sem desvios;
- PHPStan/Larastan nível 7: 77 ficheiros, zero erros;
- Pest: 60 testes passaram, 270 asserções e 2 testes de registo ignorados por a funcionalidade estar deliberadamente desativada;
- TypeScript (`tsc --noEmit`): passou;
- ESLint: passou;
- Prettier: passou;
- Vite: builds cliente e SSR concluídos, 2305 módulos cliente transformados;
- Vite: builds cliente e SSR concluídos, 2306 módulos cliente e 95 módulos SSR transformados;
- PostgreSQL 17: `migrate:fresh --seed` concluído, incluindo extensão `btree_gist` e restrição de exclusão temporal;
- PostgreSQL 17: migration incremental do marcador `last_seen_sync_run_id` aplicada com sucesso;
- PostgreSQL 17: nenhuma migration pendente; comando de expiração executado e agendamentos horário/a cada cinco minutos confirmados;
- smoke test HTTP: `/login` respondeu com estado 200 através do Nginx;
- Vite em Docker: URL pública, HMR e CORS verificados em `localhost:5173`; corrigida a página em branco causada pela publicação anterior de `0.0.0.0:5173`;
- Inertia SSR: renderização do login validada através do serviço `node`; corrigido o componente de mensagens flash que usava `usePage()` fora do contexto Inertia;
- verificação visual no browser integrado: a inspeção controlada continua impedida por uma política administrativa temporariamente indisponível; o painel administrativo foi validado por teste Inertia, TypeScript e builds cliente/SSR.
- verificação visual no browser integrado: a inspeção controlada de `localhost` continua impedida por uma política administrativa temporariamente indisponível; catálogo, cancelamento e calendário foram validados por testes Inertia, TypeScript e builds cliente/SSR.
## Decisões pendentes do contexto institucional

View File

@@ -80,6 +80,23 @@ docker compose exec app php artisan schedule:list
Administradores podem testar a ligação, iniciar uma execução e consultar as últimas dez sincronizações em `http://localhost:8000/administracao/snipe-it`. Uma execução falhada mantém o último catálogo válido e não desativa registos ausentes. Consultar `sync_runs`, os logs do worker e o painel antes de repetir.
## Ciclo de vida das pré-reservas
As requisições pendentes têm, por defeito, um prazo de decisão de 48 horas. O scheduler coloca a expiração na fila a cada cinco minutos. Para operar ou diagnosticar manualmente:
```bash
# Expirar imediatamente os pedidos vencidos
docker compose exec app php artisan reservations:expire
# Colocar a operação na fila
docker compose exec app php artisan reservations:expire --queue
# Confirmar ambos os agendamentos
docker compose exec app php artisan schedule:list
```
Uma expiração ou cancelamento muda itens `held`, `reserved`, `ready` e `delivered` para `cancelled`, libertando o período. Confirmar os eventos `reservation.expired` ou `reservation.cancelled` em `audit_logs` quando existir uma divergência operacional.
## Produção
Antes de disponibilizar: