Files
reservas/docs/ARCHITECTURE.md

6.0 KiB

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.

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

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.

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. Checkout/check-in usam um token operacional separado e guardam apenas a resposta necessária para auditoria.

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. O check-in repõe o estado e a localização habitual sincronizados do ativo. As respostas externas ficam em handover_records.snipe_response; tokens nunca são persistidos.

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, listagem de hardware, limites/paginação, checkout de hardware, check-in de hardware e formato de erros da API.

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.