# Auditoria e plano de adoção de GUIDs

Data da auditoria inicial: 2026-09-18. Atualizado em 2026-09-21.

## Progresso da implementação

- Fase 0 implantada: `AccessContext` centraliza usuário, perfil, estabelecimentos permitidos e especialista vinculado.
- Fase 1 concluída no banco local: GUID v4 em `pessoas`, `pessoas_arquivos`, `contratos`, `agendamentos`, `prontuario`, `titulos` e `usuarios`, sem alteração das PKs/FKs numéricas.
- Fase 2 concluída na borda pública de Pacientes, arquivos e Contratos: URLs, formulários e respostas usam GUID, com fallback temporário para ID numérico e resolução para as FKs internas. O número/ID interno do contrato continua preservado e exibido onde faz parte da regra do negócio.
- Fase 3 concluída na borda pública de Agenda e Prontuários: calendário, sessão, histórico, PDF, anexos e formulários usam GUID. As consultas clínicas são resolvidas dentro do estabelecimento ativo e mantêm as restrições do especialista; GUID e ID legado fora do estabelecimento retornam o mesmo resultado de não encontrado.
- Fase 4 concluída para Títulos, baixas, transferências e superfícies de Repasses: URLs, formulários e JSON de pesquisa usam GUID. `titulo_receb` recebeu GUID próprio por migration idempotente, sem alteração de sua PK/FK, e a exclusão de baixa exige o movimento subordinado ao título autorizado. O ID numérico do contrato permanece como número de negócio, conforme requisito.
- Fase 5 concluída para Usuários: editar, salvar, resetar senha e desativar usam GUID. Gestores locais ficam limitados aos estabelecimentos vinculados; o administrador global mantém o acesso existente; a proteção contra auto-desativação foi preservada.
- Testes integrados cobrem isolamento de Agenda, Prontuários, Financeiro e Usuários entre estabelecimentos, compatibilidade numérica temporária, geração UUID v4 e ausência dos IDs prioritários nos formulários/URLs principais.
- Próxima etapa: observabilidade e retirada planejada da compatibilidade numérica. A expansão para cadastros auxiliares (perfis, feriados, serviços, estabelecimentos, salas, bancos, contas, operações, formas de pagamento, integrações e documentos) permanece em ondas posteriores e não autoriza a remoção de nenhuma PK numérica.

## Objetivo e invariantes

- Manter todas as PKs e FKs numéricas existentes.
- Usar UUID v4 textual (`CHAR(36)`) somente como identificador público.
- Resolver GUID para ID internamente, sempre junto do escopo de autorização aplicável.
- Aceitar IDs numéricos antigos apenas durante uma janela explícita de transição.
- Não confundir ocultação de ID com autorização: GUID não substitui filtros de estabelecimento, perfil ou especialista.

## Estado encontrado na auditoria inicial

Na auditoria inicial, o banco não possuía coluna `guid` nas tabelas prioritárias. A Fase 1 já corrigiu esse ponto; as PKs continuam `INT AUTO_INCREMENT`, como desejado, e todos os relacionamentos permanecem numéricos.

| Domínio público | Tabela canônica | Identificador exposto hoje | Observação |
|---|---|---|---|
| Paciente | `pessoas` | `pessoas.id` | A UI e os joins usam `pessoas.id`; `pacientes.id` não é o identificador público do paciente. |
| Contrato | `contratos` | `contratos.id` | Exposto em URL, filtro GET e vários POSTs. |
| Agendamento | `agendamentos` | `agendamentos.id` | Exposto no JSON do calendário, URL AJAX, JS e POSTs. |
| Prontuário | `prontuario` | `prontuario.id` | A navegação principal da sessão usa `agendamentos.id`; o ID do prontuário também vai em POST. |
| Título | `titulos` | `titulos.id` | Exposto em edição, baixa, relatórios, transferências e pagamentos de contrato. |
| Usuário | `usuarios` | `usuarios.id` | Exposto em editar, excluir, resetar senha e formulário. |

Tabelas dependentes identificadas na auditoria: `pessoas_arquivos`, `titulo_receb`, `paciente_api_chaves`. As duas primeiras já foram migradas; `paciente_api_chaves` permanece para uma onda posterior. FKs como `paciente_id`, `contrato_id`, `agendamento_id`, `especialista_id`, `pessoa_id` e `usuario_id` permanecem numéricas.

## Superfícies encontradas

### Pacientes

- Rotas com ID: `/paciente/editar/{id}` e `/paciente/arquivo/{id}`; ações POST usam `id` e `paciente_id` (`Modules/Pacientes/routes.php`).
- A lista gera links para prontuário e edição e envia exclusão com `pessoas.id` (`Modules/Pacientes/View/index.php:132`).
- O formulário envia `id`, `paciente_id` e IDs de arquivos (`Modules/Pacientes/View/formulario.php:35`).
- O controller converte rota/POST diretamente para `int` e redireciona novamente com ID (`Modules/Pacientes/Controller/PacienteController.php:49`).
- `PacienteRepository::buscarPorId()` consulta `pessoas.id`; listagem, pesquisa, resumo e mutações não têm escopo de estabelecimento (`Modules/Pacientes/Repository/PacienteRepository.php:94`).
- O monitor da API cria link de edição por ID numérico (`Modules/Pacientes/View/api-monitor.php:53`). A API de criação não expõe rota de recurso por ID nesta versão, mas sua resposta devolve `paciente_id` numérico e o payload aceita `paciente.id`/`responsavel_id` numéricos (`Modules/Pacientes/Controller/PacienteApiController.php:29`, `Modules/Pacientes/Service/PacienteApiService.php:73`). A versão compatível deve devolver `paciente_guid`; IDs recebidos só permanecem no contrato legado e passam pela mesma autorização da chave de API.

### Contratos

- Rota `/contrato/editar/{id}`; POSTs de salvar, cancelar, agendar e adicionar sessões usam `id`/`contrato_id` (`Modules/Contratos/routes.php`).
- A listagem aceita `contrato_id` numérico no GET e exibe o número na URL (`Modules/Contratos/View/index.php:94`).
- O formulário envia `id`, `contrato_id`, `agenda_id`, `paciente_id` e `especialista_id` (`Modules/Contratos/View/formulario.php:69`).
- Controller e service recebem `int` em toda a cadeia (`Modules/Contratos/Controller/ContratoController.php:57`).
- Consultas-base `listar`, `resumo` e `buscarPorId` não limitam por estabelecimento nem pelo especialista logado (`Modules/Contratos/Repository/ContratoRepository.php:30`). O estabelecimento aparece principalmente na geração financeira. Este é um risco de acesso cruzado preexistente que precisa ser corrigido antes/de forma conjunta à publicação de GUIDs.

### Agendamentos

- Rota `/agenda/dados/{id}` e POSTs `/agenda/salvar` e `/agenda/mover` usam o ID numérico (`Modules/Agenda/routes.php`).
- O calendário mantém `id` oculto, usa `/agenda/dados/<id>` e constrói links de prontuário/contrato por IDs retornados no JSON (`Modules/Agenda/View/index.php:117`, `Modules/Agenda/View/index.php:800`).
- Há proteção específica de especialista em `AgendaService`, inclusive ao mover/editar, que precisa permanecer depois da resolução (`Modules/Agenda/Service/AgendaService.php:185`).
- As consultas de agenda são filtradas por especialista quando o usuário está vinculado, mas o modelo de estabelecimento é indireto; a tabela `agendamentos` não possui `estabelecimento_id`. O escopo deve ser derivado do contrato/títulos/configuração existente ou formalizado antes de afirmar isolamento por estabelecimento.
- A confirmação pública já usa token aleatório próprio; não deve ser substituída pelo GUID do agendamento.

### Prontuários

- Rotas de sessão usam `agendamentos.id`; histórico/PDF usam `pessoas.id` (`Modules/Prontuarios/routes.php`).
- POSTs enviam `prontuario.id`, `agendamento_id`, `paciente_id`, `especialista_id` e ID de arquivo (`Modules/Prontuarios/View/sessao.php:83`).
- O service valida acesso do especialista para sessão, edição e histórico (`Modules/Prontuarios/Service/ProntuarioService.php:41`). Essa validação deve ocorrer depois da resolução escopada do GUID e antes da leitura/mutação.
- `ProntuarioRepository::buscarAgendamento()` busca inicialmente somente por `a.id`; a checagem de especialista fica no service (`Modules/Prontuarios/Repository/ProntuarioRepository.php:109`). Não há filtro explícito por estabelecimento nessa consulta.

### Títulos

- Rotas de editar/baixar título e pagar contrato usam IDs em path; os POSTs usam `titulo_id`, `contrato_id`, `titulo_origem_id`, `titulo_destino_id` e ID de movimento (`Modules/Financeiro/routes.php`).
- IDs aparecem nas telas de títulos, contas a receber/pagar, relatório, baixa, edição, transferência e pagamento de contrato (`Modules/Financeiro/View/titulos.php:126`).
- `FinanceiroRepository::buscarTitulo(int $id)` consulta apenas `t.id`, sem estabelecimento na própria busca (`Modules/Financeiro/Repository/FinanceiroRepository.php:284`). Há filtros de estabelecimento nas listagens e transferências, mas não são uniformes nas operações por ID.
- `FinanceiroService` já calcula estabelecimentos permitidos em alguns relatórios. A regra deve ser centralizada para toda leitura/mutação de título: administrador conforme regra atual; demais usuários somente em `usuario_estabelecimentos` e/ou estabelecimento ativo da sessão.
- `titulo_receb.id` também é público na exclusão de baixa e precisa de GUID na segunda onda ou resolução estritamente subordinada a um título já autorizado.

### Usuários

- Editar, excluir e resetar senha usam ID na URL; salvar usa campo oculto `id` (`Modules/Usuarios/routes.php`, `Modules/Usuarios/View/usuarios/form.php:52`).
- Repository e service trabalham apenas com `int` (`Modules/Usuarios/Repository/UsuarioRepository.php:58`).
- As rotas passam por `AuthMiddleware`/`PermissionGate`; a regra de impedir auto-desativação está no service e deve ser preservada (`Modules/Usuarios/Service/UsuarioAdminService.php:78`).
- A listagem administrativa não é escopada por estabelecimento. Isso pode ser intencional para administradores, mas deve ser formalizado em teste: usuários sem permissão administrativa não podem alcançar as rotas; se administradores locais existirem, limitar aos estabelecimentos vinculados.

## Estratégia técnica proposta

### Identificador público e geração

Adicionar `guid CHAR(36) CHARACTER SET ascii COLLATE ascii_bin` às tabelas canônicas. Começar nullable, preencher, validar e somente então tornar `NOT NULL` e criar `UNIQUE`.

UUIDs novos devem ser gerados na aplicação com `random_bytes(16)`, fixando os bits de versão 4 e variante RFC 4122, por um componente único (por exemplo `Core/Support/Uuid.php`). Não usar `UUID()` do MySQL/MariaDB como gerador v4: em geral ele produz UUID baseado em tempo, não v4.

Para concorrência e caminhos legados de inserção, há duas opções. A recomendada é geração explícita em todos os repositories e um teste de schema/cobertura que impeça INSERT sem GUID. Trigger temporário só deve ser usado se existirem escritores externos que não possam ser atualizados simultaneamente.

### Resolução segura

Não criar um resolvedor global `guid -> id` que ignore o tenant. Cada repository deve oferecer métodos públicos escopados, por exemplo:

- `buscarPacientePorIdentificador(string $identificador, AccessContext $context)`;
- `buscarContratoPorGuidAutorizado(string $guid, AccessContext $context)`;
- `buscarAgendamentoPorGuidAutorizado(string $guid, AccessContext $context)`;
- `buscarTituloPorGuidAutorizado(string $guid, AccessContext $context)`.

Esses métodos retornam a linha/ID somente quando a permissão, estabelecimento e restrição de especialista também forem satisfeitos. Responder `404` tanto para GUID inexistente quanto para GUID fora do escopo evita enumeração do tenant.

Durante a compatibilidade, o parser aceita UUID canônico ou inteiro positivo. UUID é o caminho normal; inteiro aciona apenas o lookup legado, com as mesmas condições de autorização, e deve registrar métrica/log sem incluir dados clínicos. Nunca fazer cast antecipado de GUID para `int`, pois vira zero silenciosamente.

### Contrato de apresentação

- URLs e IDs de recursos próprios passam a usar `guid`.
- JSON para UI retorna `guid` e GUIDs relacionados (`contrato_guid`, `paciente_guid`), sem IDs internos.
- Campos de seleção enviados pelo navegador também devem usar GUID quando representam entidades públicas. O backend resolve para FK numérica antes de persistir.
- IDs de configuração de baixo risco (status, tipo, sala, serviço, conta etc.) podem migrar em ondas posteriores, mas não devem continuar aparecendo em APIs públicas novas.
- Tokens públicos existentes (confirmação e convite) continuam independentes.

## Fases de implementação

### Fase 0 — contexto de acesso e testes de caracterização

1. Introduzir um `AccessContext` imutável a partir da sessão: usuário, perfil, estabelecimento ativo, estabelecimentos permitidos e especialista vinculado.
2. Escrever testes de caracterização das permissões atuais.
3. Corrigir primeiro as buscas por ID sem escopo: contratos e títulos são prioridade; agenda/prontuário precisam de uma regra explícita de estabelecimento.
4. Definir formalmente como paciente e agendamento pertencem a estabelecimento. Hoje `pessoas` e `agendamentos` não têm `estabelecimento_id`; derivar somente de títulos é incompleto. Opções seguras: tabela de vínculo `pessoa_estabelecimentos` e `agendamento.estabelecimento_id`, mantendo todas as FKs atuais, ou derivação documentada por contrato com tratamento para avulsos.

### Fase 1 — infraestrutura e schema

1. Criar helper UUID v4 e validador canônico.
2. Migration idempotente adiciona `guid` a `pessoas`, `contratos`, `agendamentos`, `prontuario`, `titulos`, `usuarios`.
3. Backfill em lotes usando PK (`WHERE id > :ultimo_id AND guid IS NULL ORDER BY id LIMIT ...`), compatível com `SQL_SAFE_UPDATES`.
4. Validar `NULL`, duplicidade e formato/version nibble.
5. Criar índices `UNIQUE` idempotentemente e converter para `NOT NULL`.
6. Atualizar entidades Doctrine sem alterar `#[ORM\Id]` nem associações/FKs.

As migrations devem consultar `information_schema.COLUMNS` e `information_schema.STATISTICS` antes de cada DDL, preservar/restaurar o valor de `@@SQL_SAFE_UPDATES` se precisarem alterá-lo e evitar depender de `UPDATE ... WHERE guid IS NULL` sem PK. Preferir não desligar safe updates.

### Fase 2 — Pacientes e Contratos

1. Repositories geram GUID em INSERT e retornam `guid` em listagens/pesquisas.
2. Controllers recebem `string $identificador`; repositories resolvem UUID/ID legado dentro do escopo.
3. Trocar links, redirects, campos ocultos e resultados de autocomplete para GUID.
4. Arquivos permanecem subordinados ao paciente autorizado; adicionar GUID a `pessoas_arquivos` para URL direta.
5. Contratos passam a ser consultados/mutados com escopo de estabelecimento/especialista antes de adotar GUID na UI.

### Fase 3 — Agendamentos e Prontuários

1. FullCalendar e endpoints retornam `agendamento.guid` como `id` público.
2. Resolver GUIDs de paciente, contrato e especialista para as FKs internas.
3. Manter a checagem de especialista tanto em leitura quanto em edição/movimentação.
4. Sessão usa GUID do agendamento; histórico usa GUID da pessoa; POST do prontuário usa GUID do prontuário/agendamento.
5. Confirmação pública continua por token e valida o mesmo agendamento internamente.

### Fase 4 — Títulos

1. Todas as operações por título usam lookup `guid + estabelecimento autorizado`.
2. Transferência valida origem e destino no mesmo escopo permitido e mantém a verificação de mesmo estabelecimento já existente.
3. Pagamento de contrato resolve contrato por GUID autorizado e aceita lista de GUIDs de títulos, conferindo que todos pertencem ao contrato.
4. Adicionar GUID a `titulo_receb` ou manter a baixa acessível apenas por composição `titulo autorizado + movimento`, sem lookup global por ID.

### Fase 5 — Usuários e compatibilidade

1. Rotas administrativas usam GUID e mantêm `PermissionGate` e proteção contra auto-desativação/reset indevido.
2. Registrar uso das rotas numéricas antigas.
3. Após a janela de transição, responder `410`/redirecionar somente GETs seguros; rejeitar POST numérico.
4. Remover compatibilidade apenas quando logs mostrarem ausência de clientes legados. Não remover colunas `id`.

## Matriz mínima de testes de isolamento

Para cada recurso prioritário, criar dois estabelecimentos A/B, dois usuários e dois especialistas. Testar UUID e ID legado:

1. Usuário A acessa recurso A: permitido.
2. Usuário A tenta GUID de B: `404`, sem dado no corpo.
3. Usuário A tenta ID legado de B: `404`.
4. Usuário altera GUID/ID oculto no POST para B: nenhuma mutação.
5. Especialista A tenta agenda/prontuário do especialista B no mesmo estabelecimento: negado conforme regra atual.
6. Usuário sem permissão chama diretamente URL válida: `403` pelo `PermissionGate`.
7. GUID inexistente e GUID fora do escopo produzem resposta indistinguível.
8. Criação concorrente gera GUIDs v4 únicos; INSERTs mantêm PK/FK numéricas.
9. Migration roda duas vezes sem erro e sem trocar GUID existente.
10. Backfill roda com `SQL_SAFE_UPDATES=1` e deixa zero GUIDs nulos/duplicados.

O projeto não usa PHPUnit atualmente; os testes existentes são scripts integrados em `tests/`. A primeira entrega pode seguir esse padrão, mas recomenda-se adicionar PHPUnit para fixtures, isolamento transacional e matriz de autorização repetível.

## Ordem recomendada de entrega

1. Contexto de acesso + testes de isolamento e correções de escopo.
2. Migration/helper UUID + entidades.
3. Pacientes e arquivos.
4. Contratos.
5. Agenda e prontuário.
6. Títulos e baixas/transferências.
7. Usuários.
8. Observabilidade, encerramento da compatibilidade e expansão para cadastros auxiliares.

O maior risco não é a geração do GUID, mas preservar o tenant durante a resolução. A implementação deve considerar concluída cada fase apenas quando não houver ID numérico do recurso em URL/HTML/JSON e quando os testes cruzados por estabelecimento e especialista passarem tanto para GUID quanto para o caminho legado.
