# Ouvidoria — Rotas e Controladores

> **Slug:** `Ouvidoria` · **Versão analisada:** `lotep` (Laravel 5.5.50 / PHP 7.4)
>
> Superfície HTTP completa: seis rotas, dois controllers, validação inline e permissões em dois
> níveis. Não há Form Request, API Resource, presenter nem rota de API.

---

## 1. Rotas do CMS (backoffice)

Prefixo do grupo: `/cms/v2/ouvidoria`. O arquivo é carregado pelo `V2ServiceProvider` com
namespace `V2\Modules\Servicos\Ouvidoria\Controllers` e middleware global
`module.active:Ouvidoria` (`V2/Core/Providers/V2ServiceProvider.php:521-524`).

| Método | URI | Nome | Controller@action | Middleware | Permissão |
|---|---|---|---|---|---|
| GET | `/cms/v2/ouvidoria/requests` | `v2.ouvidoria.requests.index` | `Admin\OmbudsmanRequestCmsController@index` | `v2.authenticate`, `v2.permission:ouvidoria,read` | `ouvidoria` nível `read` ou `write` |
| GET | `/cms/v2/ouvidoria/requests/{id}` | `v2.ouvidoria.requests.show` | `Admin\OmbudsmanRequestCmsController@show` | idem | idem |
| POST | `/cms/v2/ouvidoria/requests/{id}/status` | `v2.ouvidoria.requests.status` | `Admin\OmbudsmanRequestCmsController@updateStatus` | + `v2.permission:ouvidoria,write` | `write` |
| POST | `/cms/v2/ouvidoria/requests/{id}/answer` | `v2.ouvidoria.requests.answer` | `Admin\OmbudsmanRequestCmsController@answer` | + `v2.permission:ouvidoria,write` | `write` |

Fonte: `V2/Modules/Servicos/Ouvidoria/Routes/cms.php:5-10`.

O `{id}` é a PK `varchar(36)`, **não** o protocolo. Não há *route model binding*: o controller
recebe `$id` e chama `findOrFail($id)`.

---

## 2. Rotas do Portal (público)

| Método | URI | Nome | Controller@action | Parâmetros | Observação |
|---|---|---|---|---|---|
| GET | `/ouvidoria` | `v2.ouvidoria.portal.index` | `Portal\OmbudsmanPortalController@index` | — | sem throttle |
| POST | `/ouvidoria/store` | `v2.ouvidoria.portal.store` | `Portal\OmbudsmanPortalController@store` | — | `throttle:10,1` |

Fonte: `V2/Modules/Servicos/Ouvidoria/Routes/web.php:5-6`. O `throttle:10,1` significa **10
requisições por minuto por endereço IP** (o key padrão do middleware `ThrottleRequests` é o IP
para usuários não autenticados). A rota de leitura não tem rate limit.

### 2.1 Coexistência com a V1

`routes/web.php:58-59` registra, no mesmo aplicativo:

```php
Route::any('/ouvidoria', 'Portal\OmbudsmanController@getIndex')->name('ouvidoria.index');
Route::any('/ouvidoria/store', 'Portal\OmbudsmanController@store')->name('ouvidoria.store');
```

O `store` da V1 começa com `dd($request->all())` (`app/Http/Controllers/Portal/OmbudsmanController.php:129`),
ou seja, se essa rota vencer a resolução, o formulário público despeja o *dump* e encerra. As
duas famílias usam URIs idênticas (`/ouvidoria` e `/ouvidoria/store`); qual vence depende da
ordem de boot dos providers (`RouteServiceProvider` versus `V2ServiceProvider`). Esse
comportamento **não foi verificado em runtime** e é um risco aberto em `08-GAPS-E-DIVIDAS.md`.

---

## 3. Rotas de API

Não existem. O diretório `V2/Modules/Servicos/Ouvidoria/Routes/` contém apenas `web.php` e `cms.php`, e o `V2ServiceProvider` só registra API quando
o arquivo existe (`V2/Core/Providers/V2ServiceProvider.php:527-534`). O portal e o CMS são a
única superfície: não há JSON, exportação, webhook ou polling.

---

## 4. Rotas de ajax

Nenhuma. As views usam formulários HTML clássicos com `POST` + `csrf_field()` e recarregam a
página. Não há chamadas `fetch`/`jQuery.ajax` no módulo.

---

## 5. Controladores

| Controller | Caminho | Actions | Observação |
|---|---|---|---|
| `OmbudsmanPortalController` | `V2/Modules/Servicos/Ouvidoria/Controllers/Portal/OmbudsmanPortalController.php` | `index`, `store` | 38 linhas; sem construtor |
| `OmbudsmanRequestCmsController` | `V2/Modules/Servicos/Ouvidoria/Controllers/Admin/OmbudsmanRequestCmsController.php` | `index`, `show`, `updateStatus`, `answer` | 93 linhas; injeta `V2AuditLogger` e `CmsActionLogger` |

Ambos estendem `V2\Core\Controllers\BaseController`, que oferece `ValidatesRequests` e o
método protegido `logActivity()` (não usado aqui).

### 5.1 `OmbudsmanPortalController`

| Action | Responsabilidade | Valida | Resposta |
|---|---|---|---|
| `index` | carrega dicionários ativos e ordenados | não | `view('v2-ouvidoria::portal.index')` |
| `store` | cria a manifestação | `$r->validate([...])` inline | `redirect()->back()` com flash de sucesso contendo o protocolo |

O `index` executa quatro consultas independentes:

```php
// V2/Modules/Servicos/Ouvidoria/Controllers/Portal/OmbudsmanPortalController.php:20-23
$types = OmbudsmanManifestationType::active()->ordered()->get();
$subjects = OmbudsmanSubject::active()->ordered()->get();
$origins = OmbudsmanOrigin::active()->ordered()->get();
$methods = OmbudsmanResponseMethod::active()->ordered()->get();
```

O `store` monta o payload a partir da requisição inteira:

```php
// V2/Modules/Servicos/Ouvidoria/Controllers/Portal/OmbudsmanPortalController.php:32-35
$data = $r->all();
$data['id'] = \v2_uuid();
$data['ip'] = $r->ip();
$request = OmbudsmanRequest::create($data);
```

Como o model tem `status`, `protocol`, `is_anonymous` e `metadata` no `$fillable`
(`OmbudsmanRequest.php:73`), esses campos chegam ao `INSERT` se o cliente os enviar. O `id` é
sobrescrito pelo controller; `ip` também.

### 5.2 `OmbudsmanRequestCmsController`

| Action | Responsabilidade | Valida | Redirect / resposta |
|---|---|---|---|
| `index` | busca, ordena e pagina | whitelist de sort/dir; sem validação de `search` | view com `requests`, `search`, `sort`, `dir` |
| `show` | detalhe com dicionários e respostas | não | view com `request` |
| `updateStatus` | altera `status` | **nenhuma** — usa `$r->input('status', 1)` | `redirect()->back()` |
| `answer` | cria resposta e força status 2 | `'answer' => 'required|string'` | `redirect()->back()` |

O `index` implementa a ordenação com whitelist:

```php
// V2/Modules/Servicos/Ouvidoria/Controllers/Admin/OmbudsmanRequestCmsController.php:33-40
$search = $r->get('search', '');
$allowedSorts = ['protocol', 'created_at', 'status'];
$sort = (string) $r->get('sort', 'created_at');
if (!in_array($sort, $allowedSorts, true)) {
    $sort = 'created_at';
}
$dir = strtolower((string) $r->get('dir', 'desc')) === 'asc' ? 'asc' : 'desc';
$requests = OmbudsmanRequest::search($search)->orderBy($sort, $dir)->paginate(20)->appends($r->query());
```

O `show` carrega tudo em uma consulta com *eager loading*:

```php
// V2/Modules/Servicos/Ouvidoria/Controllers/Admin/OmbudsmanRequestCmsController.php:58
$request = OmbudsmanRequest::with(['type','subject','origin','responseMethod','answers'])->findOrFail($id);
```

O `answer` **não** abre transação: a resposta é gravada e, em seguida, o status é atualizado.
Se a segunda escrita falhar, a resposta fica órfã de status:

```php
// V2/Modules/Servicos/Ouvidoria/Controllers/Admin/OmbudsmanRequestCmsController.php:85-88
$r->validate(['answer' => 'required|string']);
$request = OmbudsmanRequest::findOrFail($id);
OmbudsmanAnswer::create(['id' => \v2_uuid(),'answer' => $r->answer,'v2_ombudsman_request_id' => $id]);
$request->update(['status' => 2]);
```

---

## 6. Form Requests e validações

Não há classes de Form Request. As regras estão inline no controller e são o contrato de
validação:

| Superfície | Regras exatas |
|---|---|
| Portal `store` | `name` `nullable`/`string`/`max:255`; `email` `nullable`/`email`/`max:255`; `phone` `nullable`/`string`/`max:50`; `description` `required`/`string`; `v2_type_id` `nullable`/`string`/`exists:v2_ombudsman_manifestation_types,id`; `v2_subject_id` `nullable`/`string`/`exists:v2_ombudsman_subjects,id`; `v2_origin_id` `nullable`/`string`/`exists:v2_ombudsman_origins,id`; `v2_response_method_id` `nullable`/`string`/`exists:v2_ombudsman_response_methods,id` |
| CMS `answer` | `answer` `required`/`string` |
| CMS `updateStatus` | nenhuma |
| CMS `index` | nenhuma (apenas whitelist manual de `sort`/`dir`) |

Código completo do portal:

```php
// V2/Modules/Servicos/Ouvidoria/Controllers/Portal/OmbudsmanPortalController.php:31
$r->validate(['name' => 'nullable|string|max:255','email' => 'nullable|email|max:255','phone' => 'nullable|string|max:50','description' => 'required|string','v2_type_id' => 'nullable|string|exists:v2_ombudsman_manifestation_types,id','v2_subject_id' => 'nullable|string|exists:v2_ombudsman_subjects,id','v2_origin_id' => 'nullable|string|exists:v2_ombudsman_origins,id','v2_response_method_id' => 'nullable|string|exists:v2_ombudsman_response_methods,id']);
```

Campos **não** validados e ainda assim aceitos/relevantes:

- `address` (está no `fillable`, coluna `varchar(500)`; texto maior que 500 estoura no banco);
- `is_anonymous`, `status`, `protocol`, `metadata` (injeção por mass assignment);
- `user_option` (checkbox de consentimento da view, descartado pelo `fillable`).

Os `exists:` não restringem `is_active`: enviar o id de um dicionário inativo é aceito se o
cliente souber o UUID (o portal não o lista, mas a validação não bloqueia).

---

## 7. API Resources / Presenters

Nenhum. O CMS devolve `Illuminate\View\View`; o portal devolve
`Illuminate\Http\RedirectResponse`. Não há serialização JSON, DTO ou presenter. Os modelos são
passados crus para a Blade (o que é seguro porque a Blade escapa com `{{ }}`, exceto pelos
trechos que concatenam HTML no `index`, discutidos em `04-VIEWS-E-UX.md`).

---

## 8. Permissões exigidas

| Rota | Permissão | Comportamento se faltar |
|---|---|---|
| `GET /cms/v2/ouvidoria/requests` | `ouvidoria.read` (ou `write`) | middleware `none` → redirect ao dashboard; controller → `abort(403)` |
| `GET /cms/v2/ouvidoria/requests/{id}` | idem | idem |
| `POST .../status` | `ouvidoria.write` | `read` em POST → redirect com erro; `none` → dashboard; controller → 403 |
| `POST .../answer` | `ouvidoria.write` | idem |
| `GET/POST /ouvidoria*` | nenhuma | público |

Modelo de permissão (`V2/Core/Services/PermissionService.php:65-88`):

- bypass total para `config('v2.support.email')` e para usuário com role Sentinel `super-admin`;
- `permissions_v2` é um mapa `{chave: 'none'|'read'|'write'}` no `v2_roles.permissions_v2`;
- `write` implica `read`; `read` só autoriza ação `read`;
- a resolução por chave tem tolerância de caixa/aliases
  (`PermissionService::resolvePermission()`), o que faz `v2_can('ouvidoria')` casar com a chave
  canônica `Ouvidoria`.

O parâmetro textual do middleware (`:ouvidoria,read`) **não é usado**:
`V2PermissionMiddleware::handle($request, Closure $next)` não recebe `$module` nem `$action`
(`V2/Core/Middleware/V2PermissionMiddleware.php:22`). O módulo é resolvido pelo
`active_pattern` `cms/v2/ouvidoria*` e a ação pelo método HTTP (GET/HEAD = leitura; POST = escrita).

---

## 9. Breadcrumbs e menu

| Item de menu | Rota | Condição de exibição | Módulo |
|---|---|---|---|
| Ouvidoria (ícone `bi-chat-dots`, posição 71) | `v2.ouvidoria.requests.index` | manifesto do `ModuleProvider` + módulo ativo (`v2_module_flags`) + `v2_can` | `Ouvidoria` (grupo `Servicos`) |

Manifesto completo:

```php
// V2/Modules/Servicos/Ouvidoria/ModuleProvider.php:14
return['title' => 'Ouvidoria','description' => 'Sistema de ouvidoria e manifestações.','icon' => 'bi bi-chat-dots','position' => 71,'active_pattern' => 'cms/v2/ouvidoria*','route' => 'v2.ouvidoria.requests.index','group' => 'Serviços'];
```

Breadcrumbs nas views: `Serviços > Ouvidoria` no índice e
`Serviços > Ouvidoria > Manifestação` no detalhe, via
`@include('v2-cms::partials.breadcrumb', ...)` (`Views/cms/requests/index.blade.php:46-51`,
`Views/cms/requests/show.blade.php:3-9`). O item “Serviços” não tem rota.

---

## 10. Contrato de status e sort expostos pela rota

- `sort` aceita apenas `protocol`, `created_at`, `status`; qualquer outro valor cai em
  `created_at` (`...CmsController.php:34-38`).
- `dir` aceita apenas `asc`/`desc` (normalizado); qualquer outro vira `desc`.
- `status` no `POST /status` aceita **qualquer** valor enviado; não há whitelist. Um valor
  não numérico chega ao `UPDATE` de uma coluna `integer`, cujo resultado depende do *cast*
  implícito do PostgreSQL — comportamento não coberto por teste.
- `search` vazio (`''`, `null` ou `'0'`, por causa de `empty()`) devolve a lista inteira
  (`OmbudsmanRequest.php:150-152`).
