# Carta de Serviços — Gaps e Dívidas

> Todos os itens abaixo foram verificados no repositório `v6` (código + banco `prefeituras_v5`). Itens marcados **[INFERÊNCIA]** dependem de comportamento de runtime não observável por leitura estática.

## 1. Bugs e erros conhecidos

### 1.1 [CRÍTICO] O gerador de PDF não existe

| Item | Detalhe |
|---|---|
| Onde | `v6/app/Http/Controllers/Prefecture/ServiceLetter/ServiceLetterPdfController.php:14` (`use App\Services\ServiceLetterPdfService;`) e `:62` (`$pdfService = new ServiceLetterPdfService();`) |
| Evidência | A classe **não existe** em `v6/app/Services/` (conteúdo verificado: `FPdfService.php`, `Pdf/ImageProcessor.php`, `Pdf/PdfConverter.php`, `PortalTransparenciaService.php`, `Dashboard/`). Nenhuma ocorrência de `class ServiceLetterPdfService` em `v6/app/`. |
| Efeito | Clicar em "Gerar PDF" no CMS provoca **erro fatal**: o `catch (Exception $e)` do controller **não** captura `Error` (que implementa `Throwable`, não `Exception`), logo o usuário vê uma página 500 em branco, sem a mensagem amigável "Ocorreu um erro ao gerar o PDF". |
| Impacto | A funcionalidade "Gerar PDF" e "Baixar PDF Completo" no portal estão **quebradas**. |
| Correção | Implementar `App\Services\ServiceLetterPdfService` com `generate(ServiceLetterConfig $config, Collection $services): array` retornando `['path', 'filename', 'size', 'pages']`. |

### 1.2 [ALTO] CSS do formulário não existe

| Item | Detalhe |
|---|---|
| Onde | `v6/resources/views/prefecture/service-letter/services/form.blade.php` — `@section('adminlte_css')` com `assets/css/custom/contents/carta-de-servicos/style.css` |
| Evidência | `v6/public/assets/css/custom/contents/` contém apenas `category-contents` e `formularios-dinamicos`. |
| Efeito | 404 do asset no navegador (silencioso). Sem impacto funcional, mas o layout do formulário depende de estilos que não são carregados. |

### 1.3 [ALTO] Tabela de log de acessos com nome divergente no model

| Item | Detalhe |
|---|---|
| Tabela real | `service_letter_access_logs` (migration `2026_01_20_160739_create_service_letter_access_logs_table.php`) |
| Model | `v6/app/Http/Model/Prefecture/ServiceLetter/ServiceLetterAccessLog.php` → `protected $table = 'service_letters_access_logs';` |
| Efeito | A relação `ServiceLetter::accessLogs()` apontaria para uma tabela inexistente → `QueryException` |
| Agravante | A tabela **nunca é gravada** por nenhum código do módulo — o modelo inteiro é código morto |
| Correção | Remover o model e a tabela, ou alinhar o `$table` e implementar a gravação |

### 1.4 [MÉDIO] Views SQL não filtram registros excluídos (soft delete)

`listagem_servicos_carta_de_servicos` e `listagem_assuntos_carta_de_servicos` (`v6/database/migrations/2026_01_20_221003_create_service_letters_views.php`) não possuem `WHERE deleted_at IS NULL`. Como os models usam `SoftDeletes`, os serviços e assuntos excluídos **continuam aparecendo** nas listagens do CMS.

### 1.5 [MÉDIO] `id` do `<tbody>` dos canais divergente do JavaScript

| Item | Valor |
|---|---|
| Blade | `v6/resources/views/prefecture/service-letter/services/partials/channels.blade.php:20` → `<tbody id="lista_canals">` |
| JS | `v6/public/assets/js/custom/contents/service-letter/modulos.js` → `var tbody = '#lista_' + tipo + 's';` → para `tipo='canal'` resolve `#lista_canais` |

**Efeito:** ao criar ou editar um canal, a linha **não** é inserida/substituída na tabela (o `append`/`replaceWith` roda sobre um seletor inexistente). O dado **é** gravado no banco — o operador apenas não vê o resultado e acredita que falhou. Os demais recursos (`lista_etapas`, `lista_documentos`, `lista_anexos`) estão corretos.

### 1.6 [MÉDIO] Ordenação dupla das etapas

`service_letter_steps.numero` (exibido no portal, ordenado por `sortBy('numero')`) coexiste com `service_letter_steps.ordem` (usado na relação Eloquent e na tabela do CMS). Se divergirem, **o portal e o CMS mostram ordens diferentes**. Não há validação nem constraint que impeça a divergência.

### 1.7 [MÉDIO] `link_externo` não é validado

`ServiceLetterController@store/@update` valida `tipo_exibicao` mas **não** exige `link_externo` quando `tipo_exibicao = 'link_externo'`. Resultado: serviço externo sem URL → botão "Acessar Serviço Online" com `href=""`.

### 1.8 [MÉDIO] Seed com valor inválido para `menu_acesso_rapido`

`v6/database/seeds/ServiceLetterSeeder.php` grava `'menu_acesso_rapido' => true` (booleano) numa coluna `varchar(20)` cujo domínio validado é `cidadao|empresa|servidor`. O serviço semeado **não pode ser editado** pelo CMS sem corrigir o valor (a validação `in:cidadao,empresa,servidor` falha).

### 1.9 [BAIXO] `down()` das migrations de views derruba nomes inexistentes

`2026_01_20_221003_create_service_letters_views.php` → `down()` executa `DROP VIEW IF EXISTS service_letters_view` e `service_letters_subjects_view`, mas as views criadas são `listagem_servicos_carta_de_servicos` e `listagem_assuntos_carta_de_servicos`. O rollback não remove as views reais.

### 1.10 [BAIXO] `ServiceLetterPdf` com `$timestamps = false` apesar das colunas existirem

`v6/app/Http/Model/Prefecture/ServiceLetter/ServiceLetterPdf.php` declara `public $timestamps = false;`, mas a migration cria `created_at`/`updated_at`. As colunas ficam sempre `NULL`.

### 1.11 [BAIXO] `ServiceLetterAccessLog` com `$timestamps = false` apesar das colunas existirem

Mesma divergência em `service_letter_access_logs`.

### 1.12 [BAIXO] `ServiceLetterConfig` com `created_by` no `$fillable` mas sem coluna

`ServiceLetterConfig::$fillable` inclui `'created_by'`, mas a migration `2026_01_20_160648` só cria `updated_by`. Gravar `created_by` causaria erro de coluna inexistente. O controller nunca o envia — risco latente.

### 1.13 [BAIXO] `ServiceLetterAttachment` com `created_by` no `$fillable` mas sem coluna

Mesma divergência em `service_letter_attachments` (a migration não cria `created_by`).

### 1.14 [BAIXO] Exclusão de responsável não remove o arquivo do disco

`ServiceLetterController@destroyResponsavel` apaga apenas o registro. O arquivo de `foto` permanece em `storage/app/public/service-letter/responsaveis/`.

### 1.15 [BAIXO] `destroyResponsavel` com id inexistente devolve 500 em vez de 404

`ServiceLetterResponsavel::findOrFail($id)` lança `ModelNotFoundException`, capturada pelo `catch (\Exception $e)` genérico → resposta **500** com `errors: ["Ocorreu um erro ao excluir o responsável."]`. O mesmo vale para `updateResponsavel`.

### 1.16 [BAIXO] `destroy` de serviço/assunto com id inexistente

`ServiceLetterController@destroy` usa `$this->serviceLetter->find($id)` (não `findOrFail`) e chama `->delete()` sobre `null` → `Error` não capturado pelo `catch (Exception $e)` → 500 fatal. Verificado no código: `$serviceLetter = $this->serviceLetter->find($id); $serviceLetter->delete();`.

### 1.17 [BAIXO] Canais aceitam dados de outros tipos

`ServiceLetterChannelController@update` persiste `url`, `telefone` e `endereco` simultaneamente, sem validar qual se aplica ao `tipo`. O formulário oculta os campos irrelevantes via JS, mas o valor anterior permanece gravado.

## 2. Funcionalidades incompletas ou mortas

| Item | Evidência | Situação |
|---|---|---|
| `POST /cms/carta-de-servicos/servicos/set-status` | Nenhum blade/JS chama (grep em `resources/views/prefecture/service-letter/` e `public/assets/js/custom/contents/service-letter/`) | Rota funcional sem UI |
| `POST /cms/carta-de-servicos/servicos/set-destaque` | idem | Rota funcional sem UI |
| `POST /cms/carta-de-servicos/etapas/order` (`updateOrder`) | idem | Rota funcional sem UI (sem sortable no JS) |
| `service_letter_access_logs` | Nenhum `create`/`insert` no módulo | Tabela + model + relação inúteis |
| `ServiceLetterConfig.capa_conteudo_editor` | O bloco do editor está **comentado** em `config/index.blade.php` | Campo sem uso |
| `service_letter_configs.ativo` / `.publicado` | O portal **não** os consulta | Flags sem efeito prático |
| `service_letter_pdfs.status = 'publicado'` / `publicado_em` / `publicado_por` | Nenhum código grava | Caminho de publicação de PDF inexistente |
| `service_letter_pdfs.erro_mensagem` | Só é gravado se o fluxo de erro existir — mas o fluxo de erro morre com `Error` (§1.1) | Effectively unreachable |
| `service_letters.justificativa` | Campo gravado, **nunca exibido** no portal nem em view do CMS | Dado capturado e perdido |
| `service_letters.responsavel_nome/cargo/email/telefone/foto` | Gravados pelo formulário, **nunca exibidos** no portal (só a aba de responsáveis estruturados é exibida) | Duplicata morta |
| `service_letters.etapas` (HTML) e `service_letter_steps` | Ambos exibidos no portal | Duplicidade ativa (ver §5) |
| `service_letters.previsao_atendimento_detalhes`, `horario_atendimento_detalhes` | Gravados; o portal exibe `previsao_atendimento` e `horario_atendimento` (versão curta) mas **não** os `_detalhes` | Dados parcialmente perdidos |
| `service_letters.endereco_latitude/longitude` | Gravados; o portal **não** exibe (nem em mapa) | Dados não utilizados |
| `menu_acesso_rapido` | Aparece na listagem do CMS e é gravado; o portal **não** filtra por ele | Métrica sem uso público |
| `ServiceLetterChannel.descricao`, `ServiceLetterDocument.descricao` | Gravados; o portal exibe apenas `nome` dos documentos e, dos canais, só o contato | Descrições não exibidas |
| `ServiceLetterAttachment` (anexos) | **Nenhuma** view do portal renderiza anexos | Funcionalidade sem exposição pública |
| `ServiceLetterSubject.imagem` / `icone` / `icone_cor` | Gravados; o portal usa apenas `nome` no menu de assuntos | Identidade visual não usada |
| `ServiceLetterSubject.descricao` | Exibida apenas na view `index.blade.php` do portal (que é praticamente inalcançável, pois o index redireciona) | effectively morta |
| `ServiceLetterConfig.descricao`, `cabecalho`, `rodape`, `texto_antes_sumario`, `texto_depois_sumario`, `texto_encerramento`, tipografia | Consumidos apenas pelo gerador de PDF — que não existe (§1.1) | Configuração sem efeito |

## 3. Itens da v5 que foram removidos / adicionados

### 3.1 Mapa v5 → v6

**A Carta de Serviços não existe na v5.** Varredura completa de `v5/` (`find v5 -iname "*carta*"` e `find v5 -iname "*service*letter*"`) retorna apenas artefatos do pacote `cartalyst/sentinel` (`v5/config/cartalyst.sentinel.php`, `v5/vendor/cartalyst/sentinel/**`). Nenhum controller, model, view, migration, seed ou rota de Carta de Serviços na v5.

| Camada | v5 | v6 |
|---|---|---|
| Rotas CMS | — | `v6/routes/service_letter.php` (36 rotas), carregado por `require` em `v6/routes/basepro.php:512` |
| Rotas Portal | — | `v6/routes/web.php:53-56` |
| Controllers CMS | — | 8 arquivos em `v6/app/Http/Controllers/Prefecture/ServiceLetter/` |
| Controller Portal | — | `v6/app/Http/Controllers/Portal/ServiceLetterController.php` |
| Models | — | 10 arquivos em `v6/app/Http/Model/Prefecture/ServiceLetter/` |
| Migrations | — | 13 arquivos (2026-01-20 e 2026-03-30) |
| Views CMS | — | 15 blades em `v6/resources/views/prefecture/service-letter/` |
| Views Portal | — | 30 blades (6 modelos × 5) em `v6/resources/views/portal/layouts/modelo{1..6}/carta-servicos/` |
| JS | — | 4 arquivos em `v6/public/assets/js/custom/contents/service-letter/` |
| Seed | — | `v6/database/seeds/ServiceLetterSeeder.php` |
| Registro no menu | — | `modulos` id 57, `resources` id 4, `menu_orders` id 3 |
| `ServiceLetterResponsavelController` | — | **Não existe** — o CRUD de responsáveis está em `ServiceLetterController` |
| `v6/app/Http/Requests/Prefecture/ServiceLetter/**` | — | **Não existe** — toda a validação é inline |

### 3.2 Correção ao enunciado do assignment

O enunciado afirmava que o slug `carta-de-servicos` **não existiria** na tabela `modulos`. **Verificado em contrario**: a linha existe (id 57, `name='Carta de Serviços'`, `status=true`), criada pela própria migration `2026_01_20_160719_create_service_letters_table.php`, que faz `insertGetId` em `modulos` e `updateOrInsert` em `menu_orders` e `resources`.

### 3.3 Itens da v6 ausentes da v5 (não aplicável)

Não há, por definição, itens da v6 ausentes da v5 — o módulo é inteiramente novo.

## 4. Riscos de dados (migração de schema)

| # | Risco | Gravidade | Detalhe |
|---|---|---|---|
| D1 | **Divergência de tipos no pivot** | Média | `service_letters_subjects_rel.service_letter_id`/`subject_id` são `bigint`, enquanto as PK referenciadas são `integer`. As FKs existem e funcionam (cast implícito), mas qualquer `ALTER`/`dump`/`restore` entre schemas com tipos divergentes pode falhar. Recomenda-se recriar o pivot com `foreignId`. |
| D2 | **Colunas `created_by` ausentes em 2 tabelas** | Média | `service_letter_configs` e `service_letter_attachments` não têm `created_by`, mas os models listam o campo no `$fillable`. Se a v7 passar a gravar, quebra. |
| D3 | **Soft delete aplicado retroativamente** | Baixa | As 8 tabelas ganharam `deleted_at` em `2026_03_30`, **depois** de nascerem. Registros criados antes de 2026-03-30 têm `deleted_at` nulo — comportamento correto. Mas migrations intermediárias que façam `delete()` sem SoftDeletes podem ter deixado linhas "órfãs" semanticamente. |
| D4 | **Views SQL não versionadas com o schema** | Média | As views são criadas por migration, mas `down()` não as remove (§1.9). Um `migrate:fresh` as deixa no banco se a migration falhar no meio. |
| D5 | **`service_letters.slug` único e gerado** | Média | Serviços com nomes homônimos (ex.: duas secretarias com "Certidão") não podem coexistir se o slug colidir. O `catch` genérico oculta a causa. |
| D6 | **Conteúdo HTML sem sanitização** | Alta | Os campos Summernote (`sobre`, `documentacoes`, `custos`, `etapas`, `requisitos`, `informacoes_adicionais`) são gravados e exibidos com `{!! !!}` (HTML não escapado). Um XSS armazenado é possível se o editor não sanitizar. Verificar o Summernote em `v6/public/assets/plugins/` — **[NÃO VERIFICADO]** nesta investigação. |
| D7 | **`visualizacoes` sem histórico** | Baixa | Só o contador total; não há série temporal. A tabela de logs existe mas é morta. |
| D8 | **Arquivos em disco não versionados com o banco** | Média | Anexo/foto/capa/PDF ficam em `storage/app/public/`, fora do controle de versão. Um `migrate` não os move. O `Storage::delete` do anexo remove o arquivo; o do responsável, não. |

## 5. Decisões de produto pendentes

| # | Questão | Impacto |
|---|---|---|
| Q1 | Manter as **duas** representações de etapas (HTML em `service_letters.etapas` e estruturada em `service_letter_steps`)? | O portal hoje mostra as duas. Recomenda-se manter só a estruturada. |
| Q2 | Manter os campos `responsavel_*` no serviço **e** a tabela `service_letter_responsaveis`? | Duplicata; só a tabela é exibida. |
| Q3 | O módulo deve ser **desligável** pelo CMS (`config.ativo`/`config.publicado`)? | Hoje não é — só serviço a serviço. |
| Q4 | Os **anexos** devem aparecer no portal? | Hoje não aparecem em lugar nenhum do portal. |
| Q5 | O `menu_acesso_rapido` deve filtrar a listagem do portal (menu "Cidadão" / "Empresa" / "Servidor")? | O dado é capturado e nunca usado. |
| Q6 | `ordenacao_servicos = 'assunto'` deve agrupar por assunto no PDF? | O valor existe na UI mas só produz ordenação por nome. |
| Q7 | Os anexos do **portal** devem ser servidos do disco `public` ou de área autenticada? | Se contiverem dados sensíveis, `public` é inadequado. |
| Q8 | Deve haver **publicação** de PDF (`status='publicado'`)? | O status existe no enum do blade, mas nada o grava. |
| Q9 | O contador `visualizacoes` deve ser por sessão/usuário ou por IP? | Hoje é bruto, sem dedupe. |
| Q10 | O relatório de acessos (tabela morta) é requisito legal? | Se sim, implementá-lo; se não, removê-lo. |

## 6. Riscos de segurança

| # | Risco | Gravidade | Situação |
|---|---|---|---|
| S1 | **XSS armazenado** via HTML do Summernote renderizado com `{!! !!}` | Alta | A sanificação depende do editor no cliente. **[NÃO VERIFICADO]** se o backend sanitiza. Recomenda-se allowlist no servidor. |
| S2 | `carta-de-servicos.pdfs.stream` serve o PDF com `Response::make($file, 200)` e `Content-Type: application/pdf` — sem `Content-Disposition: attachment` nem `X-Content-Type-Options: nosniff` | Média | PDF inline pode permitir conteúdo ativo em alguns navegadores. |
| S3 | `carta-de-servicos.pdfs.download` usa `response()->download($path, $pdf->nome_arquivo)` — sem validação de que o `nome_arquivo` é seguro | Baixa | O nome vem do controller de geração (não de input do usuário), mas convém sanitizar. |
| S4 | Upload de anexo aceita **qualquer** mime type (só `file\|max:10240`) | Média | Permite subir HTML/SVG com script, servidos do disco `public` com o mime original gravado em `tipo_arquivo`. Recomenda-se allowlist de extensões + servir anexo com `Content-Disposition: attachment` e `X-Content-Type-Options: nosniff`. |
| S5 | Upload de anexo usa o **nome original** do arquivo no path | Média | Sem sanitização de `../` ou caracteres especiais. O `UploadedFile::getClientOriginalName()` é sanitizado pelo Symfony para o basename, mas caracteres especiais e espaços permanecem. |
| S6 | Rotas de `datatable` do módulo exigem apenas **login**, sem permissão específica | Média | Comportamento herdado do `check.route.permission` para rotas cujo nome contém `datatable`. |
| S7 | Nenhum papel possui permissões `carta-de-servicos.*` | Baixa (opcionalmente positiva) | Na prática só `super-admin` tem acesso. |
| S8 | Ausência de `check.logged` no grupo de `datatable` | Baixa | Compensado pelo `check.route.permission` herdado do controller base. |
| S9 | `carta-de-servicos.pdfs.stream`/`download` acessíveis a qualquer usuário logado com permissão de rota | Média | A permissão derivada do nome da rota é `carta-de-servicos.pdfs.stream` — precisa ser cadastrada no seeder de permissões. |

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

1. A Carta de Serviços deve ser **vinculada a um link no menu do portal** (acesso rápido)? Hoje existe apenas a rota `/carta-de-servicos`; nenhum item de menu do portal foi identificado apontando para ela.
2. Deve haver **fluxo de aprovação** (serviço rascunho → revisão → publicação)? Hoje o operador publica diretamente.
3. O **PDF** deve incluir os anexos? Hoje a lista de anexos nem é renderizada no portal, e o gerador não existe.
4. Deve existir **histórico de versões** do serviço (o conteúdo publicado pode mudar sem registro do que o cidadão viu)?
5. A **carta por secretaria** (subconjunto) deve ser gerável separadamente? A ordenação por secretaria sugere essa intenção, mas não há gerador por secretaria.
6. Deve haver **integração com o sistema de atendimento/ouvidoria** (o cidadão solicita o serviço online)? O módulo **não** tem formulário de solicitação — é apenas informativo.
7. O contador `visualizacoes` deve alimentar o Dashboard (existe um widget `ConteudosVisitadosWidget`)? Hoje apenas `service_letters.visualizacoes` é incrementado, sem integração com o dashboard.

## 8. Resumo de prioridades para a v7

| Prioridade | Item |
|---|---|
| **P0** | Implementar `ServiceLetterPdfService` (§1.1) — sem isso a funcionalidade central do módulo não existe |
| **P0** | Sanitizar o HTML dos campos Summernote no servidor (S1) |
| **P1** | Corrigir `ServiceLetterAccessLog::$table` ou remover o model + tabela (§1.3) |
| **P1** | Corrigir o `id` do `<tbody>` dos canais (§1.5) |
| **P1** | Filtrar `deleted_at` nas views SQL ou substituí-las por queries Eloquent (§1.4) |
| **P1** | Criar as permissões `carta-de-servicos.*` nos papéis (§2, S7) |
| **P2** | Unificar a ordenação de etapas (§1.6) |
| **P2** | Validar `link_externo` condicionalmente (§1.7) |
| **P2** | Corrigir o seed (`menu_acesso_rapido`) (§1.8) |
| **P2** | Restringir tipos de arquivo permitidos nos anexos (S4) |
| **P3** | Corrigir `down()` das migrations de views (§1.9) |
| **P3** | Alinhar `$fillable` dos models com as colunas reais (§1.12, §1.13) |
| **P3** | Remover o arquivo do disco ao excluir responsável (§1.14) |
| **P3** | Tratar `findOrFail` como 404 em vez de 500 (§1.15, §1.16) |
| **P3** | Decidir sobre as duplicidades (Q1, Q2) e os recursos mortos (Q3–Q10) |
