# Fornecedores — Regras e Portabilidade

> Regras de negócio não óbvias, acoplamentos e armadilhas do módulo.

---

## 1. Regras de negócio não óbvias

### 1.1 "Ativo" é um booleano nullable, não uma restrição

`suppliers.fornecedor_ativo` é `boolean` nullable. A Query de exibição é
`->where('fornecedor_ativo', true)->first()`. Como nada impede dois
fornecedores ativos, e o `first()` não tem ordenação, **qualquer um
pode ser exibido** — de forma não determinística.

### 1.2 A troca de ativo **desativa** o anterior, mesmo sendo o mesmo

`switchFornecedorAtivo` sempre desativa o ativo anterior e só reativa o
novo se for **diferente**. Ou seja: clicar no fornecedor que já está
ativo **deixa o sistema sem fornecedor ativo** (a página de Suporte e o
rodapé ficam vazios).

> Na **v5** a lógica era um *toggle*: clicar no fornecedor ativo o
> desativava, e clicar em outro desativava o anterior e ativava o novo.
> A v6 mudou para "sempre ativar o clicado". Ver `08-GAPS-E-DIVIDAS.md`.

### 1.3 O `slug` é derivado do `name` a cada gravação

`$request->merge(["slug" => Str::slug($request->name)])` acontece em
**todo** `upInsert` — inclusive na edição. Renomear o fornecedor muda o
slug. Não há índice único na coluna.

### 1.4 Telefone e WhatsApp são gravados só com dígitos

`preg_replace('/\D/', '', ...)`. O valor **validado** (com máscara) pode
ter 13 caracteres e o **gravado** tem 10. A exibição usa
`changeToFone()` para reintroduzir a máscara.

### 1.5 `fornecedor_ativo` nunca é gravado pelo formulário

A lista de campos do `SupplierRepository::structureUpInsert` **não**
inclui `fornecedor_ativo`. O status só muda pela rota de alternância.

### 1.6 O `image` e o `icon` guardam o **caminho relativo**, não a URL

`'img/images_fornecedores/' . $nomeDoArquivo`. A view monta a URL como
`/storage/{caminho}`. Se o disk `public` mudar de link simbólico, as
imagens quebram.

### 1.7 A listagem do CMS **não** usa o model

Usa a view SQL `fornecedores`, que expõe `phone` como `telefone` e
`address` como `endereco` — e **não** expõe `slug`, `locale` nem
`whatsapp`. O WhatsApp só aparece na página de Suporte.

### 1.8 A exclusão é soft delete, mas os arquivos ficam

`Supplier` usa `SoftDeletes`; a view SQL filtra `deleted_at IS NULL`. Os
arquivos em `img/images_fornecedores/` e `img/icons_fornecedores/`
**permanecem no disco**.

---

## 2. Acoplamentos

### 2.1 Dependências de saída

| Dependência | Tipo | Onde |
|---|---|---|
| `ISupplierRepository` / `SupplierRepository` | interno | controller |
| `AppTrait` / `FileTrait` | interno | repository |
| `Str` (slug), `Str::slug` | Laravel | repository |
| `Storage` (disk `public`) | Laravel | `FileTrait::saveUpload` |
| `Intervention\Image` (compressão) | externo | `FileTrait::compress` |
| `modulos` (id 6) | interno | menu |

### 2.2 Dependências de entrada

| Consumidor | Dependência | Risco |
|---|---|---|
| Menu do AdminLTE | `route('supplier.index')` + `$modulos['fornecedores']` | item some |
| **Módulo `suporte`** | `Supplier` (ativo) | página de contato vazia |
| Rodapé do AdminLTE | `Supplier` (ativo) | rodapé sem fornecedor |
| `Prefecture\DatatableController` | view SQL `fornecedores` | — |
| `resources` | **nenhum** recurso `supplier` | permissões inacessíveis |

> O módulo `fornecedores` é a **dependência de dados** do módulo
> `suporte`: se o fornecedor ativo não existir, a página de Suporte
> fica em branco. Alterações aqui têm efeito direto lá.

---

## 3. Armadilhas

### 3.1 [CRÍTICO] As rotas de escrita não têm autenticação

`supplier.store`, `supplier.update`, `supplier.destroy` e
`/cms/fornecedores_switch` **não** declaram `check.logged`, e o
`SupplierController::__construct()` não registra
`check.route.permission` (nem chama `parent::__construct()`, que é quem
o registraria).

Resultado: **qualquer pessoa com um token CSRF válido** — inclusive um
visitante que leia o token de uma página pública, ou um operador com
perfil mínimo — pode criar, alterar, excluir e ativar fornecedores.

Mitigação: aplicar `check.logged` nas quatro rotas **e** registrar
`check.route.permission` no construtor (ou usar Policy).

### 3.2 [ALTO] O módulo não tem recurso em `resources`

Não há `resources.slug = 'supplier'`. Logo:

- `has_access(['supplier.index'])` e `has_access(['supplier.create'])`
  são falsos para qualquer perfil que não seja `super-admin`;
- o item de menu **só** aparece para `super-admin`;
- os botões de ação da linha (editar/excluir/alternar) ficam
  desabilitados para todos os demais.

O módulo está, na prática, restrito ao `super-admin` — apesar de estar
"ligado" em `modulos.status`.

### 3.3 [ALTO] `Repository::upInsert` faz `dd($th)`

`v6/app/Repositories/Repository.php:64-71` captura `Throwable` e chama
`dd($th)` — a exceção é **vazada na página** em vez de retornar erro.
Com o `SupplierRequest` validando quase tudo, o caso mais provável é o
`phone`/`whatsapp` vazio virando `null` numa coluna NOT NULL.

### 3.4 [ALTO] Telefone vazio vira `null` numa coluna NOT NULL

`filter_request` converte valores falsos em `null`:

```php
if (!$valor) $filter[$campo] = null; else $filter[$campo] = $valor;
```

Um `phone` que, após o `preg_replace`, fique `""` (por exemplo, o
usuário digitou só letras) vira `null` -> violação de `NOT NULL` ->
`dd($th)`.

### 3.5 [MÉDIO] `switchFornecedorAtivo` não é transacional e não valida o id

- Desativa o anterior e depois ativa o novo, **sem transação**: se a
  segunda parte falhar, o sistema fica sem fornecedor ativo.
- `find($request->contents_id)->update(...)` com id inexistente ->
  `null->update()` -> **erro 500**.
- O parâmetro se chama `contents_id` (herança de outros módulos) em vez
  de `supplier_id` — confuso.

### 3.6 [MÉDIO] Clicar no fornecedor já ativo o desativa

Como a v6 Activation substituiu o toggle da v5, o efeito é: o operador
não consegue mais **desativar** um fornecedor pela interface. Precisa
ativar outro.

### 3.7 [MÉDIO] `fornecedor_ativo` é gravado como `null`, não `false`

`$oldSulplier->update(['fornecedor_ativo' => null])`. Funciona (o
`where('fornecedor_ativo', true)` não encontra `null`), mas deixa a
coluna com três estados (`true`, `null`) e nenhum `false`. A coluna é
nullable, então funciona — mas é um estado implícito.

### 3.8 [MÉDIO] `slug` recalculado a cada edição, sem unicidade

Trocar o nome do fornecedor muda o slug silenciosamente. Não há índice
único, então dois fornecedores com nomes iguais geram slugs iguais.

### 3.9 [MÉDIO] Sem auditoria

Nenhuma chamada a `activity()`: criar, editar, excluir e ativar
fornecedores **não** deixa rastro em `activity_log` — apesar de o módulo
controlar quem é o responsável técnico pelo portal.

### 3.10 [MÉDIO] Arquivos órfãos no disco

A exclusão (soft) não remove a imagem nem o ícone; trocar a imagem não
remove o arquivo anterior (`saveUpload` remove apenas o arquivo de
**mesmo nome**, e o nome é aleatório).

### 3.11 [BAIXO] `Supplier::$fillable` com colunas inexistentes

`telefone` e `endereco` não existem em `suppliers`. O
`SupplierRepository::structureUpInsert` limita os campos, o que evita o
erro — mas o model está errado.

### 3.12 [BAIXO] `locale` sem validação de URL

O campo é o `src` de um `<iframe>`. `required` apenas. Um valor
`javascript:...` seria injetado no `src` do iframe.

### 3.13 [BAIXO] `phone` validado antes da remoção dos não dígitos

`min:5` roda sobre o valor com máscara; o gravado pode ter menos
dígitos que o mínimo (ex.: `(11) 2345-6789` -> 10 dígitos, ok; mas
`+55 (11) 2345-6789` -> 12). O inverso — aceitar e gravar número curto
— é possível com entrada de 5+ caracteres não numéricos.

### 3.14 [BAIXO] A listagem não é paginada

`DB::table('fornecedores')->get()` — a view SQL já vem ordenada e
numerada (`row_number()`), e o Yajra aplica paginação no lado do
servidor, mas **todos** os registros são carregados no PHP.

### 3.15 [BAIXO] `SuplierTableSeeder` faz `truncate`

Executar o seed apaga todos os fornecedores cadastrados.

### 3.16 [BAIXO] `use mysql_xdevapi\Exception;`

`v6/app/Http/Controllers/Prefecture/SupplierController.php:12` importa a
classe `Exception` do driver MySQL X DevAPI — que não é usada em lugar
nenhum e não existe no ambiente. É resíduo de um copy/paste.

---

## 4. Código duplicado entre v5 e v6

| Arquivo | v5 vs v6 |
|---|---|
| `app/Http/Model/Prefecture/Supplier.php` | **idêntico** |
| `app/Http/Requests/SupplierRequest.php` | **idêntico** |
| `app/Repositories/Eloquent/Prefecture/Supplier/SupplierRepository.php` | **idêntico** |
| `resources/views/prefecture/configurations/fornecedor/**` (4 arquivos) | **idênticos** |
| `public/assets/js/custom/config/fornecedor/datatable.js` | **idêntico** |
| `database/seeds/SuplierTableSeeder.php` | **idêntico** |
| `app/Http/Controllers/Prefecture/SupplierController.php` | **DIFERENTE** (ver 4.1) |
| `database/migrations/2026_04_06_113000_alter_suppliers_table_nullable.php` | **só na v5** |

### 4.1 Diferenças no `SupplierController`

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

(applied em `store` **e** `update` — na v5 só existia no `store`)

```diff
- return redirect()->route('supplier.index');
+ return redirect()->route('supplier.index')->with('success', 'Registro cadastrado com sucesso');
```

```diff
- try { $id = $request->contents_id; $supplier = $this->supplier->getModel()->find($id); ... }
- // toggle: se já estava ativo, desativa; senão, desativa os outros e ativa
+ $oldSulplier = Supplier::where('fornecedor_ativo', true)->first();
+ if ($oldSulplier) { $oldSulplier->update(['fornecedor_ativo' => null]); }
+ if (!isset($oldSulplier->id)) { ...ativar... }
+ elseif ($oldSulplier->id != $request->contents_id) { ...ativar... }
```

Além disso, a v6 separou os `catch` de `PDOException` e `Exception` no
`destroy` e no `switchFornecedorAtivo`.

---

## 5. Onde a v6 divergiu (e por quê)

| Mudança | Efeito | Motivo provável |
|---|---|---|
| Sanitização do telefone/WhatsApp também no `update` | consistência entre cadastro e edição | correção de dado inconsistente |
| `switchFornecedorAtivo` deixa de ser toggle | o clique sempre ativa o clicado | simplifier a ação do operador |
| `catch (PDOException)` separado | mensagens de erro mais precisas | tratamento de banco |
| `->with('success', ...)` no redirect do `store` | mensagem de sucesso em session | padronização |
| **Migration `alter_suppliers_table_nullable` não portada** | `email` e `site` continuam NOT NULL na v6 | regressão de schema (a v5 já permitia nulo) |

> A não portabilidade da migration de 2026-04-06 é provavelmente um
> **erro de merge** entre as duas árvores. Deve ser verificado.

---

## 6. Riscos de migração de schema

| Risco | Detalhe | Mitigação |
|---|---|---|
| `email`/`site` NOT NULL na v6 | diverge da v5 | portar a migration ou decidir o novo contrato |
| `slug` sem unicidade | duplicidade silenciosa | `unique` + regeneração controlada |
| `fornecedor_ativo` sem restrição | vários ativos | índice parcial único (`WHERE fornecedor_ativo = true`) |
| Colunas `telefone`/`endereco` órfãs no `$fillable` | erro em `fill()` direto | remover do model |
| View SQL `fornecedores` fora do versionamento | não é criada por migration | recriar a view por migration na v7 |
| Arquivos órfãos em `img/*_fornecedores/` | consumo de disco | limpar no `destroy` + varredura periódica |
| `SuplierTableSeeder` com `truncate` | apaga dados reais | remover do fluxo de produção |

---

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

1. **Pode haver mais de um fornecedor por instalação?** O desenho
   atual (vários, um ativo) sugere que sim.
2. **O que é o "fornecedor" fora deste contexto?** Em outras
   instalações pode ser o fornecedor de um equipamento ou de um
   software de terceiro.
3. **O rodapé do portal público deve exibir o fornecedor?** Hoje
   exibe o do CMS.
4. **E-mail e site são obrigatórios?** A v5 já os havia tornado
   nullable.
5. **O "locale" (mapa) é obrigatório?** Hoje é `required`, mas sem
   validação de URL.
6. **Deve haver histórico de alterações do fornecedor ativo** (quem
   trocou e quando)? Hoje não há auditoria.

---

## 8. Checklist de correção (priorizado)

| Prioridade | Item | Arquivo |
|---|---|---|
| P0 | Adicionar `check.logged` às 4 rotas de escrita | `v6/routes/basepro.php:352-355` |
| P0 | Registrar `check.route.permission` (ou Policy) no controller | `v6/app/Http/Controllers/Prefecture/SupplierController.php:30-33` |
| P0 | Criar o recurso `supplier` (seeder idempotente) | novo seeder |
| P1 | Corrigir `Repository::upInsert` para não usar `dd($th)` | `v6/app/Repositories/Repository.php:64-71` |
| P1 | Validar o telefone **depois** da sanitização (ou impedir string vazia) | `v6/app/Http/Controllers/Prefecture/SupplierController.php:63-68` |
| P1 | Validar `locale` como URL | `v6/app/Http/Requests/SupplierRequest.php` |
| P1 | Transaction em `switchFornecedorAtivo` + validar o id | `v6/app/Http/Controllers/Prefecture/SupplierController.php:144-165` |
| P2 | Restaurar o *toggle* do fornecedor ativo (ou documentar a mudança) | mesmo arquivo |
| P2 | Remover `telefone`/`endereco` do `$fillable` | `v6/app/Http/Model/Prefecture/Supplier.php:18-32` |
| P2 | Adicionar `unique` em `slug` (e regerar) | nova migration |
| P2 | Índice parcial único em `fornecedor_ativo` | nova migration |
| P2 | Registrar auditoria (`activity()`) | `v6/app/Http/Controllers/Prefecture/SupplierController.php` |
| P2 | Portar a migration `alter_suppliers_table_nullable` (v5 -> v6) ou decidir | `v5/database/migrations/2026_04_06_113000_alter_suppliers_table_nullable.php` |
| P3 | Remover o import `mysql_xdevapi\Exception` | `v6/app/Http/Controllers/Prefecture/SupplierController.php:12` |
| P3 | Remover o `truncate` do `SuplierTableSeeder` | `v6/database/seeds/SuplierTableSeeder.php` |
| P3 | Versionar a view SQL `fornecedores` como migration | nova migration |
