# Fornecedores — Rotas e Controllers

> Todas as rotas no grupo `cms` de `v6/routes/basepro.php`.

---

## 1. Rotas

| Método | URI | Nome | Controller@action | Middleware | Linha |
|---|---|---|---|---|---|
| GET | `/cms/fornecedores` | `supplier.index` | `Prefecture\SupplierController@index` | `web` + `check.logged` | `v6/routes/basepro.php:349` |
| GET | `/cms/fornecedores/cadastrar` | `supplier.create` | `@create` | `web` + `check.logged` | 350 |
| GET | `/cms/fornecedores/editar/{id}` | `supplier.edit` | `@edit` | `web` + `check.logged` | 351 |
| PUT | `/cms/fornecedores/atualizar/{id}` | `supplier.update` | `@update` | `web` | 352 |
| POST | `/cms/fornecedores_switch` | *(sem nome)* | `@switchFornecedorAtivo` | `web` | 353 |
| POST | `/cms/fornecedores` | `supplier.store` | `@store` | `web` | 354 |
| DELETE | `/cms/fornecedores/excluir/{id}` | `supplier.destroy` | `@destroy` | `web` | 355 |
| GET | `/cms/datatable/fornecedor` | `fornecedor` | `Prefecture\DatatableController@get_fornecedor` | `web` | 432 |

### 1.1 Ausência de autenticação nas rotas de escrita

| Rota | `check.logged`? | `check.route.permission`? |
|---|---|---|
| `supplier.index` | **sim** | não |
| `supplier.create` | **sim** | não |
| `supplier.edit` | **sim** | não |
| `supplier.store` | **NÃO** | não |
| `supplier.update` | **NÃO** | não |
| `supplier.destroy` | **NÃO** | não |
| `/cms/fornecedores_switch` | **NÃO** | não |

O `SupplierController` **não** registra `check.route.permission` no
construtor:

```php
public function __construct()
{
    $this->supplier = app(SupplierRepository::class);
}
```

E a classe base `Prefecture\Controller`, que registra esse middleware no
seu próprio construtor, **não** é acionada — o construtor da subclasse
não chama `parent::__construct()`:

```php
// v6/app/Http/Controllers/Prefecture/Controller.php:22-25
public function __construct()
{
    $this->middleware('check.route.permission');
}
```

Como o grupo `/cms` é carregado com o middleware `web` (que inclui
`VerifyCsrfToken`), as rotas de escrita exigem um **token CSRF** — o que
impede o ataque externo direto, mas **não** impede que um operador
autenticado (com qualquer perfil) crie, altere, exclua ou ative
fornecedores.

> A rota `POST /cms/fornecedores_switch` é a mais exposta: **não tem
> nome** (não pode ser referenciada com `route()`), **não tem CSRF
> próprio além do do grupo** e **não tem permissão**.

### 1.2 A rota `supplier.update` ignora o `{id}` na URL

`PUT /cms/fornecedores/atualizar/{id}` -> `SupplierController@update($request, $id)`.
O `$id` **é** usado (`upInsert($request, $id)`), ok. Mas a rota usa
`PUT` com o id no **caminho** em vez do corpo — padrão incomum no
restante do CMS, que usa `PUT /cms/{recurso}/{id}`.

---

## 2. Controller

`v6/app/Http/Controllers/Prefecture/SupplierController.php` (166 linhas).

| Action | Comportamento | Linha |
|---|---|---|
| `__construct()` | resolve o `SupplierRepository` pelo container | 30-33 |
| `index()` | view `prefecture.configurations.fornecedor.index` | 40-44 |
| `create()` | view `prefecture.configurations.fornecedor.create` | 50-53 |
| `store(SupplierRequest $request)` | normaliza telefone/WhatsApp, chama `upInsert`, flash e redirect | 61-85 |
| `edit($id)` | busca pelo id; se não achar, flash e redirect; senão view `...fornecedor.edit` | 88-100 |
| `update(SupplierRequest $request, $id)` | normaliza telefone/WhatsApp, `upInsert($request, $id)` | 104-126 |
| `destroy($id)` | `$this->supplier->destroy($id)` -> JSON `{success}` / `{erro}` | 129-141 |
| `switchFornecedorAtivo(Request $request)` | desativa o ativo anterior e ativa o `contents_id` | 144-165 |

### 2.1 Normalização do telefone

```php
$request->merge([
    'phone'    => preg_replace('/\D/', '', $request->phone),
    'whatsapp' => preg_replace('/\D/', '', $request->whatsapp),
]);
```

Aplicada em `store` e em `update` **antes** de gravar. Observação: se o
telefone tiver apenas caracteres não numéricos, o resultado é a string
vazia `''` — que, pelo `filter_request`, vira `null` (a coluna é NOT
NULL -> erro de banco).

### 2.2 `switchFornecedorAtivo` (comportamento da v6)

```php
$oldSulplier = Supplier::where('fornecedor_ativo', true)->first();
if ($oldSulplier) {
    $oldSulplier->update(['fornecedor_ativo' => null]);
}

if (!isset($oldSulplier->id)) {
    $this->supplier->getModel()->find($request->contents_id)->update(['fornecedor_ativo' => true]);
} elseif ($oldSulplier->id != $request->contents_id) {
    $this->supplier->getModel()->find($request->contents_id)->update(['fornecedor_ativo' => true]);
}
```

Pontos:

- Desativa o ativo anterior **sempre** (grava `null`, não `false`).
- Só ativa o novo se for **diferente** do anterior (ou se não havia
  anterior).
- Se `contents_id` não existir, `find()` retorna `null` e
  `->update()` em `null` gera **erro 500**.
- Não há transação: se a segunda parte falhar, fica **sem** fornecedor
  ativo.
- Na **v5** a lógica era um *toggle*: clicar no fornecedor já ativo o
  **desativava**; na v6, clicar sempre **ativa** o clicado (e desativa
  o anterior). Ver `08-GAPS-E-DIVIDAS.md`.

### 2.3 `destroy`

```php
if ($this->supplier->destroy($id)) {
    return response()->json(['success' => 'Fornecedor deletado com sucesso.'], 200);
}
```

O `destroy` do `Repository` faz `getModel()->where('id', $id)->delete()`
dentro de uma transação — com `SoftDeletes`, é um **soft delete**. O
arquivo da imagem e do ícone **não** são removidos do disco.

---

## 3. Form Request

`v6/app/Http/Requests/SupplierRequest.php`:

| Campo | Regra | Observação |
|---|---|---|
| `name` | `required\|min:5\|max:120` | |
| `email` | `required\|email` | a coluna é NOT NULL na v6 |
| `site` | `required` | idem |
| `phone` | `required\|min:5` | validado **antes** da remoção dos não dígitos |
| `address` | `required\|min:5` | |
| `image` | `sometimes\|required\|image\|mimes:jpeg,png,jpg,gif` | |
| `icon` | `sometimes\|required\|image\|mimes:jpeg,png,jpg,gif` | |
| `locale` | `required` | apenas "presente"; sem validação de URL |

`authorize()` retorna `true` — **não há** autorização no Form Request.

Mensagens customizadas em português para todos os campos.

> `phone` com `min:5` é validado **antes** do `preg_replace`: um telefone
> como `"(11) 2345-6789"` (13 caracteres) passa, mas o resultado gravado
> tem 10 dígitos. Já `"1234"` (4 caracteres) é reprovado, mesmo que
> tivesse 4 dígitos.

---

## 4. Repository

`v6/app/Repositories/Eloquent/Prefecture/Supplier/SupplierRepository.php`:

```php
protected function structureUpInsert($request): array
{
    $fields = ['name', 'slug', 'email', 'site', 'phone', 'address', 'locale', 'image', 'icon', 'whatsapp'];
    return $this->filter_request($request, $fields);
}

public function upInsert($request, int $id = null)
{
    $request->merge(["slug" => Str::slug($request->name)]);

    if ($this->validFile($request, "image"))
        $request->merge(["image" => 'img/images_fornecedores/'.$this->saveUpload($request->image, 'img/images_fornecedores/')]);

    if ($this->validFile($request, "icon"))
        $request->merge(["icon" => 'img/icons_fornecedores/'.$this->saveUpload($request->icon, 'img/icons_fornecedores/')]);

    return parent::upInsert($request, $id);
}
```

Observações:

- O `slug` é **sempre** recalculado a partir do `name`.
- `fornecedor_ativo` **não** está na lista de campos — nunca é gravado
  por `upInsert` (só por `switchFornecedorAtivo`).
- `filter_request` converte valores falsos em `null` — inclusive
  `fornecedor_ativo = false` (que viraria `null`).
- `parent::upInsert` faz `updateOrCreate(['id' => $id], $data)`; com
  `$id = null`, cria um novo registro.
- `parent::upInsert` captura `Throwable` e chama **`dd($th)`** — a
  exceção vaza na tela.

---

## 5. Permissões exigidas

| Recurso | Permissão | Onde |
|---|---|---|
| Item de menu "Fornecedores" | `supplier.index` + `$modulos['fornecedores']` | `v6/resources/views/vendor/adminlte/page.blade.php:275` |
| Botão "Cadastrar" | `supplier.create` | `v6/resources/views/prefecture/configurations/fornecedor/index.blade.php:10` |
| Botão editar (linha) | `supplier.edit` | injetado por `struct_views` no JSON |
| Botão excluir (linha) | `supplier.delete` | idem |
| Botão ativar/desativar (linha) | `supplier.restore` / `supplier.inactivate` | idem |

**Nenhuma** dessas permissões existe no catálogo `resources` (não há
recurso `supplier`). Logo, para qualquer perfil que não seja
`super-admin`, o botão "Cadastrar" **não** aparece e os botões de ação
da linha ficam desabilitados — mas as rotas de escrita continuam
acessíveis (ver 1.1).

---

## 6. API Resources / Presenters

Nenhum. A listagem usa a view SQL + `struct_views()` do
`DatatableController` (flags `edit`, `delete`, `active`, `inactivate`).

---

## 7. Rotas do portal público

Nenhuma. O módulo é **exclusivamente CMS**.
