feat: establish reservation portal foundation

This commit is contained in:
2026-08-04 15:24:08 +01:00
commit c96264ae96
226 changed files with 32232 additions and 0 deletions

68
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,68 @@
# Arquitetura
## Contexto
O portal é uma aplicação independente e complementar ao Snipe-IT. Usa um monólito modular para reduzir custo operacional, mas separa os domínios internamente para evitar acoplamento entre inventário, reservas, aprovações e operações.
```text
Browser
│ HTTPS
▼
Nginx ──► Laravel + Inertia/React
├── PostgreSQL (estado do portal)
├── Redis (cache, filas, locks)
├── SMTP/Mailpit
└── Snipe-IT REST API
```
## Responsabilidade dos sistemas
| Informação/operação | Sistema oficial |
|---|---|
| Etiqueta, série, modelo, categoria e localização habitual | Snipe-IT |
| Estado operacional e atribuição física atual | Snipe-IT |
| Checkout e check-in efetivos | Snipe-IT via API |
| Pedidos futuros e calendário | Portal |
| Aprovação, rejeição, cancelamento e conflitos | Portal |
| Entrega/devolução e evidência operacional | Portal, sincronizada com Snipe-IT |
| Auditoria do fluxo de requisição | Portal |
## Módulos
- `app/Domain/Identity`: perfis, permissões e identidades externas.
- `app/Domain/Inventory`: cache local de ativos e consultas do catálogo.
- `app/Domain/Reservations`: disponibilidade, estados, regras e requisições.
- `app/Domain/Approvals`: decisões e concorrência de aprovação (a criar).
- `app/Domain/Handovers`: entrega, devolução e incidentes (a criar).
- `app/Domain/Audit`: logs imutáveis pela interface.
- `app/Integrations/SnipeIt`: único ponto de comunicação com Snipe-IT (a criar).
## Consistência temporal
Os períodos usam `[starts_at, ends_at)`: uma reserva que termina às 11:00 não entra em conflito com outra que começa às 11:00.
A defesa ocorre em duas camadas:
1. `AvailabilityService` produz erros de negócio legíveis;
2. PostgreSQL mantém a garantia final com uma restrição `EXCLUDE USING gist` nos estados bloqueantes.
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.
## Fluxo de dados Snipe-IT
1. Um job consulta a API paginada com token de leitura.
2. Um mapper reduz a resposta aos campos documentados.
3. `AssetSyncService` faz upsert por `snipe_asset_id` e calcula `content_hash`.
4. Ativos ausentes numa sincronização completa ficam `active = false`; não são apagados para preservar histórico.
5. Antes de aprovação ou entrega, o ativo é validado novamente.
6. Checkout/check-in usam um token operacional separado e guardam apenas a resposta necessária para auditoria.
## Segurança
- Sem registo público.
- Cookies seguros e HTTPS em produção.
- Policies em cada ação de domínio.
- Tokens e segredos apenas no ambiente.
- Dois tokens Snipe-IT com menor privilégio.
- MFA obrigatório a definir para perfis privilegiados antes do piloto.
- Logs de auditoria não expostos a operações de update/delete.

43
docs/DECISIONS.md Normal file
View File

@@ -0,0 +1,43 @@
# Registo de decisões técnicas
## ADR-001 — Monólito modular Laravel/Inertia
**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.
## ADR-002 — PostgreSQL como garantia de conflitos
**Data:** 2026-08-04
**Estado:** aceite
Usar PostgreSQL e períodos semiabertos `[início, fim)`. A lógica aplicacional melhora a mensagem ao utilizador; uma restrição de exclusão GiST garante que concorrência ou um erro aplicacional não cria sobreposição bloqueante.
## ADR-003 — Cache local, sem replicação total do Snipe-IT
**Data:** 2026-08-04
**Estado:** aceite
Guardar apenas os campos de ativos necessários ao catálogo e reservas. A sincronização desativa registos ausentes em vez de os apagar. A base de dados do Snipe-IT nunca é acedida diretamente.
## ADR-004 — Registo público desativado
**Data:** 2026-08-04
**Estado:** aceite
Contas são criadas por administrador, convite ou identidade institucional verificada. Mantêm-se recuperação de palavra-passe, verificação de email, 2FA e passkeys do starter kit.
## ADR-005 — Ambiente reproduzível em Docker Compose
**Data:** 2026-08-04
**Estado:** aceite
Desenvolvimento e primeira instalação utilizam serviços separados para Nginx, PHP-FPM, PostgreSQL, Redis, Vite, filas, scheduler e Mailpit. Produção deve usar imagens imutáveis, HTTPS e gestão externa de segredos, mantendo a mesma topologia lógica.
## ADR-006 — Português de Portugal na interface
**Data:** 2026-08-04
**Estado:** aceite
Textos funcionais são escritos em português europeu. Nomes de código, eventos e tabelas permanecem em inglês para consistência técnica.

69
docs/PROJECT_STATUS.md Normal file
View File

@@ -0,0 +1,69 @@
# Estado do projeto e handoff
**Atualizado em:** 4 de agosto de 2026
**Fase:** Fase 1 concluída; primeiro fluxo vertical da Fase 3 concluído
**Último agente:** Codex
## Resultado disponível
- Projeto oficial Laravel 13 React Starter Kit criado.
- Autenticação local via Fortify, recuperação de palavra-passe, verificação de email, 2FA e passkeys.
- Registo público desativado conforme o plano.
- Estrutura relacional de utilizadores, identidades externas, perfis e permissões.
- Cache local de ativos Snipe-IT.
- Modelo completo inicial para reservas, itens, aprovações, bloqueios, entrega/devolução, sincronizações e auditoria.
- Proteção PostgreSQL contra períodos sobrepostos para o mesmo equipamento através de `EXCLUDE USING gist` e intervalos `[início, fim)`.
- Serviço de disponibilidade que considera estado local, atribuição, reservas e bloqueios.
- Criação transacional de uma requisição pendente com vários equipamentos e registo de auditoria.
- Dashboard pessoal, catálogo com pesquisa/filtros, formulário de nova requisição e histórico.
- Docker Compose com Nginx, PHP-FPM, PostgreSQL, Redis, Node/Vite, filas, scheduler e Mailpit.
- Dados fictícios e duas contas locais para desenvolvimento.
## Limites conhecidos
- A API real do Snipe-IT ainda não está implementada. 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 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.
## Próxima tarefa recomendada
Implementar a **Fase 2 — cliente e sincronização Snipe-IT**:
1. abrir `docs/ARCHITECTURE.md` e `config/services.php`;
2. criar `app/Integrations/SnipeIt/SnipeItClient.php`, DTOs, mapper e exceções;
3. implementar paginação, timeout, retry limitado e token de leitura;
4. criar `AssetSyncService`, job e comando `snipe:sync-assets`;
5. testar com `Http::fake()` paginação, falhas e desativação de ativos ausentes;
6. criar página administrativa de diagnóstico apenas para administradores;
7. atualizar este documento e o changelog.
## Testes da última sessão
Validação concluída em 4 de agosto de 2026:
- `docker compose exec -T app composer test`: passou;
- Laravel Pint: 78 ficheiros sem desvios;
- PHPStan/Larastan nível 7: 59 ficheiros, zero erros;
- Pest: 44 testes passaram, 157 asserções e 2 testes de registo ignorados por a funcionalidade estar deliberadamente desativada;
- TypeScript (`tsc --noEmit`): passou;
- ESLint: passou;
- Prettier: passou;
- Vite: build de produção concluído, 2304 módulos transformados;
- PostgreSQL 17: `migrate:fresh --seed` concluído, incluindo extensão `btree_gist` e restrição de exclusão temporal;
- smoke test HTTP: `/login` respondeu com estado 200 através do Nginx;
- verificação visual no browser integrado: não executada devido a uma política administrativa temporariamente indisponível no browser desta sessão; não foi detetado erro da aplicação.
## Decisões pendentes do contexto institucional
Estas decisões não bloqueiam o desenvolvimento com defaults seguros, mas devem ser confirmadas antes do piloto:
- versão e URL da instância Snipe-IT;
- categorias e estados requisitáveis;
- tenant, client ID e redirect URI do Entra ID;
- matriz final de aprovadores por categoria/departamento;
- duração máxima, antecedência e margens por categoria;
- domínio institucional e método de emissão TLS;
- SMTP institucional.

44
docs/REQUIREMENTS.md Normal file
View File

@@ -0,0 +1,44 @@
# Requisitos e âmbito
Este documento resume o plano funcional fornecido em 4 de agosto de 2026. Alterações de âmbito devem ser registadas como decisão.
## Fluxo obrigatório do MVP
```text
Consultar → Requisitar → Aprovar → Reservar → Entregar → Devolver → Concluir
```
## Funcionalidades do MVP
1. Login local e Microsoft Entra ID.
2. Perfis: requisitante, aprovador, operador, administrador e acesso de auditoria.
3. Catálogo apenas com ativos requisitáveis do Snipe-IT.
4. Pesquisa por nome, categoria, modelo, localização, etiqueta, estado e período.
5. Requisição de um ou vários equipamentos com início, fim, finalidade, local e observações.
6. Pré-reserva pendente com prazo de aprovação.
7. Aprovação, rejeição, esclarecimentos e substituição por equivalente.
8. Prevenção de conflitos e margens configuráveis.
9. Entrega manual com checkout Snipe-IT.
10. Devolução manual com check-in, estado, acessórios e incidentes.
11. Notificações por email e internas.
12. Bloqueios de manutenção, histórico e auditoria.
13. Administração de utilizadores, regras e integrações.
14. Interface responsiva para computador, tablet e telemóvel.
## Regras invariantes
- Uma requisição não se torna reserva sem aprovação.
- Nenhum equipamento pode ter duas reservas bloqueantes sobrepostas.
- O checkout nunca é executado automaticamente por ter chegado a hora.
- Falhas Snipe-IT nunca produzem sucesso silencioso.
- O último cache válido pode manter o catálogo, mas operações críticas exigem validação recente.
- Ativos históricos não são apagados por deixarem de ser requisitáveis.
- Todas as decisões e operações físicas ficam auditadas.
## Fora do primeiro MVP
Reservas recorrentes, kits, reserva por modelo, Teams, calendário Microsoft 365, assinatura digital, fotografias, PWA, push, lista de espera, aprovações multinível e previsão de procura.
## Critérios de aceitação
O MVP só está pronto quando todos os fluxos acima possuem testes automatizados, controlo de acesso, tratamento explícito de falhas, documentação técnica/utilizador, backups testados e validação responsiva.

49
docs/RUNBOOK.md Normal file
View File

@@ -0,0 +1,49 @@
# Runbook de desenvolvimento e operação
## Desenvolvimento local
1. Copiar `.env.example` para `.env`.
2. Executar `docker compose build`.
3. Gerar a chave com `docker compose run --rm app php artisan key:generate`.
4. Iniciar com `docker compose up -d`.
5. Preparar dados com `docker compose exec app php artisan migrate --seed`.
6. Abrir o portal em `http://localhost:8000` e emails em `http://localhost:8025`.
## Diagnóstico
```bash
docker compose ps
docker compose logs --tail=100 app
docker compose logs --tail=100 queue
docker compose exec app php artisan about
docker compose exec app php artisan migrate:status
docker compose exec postgres pg_isready -U reserva -d reserva
docker compose exec redis redis-cli ping
```
## Atualização de código
```bash
docker compose exec app composer install
docker compose exec node npm install
docker compose exec app php artisan migrate --force
docker compose exec app php artisan optimize:clear
docker compose restart queue scheduler
```
## Produção
Antes de disponibilizar:
- definir `APP_ENV=production`, `APP_DEBUG=false` e HTTPS;
- substituir todas as palavras-passe do exemplo;
- montar segredos fora do repositório;
- remover exposição pública das portas PostgreSQL, Vite e Mailpit;
- configurar volumes persistentes e backups cifrados;
- executar `php artisan optimize` e frontend compilado;
- definir health checks, alertas e rotação de logs;
- testar recuperação de backup e falha Snipe-IT.
## Backups
Salvaguardar diariamente PostgreSQL e uploads. Manter retenção semanal/mensal e uma cópia fora do servidor. Segredos são salvaguardados através de procedimento separado. Executar um teste de restauro periódico e registar o resultado.