# Task 007 — Laboratório `Validar`

**Depende de:** 002, 003, 005, 008
**Bloqueia:** 010, 013

## Objetivo

Validar código fornecido pelo usuário. Confere expiração, tentativas, sucesso. Em sucesso, dispara evento registrado para o contexto.

## Deliverables

### `Validators/ValidarOtpValidator.php`

- `contexto`, `referencia_externa`, `codigo` obrigatórios.
- `codigo` deve casar com `^\d{4,8}$`.

### `Laboratorios/ValidarOtpLaboratorio.php`

Construtor `(array $entrada)`. Comportamento:

1. Validator.
2. `OtpVerificacaoSonda::findAtivoPorContextoEReferencia($contexto, $referencia_externa)`.
   - Se não houver OTP ativo → `notification_class = danger`, `motivo = 'sem_otp_ativo'`.
3. Verifica expiração:
   - `now > expira_em` → marca status `EXPIRADO`, persiste, registra evento `expirado`, retorna `notification_class = danger`, `motivo = 'expirado'`.
4. Verifica `aceitaTentativaValidacao()` no enum:
   - Se não aceita (status diferente de ENVIADO) → erro claro.
5. Compara código:
   - `password_verify($entrada['codigo'], $otp->codigo_hash)` true:
     - status → `VALIDADO`, `validado_em = now`, persiste.
     - Registra evento `validado`.
     - Resolve `OtpEventoRegistry::resolver($contexto)` se houver registro e chama `aoValidar($otp, $entrada['extras'] ?? [])`. Erros do evento → log + evento `falha_evento` (não reverte).
     - Resposta `notification_class = success`, `otp_id`, `status = validado`, `dados_adicionais` (devolve o JSON ao consumidor).
   - false:
     - `tentativas++`. Persiste.
     - Se `tentativas >= max_tentativas`: status → `INVALIDADO_TENTATIVAS`. Evento `invalidado_tentativas`. Retorna `notification_class = danger`, `motivo = 'tentativas_excedidas'`.
     - Caso contrário: evento `tentativa_errada`. Retorna `notification_class = danger`, `motivo = 'codigo_incorreto'`, `tentativas_restantes`.

## Códigos de erro retornados (motivo)

`sem_otp_ativo`, `expirado`, `cancelado`, `ja_validado`, `codigo_incorreto`, `tentativas_excedidas`, `transicao_invalida`.

Documentar no docblock para consumidores tratarem.

## Critério de aceite

- Falha do Evento não reverte status `VALIDADO`.
- Comparação por `password_verify` (constant-time).
- Cobertura nos testes (012) cobre cada caminho (sucesso, expirado, errado, tentativas excedidas, ja_validado, sem_otp).
