# Ouvidoria — Gaps e Dívidas

> Todos os itens foram verificados no repositório `v6` (código + banco `prefeituras_v5`). Itens marcados **[INFERÊNCIA]** dependem de comportamento de runtime não observável por leitura estática.

## 1. Regressão v5 → v6 (destaque do módulo)

### 1.1 [ALTO] O módulo de Relatório Estatístico de Ouvidoria do CMS **existia na v5 e não existe na v6**

**Evidência — v5 (tudo verificado por leitura):**

| Artefato | Caminho v5 |
|---|---|
| Controller | `v5/app/Http/Controllers/Prefecture/RelatorioEstatisticoOuvidoriaController.php` |
| Model | `v5/app/Http/Model/Prefecture/RelatorioEstatisticoOuvidoria.php` |
| Form Request | `v5/app/Http/Requests/RelatorioEstatisticoOuvidoriaRequest.php` |
| Migration | `v5/database/migrations/2026_05_27_135849_create_relatorios_estatisticos_ouvidoria_table.php` |
| Views | `v5/resources/views/prefecture/relatorio-estatistico-ouvidoria/{index,create,edit,form}.blade.php` |
| JS | `v5/public/assets/js/custom/config/relatorio-estatistico-ouvidoria/datatable.js` |
| Rotas | `v5/routes/basepro.php:339-344` e `v5/routes/basepro.php:481` |

**Evidência — v6 (ausência verificada):**

- `grep -r "RelatorioEstatisticoOuvidoria" v6/` → **0 ocorrências**.
- `RelatorioEstatisticoOuvidoriaController.php` em `v6/app/Http/Controllers/Prefecture/` → **não existe**.
- `RelatorioEstatisticoOuvidoria.php` em `v6/app/Http/Model/Prefecture/` → **não existe**.
- `v6/resources/views/prefecture/relatorio-estatistico-ouvidoria/` → **não existe**.
- `v6/database/migrations/*relatorio*` → **nenhum**.
- Tabela `relatorios_estatisticos_ouvidoria` no banco `prefeituras_v5` → **não existe** (verificado com `\dt relatorios*`).

**O que o módulo fazia (v5):**

```php
// v5/app/Http/Controllers/Prefecture/RelatorioEstatisticoOuvidoriaController.php
public function index(): View    { return view('prefecture.relatorio-estatistico-ouvidoria.index'); }
public function create(): View   { return view('prefecture.relatorio-estatistico-ouvidoria.create'); }
public function store(RelatorioEstatisticoOuvidoriaRequest $request): RedirectResponse
{
    $data = $request->only('ano', 'descricao');
    if ($request->hasFile('arquivo')) {
        $path = $this->saveUpload($request->file('arquivo'), 'docs', null, null, false);
        $data['arquivo'] = "docs/{$path}";
    }
    RelatorioEstatisticoOuvidoria::create($data);
    flash('Relatório cadastrado com sucesso.')->success();
    return redirect()->route('relatorio-estatistico-ouvidoria-cms.index');
}
// edit / update / destroy análogos
```

**Regras do Form Request (v5):**

```php
'ano'       => 'required|string|max:10',
'descricao' => 'required|string|max:255',
'arquivo'   => 'nullable|file|mimes:pdf|max:20480',   // PDF, até 20 MB
```

**Tabela (v5):** `relatorios_estatisticos_ouvidoria` — `id` bigIncrements, `ano` string nullable, `descricao` text nullable, `arquivo` string nullable, `timestamps`, `softDeletes`.

**Rotas (v5, `v5/routes/basepro.php:339-344`):**

| HTTP | URI | Nome |
|---|---|---|
| GET | `relatorios-estatisticos-ouvidoria` | `relatorio-estatistico-ouvidoria-cms.index` |
| GET | `relatorios-estatisticos-ouvidoria/cadastrar` | `relatorio-estatistico-ouvidoria-cms.create` |
| GET | `relatorios-estatisticos-ouvidoria/editar/{id}` | `relatorio-estatistico-ouvidoria-cms.edit` |
| PUT | `relatorios-estatisticos-ouvidoria/atualizar/{id}` | `relatorio-estatistico-ouvidoria-cms.update` |
| POST | `relatorios-estatisticos-ouvidoria` | `relatorio-estatistico-ouvidoria-cms.store` |
| DELETE | `relatorios-estatisticos-ouvidoria/excluir/{id}` | `relatorio-estatistico-ouvidoria-cms.destroy` |

Listagem via `GET /cms/datatable/relatorio-estatistico-ouvidoria` → `Prefecture\DatatableController@get_relatorio_estatistico_ouvidoria` (`v5/app/Http/Controllers/Prefecture/DatatableController.php:461-470`), que lê `DB::table('relatorios_estatisticos_ouvidoria')->whereNull('deleted_at')->orderBy('ano','desc')`.

**O que sobreviveu na v6:** apenas a rota **pública** `/relatorio-estatistico-ouvidoria` (`v6/routes/web.php:23`), que é o relatório **dinâmico**, calculado a partir de `ombudsman_requests` pelo `Portal\OmbudsmanController@relStatitics`. Não há mais cadastro, edição, listagem nem PDFs institucionais anuais.

**Impacto:** a prefeitura perdeu a possibilidade de publicar o relatório anual oficial em PDF (com prefácio, metodologia, assinaturas) e de listá-los no CMS. A rota pública passa a exibir apenas agregados calculados.

**Plano de recuperação:** ver `06-MIGRACAO-MODERNA.md` §8 (nova tabela `ombudsman_reports`) e `09-MAPA-V5-V6.md` §4.

> **Dossiê complementar:** o módulo v5-only tem dossiê próprio em `../relatorio-estatistico-ouvidoria/` (9 arquivos), com o detalhamento do controller, model, request, migration, views e JS da v5. Este documento registra apenas a **regressão** vista pela lente da Ouvidoria.

### 1.2 [ALTO] Regressão: anexos em respostas

| Item | v5 | v6 |
|---|---|---|
| Método `OmbudsmanAnswer::archives()` | **ativo** (`belongsToMany`) | **comentado** |
| Tabela `ombudsman_answer_archives` | **existe** (`v5/database/migrations/2026_08_06_162636_create_ombudsman_answer_archives_table.php`) | **não existe** |
| Chamada no controller | `$answer->archives()->attach($insertArchive->id)` funciona | **falha** |

**Efeito verificado:** responder um chamado **com anexo** no CMS v6 produz exceção (método inexistente ou tabela inexistente), capturada pelo `catch (\Exception $e)` do controller, que executa `dd($e)` — o operador vê um dump e a resposta **não** é salva (rollback da transação).

### 1.3 [MÉDIO] Regressão: campos demográficos do cidadão

A migration `v5/database/migrations/2026_05_25_000000_add_new_fields_to_ombudsman_requests_table.php` adiciona a `ombudsman_requests`: `cpf_cnpj`, `gender`, `education_level`, `birth_date`, `attachment`. **Nenhum** desses campos existe na v6 (verificado no `\d` da tabela). Também não há campo no formulário do portal v6.

> **Pergunta em aberto:** a prefeitura usa esses dados para cruzar com outras bases? A remoção pode ter sido intencional (LGPD) ou uma perda. Ver `09-MAPA-V5-V6.md`.

## 2. Bugs e erros conhecidos

### 2.1 [CRÍTICO] `OmbudsmanRequestController@update` altera sempre o primeiro registro

```php
public function update(Request $request)
{
    $ombudsman = OmbudsmanRequest::first();
    $ombudsman->update($request->all());
    return response()->json(['message' => 'Solicitação atualizada com sucesso!']);
}
```

Não há filtro por `id` nem validação. Qualquer `PUT /cms/chamados-da-ouvidoria/{id}` (rota gerada pelo `Route::resource`) **sobrescreve a primeira manifestação da tabela** com todos os campos do request — inclusive `protocol`, `status_id`, `ip`, `token`.

**Efeito:** perda silenciosa de dados em produção.

### 2.2 [ALTO] 7 dos 8 controllers CMS não registram `check.route.permission`

`Prefecture\Controller::__construct()` (`v6/app/Http/Controllers/Prefecture/Controller.php:20-24`) registra o middleware. Sete controllers sobrescrevem `__construct` **sem** chamar `parent::__construct()`:

| Controller | Construtor sobrescrito |
|---|---|
| `OmbudsmanRequestController` | sim (`__construct(OmbudsmanRequest $ombudsmanRequest)`) |
| `OmbudsmanSubjectController` | sim |
| `OmbudsmanSectorController` | sim |
| `OmbudsmanOriginController` | sim |
| `OmbudsmanResponseMethodController` | sim |
| `OmbudsmanManifestationTypeController` | sim |
| `OmbudsmanCannedResponseController` | sim |
| `OmbudsmanAnswerController` | **não** — herda (único protegido) |

[INFERÊNCIA] As rotas desses 7 controllers **não** passam por `check.route.permission`, que é o mecanismo que exige login via Sentinel e verifica `hasAnyAccess`. O único outro requisito de autenticação na cadeia é o grupo `/cms` de `routes/basepro.php`, que aplica `before: csrf` mas **não** `check.logged` (verificado: `check.logged` aparece 23× em `routes/basepro.php`, todos em rotas específicas — nenhuma no grupo `/cms`).

> **Teste obrigatório em ambiente real:** abrir `/cms/assuntos` (ou `/cms/chamados-da-ouvidoria`) **deslogado** e verificar o comportamento.

### 2.3 [ALTO] `dd($e)` em produção

`OmbudsmanAnswerController@store` e `OmbudsmanRequestController@prorrogar_chamado` chamam `dd($e)` nos blocos `catch`. Isso interrompe a execução com um dump completo (rastreamento de pilha, caminhos absolutos do servidor, potencialmente dados de conexão) em vez da mensagem amigável preparada logo abaixo.

### 2.4 [ALTO] Validação de `modality` inconsistente com o formulário

O `Portal\OmbudsmanController::validated()` declara `'modality' => 'required|integer|not_in:0'`, mas:
- o array de modalidades do controller é `['identificado' => 'Identificado', 'sigiloso' => 'Sigiloso', 'anonimo' => 'Anônimo']` — **strings**;
- o formulário envia a **chave** da opção (`identificado`/`sigiloso`/`anonimo`);
- o JS compara com `'anonimo'` (string).

A regra `integer` **reprovaria** o payload real. [INFERÊNCIA] há divergência entre o código implantado e o formulário — **exige teste em ambiente real**; se o formulário em produção enviar `0`/`1`/`2`, então o `select` do portal envia opções diferentes das do array do controller.

### 2.5 [ALTO] Captcha não validado

`'g-recaptcha-response' => 'required|recaptcha'` está **comentada** em `Portal\OmbudsmanController::validated()`, mas o formulário renderiza `{!! Recaptcha::render() !!}`. O captcha é exibido, **não validado**. Proteção apenas visual contra robôs.

### 2.6 [ALTO] Comparação de modalidade com acento que nunca casa

```php
if ($ombudsman->modality == 'Anônimo') {
    $ombudsman->anonymous = true;
}
```

O array de modalidades usa a chave `'anonimo'` (sem acento, minúsculo). A comparação **nunca** é verdadeira. O flag `anonymous` depende só de `$request->has('anonymous')` — que não está no formulário do portal (o formulário tem o select de modalidade, não um checkbox `anonymous`). Consequência: **uma manifestação anônima provavelmente não é marcada como `anonymous`** — o que afeta `masked_name` e a coluna `anonimo` da view `chamados_ouvidoria`.

### 2.7 [MÉDIO] Colisão de protocolo

`'OUV-' . now()->format('dmY-His')` tem granularidade de **segundo**, e `protocol` tem constraint UNIQUE. Dois chamados no mesmo segundo colidem → `PDOException` **não capturada** no `store` do portal (não há try/catch) → **erro 500**.

### 2.8 [MÉDIO] Falha fatal na tela de resposta com log sem assunto

`v6/resources/views/prefecture/ombudsman/request/answer/create.blade.php` renderiza `{{$log->subjectFrom->title}}` e `{{$log->subjectTo->title}}` **sem** `optional()` — ao contrário do bloco de setor, que usa `optional($log->sectorFrom)->title ?? '-'`. Se um log tiver `subject_from_id` ou `subject_to_id` nulo, o acesso a `null->title` gera **erro fatal** na renderização.

Quando isso ocorre? Ao encaminhar um chamado para um assunto inexistente (§2.9), o log é gravado com `subject_to_id` = `subject_id` atual (não nulo) — mas `subject_from_id` é o assunto antigo (não nulo). Cenário de risco: logs criados porRotinas futuras com um dos lados nulo.

### 2.9 [MÉDIO] Encaminhamento por título de assunto

`OmbudsmanSubject::where('title', $new_subject_title)->first()` — busca **por título**, não por id. Títulos duplicados (o `title` não tem constraint UNIQUE em `ombudsman_subjects`) levam ao primeiro encontrado, potencialmente errado. Sem validação no controller (o `enviar_chamado` não usa Form Request).

### 2.10 [MÉDIO] `sector_from_id`/`sector_to_id` gravados com id de setor, FK para `contents`

```php
'sector_from_id' => $old_sector_id,              // ombudsman_sectors.id
'sector_to_id'   => $ombudsmanRequest->subject->sector_id,   // ombudsman_sectors.id
```

As FKs apontam para `contents(id)`. Funciona **por coincidência**: o `OmbudsmanSectorSeeder` importa as secretarias para `ombudsman_sectors` **preservando os IDs**. Qualquer setor criado manualmente com id diferente do id da secretaria quebra a FK.

### 2.11 [MÉDIO] Relations com classes inexistentes

| Model | Relação | Classe importada | Existe? |
|---|---|---|---|
| `OmbudsmanOrigin` | `ombudsmans()` | `App\Ombudsman` | **Não** |
| `OmbudsmanResponseMethod` | `ombudsmans()` | `App\Ombudsman` | **Não** |
| `OmbudsmanSubject` | `ombudsmans()` | `App\OmbudsmanRequest` | **Não** |
| `OmbudsmanManifestationType` | `ombudsmans()` | `App\OmbudsmanRequest` | **Não** |

Qualquer chamada a essas relações gera `Class "App\Ombudsman" not found`. Nenhuma é usada no fluxo atual — são código morto.

### 2.12 [MÉDIO] `OmbudsmanOrigin` com `description` no `$fillable` sem coluna

`protected $fillable = ['title', 'description'];` — a tabela `ombudsman_origins` **não tem** a coluna `description` (verificado no `\d`). O `OmbudsmanOriginRequest` só valida `title`, então não quebra hoje — mas qualquer uso futuro causa `QueryException`.

### 2.13 [BAIXO] `OmbudsmanRequest` sem `SoftDeletes` mas com coluna `deleted_at`

Cinco models têm a coluna `deleted_at` na tabela mas **não** usam `SoftDeletes`: `OmbudsmanRequest`, `OmbudsmanOrigin`, `OmbudsmanResponseMethod`, `OmbudsmanManifestationType`, `OmbudsmanCannedResponse`.

Consequências:
- `Model::all()` e `Model::where(...)` **incluem** os registros "excluídos".
- Os escopos do model usados pelo relatório (`getByBetweenDate`, `getByBetweenDateWithAnswer`, `byDateBetweenMain`) **não filtram** `deleted_at` → o relatório público pode contar manifestações excluídas.
- As views SQL **filtram** (`WHERE deleted_at IS NULL`), criando uma divergência entre o que o CMS mostra e o que o relatório conta.

### 2.14 [BAIXO] `Route::any` nas rotas do portal

`Route::any('/ouvidoria', ...)` e `Route::any('/ouvidoria/store', ...)` aceitam qualquer método HTTP. Um `GET /ouvidoria/store` com os campos na query string criaria uma manifestação (e dispararia 2 e-mails).

### 2.15 [BAIXO] Descrição de log do activity imprecisa

`prorrogar_chamado` registra `'Chamado da ouvidoria respondido'` (e o detalhe menciona `Status:prorrogada`). O evento é uma **prorrogação**, não uma resposta — confunde a auditoria.

### 2.16 [BAIXO] `OmbudsmanPortalRequest` com `authorize()` = `false`

Classe não utilizada, com `authorize()` retornando `false` (bloquearia todos os requests se plugada) e regras apontando para a tabela inexistente `ombudsman_types` (a real é `ombudsman_manifestation_types`).

### 2.17 [BAIXO] Caracteres de acento na comparação de `modality` (ver §2.6)

Detalhado em §2.6.

## 3. Funcionalidades incompletas ou mortas

| Item | Evidência | Situação |
|---|---|---|
| `ombudsman_answer_archives` (pivô) | Ausente na v6; `archives()` comentado | **Anexos em respostas não funcionam** |
| `OmbudsmanOrigin::ombudsmans()` | Aponta para classe inexistente | Código morto |
| `OmbudsmanResponseMethod::ombudsmans()` | idem | Código morto |
| `OmbudsmanSubject::ombudsmans()` | Aponta para `App\OmbudsmanRequest` (inexistente) | Código morto |
| `OmbudsmanManifestationType::ombudsmans()` | idem | Código morto |
| `prefecture/ombudsman/index.blade.php` | Carrega `contents/feriados-municipais/datatable.js` e verifica `feriados-municipais.delete` | **Cópia do CRUD de Feriados Municipais** — sem controller |
| `prefecture/ombudsman/edit.blade.php` e `form.blade.php` | Formulário "Configurações de Ouvidoria" | **Sem controller/rota** — a configuração da Ouvidoria não é editável |
| `portal/layouts/modelo{1,2,5,6}/informacoes-instituicao/ouvidoria/teste/*` | 3 templates por modelo, em 4 modelos | **Não usados** — o controller usa sempre `prefecture.emails.*` |
| `portal/layouts/modelo{1..6}/informacoes-instituicao/ouvidoria/email-admin/*` | 3 templates por modelo | **Não usados** |
| `ombudsman_canned_responses` (tabela) | Só lida (via `respostas_prontas`); nunca gravada pelo módulo | Depende do CMS |
| `full_list_requests_disabled` (configuration) | Lida no `relStatitics` e passada à view | Uso [NÃO VERIFICADO] na view |
| `ombudsman_request_logs.type` enum | Valores `status_change`, `closed`, `reopened`, `comment` | **Nunca gravados** — só `forward` é usado |
| `OmbudsmanRequest::accessLogs` / `ombudsman_request_logs` (v5) | — | — |
| `configurations.response_time` | Usado só para **exibir** o prazo ao cidadão | **Não** usado no cálculo do relatório (§4) |
| Statuses 5, 6, 7 (Aberta, Finalizada, Cancelada) | Não usados pelo módulo | Legado do `statuses` compartilhado |
| `user_option` (campo) | Gravado, não exibido no CMS nem no portal | Dado capturado e perdido |
| `ip` e `browser` | Gravados; exibidos no e-mail ao setor e no JSON do CMS | Usos questionáveis (ver §5) |

## 4. Inconsistências de regra de negócio

| # | Inconsistência | Detalhe |
|---|---|---|
| I1 | **Duas fontes para o prazo** | O relatório usa o literal `20` no `whereRaw`; o prazo informado ao cidadão vem de `configurations.response_time` (valor atual: 20). Alterar a configuração **não** altera o relatório. |
| I2 | **Status 4 medido por `updated_at`** | `checkParameters()` usa `updated_at` para status ≠ 1,2,3. Editar um chamado prorrogado altera o cálculo de prazo. |
| I3 | **`groupBy` no escopo de contagem** | `getByBetweenDate` encadeia `->groupBy('status_id')->count()`. Sem impacto hoje (só `count()` é usado), mas torna a query ambígua. |
| I4 | **Prazo truncado ao dia** | `EXTRACT(DAY FROM interval)` trunca: 20 dias e 23 horas = 20 dias (no prazo); 20 dias e 1 minuto = 20 dias (no prazo). O corte é no dia inteiro. |
| I5 | **Contagem inclui registros excluídos** | §2.13 |
| I6 | **`modalidade` na view vs `anonymous` no model** | A view calcula `anonimo` a partir de `r.anonymous` e `modalidade` a partir de `r.modality` — dois campos independentes que podem divergir. |
| I7 | **Busca do assunto de destino por título** | §2.9 |
| I8 | **`masked_name` para `sigiloso`** | `maskName($name, $modality)` — o tratamento da modalidade `sigiloso` [NÃO VERIFICADO] em detalhe dentro do trait. |

## 5. Riscos de segurança e LGPD

| # | Risco | Gravidade | Situação |
|---|---|---|---|
| S1 | **Consulta por protocolo expõe dados pessoais em claro** | Alta | `getOmbudsmanByProtocol` devolve o model completo (`name`, `email`, `phone`, `address`, `description`, `ip`, `browser`, `token`). A página "Consultar Solicitação" exibe `description` e `name` em claro a quem tiver o protocolo. |
| S2 | **`token` (hash) exposto no JSON do CMS** | Média | `dados_do_chamado` devolve o model completo. |
| S3 | **`ip` e `browser` do cidadão enviados ao setor** | Média | O `enviar_chamado` envia `ip` e `browser` ao destinatário do encaminhamento. |
| S4 | **Captcha não validado** | Alta | §2.5 |
| S5 | **`Route::any` permite criação por GET** | Média | §2.14 |
| S6 | **7 controllers sem `check.route.permission`** | Alta | §2.2 — [INFERÊNCIA] |
| S7 | **`dd($e)` expõe detalhes internos** | Alta | §2.3 |
| S8 | **Exportação do DataTable do CMS com dados em claro** | Média | A view `chamados_ouvidoria` expõe `protocolo`, `descricao_resumida` (100 chars) e `endereco`; o botão de exportar está disponível. |
| S9 | **E-mail ao cidadão não mascarado** | Baixa (por decisão) | O cidadão precisa receber a resposta real. |
| S10 | **Dependência externa de mascaramento** | Média | `maskTextWithAi` — se a API falhar **dentro** da transação, o chamado é revertido e o cidadão não consegue se manifestar. |
| S11 | **Ausência de rate limit** | Média | Nenhum throttle nas rotas de criação/consulta/contestação. |
| S12 | **Nenhum papel com permissões `ouvidoria.*`** | Média | Na prática só `super-admin` acessa — o que *reduz* a exposição, mas indica que o controle de acesso não foi configurado. |
| S13 | **Ausência de `SoftDeletes` nos models** | Média | Exclusão de manifestação não remove o registro do banco; os dados pessoais permanecem (ver §2.13). |

## 6. Riscos de dados (migração de schema)

| # | Risco | Gravidade | Detalhe |
|---|---|---|---|
| D1 | **Divergência de colunas v5 × v6 em `ombudsman_requests`** | Alta | v5: `modality`, `cpf_cnpj`, `gender`, `education_level`, `birth_date`, `attachment`. v6: `modality`, `token`, `answer_contestation*`, `mask_description`. A migração de dados precisa mesclar dois conjuntos. |
| D2 | **Datas de migration divergentes para a mesma coluna** | Média | `modality` é adicionada por `2026_05_25` (v5) e por `2026_02_04` (v6) — se o banco for recriado a partir das migrations da v6, a coluna `modality` existirá; se a base for herdada da v5, a coluna também existe (mesmo nome) — mas com **conjunto de colunas complementar** diferente (a v5 não tem `token` nem `answer_contestation`). |
| D3 | **Tabela `ombudsman_answer_archives` só na v5** | Alta | Os vínculos resposta↔anexo precisam ser recriados a partir da v5 (se a base v6 não os tiver). |
| D4 | **FK de `ombudsman_request_logs.sector_*` para `contents`** | Média | Migrar para `ombudsman_sectors` exige mapear por `content_id`. |
| D5 | **Views SQL recriadas em 3 migrations** | Média | `chamados_ouvidoria` foi criada e recriada duas vezes; `assuntos_ouvidoria`, idem. Na v7, substituir por queries Eloquent elimina essa fragilidade. |
| D6 | **Status ids compartilhados com outros módulos** | Média | `statuses` é usada por vários módulos; os ids 1-4 têm significado fixo neste módulo mas podem ter outros significados em outros. |
| D7 | **Dados pessoais sem expurgo** | Média | Não há rotina de expurgo nem anonimização de manifestações antigas; `mask_description` só protege o relatório, não o banco. |
| D8 | **Anexos de resposta em disco público** | Média | `storage/app/public/answer/{id}/` — servidos publicamente pelo `Archive->path` (`url("storage/answer/...")`). |

## 7. Perguntas em aberto para o negócio

1. **Os campos demográficos** (`cpf_cnpj`, `gender`, `education_level`, `birth_date`) foram removidos **intencionalmente** por LGPD, ou por perda? Se intencional, o relatório estatístico perde a possibilidade de cruzar por perfil do solicitante.
2. **O Relatório Estatístico institucional (PDF anual)** deve voltar? Ele permitia publicar o documento oficial com metodologia e assinaturas, que o relatório dinâmico não substitui.
3. **A máscara de `sigiloso`** deve ocultar o nome por completo no CMS? Hoje `masked_name` é usado apenas no relatório público — no CMS o nome é exibido em claro (necessário para o atendimento).
4. **Quantas contestações** devem ser aceitas? O modelo comporta 2; a lógica atual sobrescreve a 1ª numa 3ª tentativa.
5. **Qual o prazo oficial** de resposta: 20 dias (hardcoded no relatório) ou `configurations.response_time`? Devem ser a mesma fonte.
6. **O que deve acontecer com o chamado quando o assunto é desativado** (`ombudsman_subjects.ativo`)? O formulário do portal só lista assuntos não excluídos; não há filtro por `ativo` — que não existe na tabela de assuntos (só `deleted_at`).
7. **O armazenamento dos anexos em respostas** deve ser recuperado com a tabela pivô ou substituído por `Storage` + tabela própria?
8. **O relatório deve incluir manifestações anônimas?** Hoje inclui (só mascara o nome).

## 8. Resumo de prioridades para a v7

| Prioridade | Item |
|---|---|
| **P0** | Corrigir `OmbudsmanRequestController@update` (§2.1) — perda de dados |
| **P0** | Restaurar os anexos em respostas (pivô + método) (§1.2) |
| **P0** | Registrar `check.route.permission` nos 7 controllers (§2.2) |
| **P0** | Remover `dd($e)` (§2.3) |
| **P0** | Corrigir a validação de `modality` (§2.4) e reativar o captcha (§2.5) |
| **P0** | Mascarar a resposta da consulta por protocolo (§5 S1) |
| **P1** | Corrigir a comparação de `modality == 'Anônimo'` (§2.6) |
| **P1** | Gerar protocolo sem colisão (§2.7) |
| **P1** | Corrigir o `null->title` na tela de resposta (§2.8) |
| **P1** | Unificar a fonte do prazo (I1) |
| **P1** | Corrigir o FK de `sector_from_id`/`sector_to_id` (§2.10) |
| **P1** | Recriar o módulo de Relatório Estatístico de Ouvidoria (§1.1) |
| **P2** | Adicionar `SoftDeletes` aos 5 models (§2.13) |
| **P2** | Trocar `Route::any` por `GET`/`POST` (§2.14) |
| **P2** | Corrigir as relações com classes inexistentes (§2.11) |
| **P2** | Buscar o assunto de destino por **id**, não por título (§2.9) |
| **P2** | Rate limiting nas rotas públicas (§5 S11) |
| **P3** | Remover `OmbudsmanPortalRequest` (§2.16) |
| **P3** | Remover `description` do `$fillable` de `OmbudsmanOrigin` (§2.12) |
| **P3** | Remover as views órfãs e os templates de e-mail não usados (§3) |
| **P3** | Corrigir a descrição do log de activity (§2.15) |
| **P3** | Mover anexos de resposta para disco privado (§6 D8) |
