Files
reservas/docs/ARCHITECTURE.md

109 lines
9.7 KiB
Markdown

# 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/Administration`: políticas persistentes, modelos de notificação e configuração funcional.
- `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, histórico e concorrência de aprovação.
- `app/Domain/Operations`: entrega, devolução, faltas, atrasos e incidentes.
- `app/Domain/Audit`: logs imutáveis pela interface.
- `app/Integrations/SnipeIt`: cliente HTTP, DTOs, mapper e exceções da API Snipe-IT.
- `app/Integrations/Microsoft`: cliente OIDC de tenant único, descoberta e validação de tokens.
## Identidade institucional
O login Microsoft usa Authorization Code Flow com OIDC e PKCE. O portal descobre `authorization_endpoint`, `token_endpoint`, emissor e `jwks_uri` no documento `.well-known` específico do tenant. O `id_token` só é aceite após validação RS256, expiração, emissor, audiência, tenant e `nonce`; `state` é consumido uma única vez e o fluxo expira ao fim de dez minutos. A rotação de chaves provoca uma atualização imediata do JWKS em cache e uma segunda validação.
A identidade externa é associada explicitamente a uma conta local já autenticada, após confirmação recente da palavra-passe. Uma coincidência de email nunca cria nem associa contas automaticamente. O portal persiste apenas `sub`, tenant, email apresentado e um subconjunto de claims de perfil; códigos e tokens não são guardados. Utilizadores inativos continuam impedidos de entrar. O login local permanece disponível como contingência operacional.
## 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.
`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.
`ApprovalService` bloqueia a requisição e os ativos antes de aprovar ou substituir, revalida a disponibilidade e regista cada passo em `approval_actions` e `audit_logs`. Uma substituição cancela o item original e cria um novo `held`, apenas na mesma categoria. Pedidos e respostas de esclarecimento conservam o histórico e renovam explicitamente o prazo.
Notificações implementam `ShouldQueue`, usam `afterCommit` e seguem dois canais: email e tabela `notifications`. A caixa interna consulta exclusivamente a relação polimórfica do utilizador autenticado, evitando acesso horizontal a notificações de terceiros.
`ReservationPolicy` fornece defaults versionados e sobreposições guardadas em `portal_settings`. Antecedência, duração, limite de itens e prazo são validados no pedido e no serviço de domínio. A margem operacional é materializada em `reservation_items.buffer_starts_at`/`buffer_ends_at`; a restrição GiST usa estes campos, mantendo a proteção contra corridas. Uma alteração de política só afeta novas reservas, preservando o contrato temporal das existentes.
Os relatórios usam consultas de leitura com período máximo de 366 dias. A exportação CSV é transmitida em blocos e neutraliza conteúdos interpretáveis como fórmulas. Auditoria, relatórios e calendário global têm Gates próprios; o perfil `auditor` permanece exclusivamente de leitura.
`NotificationTemplateService` mantém defaults no código e sobreposições em `portal_settings`. Apenas variáveis conhecidas (`{reference}`, `{requester}`, `{starts_at}`, `{ends_at}`, `{comment}`) são substituídas; a alteração de modelos é auditada e nunca inclui credenciais ou configuração SMTP.
## 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`, calcula `content_hash` e marca cada ativo com `last_seen_sync_run_id`.
4. Apenas após todas as páginas serem recebidas com sucesso, ativos cujo marcador não corresponde à execução atual ficam `active = false`; não são apagados para preservar histórico.
5. Antes de aprovação ou entrega, o ativo é validado novamente.
6. Imediatamente antes de cada checkout, o portal faz um `GET /hardware/{id}` com o token de leitura e bloqueia a operação se existir qualquer atribuição remota.
7. Checkout/check-in usam um token operacional separado e guardam apenas a resposta necessária para auditoria.
8. Antes de um check-in, o portal exige um checkout confirmado em `handover_records` para o mesmo requisitante e confirma por `GET` que o ativo continua atribuído a esse utilizador Snipe-IT.
`HandoverService` mantém a ordem explícita “Snipe-IT primeiro, confirmação local depois” para cada equipamento. Uma requisição com vários itens não é apresentada como concluída se apenas parte dos movimentos for aceite: os itens confirmados preservam o seu `HandoverRecord`, os restantes mantêm o estado anterior e a requisição passa a `incident`. As escritas externas não têm retry automático porque um timeout pode ocorrer depois de o Snipe-IT executar a operação; repetir sem reconciliação poderia criar um resultado enganador.
O checkout associa o ativo a um utilizador Snipe-IT. O operador confirma o ID remoto na primeira entrega e o portal guarda-o em `users.snipe_user_id` para utilizações futuras. Um ativo já atribuído nunca recebe um checkout do portal. O check-in repõe o estado e a localização habitual sincronizados do ativo, mas apenas quando o movimento de saída foi confirmado pelo portal e a atribuição remota ainda corresponde ao mesmo destinatário. As respostas externas ficam em `handover_records.snipe_response`; tokens nunca são persistidos.
A instância Snipe-IT ligada ao projeto é de produção e é tratada como inventário imutável. O portal não cria, edita nem elimina ativos, modelos, produtos ou outra informação mestre. Os únicos pedidos de escrita autorizados no cliente são os endpoints estritos de checkout e check-in, invocados por uma ação operacional legítima. Diagnósticos, sincronizações e validações técnicas usam exclusivamente `GET`.
Uma falha intermédia pode deixar upserts válidos das páginas anteriores, mas nunca executa a desativação global. A sincronização seguinte converge o cache. Um lock distribuído e um job único impedem duas sincronizações simultâneas.
Referências oficiais usadas para o contrato: [autenticação da API](https://snipe-it.readme.io/reference/authenticating-with-the-api), [listagem de hardware](https://snipe-it.readme.io/reference/hardware-list), [limites/paginação](https://snipe-it.readme.io/reference/api-throttling), [checkout de hardware](https://snipe-it.readme.io/reference/hardware-checkout), [check-in de hardware](https://snipe-it.readme.io/reference/hardware-checkin) e [formato de erros da API](https://snipe-it.readme.io/reference/api-overview).
## 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.
## Saúde e recuperação
`/health/live` confirma que o processo HTTP responde. `/health/ready` confirma acesso à base de dados e um ciclo de escrita/leitura no cache, devolvendo apenas o estado global e nunca detalhes de infraestrutura. O health check do Nginx usa readiness.
Os scripts em `ops/` produzem um `pg_dump` custom com permissões restritas, validam a estrutura e permitem restaurar para uma base temporária descartável. Encriptação, retenção, cópia externa e agendamento pertencem à plataforma de produção e são acompanhados pelo checklist operacional.