# Órgãos — API de Dados Abertos (SIM)

> Complemento de `03-ROTAS-E-CONTROLADORES.md`. Detalha a superfície REST de dados abertos do módulo
> Órgãos: rota, controller, base compartilhada, Form Request, Resource e contrato de saída.
> A camada de testes correspondente está em `07-TESTES.md` (seções 4 e 9); os bugs estão em
> `08-GAPS-E-DIVIDAS.md` (B1).

---

## 1. Rota

`v6/routes/api.php:17-31`

```php
Route::group(["prefix" => 'v1', "as" => 'v1.'], function () {
    ...
    Route::group(['prefix' => 'orgaos', 'as' => 'orgaos.'], function () {
        Route::get('/', 'Swagger\V1\Portal\OrgaoController@index')->name('index');
    });
    ...
});
```

| Método | URI | Nome | Controller@action | Middleware |
|---|---|---|---|---|
| GET | `/api/v1/orgaos` | `v1.orgaos.index` | `Swagger\V1\Portal\OrgaoController@index` | `api` |

| Parâmetro | Tipo | Validação | Efeito real no legado |
|---|---|---|---|
| `titulo` | string | `nullable`, `string` (`OrgaoRequest`) | **Nenhum** — o filtro é descartado (bug B1) |
| `descricao` | string | `nullable`, `string` (`OrgaoRequest`) | **Nenhum** — idem |
| `limite` | integer | **não validado** | Aplicado via `->limit()` em `Controller::getContent()` |

Não há autenticação nem rate limiting na rota.

## 2. `OrgaoController@index`

`v6/app/Http/Controllers/Swagger/V1/Portal/OrgaoController.php`

```php
class OrgaoController extends Controller
{
    public $category = 'orgaos';

    public function index(OrgaoRequest $request)
    {
        $title = $request->get('titulo');
        $description = $request->get('descricao');


        $contents = $this->getContent();

        if($title) {
            $contents->whereRaw($this->caseInsensitiveAccentFreeSearch($title, 'title'));
        }

        if($description) {
            $contents->whereRaw($this->caseInsensitiveAccentFreeSearch($description, 'text'));
        }

        $contents = $contents->get();
        return OrgaoResource::collection($contents);
    }
}
```

**BUG CONFIRMADO:** `Illuminate\Database\Eloquent\Builder::whereRaw()` **retorna uma nova instância** e
não altera a original. Como o retorno é descartado, os filtros por `titulo` e `descricao` **nunca** são
aplicados ao conjunto retornado. O mesmo defeito existe nos controllers irmãos de secretarias,
legislação, notícias e publicações, que reaproveitam o mesmo padrão a partir da base compartilhada.

Correção mínima (na base, resolve os cinco endpoints):

```php
// (ilustrativo)
$contents = $this->getContent()
    ->when($title, fn ($q) => $q->whereRaw($this->caseInsensitiveAccentFreeSearch($title, 'title')))
    ->when($description, fn ($q) => $q->whereRaw($this->caseInsensitiveAccentFreeSearch($description, 'text')));
```

## 3. Base compartilhada — `Swagger\V1\Portal\Controller::getContent()`

`v6/app/Http/Controllers/Swagger/V1/Portal/Controller.php`

```php
public function getContent()
{
    $this->limit = request()->get('limite');
    $content = Content::query()->with('archives');

    $directExists = Content::query()->whereHas('categoryContents', function ($query) {
        return $query->where('slug', $this->category);
    })->exists();

    $nestedExists = Content::query()->whereHas('categoryContents.categoryContents', function ($query) {
        return $query->where('slug', $this->category);
    })->exists();

    if ($directExists || $nestedExists) {
        $content->where(function ($query) use ($directExists, $nestedExists) {
            if ($directExists) {
                $query->orWhereHas('categoryContents', function ($query) {
                    return $query->where('slug', $this->category);
                });
            }
            if ($nestedExists) {
                $query->orWhereHas('categoryContents.categoryContents', function ($query) {
                    return $query->where('slug', $this->category);
                });
            }
        });
    }

    $content->orderByDesc('publication_date')->orderByDesc('clicks');
    if($this->limit && $this->limit != '') {
        $content->limit($this->limit);
    }
    return $content;
}
```

Observações:

- O filtro por categoria é feito por `orWhereHas` dentro de um `where(function(){})`, o que restringe
  corretamente aos conteúdos da categoria `orgaos` (id 32) **e** às suas subcategorias.
- `directExists` e `nestedExists` executam **duas queries extras** antes de qualquer filtro.
- `limit` só é aplicado se `limite` vier preenchido; sem o parâmetro, a API retorna **todos** os órgãos
  (com `with('archives')`, ou seja, todos os arquivos de cada órgão).
- A ordenação é `publication_date DESC, clicks DESC`. Os 3 órgãos atuais têm `publication_date` NULL, de
  modo que a ordenação efetiva é definida apenas por `clicks`.

### 3.1 Busca sem acentos — `caseInsensitiveAccentFreeSearch()`

```php
public function caseInsensitiveAccentFreeSearch($search, $column)
{
    $acentos = array(
        'A' => '/&Agrave;|&Aacute;|&Acirc;|&Atilde;|&Auml;|&Aring;/',
        ...
    );

    $replaceToNonAccentsChars = preg_replace($acentos, array_keys($acentos), htmlentities($search, ENT_NOQUOTES, 'UTF-8'));
    $toLowerChars = mb_strtolower($replaceToNonAccentsChars, 'utf-8');
    return "lower(
        TRANSLATE($column,'áéíóúàèìòùãõâêîôôäëïöüçÁÉÍÓÚÀÈÌÒÙÃÕÂÊÎÔÛÄËÏÖÜÇ','aeiouaeiouaoaeiooaeioucAEIOUAEIOUAOAEIOOAEIOUC')
    ) like '%$toLowerChars%'";
}
```

Características:

- Remove acentos do **termo de busca** via substituição de entidades HTML e `mb_strtolower`.
- Compara com `lower(TRANSLATE(coluna, ...))` — normaliza a coluna no SQL.
- **Interpola o termo diretamente no SQL, sem binding.** Como o efeito atual é nulo (bug B1), o risco
  fica latente; **ao corrigir B1, é obrigatório usar binding** (`whereRaw($sql, [$termo])`).
- A coluna é interpolada pelo nome literal (`'title'`, `'text'`), nunca pelo usuário — esse trecho não é
  injetável.

## 4. Form Request — `OrgaoRequest`

`v6/app/Http/Requests/Swagger/V1/Portal/OrgaoRequest.php`

```php
public function authorize()
{
    return true;
}

public function rules()
{
    return [
        "titulo" => ["nullable", "string"],            
        "descricao" => ["nullable", "string"],
    ];
}

protected function failedValidation(Validator $validator)
{
    throw new HttpResponseException(response()->json([
        'errors' => $validator->errors(),
    ], 422));
}
```

Observações:

- `authorize()` retorna `true` (endpoint público).
- `failedValidation` sobrescreve o handler padrão para devolver **422 com envelope `errors`** (JSON),
  em vez do redirect de um Form Request de formulário web.
- O parâmetro `limite` **não é validado** por este Request (apenas lido em `Controller::getContent()`).

## 5. API Resource — `OrgaoResource`

`v6/app/Http/Resources/Swagger/V1/Portal/OrgaoResource.php`

```php
class OrgaoResource extends Resource
{
    public function toArray($request)
    {        
        $description = strip_tags(trim(data_get($this, "text")));
        return [
            "titulo" => data_get($this, "title"),            
            "descricao" => $this->when($description != "", $description),
            "interacoes" => data_get($this, "clicks"),
            "slug" => data_get($this, "slug"),
            "arquivos" => ArchiveResource::collection($this->whenLoaded("archives"))
        ];
    }
}
```

Regras do contrato:

| Chave | Origem | Regra |
|---|---|---|
| `titulo` | `contents.title` | sempre presente |
| `descricao` | `contents.text` | `strip_tags(trim(...))`; **ausente** se o texto for vazio (`when`) |
| `interacoes` | `contents.clicks` | inteiro; incrementado a cada visita à página do órgão |
| `slug` | `contents.slug` | usado para montar a URL `/a-instituicao/orgaos/{slug}` |
| `arquivos` | relação `archives` | `ArchiveResource::collection($this->whenLoaded("archives"))` |

`ArchiveResource` (`v6/app/Http/Resources/Swagger/V1/Portal/ArchiveResource.php`):

```php
return [
    "titulo" => $this->when(!empty($title), $title),
    "descricao" => $this->when(!empty($description), $description),
    "tipo" => $this->when(!empty($type), $type),
    "url" => $this->when(!empty($path), url($path)),
];
```

## 6. Formato de saída

```json
{
  "data": [
    {
      "titulo": "PROCON",
      "descricao": "Em breve...",
      "interacoes": 0,
      "slug": "procon",
      "arquivos": [
        { "titulo": "...", "descricao": "...", "tipo": "...", "url": "https://..." }
      ]
    }
  ]
}
```

| Caso | Payload |
|---|---|
| Órgão sem `text` | `descricao` **ausente** (não `null`) |
| Órgão sem `archives` | `arquivos` = `[]` (a relação é eager-loaded por `getContent()`) |
| Nenhum órgão | `{ "data": [] }` |
| Parâmetro inválido (`titulo` como array) | `422` com `{ "errors": { ... } }` |

## 7. Migração da API para Laravel moderno

O endpoint público é o contrato mais sensível do módulo. Ver `06-MIGRACAO-MODERNA.md`, seções 6.1 e 7,
para o controller e o resource propostos. Checklist de equivalência:

- [ ] Mesma URI `/api/v1/orgaos` e o mesmo nome de rota.
- [ ] Mesmo envelope `data` e mesmas chaves (`titulo`, `descricao`, `interacoes`, `slug`, `arquivos`).
- [ ] `descricao` continua sendo texto puro (`strip_tags`) e continua ausente quando vazio.
- [ ] `interacoes` continua espelhando `clicks`.
- [ ] Os filtros `titulo`/`descricao` passam a funcionar (correção do bug B1) — **com binding**.
- [ ] `limite` passa a ter um valor padrão documentado (hoje não tem).
