# TV Câmara — Regras de Negócio e Portabilidade

> **Escopo:** módulo `tv-camara` na `v6`. Regras observáveis, armadilhas do
> legado e acoplamentos que afetam a reconstrução.

---

## 1. Regras de negócio

### 1.1 Visibilidade no portal

Um vídeo aparece em `/tv-camara` **somente se** cumprir as três condições
seguintes (`v6/app/Http/Controllers/Portal/TvCamaraController.php:23-30`):

| # | Condição | Campo |
|---|---|---|
| 1 | Está ativado | `status = true` |
| 2 | Não foi excluído | `deleted_at IS NULL` |
| 3 | A data de publicação já passou | `publication_date <= now()` |

Ordenação: `publication_date DESC`. Paginação: 12 por página.

### 1.2 Visibilidade na home

| Regra | Implementação |
|---|---|
| O bloco só existe se o módulo estiver habilitado | `isset($modulos['tv-camara']) && $modulos['tv-camara']` (`v6/app/Providers/ModulosServiceProvider.php:22-50`) |
| No máximo **3** vídeos | `->limit(3)` (`v6/app/Http/Controllers/Portal/PortalController.php:161-163`) |
| Ordenados por `publication_date DESC` | idem |
| Sem filtro de data futura | a query da home **não** aplica `publication_date <= now()` |

### 1.3 Regra dos "3 ativos"

No `store()` e no `update()` do controller do CMS:

```php
$contents = TvCamara::where('status', 'true')->orderBy('publication_date', 'DESC')->get();
if (count($contents) == 3 && $content->status == 0) {
    $contents[2]->status = 0;
    $contents[2]->save();
}
```

Leitura correta da regra:

1. A consulta devolve **todos** os registros com `status = true` (a string
   `'true'` é convertida para `true` pelo PostgreSQL), ordenados por data
   descente.
2. A desativação do terceiro só ocorre **se o novo registro já nasce
   desativado** (`$content->status == 0`).
3. `$contents[2]` é o **mais antigo** entre os ativos (índice 2 de uma lista de
   3), e é desativado.
4. O `count($contents) == 3` é exato: com 4 ou mais ativos, nada é desativado.

**Contradição de semântica**: o rótulo do checkbox no CMS é "Exibir na página
inicial" (`v6/resources/views/prefecture/contents/tv-camara/form.blade.php:75-79`),
mas o `status` controla a exibição na **listagem** `/tv-camara`; a home mostra
sempre os 3 mais recentes entre os ativos. O operador não tem como escolher
quais 3 vídeos aparecem na home.

### 1.4 Validações

| Campo | Regra |
|---|---|
| `title` | obrigatório, máximo 120 caracteres |
| `publication_date` | obrigatório (texto livre, máximo 200 caracteres) |
| `link` | obrigatório, máximo 200 caracteres |
| `text` | sem regra no Form Request, apesar de o formulário exigir |
| `status` | sem regra; checkbox |

### 1.5 Data de publicação

O controller converte o valor enviado pelo formulário:

```php
$date = date('Y-m-d H:i', strtotime(str_replace('/', '-', $request->publication_date)));
```

O formato esperado é `d/m/Y H:i` (máscara `br-data-tempo` do input). Um valor
em outro formato (por exemplo `2024-05-01 10:00`) é interpretado por
`strtotime` de forma diferente, sem erro aparente.

### 1.6 Slug

```php
'slug' => str_slug($request->title),
```

`str_slug` é o helper do `illuminate/support`
(`v6/vendor/laravel/framework/src/Illuminate/Support/helpers.php:991`), com
idioma padrão `en`, o que translitera acentos: "Sessão" → "sessao".

### 1.7 Exclusão

- Exclusão individual e em lote são **soft delete** (`deleted_at`).
- A FK `archives.tv_camara_id` é `ON DELETE CASCADE`, mas só atua em delete
  físico (não ocorre no soft delete): as imagens de destaque permanecem no
  banco e no storage após a exclusão do vídeo.
- Não há rota de restauração.

## 2. Armadilhas (armadilhas do legado)

| # | Armadilha | Onde | Impacto |
|---|---|---|---|
| A1 | `down()` da migration faz `Schema::dropIfExists('tv_camara')` — nome sem o "s" | `v6/database/migrations/2024_11_11_164932_create_tv_camara_table.php:36` | O `rollback` da migration **não derruba** a tabela `tv_camaras`; o estado do banco diverge do esperado. |
| A2 | `status` é `boolean nullable` | `v6/database/migrations/2024_11_11_164932_create_tv_camara_table.php:21` | `NULL` significa "desativado" no portal e "Desativado" na view SQL, mas o `setStatus` trata `null` como `false` e o `index` do portal filtra por `= true` — três interpretações para o mesmo valor. |
| A3 | Namespace do model: `App\Prefecture` (dentro de `app/Http/Model/Prefecture/`) | `v6/app/Http/Model/Prefecture/TvCamara.php:3` | Difere do padrão `App\Http\Model\Prefecture` do restante da plataforma; a autoload funciona por classmap. Importações erradas quebram o controller. |
| A4 | A regra dos 3 ativos só desativa o terceiro quando o **novo** item está desativado | `v6/app/Http/Controllers/Prefecture/TvCamaraController.php:36-40` e `:114-118` | Cadastrar um 4º vídeo **ativo** deixa 4 ativos; a regra nunca é aplicada. |
| A5 | `str_slug` do título gera colisão silenciosa de `slug` | `v6/app/Http/Controllers/Prefecture/TvCamaraController.php:47` e `:126` | Não há índice único; dois vídeos com o mesmo título ficam com o mesmo `slug` e o portal (`where('slug',$slug)->firstOrFail()`) exibe sempre o mais antigo, sem ordenação. |
| A6 | `publication_date` com `default(date('Y/m/d H:i'))` avaliado **no momento da migration** | `v6/database/migrations/2024_11_11_164932_create_tv_camara_table.php:23` | O default é gravado como literal no banco: verificado no PostgreSQL como `'2026-10-21 08:52:00'::timestamp`. Qualquer registro criado **sem** informar a data recebe a data da instalação, não a data atual. |
| A7 | `text` renderizado com `{!! !!}` | `v6/resources/views/portal/layouts/modelo1/tv-camara/show.blade.php:29` | HTML do Summernote é injetado sem sanitização: risco de XSS armazenado para quem tem permissão de edição. |
| A8 | `show()` do portal não filtra `status` nem `publication_date` | `v6/app/Http/Controllers/Portal/TvCamaraController.php:33-40` | Vídeo desativado ou com data futura continua acessível por URL direta. |
| A9 | Slugs de permissão divergentes | `v6/resources/views/prefecture/contents/tv-camara/index.blade.php:16` e `v6/app/Http/Controllers/Prefecture/DatatableController.php:504-520` | `tv-camaras.delete` (com "s") e `tv_camara.*` (com underscore) não existem na matriz de permissões; os botões somem para perfis não `super-admin`. |
| A10 | Breadcrumb da página de detalhes passa o **título** no lugar do slug | `v6/routes/breadcrumbs.php:434` | Gera a URL `/tv-camara/{título}`, que não resolve em `firstOrFail()`. |
| A11 | N+1 na listagem do portal | `v6/app/Http/Controllers/Portal/TvCamaraController.php:23-30` | A view acessa `$content->archives[0]` sem `with('archives')`: uma query por item (até 12 por página). |
| A12 | ID cru do YouTube não funciona no embed | `v6/app/Helpers/functions.php:607-665` | O formulário sugere o formato "ID cru" (placeholder), mas o helper só converte URLs; o ID cru vira `https://<id>` e não reproduz. |
| A13 | `$content->source` e `$content->author` não existem na tabela | `v6/resources/views/portal/layouts/modelo1/tv-camara/show.blade.php:14` | O bloco "Do(a) ... por ..." nunca é renderizado. |
| A14 | Código morto de galeria no `edit.blade.php` | `v6/resources/views/prefecture/contents/tv-camara/edit.blade.php` | O widget `jquery.uploadfile` aponta para `#multipleupload`, elemento inexistente; o link "Voltar" usa `route('listar-conteudo', $content->slug)`, rota do módulo de conteúdos, não da TV Câmara. |
| A15 | `TvCamara` não tem relação `linksUnique()` | `v6/app/Http/Model/Prefecture/TvCamara.php` vs `v6/app/Http/Presenters/ContentPresenter.php:96` | O presenter acessa uma relação inexistente; o operador `??` mascara o `null`. |
| A16 | A view SQL ordena por `created_at`, o portal por `publication_date` | migration da view vs controller do portal | A ordem da listagem do CMS e a do portal podem divergir para vídeos com data de publicação editada. |
| A17 | `to_char(...::timestamp with time zone)` sobre coluna `without time zone` | migration da view | O fuso do servidor do banco (UTC no container) pode deslocar a data exibida no CMS em relação à data real. |

## 3. Acoplamentos

### 3.1 Dependências de outros módulos

| Depende de | Ponto de contato |
|---|---|
| Galeria genérica (`archives`) | Coluna `tv_camara_id` + `Archive` model; a FK é `ON DELETE CASCADE`. |
| `Configuration` (chave `layout`) | `getTemplateActived()` define a pasta do tema. |
| `modulos` / `resources` / `menu_orders` | Habilitação do bloco da home, permissões e item de menu do CMS. |
| Componentes de exclusão em lote | `prefecture.components.deleteByLoteSubmitButton` e `deleteByLoteMasterCheckbox`. |
| `datatable.js` / `table.js` / `deleteAll.js` | Infraestrutura de listagem compartilhada. |
| Home do portal | `PortalController@index` monta `$tvCamaras`. |

### 3.2 Dependências de terceiros

| Pacote | Uso |
|---|---|
| `laracasts/presenter` | `ContentPresenter` (datas e miniatura). |
| `yajra/laravel-datatables` | Listagem do CMS. |
| `cartalyst/sentinel` | Login, papéis e permissões. |

### 3.3 Quem depende deste módulo

- Home do portal (bloco "TV Câmara").
- `MenuServiceProvider` (menu lateral do CMS, `v6/app/Providers/MenuServiceProvider.php:291-293`).
- `admin/modulos/form.blade.php` (agrupamento do módulo na estrutura "câmara").

## 4. Onde a v6 diverge da v5

**A v6 introduz o módulo do zero.** Não existe, na `v5`, nenhuma rota,
migration, model, view, seed ou referência a `tv-camara` / `tv_camara` /
"TV Câmara" (verificado por busca textual em `v5/`). Todos os artefatos
datam de novembro de 2024:

| Artefato | Data |
|---|---|
| `v6/database/migrations/2024_11_11_164932_create_tv_camara_table.php` | 11/11/2024 |
| `v6/database/migrations/2024_11_11_190611_create_tv_camara_view.php` | 11/11/2024 |
| `v6/database/migrations/2024_11_16_121841_add_tv_camaras_id_in_archives.php` | 16/11/2024 |

Não há, portanto, código copiado da v5: o módulo foi escrito do zero, o que
explica a coexistência de padrões divergentes (namespace do model, slug de
permissão com "s", uso de `tv_camara` com underscore nos flags do DataTable).

## 5. Itens a preservar na reconstrução

| Item | Motivo |
|---|---|
| Limite de 3 vídeos na home | Comportamento visual acordado com a Câmara. |
| Filtro `status = true` **e** `publication_date <= now()` na listagem pública | Evita publicar vídeos agendados. |
| Paginação de 12 itens | Densidade da listagem. |
| Conversão por provedor (YouTube, Vimeo, Facebook, Instagram) | Formato do player na página de detalhes. |
| Suporte a **ID cru do YouTube** (hoje quebrado no embed) | O formulário sugere esse formato; corrigir é ganho de usabilidade, desde que o embed continue igual para URLs. |
| Upload de imagem de destaque com recorte (crop) | Identidade visual dos cards. |
| Soft delete | Permite auditoria e, no futuro, restauração. |
