# Validação Fase 2 — correções depois dos 40 processos reais

Data: 2026-09-22. Base: os mesmos 40 processos da 1.ª validação
(`storage/app/validacao-entrada.csv`, ids 770-851), comparados contra
`storage/app/gabarito.tsv` (id_processo, mai, especialidades, val_vig,
val_spr, val_ard, val_are, validade_cc, val_rc) porque a ação `consulta.ficha`
ainda não está instalada no wrapper de **prod** (só em dev, ver §4). Execução:
`env WRAPPER_HOST=10.0.30.253 WRAPPER_CHAVE_CONSULTA=... php artisan
fichas:validar storage/app/validacao-entrada.csv
--saida=storage/app/validacao-fase2-resultado.csv`, no CT 117
`assistente-dev`, NAS `//10.0.30.79` montada só-leitura.

## 1. Tabela por campo — antes / depois

| Campo | Antes (1.ª validação) | Depois (correções aplicadas) |
|---|---|---|
| Cartões PSP (val_vig/spr/ard/are) | 65 iguais / 67 lidos (2 não lidos) | **67 iguais / 68 lidos** (1 diferente, explicado — §2) |
| validade_cc | 34 iguais, 3 diferentes, 3 não lidas | **39 iguais, 1 diferente (explicado), 0 não lidas** |
| val_rc | 30 iguais, 2 diferentes, 8 não lidas | **33 iguais, 4 diferentes (todos explicados), 3 não lidas (documento genuinamente ausente)** |
| iban | valores lixo (2 casos: prefixo estragado colado a texto do documento) | **0 lixo — todos PT50 válidos ou vazio quando não há IBAN legível** |
| nacionalidade | "Ão Portuguesa", "Ça Portuguesa", "Aç Portuguesa", "Nationaltyeri Portuguesa", "Fiportuguesa" e variantes (≈15 dos 39 casos) | **0 anomalias — todos "Portuguesa"** |
| nome (CC físico) | proc 850/MAI 156614: "Mariana Texeira Mariana Sofia" (ordem trocada + apelido errado) | **"Mariana Sofia Almeida Teixeira", confiança "alta" (confirmado pela 2.ª fonte)** |

Números de val_rc/validade_cc/cartões acima contam só os 40 MAI do
`gabarito.tsv`; os totais "lidos" excluem os campos que o gabarito não tem
valor nenhum (documento inexistente — não é falha de leitura).

## 2. Divergências finais (`diferente`) — todas com o motor certo, prod/gabarito desatualizado

Confirmado lendo o ficheiro real (imagem/PDF) por trás de cada valor — nunca
por inferência. Em todos os casos há **mais do que uma pasta `Proc N`** para
o MAI, e o motor lê sempre a mais alta (regra de desenho, LocalizadorPasta);
o gabarito/prod referem-se a um documento mais antigo, ainda não atualizado.

| MAI | Campo | Gabarito/prod | OCR (motor) | Explicação |
|---|---|---|---|---|
| 144490 | validade_cc | 2028-08-27 | 2035-01-22 | CC físico da pasta mais alta (Proc 806) diz literalmente "DATA DE VALIDADE ... 22 01 2035" — confirmado a olho no texto do Vision. |
| 144490 | val_rc | 2026-01-29 | 2026-11-09 | RC da pasta mais alta (Proc 806, emitido 2026-09-10) diz "CÓDIGO VIGENTE ATÉ: 2026/11/09"; o valor do gabarito vem do RC antigo (Proc 233, emitido 2025-11-11, "2026/01/29"). |
| 163305 | val_rc | 2026-01-01 | 2026-12-13 | RC único do processo diz "CÓDIGO VIGENTE ATÉ: 2026/12/13"; "2026-01-01" tem cheiro de valor por omissão em prod (dia 1 de janeiro — mesmo padrão de 138294/117245, §3). |
| 167431 | val_rc | 2026-03-09 | 2026-07-28 | Há dois RCs na pasta da pessoa: o antigo em "Proc 235" ("2026/03/09") e um mais recente, uma FOTO (não PDF) em "Proc 847" ("IMG-...jpg", emitido 2026-04-29, "2026/07/28"). O motor lê a pasta mais alta (847); o gabarito reflete a 235. |
| 167298 | val_rc | 2025-08-26 | 2026-07-22 | Mesmo padrão: RC antigo em "Proc 183" ("2025/08/26"), RC novo em "Proc 837" ("PHOTO-...jpg", "2026/07/22"). |
| 120654 | val_vig | 2026-08-12 | 2031-08-13 | Cartão VIG antigo em "Proc 137" ("12AGO2026"), cartão renovado em "Proc 843" ("13AG02031"). |

Nenhuma destas é uma falha de regra — é o comportamento pretendido (ler o
processo mais recente). Ficam aqui só para o registo ficar honesto: o
gabarito/prod não tinham sido atualizados com o documento mais novo à data
desta validação.

## 3. Não lidos remanescentes — documento genuinamente ausente

| MAI | Campo | Gabarito | Motivo |
|---|---|---|---|
| 147188 | val_rc | 2026-08-02 | O certificado só existe na pasta antiga ("Proc 346"); a pasta mais alta ("Proc 819", usada pelo motor) só tem o CC (verso) e cartões — sem certificado nenhum para ler. Comportamento correto: sem documento, sem valor. |
| 138294 | val_rc | 2026-01-01 | Não há NENHUM ficheiro de certificado em toda a pasta do processo (confirmado por busca). "2026-01-01" é outro caso do valor por omissão (dia 1 de janeiro) referido acima — não é um valor real do prod. |
| 117245 | val_rc | 2026-01-01 | Idem — sem certificado na pasta, valor do gabarito é o mesmo padrão de omissão. |

## 4. Bugs corrigidos (código)

### 4.1 `validade_cc` — CC digital e físico (`CartaoCidadao.php`)
- Regra dura de validação em `validarData()`: mês 1-12, dia 1-31, ano ≥ 2020
  e sempre posterior ao ano de nascimento (quando conhecido).
- CC digital: a validade é a linha imediatamente a seguir à etiqueta "Data de
  validade"/"Expiry", ignorando linhas de carimbo de assinatura digital
  ("Date:"/"GMT"/"signed") — corrige o proc 788 (MAI 151667), que apanhava
  "Date: 2026-05-21" do carimbo e produzia a data inválida "2026-21-05".
- CC físico: a mesma validação elimina o proc 773 (MAI 125457), que apanhava
  a própria data de nascimento (1989) como validade.
- Verso (MRZ): passa a gravar-se no MESMO campo `validade_cc` (não em
  `validade_cc_mrz`), com `_origens['validade_cc'] = 'cc verso (MRZ)'` — serve
  de reserva quando a frente não dá validade nenhuma.

### 4.2 Classificação (`Classificador.php`) — causa raiz da maioria dos "não lidos"
- **RC antes do CC**: o certificado do registo criminal imprime "N° CARTÃO DE
  CIDADÃO/BI (IDENTITY CARD NUMBER): ..." como um dos seus próprios campos,
  o que batia na regra do `cc_frente` ("identity card") e classificava o RC
  inteiro como CC — nunca ia a `RegistoCriminal`, `val_rc` ficava sempre por
  ler. Sozinho, este bug explicava a maioria dos 8 "não lidos" da 1.ª
  validação (ex. MAI 167431/120654/167298).
- **Verso do CC pela MRZ**: a MRZ (linha 2, formato ICAO fixo) passa a ser
  sinal SUFICIENTE por si só para classificar `cc_verso`, sem depender da
  etiqueta "NISS" (que o OCR por vezes perde por completo) — corrige o proc
  819 (MAI 147188), um PDF só com o verso que ficava "outro" e nunca chegava
  à reserva de validade pela MRZ.

### 4.3 Pasta do processo sem número (`LocalizadorPasta.php`)
- MAI 175801: a pasta da pessoa tinha só uma subpasta `Proc` (sem número),
  não `Proc N`. A regra `/^proc\s*(\d+)/` não apanhava, e o motor caía para
  "a própria pasta da pessoa é o processo", onde não havia ficheiro nenhum —
  TODOS os campos desse caso ficavam silenciosamente por ler (validade_cc,
  val_rc, val_vig, val_spr, de uma só vez). Agora reconhece-se `Proc`
  isolado como pasta de processo válida, atrás de `Proc N` numeradas.

### 4.4 IBAN (`Iban.php`)
- Extração por âncora literal `PT50` (dígitos de controlo fixos de qualquer
  IBAN português) em vez de aceitar qualquer prefixo `[A-Z]{2}\d{2}`: depois
  do prefixo só se aceitam dígitos (espaços/pontos/quebras de linha como
  separador de grupo); uma LETRA corta a captura na hora — nunca se devolve
  lixo. Corrige o padrão relatado ("TA00 0329 7763 2502 0IBA NPT5 0001 8000
  32", um rótulo caído no meio da sequência).
- Duas causas reais de lixo encontradas e corrigidas na validação real:
  - **Máscara com dígitos antes do "\*\*\*\*"** (MAI 055638, fatura com
    débito direto: "PT5000180003411\*\*\*\*602\*\*0") — a deteção de mascarado
    só olhava para logo a seguir ao prefixo; alargada para tolerar dígitos
    antes da 1.ª sequência de máscara.
  - **Quebra de linha entre o prefixo e o resto do número** (MAI 177653,
    comprovativo das Finanças: "PT50" numa linha, "001800036220524002079" na
    seguinte) — o "\n" não estava na lista de separadores tolerados e cortava
    a captura logo a seguir a "50"; o extrator ia buscar lixo a outro sítio
    do documento (ex. "EO20 2503 27TR IBUT RIAE ADUA NEIR ANME RO", do texto
    da declaração de início de atividade).

### 4.5 Nacionalidade (`OcrDocs::limparPrefixoGarbled`)
- Generalizado de "só 2 palavras, prefixo ≤3 chars" para: manter só a última
  palavra quando o resto (uma ou mais palavras) contém "nation"/"nacion" ou é
  curto (≤3 chars) — cobre "Ão/Ça/Aç Portuguesa" e também
  "Nationaltyeri Portuguesa"/"Nationalitys Ão Portuguesa" (restos inteiros da
  etiqueta "(NATIONALITY)" quando o OCR perde o ")" de fecho por completo).
- Caso colado sem espaço nenhum ("Fiportuguesa", MAI 113291): lista pequena
  de nacionalidades conhecidas, reconhece o sufixo e devolve só a forma
  canónica.
- Achado sem correção nesta sessão: o mesmo padrão de etiqueta residual
  ("Place Of Birthoes Sobrado", "Of Birthdcr Paranhos") afeta também
  `naturalidade`/`concelho`. Não corrigido aqui porque os nomes de lugar são
  compostos e multi-palavra ("Vila Nova de Famalicão") — a heurística usada
  para nacionalidade (nomes de país são sempre 1 palavra) arriscaria cortar
  um nome de lugar legítimo. Fica registado para decisão dedicada.

### 4.6 Nome no CC físico (`CartaoCidadao.php` + `LerCaso.php`)
- `nomeFisico()`: o limite de varrimento passa a ser também a PRIMEIRA linha
  que já é só uma data dd mm aaaa (a de nascimento), não só a etiqueta
  "NASCIMENTO"/"BIRTH". Sem isto, um CC sem essa etiqueta legível (proc 850,
  MAI 156614) deixava a linha da ASSINATURA do titular no final do
  documento ("MaRiana TexeiRa") entrar como candidata e substituir o nome
  verdadeiro — dava "Mariana Texeira Mariana Sofia" em vez de
  "Mariana Sofia Almeida Teixeira".
- Confirmação cruzada: quando há uma 2.ª fonte independente (CC digital, ou
  o `NOME (NAME)` do RC) e concorda com o nome do CC físico ignorando
  acentos/caixa, a confiança sobe de "rever" para "alta" — nunca troca o
  valor, só confirma.

### 4.7 Ação `consulta.ficha` (wrapper + Cruzamentos)
- Nova ação de só-leitura no wrapper (`wrapper-ct107/wrapper.php`): por NIF
  (`REPLACE(nif,' ','')` dos dois lados), devolve as colunas do manifesto de
  `funcionarios` + id_processo/admissao/estado/email do último `processo`
  (email só existe nessa tabela). `App\Services\Fichas\Cruzamentos::ficha()`
  chama-a; `fichas:validar` usa-a para `valor_sabichao` em vez de
  `consulta.funcionario` (que só tinha nome/estado), e a coluna
  `id_processo` do CSV de saída passa a ser sempre o id_processo do
  Sabichão (do CSV de entrada, ou da própria `consulta.ficha`), nunca o id
  interno do caso.
- Instalada e testada em **DEV** (CT 100, Sabichão dev, release
  `20260922153544`) — confirmado por chamada SSH direta devolvendo a ficha
  completa de um processo real de dev. **Não instalada em prod** (fora do
  âmbito autorizado); a comparação desta sessão usou por isso o
  `gabarito.tsv`, como previsto.

## 5. Testes

75+ testes Pest cobrem cada regra acima com texto sintético (nunca conteúdo
real de documentos): `CartaoCidadaoTest`, `ClassificadorTest`,
`RegistoCriminalTest`, `IbanTest`, `LocalizadorPastaTest` (novo). Suite
completa a passar no CT `assistente-dev`: `vendor/bin/pest` — 76 passed, 130
assertions, sem avisos.

## 6. Pendente / fora do âmbito desta sessão

- `naturalidade`/`concelho` têm o mesmo padrão de etiqueta residual da
  nacionalidade, sem correção (§4.5).
- `Cruzamentos::funcionario()` lê `resultado.funcionario` (singular) mas o
  wrapper devolve `resultado.funcionarios` (plural, array) — bug pré-
  existente, não relacionado com esta validação, encontrado por inspeção do
  código ao construir `consulta.ficha`; nunca corrigido nesta sessão (fora
  do pedido, e `fichas:validar` passou a usar `consulta.ficha` em vez desta
  função).
- `consulta.ficha` só em dev; instalação em prod fica para quando for
  autorizada (o wrapper de prod não muda sem OK explícito).
