# Rotas e Controllers

## 1. Rotas públicas (portal)

Arquivo: `V2/Modules/DadosAbertos/TransparenciaFiscal/Routes/web.php`
(montado com middleware `web` + `MaintenanceMiddleware`, sem prefixo de grupo).

| Método | URI | Nome | Controller@ação | Middleware |
|---|---|---|---|---|
| GET | `/transparencia` | `v2.transparencia.portal.index` | `Portal\TransparencyPortalController@index` | `web`, maintenance |
| GET | `/transparencia/{slug}` | `v2.transparencia.portal.show` | `Portal\TransparencyPortalController@show` | `web`, maintenance |

Código real:

```php
// V2/Modules/DadosAbertos/TransparenciaFiscal/Routes/web.php
Route::get('/transparencia', 'Portal\TransparencyPortalController@index')->name('v2.transparencia.portal.index');
Route::get('/transparencia/{slug}', 'Portal\TransparencyPortalController@show')->name('v2.transparencia.portal.show');
```

Notas:

- não há restrição de formato no `{slug}`;
- não há rota de busca, exportação ou API;
- a rota legada V1 continua registrada em paralelo
  (`routes/web.php:102-111`, `Portal\TransparencyController`), mas **não é
  alcançável**: `V2ServiceProvider` é declarado antes do `RouteServiceProvider`
  (`config/app.php:218`, com comentário explícito em `:213-217`), então a rota
  V2 registra primeiro e vence a resolução de `/transparencia`;
- os aliases V1 (`/transparencia-fiscal`, `/portal-da-transparencia`,
  `/portal-transparencia`) continuam ativos e servem o controlador legado.

## 2. Rotas de CMS

Arquivo: `V2/Modules/DadosAbertos/TransparenciaFiscal/Routes/cms.php`.
Prefixo `cms/v2/transparencia`, middleware de grupo
`['v2.authenticate', 'v2.permission:transparencia,read']`, namespace
`V2\Modules\DadosAbertos\TransparenciaFiscal\Controllers`.

| Método | URI | Nome | Controller@ação | Middleware adicional |
|---|---|---|---|---|
| GET | `/cms/v2/transparencia` | `v2.transparencia.items.index` | `Admin\TransparencyCmsController@index` | — |
| GET | `/cms/v2/transparencia/create` | `v2.transparencia.items.create` | `@create` | `v2.permission:transparencia,write` |
| POST | `/cms/v2/transparencia` | `v2.transparencia.items.store` | `@store` | idem |
| GET | `/cms/v2/transparencia/{id}/edit` | `v2.transparencia.items.edit` | `@edit` | idem |
| POST | `/cms/v2/transparencia/{id}` | `v2.transparencia.items.update` | `@update` | idem |
| POST | `/cms/v2/transparencia/{id}/delete` | `v2.transparencia.items.destroy` | `@destroy` | idem |

Além disso, o provider monta `module.active:TransparenciaFiscal` no grupo CMS
(`V2/Core/Providers/V2ServiceProvider.php:521-524`).

Importante: os parâmetros `transparencia,read|write` passados ao alias
`v2.permission` são **inertes** — o middleware não os recebe (ver §3.2). A
distinção entre leitura e escrita, na prática, é feita pelo método HTTP e pelo
`v2_can()` dentro do controller.

Observações:

- `update` e `destroy` usam **POST**, não `PUT`/`DELETE` — padrão da casa no
  Laravel 5.5 (`Route::match` ou POST + sufixo);
- **não existe rota de categorias** (nem index, nem create). Categorias só
  nascem por seeder/importador;
- `{id}` é UUID sem `where` de formato → qualquer string chega ao
  `findOrFail`.

## 3. Middlewares e autorização

### 3.1 Cadeia real

```php
// V2/Modules/DadosAbertos/TransparenciaFiscal/Routes/cms.php:5
Route::group(['prefix' => 'cms/v2/transparencia','middleware' => ['v2.authenticate','v2.permission:transparencia,read']], function () {
```

Aliases registrados em `app/Http/Kernel.php:74-76`:

| Alias | Classe |
|---|---|
| `module.active` | `V2\Core\Middleware\ModuleAccessMiddleware` |
| `v2.authenticate` | `V2\Core\Middleware\CmsAuthenticate` |
| `v2.permission` | `V2\Core\Middleware\V2PermissionMiddleware` |

### 3.2 Como a permissão é realmente resolvida

`V2PermissionMiddleware::handle($request, Closure $next)` **não recebe** os
parâmetros `transparencia,read`. Ele percorre `v2.modules.configs` e encontra o
módulo cujo `active_pattern` casa com a URL
(`V2/Core/Middleware/V2PermissionMiddleware.php:40-48`), obtendo a chave
canônica `TransparenciaFiscal`.

| Passo | Regra | Linha |
|---|---|---|
| Sem login | 401 JSON ou redirect para `v2.cms.login` | `:25-30` |
| Grupo `Support` | só `config('v2.support.email')` | `:51-67` |
| Suporte/super-admin | bypass total | `:70-72` |
| URL fora de módulo | libera | `:75-77` |
| `none` | 403/redirect dashboard | `:85-99` |
| `read` | libera GET/HEAD; bloqueia escrita | `:102-122` |
| `write` | libera | `:125-127` |

### 3.3 Checagem redundante no controller

Todos os métodos repetem a checagem via helper:

```php
// V2/Modules/DadosAbertos/TransparenciaFiscal/Controllers/Admin/TransparencyCmsController.php:29-31
if (!\v2_can('transparencia', 'read')) {
    abort(403);
}
```

`v2_can('transparencia', ...)` resolve o alias para `TransparenciaFiscal`
(`V2/Core/Services/PermissionService.php:28`) e retorna `true` para o e-mail de
suporte (`V2/Core/Helpers/helpers.php:20-22`). Como o middleware já barrou,
essas linhas são defesa em profundidade — e a única forma de acesso quando a
rota é chamada fora do grupo (ex.: testes unitários do controller).

`index` checa `read` (`:29`); os demais checam `write` (`:57`, `:67`, `:83`,
`:94`, `:109`).

## 4. Actions do CMS em detalhe

### 4.1 `index`

```php
// V2/Modules/DadosAbertos/TransparenciaFiscal/Controllers/Admin/TransparencyCmsController.php:32-50
$search = $r->get('search', '');
$catId = $r->get('category', '');
$sort = $r->get('sort', 'reference_date');
$dir = $r->get('dir', 'desc');
$allowed = ['title','value','reference_date','is_active','created_at'];
$sort = in_array($sort, $allowed) ? $sort : 'reference_date';
$dir = in_array($dir, ['asc','desc']) ? $dir : 'desc';
$items = V2TransparencyItem::search($search)->when($catId, function ($q) use ($catId) {
    return $q->where('v2_transparency_category_id', $catId);
})->orderBy($sort, $dir)->paginate(20)->appends($r->query());
$cats = V2TransparencyCategory::ordered()->get();
```

| Parâmetro | Efeito | Validação |
|---|---|---|
| `search` | `ILIKE %termo%` em `title` | nenhuma (binding parametrizado) |
| `category` | igualdade exata em `v2_transparency_category_id` | nenhuma; id inválido ⇒ lista vazia |
| `sort` | coluna de ordenação | whitelist `title, value, reference_date, is_active, created_at` |
| `dir` | `asc`/`desc` | whitelist |
| página | `?page=` | `paginate(20)` + `appends(query)` |

Não há filtro por período, status ou categoria hierárquica. A lista de
categorias do filtro é **plana e inclui inativas** (`ordered()` sem `active()`).

Efeito colateral: grava log `transparencia/index` com total, busca, categoria,
sort e dir a cada abertura da tela (`:43-49`).

### 4.2 `store`

```php
// V2/Modules/DadosAbertos/TransparenciaFiscal/Controllers/Admin/TransparencyCmsController.php:65-76
$data = $r->validated();
$data['id'] = \v2_uuid();
$item = V2TransparencyItem::create($data);
$this->audit->activity($item, 'created', ['title' => $item->title]);
$this->cmsLog->info('transparencia', 'store', ['id' => $item->id, 'title' => $item->title]);
return redirect()->route('v2.transparencia.items.index')->with('success', 'Item criado.');
```

`slug` não é enviado pelo formulário; o hook `creating` do model o gera.

### 4.3 `update`

```php
// V2/Modules/DadosAbertos/TransparenciaFiscal/Controllers/Admin/TransparencyCmsController.php:97-101
$item = V2TransparencyItem::findOrFail($id);
$item->update($r->validated());
$this->audit->activity($item, 'updated', ['title' => $item->title]);
```

`$r->validated()` não inclui `slug` nem `id` — o slug permanece o da criação
mesmo se o título mudar.

### 4.4 `destroy`

```php
// V2/Modules/DadosAbertos/TransparenciaFiscal/Controllers/Admin/TransparencyCmsController.php:112-117
$item = V2TransparencyItem::findOrFail($id);
$t = $item->title;
$item->delete();
$this->audit->activity($item, 'deleted', ['title' => $t]);
```

Soft delete (trait `SoftDeletes`); o item sai do portal (`active()` não filtra
`deleted_at`, mas o global scope do SoftDeletes sim).

## 5. Form Request

`V2/Modules/DadosAbertos/TransparenciaFiscal/Requests/TransparencyRequest.php`

```php
// :12-23
public function authorize()
{
    return \v2_can('transparencia', 'write');
}
public function rules()
{
    $id = $this->route('id') ?: 'NULL';
    return['title' => 'required|string|max:255','description' => 'nullable|string','value' => 'nullable|numeric|min:0','reference_date' => 'nullable|date','is_active' => 'boolean','v2_transparency_category_id' => 'nullable|string|exists:v2_transparency_categories,id'];
}
```

| Campo | Regras | Observação |
|---|---|---|
| `title` | required, string, max 255 | mensagem customizada `'O título é obrigatório.'` |
| `description` | nullable, string | sem limite de tamanho |
| `value` | nullable, numeric, min 0 | aceita notação científica; não valida escala de 2 casas |
| `reference_date` | nullable, date | aceita qualquer formato `strtotime` |
| `is_active` | boolean | checkbox ausente ⇒ não validado, e o `update` mantém o valor anterior |
| `v2_transparency_category_id` | nullable, string, exists | `exists` em tabela sem escopo de soft delete/ativa |

Problemas:

1. **`$id` calculado e não usado** (`:21`) — resquício de regra `unique` que
   nunca existiu para `slug`.
2. `authorize()` duplica o `v2_can()` do controller; se a rota não estiver no
   grupo, retorna 403 antes do controller — comportamento divergente entre
   chamadas HTTP e testes diretos.
3. `is_active` não tem `sometimes`/default: em `store` o `validated()` pode não
   conter a chave, e o default do banco (`true`) vale. Em `update`, desmarcar o
   checkbox **não envia o campo**, então `update()` nunca desativa por esse
   caminho — o formulário de edição envia `is_active=1` quando marcado e omite
   quando desmarcado; como `validated()` só inclui o que veio, o item
   permanece ativo. Ver `08-GAPS-E-DIVIDAS.md`.
4. `value` aceita notação científica e qualquer escala decimal (`1.5e3`, `10.999`),
   o que conflita com `decimal(15,2)` e com a exibição em centavos.

## 6. Presenters / API Resources

Não existem. As views acessam o model diretamente; não há serialização JSON nem
transformação de saída.

## 7. Permissões exigidas

| Superfície | Chave canônica | Nível | Onde é exigido |
|---|---|---|---|
| CMS (todas as rotas) | `TransparenciaFiscal` | `read` (GET) / `write` (POST) | `V2PermissionMiddleware` |
| CMS (controllers) | `transparencia` → alias `TransparenciaFiscal` | `read`/`write` | `v2_can()` |
| Portal | nenhuma | público | — |
| Importador | `Support` (rota de Migração Legada) | — | outro módulo |

A matriz de perfis grava `permissions_v2` como jsonb
(`v2_roles.permissions_v2`) e `users.v2_role_id`; o teste
`tests/Feature/V2/Rbac/CommonUserModuleAccessE2ETest.php:52` valida as duas
grafias (`TransparenciaFiscal` e `transparencia`).
