# 02 — Máquina de estados de `fichas_casos`

Baseado na tabela do PLANO.md (secção "Modelo de dados") e no fluxo de um caso (secção "Fluxo de um caso").

## Estados

| Estado | Significado |
|---|---|
| `registado` | Caso criado (individual ou linha de lote), validação imediata feita (NIF, MAI, zona), ainda não lido. |
| `em_leitura` | Job `LerCaso` a correr: localizar pasta, ler documentos, classificar, extrair, cruzar. |
| `por_rever` | Leitura terminada, pelo menos um campo com confiança abaixo do limiar ou marcado para revisão humana. |
| `bloqueado` | Impedimento funcional que a leitura por si só não resolve (0 ou >1 pastas, documento de outra pessoa, RC sem finalidade, NIF `Ativo` inesperado, etc.) — precisa de decisão humana fora da simples edição de campo. |
| `aprovado` | Utilizador aprovou a tabela; snapshot do manifesto + hashes dos documentos gravado. |
| `dry_run_ok` | Dry-run via wrapper (`inserir.php` sem `--apply`) correu sem erros sobre o manifesto aprovado. |
| `a_aplicar` | Apply em curso (wrapper a correr `--apply`); estado transitório, curto. |
| `inserido_verificado` | Apply concluído, verificação pós-inserção (SELECT ao Sabichão) confirmou o manifesto — estado terminal de sucesso. |
| `cancelado` | Terminal — o utilizador desistiu do caso antes de aplicar. |
| `ja_inserido` | Terminal — durante as verificações descobriu-se que o NIF já estava `Ativo` com a mesma admissão/zona/local (outra via já o inseriu); não há nada para fazer. |
| `erro_tecnico` | Terminal (ou reencaminhável) — falha técnica (timeout SSH sem reconciliação possível, exceção não tratada, schema incompatível no alvo) distinta de `bloqueado` (ver secção 4). |

## 1. Tabela de transições

| De | Para | Gatilho | Quem |
|---|---|---|---|
| — | `registado` | Novo caso submetido (formulário ou linha de lote), validação síncrona passa | RH (webapp) |
| `registado` | `em_leitura` | Motor apanha o caso na fila (lease adquirido) | Motor |
| `em_leitura` | `por_rever` | Leitura terminada com pelo menos 1 campo abaixo do limiar de confiança da política (04-POLITICA-CONFIANCA.md) | Motor |
| `em_leitura` | `bloqueado` | Impedimento funcional detetado (0/>1 pastas, documento doutra pessoa, RC sem finalidade válida, NIF `Ativo` sem ser re-admissão, reserva inexistente) | Motor |
| `em_leitura` | `erro_tecnico` | Exceção não tratada, NAS inacessível a meio, mini indisponível sem fallback possível, schema inesperado | Motor |
| `por_rever` | `por_rever` | RH edita um campo (fica `origem='utilizador'`, alimenta `fichas_fiabilidade`) | RH |
| `por_rever` | `aprovado` | RH clica "Aprovar" com todas as linhas resolvidas (alta ou editadas) | RH |
| `bloqueado` | `registado`/`em_leitura` | RH resolve o bloqueio (ex. cria a reserva em falta) e pede releitura | RH → Motor |
| `bloqueado` | `cancelado` | RH decide não avançar | RH |
| `aprovado` | `por_rever` | Qualquer edição de campo depois de aprovado, ou hash de documento/manifesto deixa de bater | RH ou Motor (deteção de mudança) |
| `aprovado` | `dry_run_ok` | Dry-run via wrapper sem erros | Motor (botão "Dry-run") |
| `aprovado`/`dry_run_ok` | `erro_tecnico` | Dry-run falha por razão técnica (SSH, wrapper, schema) | Motor |
| `dry_run_ok` | `a_aplicar` | RH clica "Aplicar em DEV" ou "Aplicar em PROD" (confirmação dupla em prod) | RH |
| `a_aplicar` | `inserido_verificado` | Apply retorna exit 0 e a verificação pós-inserção (SELECT) confirma todas as tabelas | Motor |
| `a_aplicar` | `erro_tecnico` | Apply falha (exit != 0), timeout sem confirmação de sucesso/rollback, verificação pós-inserção falha | Motor |
| `a_aplicar` | `ja_inserido` | Verificação mostra que a inserção já existia antes deste apply (corrida com outra via) | Motor |
| `erro_tecnico` | `em_leitura`/`aprovado` (conforme onde falhou) | RH pede novamente depois de a causa técnica estar resolvida | RH |
| `inserido_verificado` | `inserido_verificado` | Download de PDF / envio de email (não muda o estado; regista-se em `fichas_execucoes` e marca visível na fila) | RH |

## 2. O que invalida a aprovação

Regra do plano (princípio 3): "Edição de campo, nova leitura ou mudança de um ficheiro na NAS anula aprovação e dry-run."

Concretamente, qualquer um destes eventos faz `aprovado`/`dry_run_ok` regredir a `por_rever` (e marca `fichas_aprovacoes.invalidada_em` + motivo):
- Edição de qualquer campo pela RH (mesmo que fosse confiança alta).
- Hash de um documento em `fichas_documentos` deixa de corresponder ao ficheiro atual na NAS (releitura periódica ou verificação no momento do dry-run/apply).
- Hash do `manifesto_json` gravado em `fichas_aprovacoes` deixa de corresponder ao manifesto reconstruído a partir dos campos atuais do caso.
- Nova leitura solicitada explicitamente pelo RH (ex. documento substituído na NAS).

## 3. Estados terminais

- `cancelado` — decisão humana explícita, sem inserção.
- `ja_inserido` — nada a fazer, informativo.
- `inserido_verificado` — sucesso.
- `erro_tecnico` — terminal do ponto de vista da fila automática, mas reabrível manualmente pela RH depois de investigar (não é "sem saída", é "sem retry automático").

## 4. `bloqueado` vs `erro_tecnico`

| | `bloqueado` | `erro_tecnico` |
|---|---|---|
| Natureza | Funcional — falta uma decisão humana ou um pré-requisito de negócio | Técnica — falha de infraestrutura, dado inesperado no schema, exceção |
| Exemplos | 0 ou >1 pastas na NAS; documento pertence a outra pessoa; RC sem finalidade de segurança privada; RC caducado (pergunta-se sempre, mesmo já autorizado antes); morada divergente entre fontes oficiais; NIF já `Ativo` sem ser re-admissão; sem reserva `Reservado` | NAS desmontada a meio da leitura; mini indisponível e sem fallback (o caso não bloqueia a app, mas o job fica sem poder avançar); timeout SSH sem confirmação; schema do alvo incompatível (coluna em falta); exceção não tratada no motor |
| Ação esperada | RH decide/corrige (ex. cria reserva, escolhe qual documento é o certo, confirma avançar apesar do RC) | Equipa técnica investiga; sem decisão de negócio possível só com os dados disponíveis |
| Reversível por quem | RH, pela própria interface | Normalmente exige intervenção técnica (logs, reconciliação por SELECT) |
| Efeito na fila | Não bloqueia outros casos | Não bloqueia outros casos (fila é por caso) |

Nota: "Mini desligado" não é nem `bloqueado` nem `erro_tecnico` imediato — o plano diz explicitamente que "app e apply não dependem dele; casos ficam `registado` com aviso". Só evolui para `erro_tecnico` se um job já em `em_leitura` não conseguir terminar dentro do lease.

## 5. Recuperação de jobs presos (lease/heartbeat)

- Cada job `LerCaso` adquire um **lease** ao mudar o caso para `em_leitura` (registo de quando começou + heartbeat periódico).
- Se o heartbeat parar (worker caído, mini indisponível sem resposta) por mais do que o timeout do lease, o caso é considerado preso.
- Recuperação: o caso volta a `registado` (para ser reapanhado do zero) ou passa a `erro_tecnico` se já tinha havido uma tentativa anterior falhada no mesmo lease — critério exato (nº de tentativas antes de desistir automaticamente) **A CONFIRMAR** na Fase 2, o plano não fixa esse número.
- Limite de backlog configurável (proposta 200 no plano) evita que casos presos acumulem sem visibilidade.

## 6. Prevenção de duplicados

Exclusão por MAI+NIF+admissão ao registar um novo caso (fila, PLANO.md secção "Modelo de dados") — impede dois casos simultâneos para a mesma pessoa/admissão.
