# Configurações da Instituição — Regras e Portabilidade

---

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

| # | Regra | Onde está implementada |
|---|---|---|
| R1 | A tela renderizada depende do **slug da rota**, não do módulo: `configuracoes` → categoria `site`. | `ConfigurationController@editByCategory` |
| R2 | A aba é escolhida por **faixa numérica de `order`**, não por `key`. | `form.blade.php` |
| R3 | `structure_type` e `email_suporte` **nunca** são renderizados pelo form genérico. | `loop_config.blade.php` |
| R4 | Ao salvar a categoria `site`, o `config.json` é reescrito **antes** de qualquer validação de conteúdo das chaves. | `updateByCategory()` |
| R5 | `app_env` e `app_debug` são gravados como `'local'/'prod'` e `'true'/'false'` (strings), e o `config.json` usa esse valor; a tabela `configurations` recebe o valor original `'1'/'0'`. | `updateByCategory()` |
| R6 | Chave `layout`, se enviada, precisa ser uma de `modelo1..modelo6`; caso contrário, flash de erro e `redirect()->back()`. | `updateByCategory()` |
| R7 | Chaves GA/Dashboard são removidas do `$dataForm` antes do loop genérico e gravadas pelo `ConfigurationRepository`. | `extractGaData()`, `extractDashboardData()` |
| R8 | GA só é persistido se o usuário tiver `dashboard-config.edit`; caso contrário as chaves são **silenciosamente ignoradas**. | `extractGaData()` |
| R9 | Property ID precisa ser **somente dígitos**; Measurement ID apenas é normalizado com `trim`. | `extractGaData()` |
| R10 | A Service Account JSON precisa ter `client_email`, `private_key` e `token_uri`. | `extractGaData()` |
| R11 | A frequência de refresh do dashboard precisa estar entre 1 e 1440 minutos. | `extractDashboardData()` |
| R12 | Widgets indisponíveis mas já habilitados são **preservados** na próxima gravação (para reativação futura). | `extractDashboardData()` |
| R13 | Selos sem `posicao` preenchida são descartados (`continue`) e não são salvos. | `updateByCategory()` |
| R14 | Selos com upload gravam no disco `public`, pasta `selos/`, e registram `nome_original`. | `updateByCategory()` |
| R15 | Antes de gravar os selos, `migrarCaminhosSelosAntigos()` converte `storage/...` e `img/selos/...` para `selos/...`. | `migrarCaminhosSelosAntigos()` |
| R16 | Arquivos de logo/favicon são redimensionados para 275px de largura (paisagem) ou 75px de altura (retrato); `imagem-acesso-rapido` e `background` **não** são redimensionados. | `updateByCategory()` |
| R17 | Toda chave gravada dispara `Cache::pull('structureType')`; chaves `portal_transparencia_*` também invalidam `portal_transparencia_config`. | `updateByCategory()` |
| R18 | O `pagina_em_manutencao` só vale para **visitantes não logados** (Sentinel). | `v6/app/Http/Middleware/CheckSiteManutention.php` |
| R19 | O `structure_type` grava o perfil prefeitura/câmara e é editado na tela de módulos, não na de configurações. | `v6/app/Http/Controllers/Admin/ModulosController.php` |

## 2. Acoplamentos

### 2.1 Dependências de saída (este módulo → outros)

| Módulo | Acoplamento |
|---|---|
| Dashboard | `dashboard_enhanced_enabled`, `dashboard_widget_refresh_minutes`, `dashboard_widgets_enabled`, `google_analytics_*` (categoria `dashboard`, id 8). |
| Portal de Transparência Fiscal | `portal_transparencia_ativo`, `portal_transparencia_codigo_ente`, `portal_transparencia_url_portal` (`v6/app/Services/PortalTransparenciaService.php`). |
| Solicitações LAI / Ouvidoria / Transparência | `email_lai`, `email_denuncia`, `email_ouvidoria`, `response_time`, `full_list_requests_disabled`. |
| Formulários dinâmicos | `link_form_artist_registration`, `link_form_equipment_registration` (slug do formulário). |
| Perguntas e respostas | `nome_prefeito` (assinatura das respostas). |
| Portal (newsletter) | `newsletters`, `noticias`, `ultimas_noticias`. |
| Módulos do sistema | `structure_type` (perfil da instituição). |
| Gemino / anonimização | `gemini_api_key` (`v6/app/Http/Traits/DataMaskTrait.php`). |

### 2.2 Dependências de entrada (outros → este módulo)

| Origem | Consumo |
|---|---|
| `v6/app/Providers/ConfigHeaderServiceProvider.php` | Lê 33 chaves, entre elas `titulo_site`, `descricao`, `palavras_chaves`, `logo`, `favicon`, `codigo_tempo`, `qtd_population`. |
| `v6/app/Providers/FooterServiceProvider.php` | Lê `expediente`, `numero_versao`, `incorporacao_head`, `incorporacao_rodape`. |
| `v6/app/Providers/MenuServiceProvider.php` | Lê `menu_interativo`, `contact_link`. |
| `v6/app/Http/Middleware/CheckSiteManutention.php` | Lê `pagina_em_manutencao`. |
| `v6/app/Helpers/functions.php` | Lê `noticias`, `ultimas_noticias`, `newsletters`, `layout` (`getActualLayout()`, linha 1130). |
| `v6/config/app.php`, `v6/config/mail.php`, `v6/config/recaptcha.php` | Leem o `config.json` gerado aqui. |
| `v6/app/Http/Controllers/Portal/SelosController.php` | Lê `selos_institucionais`. |

## 3. Armadilhas

| Armadilha | Onde | Impacto |
|---|---|---|
| **N+1 no Form Request** | `v6/app/Http/Requests/ConfigurationRequest.php` | Uma query por campo enviado (57 campos = 57 queries). |
| **`key` sem índice único** | tabela `configurations` | `Configuration::where('key',$key)->first()` grava só a 1ª ocorrência; chaves duplicadas ficam invisíveis. |
| **Chaves desconhecidas ignoradas** | `updateByCategory()` (`if (!$config) continue;`) | Um POST com campo não cadastrado não gera erro. |
| **Divergência de permissão leitura × gravação** | `editByCategory` vs `updateByCategory` | Leitura exige `<recurso>.edit`; gravação exige sempre `configuracoes.edit`. |
| **`echo` antes do redirect** | `updateByCategory()` | `"JSON file created successfully..."` é impresso; com `output_buffering=0` os cabeçalhos são enviados antes do redirect. |
| **`config.json` fora do versionamento** | `base_path()."/config.json"` | Arquivo ausente ou incompleto derruba o boot (ler `$data->config[0..2]`). |
| **Posição posicional do JSON** | `v6/config/app.php`, `mail.php`, `recaptcha.php` | Reordenar os blocos quebra a aplicação. |
| **Chaves GA sem permissão são ignoradas em silêncio** | `extractGaData()` | O operador não recebe aviso de que o dado não foi salvo. |
| **Cache estático por processo** | `v6/app/Services/Dashboard/ConfigurationRepository.php` (`self::$staticCache`) | Em processos long-lived (Octane/queue), o valor pode ficar obsoleto; `resetStaticCache()` existe só para testes. |
| **`Cache::pull('structureType')` a cada chave** | `updateByCategory()` | Purga o cache mesmo para chaves sem relação. |
| **Faixas numéricas de `order`** | `form.blade.php` | Inserir uma configuração com `order` 20 muda a aba sem aviso. |
| **`order` não é único** | tabela `configurations` | Colisões (15, 24, 27, 28, 33) exigem ordenação secundária; a aba Institucional concatena três recortes. |
| **`sortBy('id')->sortBy('order')`** | aba Opções | Ordena por `order` e, dentro do mesmo `order`, por `id` — comportamento difícil de prever. |
| **Máscara `notnull` NULL** | `configurations.notnull` | Linhas com `notnull` nulo não geram `required`, mesmo visualmente obrigatórias. |
| **Regra `min` condicionada a `max_input`** | `ConfigurationRequest::rules()` | `if ($config->max_input != '')` decide **também** a regra `min`. |
| **`Configuration` sem `$casts`** | model | `notnull`, `max_input`, `min_input`, `order` comparados com `==` (string vs int). |
| **Relação `hasOne` invertida** | `Configuration::categoryConfiguration()` | `whereHas('categoryConfiguration', ...)` funciona, mas a semântica é `belongsTo`. |
| **Imagem salva em dois lugares** | `updateByCategory()` | Grava em `storage/app/public/configuracoes/` (via Intervention) e em `storage_path('app/public/configuracoes')` — o mesmo caminho, duas chamadas. |
| **Arquivo antigo só é removido se contiver “configuracoes”** | `updateByCategory()` | Logos padrão em `assets/images/...` não são apagados. |
| **Composer de módulos ausente** | `v6/app/Providers/ModulosServiceProvider.php` | `$modulos` não chega a `admin.layouts.app` nem a `adminlte::page`; o menu “Configurações” some. |
| **`ConfigurationsComposer` morto** | `v6/app/Http/Views/Composers/ConfigurationsComposer.php` | Não registrado em nenhum provider. |

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

| Arquivo | v5 | v6 |
|---|---|---|
| `app/Http/Model/Prefecture/Configuration.php` | idêntico | idêntico |
| `app/Http/Controllers/Prefecture/ConfigurationController.php` | 1 versão | reescrito (GA, Dashboard, selos, `config.json`) |
| `app/Http/Requests/ConfigurationRequest.php` | 1 versão | reescrito (regras dinâmicas) |
| `database/seeds/ConfigurationsTableSeeder.php` | 711 linhas | 1125 linhas |
| `database/seeds/CategoryConfigurationsTableSeeder.php` | presente | presente (8 categorias) |
| `database/seeds/PrivacyBarSeeder.php` | **presente** | **removido** (as 4 chaves foram para `ConfigurationsTableSeeder.php`) |
| `resources/views/prefecture/configurations/configuracoes/form.blade.php` | 1 versão | reescrito (abas Dashboard/Selos/Opções) |
| `resources/views/prefecture/configurations/configuracoes/edit.blade.php` | 1 versão | reescrito (abas) |
| `resources/views/prefecture/configurations/configuracoes/loop_config.blade.php` | 1 versão | reescrito |
| `resources/views/prefecture/configurations/temas/*` | — | idêntico à v5 |
| `resources/views/prefecture/configurations/barra-privacidade/*` | — | idêntico à v5 |

## 5. Onde a v6 divergiu da v5

| Mudança | Detalhe |
|---|---|
| Tabela `modulos` | Introduzida em nov/2024 (`v6/database/migrations/2024_11_01_150206_create_modulos_table.php`), com `resources.modulo_id` e `menu_orders.modulo_id`. |
| Tela `/cms/modulos` | `v6/app/Http/Controllers/Admin/ModulosController.php` + `v6/resources/views/admin/modulos/form.blade.php`, com `structure_type`. |
| Bloco Dashboard / Google Analytics | `v6/app/Services/Dashboard/ConfigurationRepository.php`, `v6/app/Services/Dashboard/WidgetRegistry.php`, `extractGaData()`, `extractDashboardData()`. |
| `config.json` gerado pelo CMS | Consumido por `v6/config/app.php`, `v6/config/mail.php`, `v6/config/recaptcha.php`. |
| Endpoints JSON de selos | `updateSelosInstitucionais`, `createSelosInstitucionais`, `deleteSelosInstitucionais` — **sem rota**. |
| Validação de template | `layout` precisa ser `modelo1..modelo6` (a v5 aceitava qualquer valor). |
| Selos em disco `public` | a v5 guardava em `public/img/selos/`; a v6 usa o disco `public` em `selos/` e inclui `migrarCaminhosSelosAntigos()`. |
| Removido da v6 | Validação “É obrigatório selecionar uma posição para todos os selos” e a validação de `quantidade_noticias_ultimas_noticias` (número par entre 2 e 20). |

## 6. Portabilidade para PHP 8.x / Laravel 11-12

| Item legado | Impacto na migração |
|---|---|
| PHP 7.2 (`str_slug`, `str_random`) | `str_slug` foi removido do Laravel 5.8+; `str_random` virou `Str::random`. Usar `Str::slug` e `Str::random`. |
| `Intervention\Image\ImageManagerStatic` | API estática; na v7 preferir `ImageManager` com injeção. |
| `DB::table('states')` em controller | Mover para um repositório/estado Eloquent. |
| `Route::getCurrentRoute()->getName()` dentro do Form Request | Substituir por Form Requests distintos por rota. |
| `activity()` helper | Manter ou trocar por um `AuditLog` explícito com eventos de domínio. |
| `has_access()` global | Substituir por Policies + `Gate`. |
| `Sentinel::inRole('super-admin')` | Migrar para `spatie/laravel-permission` ou Gates. |
| `config.json` lido no boot | Eliminar; usar `.env` + cache de runtime, com a chave de runtime atualizada pela tela. |
| `echo` no controller | Remover; usar `Log::info()`. |
| `Crypt::encrypt` manual | `encrypted` cast do Laravel 11. |
