# Carta de Serviços — Regras e Portabilidade

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

### 1.1 Publicação

| Regra | Onde | Efeito observável |
|---|---|---|
| Um serviço só aparece no portal se `ativo = true` **e** `publicado = true` | `Portal\ServiceLetterController@show` e `@view` | Um serviço recém-criado (não publicado) fica invisível no portal |
| `publicado_em` é gravado apenas na **primeira** publicação | `ServiceLetterController@update` (`if ($request->has('publicado') && !$serviceLetter->publicado)`) e `@store` | Despublicar e republicar **preserva** a data original |
| `destaque` é independente de `publicado` | coluna independente | Serviço pode ser destacado e não estar publicado |
| A listagem do CMS **não** filtra `deleted_at` | views SQL | Serviços excluídos continuam na listagem (ver §4.1) |

### 1.2 Modos de exibição

| `tipo_exibicao` | Comportamento |
|---|---|
| `pagina` | O portal mostra a ficha completa; o CMS exibe as 5 abas (Responsáveis, Canais, Etapas, Documentos, Anexos) |
| `link_externo` | O portal **não** mostra as abas; mostra apenas o botão "Acessar Serviço Online" apontando para `link_externo` |

O controller **não** valida `link_externo` quando `tipo_exibicao = link_externo` — um serviço pode ser salvo sem URL, resultando em botão com `href=""`.

### 1.3 Formas de acesso

As flags `acesso_online`, `acesso_telefone`, `acesso_presencial` controlam **apenas** os badges mostrados no portal. Elas **não** são validadas contra a existência de canais: um serviço pode exibir o badge "Telefone" sem nenhum `service_letter_channels` do tipo `telefone`. As duas informações são mantidas de forma independente pelo operador.

### 1.4 Canais

- `tipo` ∈ {`online`, `telefone`, `presencial`} — validado **apenas no `store`**; o `update` não valida.
- O formulário mostra apenas o campo de contato correspondente ao `tipo` selecionado (toggle JS em `modulos.js`), mas o controller aceita e persiste **todos** os campos (`url`, `telefone`, `endereco`) simultaneamente. Se o operador trocar o tipo sem limpar os campos, o canal fica com dados dos dois tipos.
- A ordenação é por `ordem` (relação Eloquent e partial Blade).

### 1.5 Etapas

- `numero` (obrigatório, integer) e `ordem` (smallint, default 0) coexistem.
- O **portal** exibe as etapas ordenadas por `numero` (`$service->steps->where('ativo', true)->sortBy('numero')`).
- A **relação Eloquent** ordena por `ordem` (`->hasMany(...)->orderBy('ordem')`).
- A **tabela do CMS** exibe na ordem da relação (por `ordem`).
- **Consequência:** se `numero` e `ordem` divergirem, o portal e o CMS mostram ordens diferentes. O campo `numero` é preenchido manualmente pelo operador (o JS sugere `length + 1` ao adicionar). Ver §4.2.

### 1.6 Documentos

- `obrigatorio` default `true` na migration — o padrão é "exigido", não "opcional".
- O portal exibe **duas** listas de documentos: o HTML livre `service_letters.documentacoes` e a lista estruturada `service_letter_documents` (só as com `ativo = true`).

### 1.7 Responsáveis — validação cruzada

`ServiceLetterController::validateResponsavel()` implementa uma regra condicional:

```
se nome, cargo, email e telefone estão TODOS vazios  → telefone é obrigatório
se email e telefone estão AMBOS vazios                 → nome e cargo são obrigatórios
caso contrário                                           → sem requisitos
```

Ou seja: um responsável precisa de **pelo menos um canal de contato** (e-mail ou telefone); se escolher telefone, precisa de nome e cargo.

> **Observação:** a mensagem `'O telefone é obrigatório quando todos os outros campos estão vazios.'` está associada à regra `telefone.required`, mas a regra só é aplicada quando **todos** os campos estão vazios — nesse caso o telefone é a única forma de contato, o que é coerente.

### 1.8 Anexos

- Limite: `max:10240` KB (10 MB) — validado apenas no `store`.
- `tipo` (mime type) e `tamanho_bytes` são gravados automaticamente.
- O nome exibido (`nome`) cai para o nome original do arquivo quando não informado.
- O diretório de armazenamento inclui o id do serviço: `service-letter/attachments/{service_letter_id}/{timestamp}_{nomeOriginal}`.
- Não há sanitização do nome original do arquivo — risco de colisão (o prefixo de timestamp reduz, mas não elimina) e de caracteres especiais preservados no path.

### 1.9 Configuração (singleton)

- O controller usa `first()` para ler e `firstOrCreate([])` para gravar. **Não há unicidade garantida no banco**: `service_letter_configs` não tem constraint de unicidade. Se houver mais de uma linha, `first()` devolve arbitrariamente a de menor `id`.
- `publicado` e `ativo` da configuração **não são consultados** pelo portal — a publicação do módulo no portal é controlada **apenas** pelas flags do serviço. Ver §4.5.

### 1.10 PDF

- A geração considera apenas serviços com `ativo = true` **e** `publicado = true`.
- A ordenação usa `secretaria_id` quando `ordenacao_servicos = 'secretaria'`; qualquer outro valor → ordem por `nome`. O valor `assunto` do formulário **não** ordena por assunto — é apenas um rótulo.
- O PDF mais recente disponível no portal é o de `status = 'gerado'`. O status `'publicado'` **nunca é gravado** por nenhum código do módulo — nenhum controller implementa a publicação de PDF. Ver §4.4.

### 1.11 Registro de acessos

- `service_letters.visualizacoes` é incrementada a cada visita à ficha do serviço (`$service->increment('visualizacoes')`).
- A tabela `service_letter_access_logs` existe (com unique `(service_letter_id, data_acesso, origem)`) mas **nenhum código grava nela**. Ver §4.3.

## 2. Acoplamentos

### 2.1 Dependências do módulo Carta de Serviços

| Depende de | Tipo | Observação |
|---|---|---|
| `contents` (secretarias) + `category_contents` | **forte** | ID da categoria **hardcoded** = 7 (`SecretariaController@create/edit`: `Content::where('category_contents_id', 7)`). Não há constante nem consulta por slug. Se a categoria mudar de id no banco, o formulário fica vazio. |
| `modulos` / `menu_orders` / `resources` | forte | Inseridos na migration do módulo; o menu do CMS depende do `menu_orders.modulo_id` |
| `users` | fraca | `created_by`, `updated_by`, `gerado_por` |
| `Configuration` (`layout`) | forte | `getTemplateActived()` — determines qual template do portal renderiza |
| `ActivityLog` (spatie) | fraca | Auditoria |

### 2.2 O que o módulo Carta de Serviços NÃO depende

- **Não** usa `repositories` (ao contrário de módulos maduros do sistema).
- **Não** usa `App\Services\`.
- **Não** usa `FormRequest`.
- **Não** usa `API Resource` / presenter.
- **Não** usa `Policy` do Laravel — a autorização é feita pelo middleware global `check.route.permission` + `has_access()` nos blades.

## 3. Onde a v6 divergiu da v5

**A Carta de Serviços não existe na v5.** Verificado por varredura completa de `v5/` (`find v5 -iname "*carta*"` → somente `cartalyst`; nenhum controller, model, view, migration ou seed relacionado).

O módulo é, portanto, **inteiramente novo na v6**, introduzido pelas 13 migrations de janeiro/março de 2026. Não há "código copiado da v5" — mas há **padrões herdados do restante da v6** que ele reproduz:

| Padrão herdado | Exemplo no módulo |
|---|---|
| Controllers com `use App\Http\Controllers\Admin\Controller` (o controller **Admin**, não o `Prefecture`) | todos os 9 controllers do CMS |
| Auditoria via `activity()->performedOn()->log()` | em todos os controllers |
| Listagens via `DB::table(view_sql)` + Yajra DataTables + `struct_views()` | `DatatableController@get_service_letter_services_list` |
| Validação inline com `$request->validate()` | `ServiceLetterController@store/update` |
| Flash messages via `laralabs/toaster` (`flash(...)->success()`) | idem |
| Soft deletes adicionados **depois** da criação das tabelas (migration de 2026-03-30) | 8 tabelas |

## 4. Armadilhas

### 4.1 Views SQL não filtram soft delete

`listagem_servicos_carta_de_servicos` e `listagem_assuntos_carta_de_servicos` não têm `WHERE deleted_at IS NULL`. Serviços e assuntos excluídos permanecem na listagem do CMS. Como o `ServiceLetterController@destroy` usa `$serviceLetter->delete()` (soft delete, pois o model usa `SoftDeletes`), a exclusão **não** remove a linha da view.

### 4.2 Ordenação dupla de etapas (`numero` vs `ordem`)

Detalhado em §1.5. O `ServiceLetterStepController@updateOrder` existe para reordenar por `ordem`, mas **não é chamado por nenhum JS** — o `modulos.js` não implementa drag-and-drop nem sortable. Na prática, `ordem` só muda se o operador digitar o valor no modal.

### 4.3 Tabela `service_letter_access_logs` órfã — com nome divergente

| Item | Valor |
|---|---|
| Tabela criada pela migration | `service_letter_access_logs` (singular) |
| Tabela declarada no model `ServiceLetterAccessLog` | `service_letters_access_logs` (plural) |
| Escrita em algum lugar? | **Não** — nenhum `create`/`insert` no módulo |
| `ServiceLetter::accessLogs()` | `hasMany(ServiceLetterAccessLog::class, 'service_letter_id')` |

Se alguém passar a usar a relação, ela não resolverá → erro de tabela inexistente. A correção é alinhar o `$table` do model ao nome real (ou renomear a tabela).

### 4.4 Gerador de PDF ausente

`ServiceLetterPdfController@generate` instancia `new ServiceLetterPdfService()` (`use App\Services\ServiceLetterPdfService;`). **A classe não existe** em `v6/app/Services/` (verificado: `app/Services/` contém apenas `FPdfService.php`, `Pdf/`, `PortalTransparenciaService.php` e `Dashboard/`). O resultado é um `Error: Class "App\Services\ServiceLetterPdfService" not found` capturado pelo `catch (Exception $e)` — que, no PHP 7, **não** captura `Error` (que estende `Throwable`, não `Exception`). Consequência: **erro fatal 500 não tratado**, sem a mensagem amigável de "Ocorreu um erro ao gerar o PDF". Ver `10-PDF-E-ANEXOS.md`.

### 4.5 Flags de configuração ignoradas pelo portal

`service_letter_configs.ativo` e `.publicado` existem e são editáveis no CMS, mas `Portal\ServiceLetterController` **não** os consulta. Não há como desligar a Carta de Serviços inteira pelo CMS — apenas serviço a serviço.

### 4.6 Rotas sem consumidor

| Rota | Situação |
|---|---|
| `POST /cms/carta-de-servicos/servicos/set-status` | Nenhum blade/JS chama |
| `POST /cms/carta-de-servicos/servicos/set-destaque` | Nenhum blade/JS chama |
| `POST /cms/carta-de-servicos/etapas/order` | Nenhum JS chama |

### 4.7 `ServiceLetterPdf` com `$timestamps = false` mas com colunas na tabela

A migration cria `created_at`/`updated_at`; o model declara `public $timestamps = false;`. As colunas ficam sempre `NULL`.

### 4.8 `down()` das migrations de views derruba nomes errados

`2026_01_20_221003_create_service_letters_views.php` cria `listagem_servicos_carta_de_servicos` e `listagem_assuntos_carta_de_servicos`, mas o `down()` derruba `service_letters_view` e `service_letters_subjects_view` — nomes que **não existem**. Rollback não limpa as views.

### 4.9 SQL específico do Postgres

A busca do portal usa `translate(lower(nome), 'àáâãäéèëêíìïîóòõöôúùüûçÇ', 'aaaaaeeeeiiiiooooouuuucc') ilike ?`. Isso é **Postgres-específico** (`translate` + `ilike`) e não funciona em MySQL/SQLite. A modernização precisa de full-text search ou de uma camada de compatibilidade.

### 4.10 `clean_slug` como normalizador de busca

`clean_slug()` (`v6/app/Helpers/functions.php:823-847`) substitui acentos por seus equivalentes ASCII e **remove** qualquer caractere fora de `[A-Za-z0-9. ]` — ou seja, mantém espaços e pontos, e apaga hífens, barras, aspas etc. É usado **antes** do `whereRaw`, mas a query já normaliza com `translate(lower(...))`. A dupla normalização é redundante, mas **não quebra**: o padrão `%texto%` só deixa de casar se o termo digitado contiver caracteres que `clean_slug` apaga (ex.: `saúde` → `saude`, o que ainda casa com `translate`).

### 4.11 `Str::slug` e colisão de slug

`slug` é `UNIQUE` e gerado por `Str::slug($request->nome)`. Dois serviços com o mesmo nome (ou nomes que gerem o mesmo slug) violam a constraint. O `catch (Exception $e)` do controller **captura** a `PDOException` (que estende `Exception`) e mostra a mensagem genérica "Ocorreu um erro!", sem indicar a causa. O usuário perde o formulário (`withInput()` é preservado, o que é positivo).

### 4.12 Upload fora do disco `public` (responsável do serviço)

Em `ServiceLetterController@store/@update`, o upload de `responsavel_foto` usa `$file->storeAs('service-letter/responsaveis', $name)` **sem argumento de disco**; em `storeResponsavel`/`updateResponsavel` usa `storeAs(..., 'public')`. Com `FILESYSTEM_DRIVER=public` (default em `v6/config/filesystems.php:16`) ambos gravam em `storage/app/public/`. Mas o `destroyResponsavel` **não** apaga o arquivo do disco — apenas a linha do banco. Arquivos órfãos acumulam.

### 4.13 Exclusão de anexo apaga o arquivo (mas a escrita usa caminho com prefixo duplo)

`ServiceLetterAttachmentController@store` grava `'arquivo' => 'storage/' . $path`, onde `$path` já é o retorno de `storeAs()` (ex.: `service-letter/attachments/7/20260101120000_doc.pdf`). Resultado: `storage/service-letter/attachments/7/...`. O `destroy` faz `Storage::delete(str_replace('storage/', '', $attachment->arquivo))` → apaga `service-letter/attachments/7/...` no disco `public`. **Consistente.** Mas o JavaScript monta o link de download como `'/' + data.arquivo` → `/storage/service-letter/...`, que depende do symlink `public/storage` → `storage/app/public`. Verificar se o symlink existe no deploy.

### 4.14 N+1 e consultas dentro de views

- `nav.blade.php` executa uma **consulta ao banco dentro do blade** (`ServiceLetterPdf::where('status','gerado')->...->first()`), a cada renderização da página de assunto.
- `Portal\ServiceLetterController@show` carrega a lista de secretarias para o filtro com um `get()` completo de serviços + `with('secretaria')` — todas as linhas em memória, sem paginação.

### 4.15 Ausência de `deleted_at` na tabela de logs com `timestamps`

`service_letter_access_logs` tem `created_at`/`updated_at` (migration) mas o model declara `$timestamps = false` — mais uma divergência model↔schema.

### 4.16 Permissões não cadastradas

Nenhum papel do banco possui chaves `carta-de-servicos.*` (verificado no JSON de `roles.permissions`). Na prática, apenas `super-admin` visualiza os botões e acessa as rotas do módulo.

### 4.17 CSS referenciado inexistente

`resources/views/prefecture/service-letter/services/form.blade.php` e `edit.blade.php` referenciam `assets/css/custom/contents/carta-de-servicos/style.css`, arquivo **inexistente** em `v6/public/assets/css/custom/contents/` (que contém apenas `category-contents` e `formularios-dinamicos`).

## 5. Código duplicado dentro do módulo

| Duplicação | Onde |
|---|---|
| Renderização do `<tr>` de cada recurso | `renderCanalRow`, `renderEtapaRow`, `renderDocumentoRow`, `renderAnexoRow` em `modulos.js` **e** o HTML Blade equivalente nos partials. Um novo campo exige alteração em 2 lugares. |
| `submitForm`/`deleteItem`/`reloadTable`/`checkEmptyTable`/`showErrors` | `modulos.js` **e** `responsaveis.js` (versão paralela com nomes `reloadTabelaResponsaveis`, `showErrors`, `checkEmptyTable`) |
| Escolha de layout do portal | `getTemplateActived()` herdado de `Admin\Controller` (não há método próprio no `Portal\ServiceLetterController`) |
| Métrica de "serviços publicados" | replicada em `ServiceLetterPdfController@generate` e `Portal\ServiceLetterController@show`/`@view` |
