# Carta de Serviços — Testes

## 1. Estratégia

O módulo **não possui nenhum teste** no legado (verificado: `v6/tests/Feature/` e `v6/tests/Unit/` não contêm arquivos relacionados a `ServiceLetter`; a suíte existente cobre apenas o Dashboard e o módulo de Revisões).

Estratégia proposta para a v7:

| Camada | Tipo | Ferramenta | Foco |
|---|---|---|---|
| Unit | Testes de modelo e de regra | PHPUnit + Pest | casts, enums, escopo `publicados`, `scopeBuscar`, geração de slug, validação cruzada de responsável |
| Feature (CMS) | Testes de rota/autorização/CRUD | Pest + `LazilyRefreshDatabase` | validação, persistência, permissões, resposta JSON, upload |
| Feature (Portal) | Testes de rota pública | Pest | listagem, filtro, paginação, ficha, PDF, contador de visualizações |
| Contrato de API | Testes de Resource | Pest | formato JSON estável |
| PDF | Teste de integração do `ServiceLetterPdfService` | Pest | arquivo gerado, número de páginas, conteúdo esperado |

### 1.1 Ambiente de teste

O `phpunit.xml` legado (`v6/phpunit.xml`) define:

```xml
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
```

**Ajuste necessário na v7:** `QUEUE_DRIVER=sync` invalida o teste da geração assíncrona de PDF. Trocar por `QUEUE_CONNECTION=database` ou `sync` **apenas** nos testes que exigem execução imediata (`Bus::fake()` resolve o resto).

**Banco:** o legado usa PostgreSQL com PostGIS. Os testes devem rodar contra **PostgreSQL** (as views SQL e o `ilike`/`translate` são Postgres-específicos). Recomenda-se `RefreshDatabase` com schema recreated por teste, ou uma base dedicated com `migrate:fresh --seed`.

### 1.2 Fixtures necessárias

Nenhuma factory existe para o módulo (verificado: `v6/database/factories/` contém apenas `HighlightFactory`, `ManualAreaFactory`, `ManualFactory`, `UserFactory`). Criar:

| Factory | Campos |
|---|---|
| `ServiceLetterFactory` | `nome` (único), `slug` derivado, `secretaria_id`, `menu_acesso_rapido='cidadao'`, `tipo_exibicao='pagina'`, `ativo=true`, `publicado=true`, `publicado_em=now()`, `gratuito=true` |
| `ServiceLetterSubjectFactory` | `nome`, `slug` único, `tipo_visual='icone'`, `ordem`, `ativo=true` |
| `ServiceLetterConfigFactory` | `nome='Carta de Serviços'`, `exibir_sumario=true`, `ordenacao_servicos='secretaria'` |
| `ServiceLetterChannelFactory` | `tipo='online'`, `url`, `ordem`, `ativo=true` |
| `ServiceLetterStepFactory` | `numero`, `titulo`, `prazo`, `ordem`, `ativo=true` |
| `ServiceLetterDocumentFactory` | `nome`, `obrigatorio=true`, `ordem` |
| `ServiceLetterAttachmentFactory` | `nome`, `arquivo` (fake), `tipo_arquivo`, `tamanho_bytes` |
| `ServiceLetterResponsavelFactory` | `nome`, `cargo`, `email`, `ordem`, `ativo=true` |

Além disso, uma factory de `Content` da categoria "Secretarias" (`category_contents_id` = id da categoria com `slug='secretarias'`, resolvido por slug — **não** por id fixo 7).

### 1.3 Seed de ambiente de teste

Criar `ServiceLetterSeeder` (ou um `CartaDeServicosDemoSeeder`) para o ambiente de demonstração:

| Given | Valor |
|---|---|
| Categoria | `category_contents` com `slug='secretarias'` |
| Secretaria | 1 `Content` na categoria |
| Assuntos | 3 (Saúde, Educação, Transporte) com `ordem` 1..3 |
| Serviços | 6 (2 por assunto), metade `publicado`, metade não |
| Etapas | 2..4 por serviço, com `numero` sequencial e `prazo` preenchido |
| Canais | 1 de cada tipo por serviço |
| Documentos | 2 por serviço (1 obrigatório, 1 opcional) |
| Responsáveis | 1 por serviço |
| Config | 1 registro singleton |

## 2. Como rodar

```bash
# v6 (legado) — a suíte existente
cd v6 && ./vendor/bin/phpunit

# v7 (proposto)
php artisan test --testsuite=Feature
php artisan test --filter=ServiceLetter
php artisan test --filter=CartaDeServicos
```

## 3. Casos de teste — Portal

### 3.1 Listagem por assunto

| # | Arrange | Act | Assert |
|---|---|---|---|
| P1 | 1 assunto ativo, 2 serviços `ativo&&publicado` vinculados, 1 serviço não publicado | GET `/carta-de-servicos/{slug}` | 200; view `carta-servicos.show`; apenas 2 serviços no HTML |
| P2 | idem | GET `/carta-de-servicos/{slug}` | a resposta **não** contém o nome do serviço não publicado |
| P3 | 1 assunto ativo com 13 serviços publicados | GET `/carta-de-servicos/{slug}` | paginação: 12 itens na 1ª página; `links()` presente |
| P4 | 1 assunto ativo | GET `/carta-de-servicos/{slug}` | `next_page_url` aponta para `?page=2` preservando `texto`/`secretaria` |
| P5 | assunto inexistente | GET `/carta-de-servicos/nao-existe` | redirect para `carta-servicos.index` |
| P6 | assunto com `ativo=false` | GET `/carta-de-servicos/{slug}` | redirect para `carta-servicos.index` |

### 3.2 Redirect da home

| # | Arrange | Act | Assert |
|---|---|---|---|
| P7 | 2 assuntos ativos | GET `/carta-de-servicos` | redirect para `carta-servicos.show` com o slug do **primeiro** assunto (ordenado por `nome`) |
| P8 | nenhum assunto ativo | GET `/carta-de-servicos` | 200; view `carta-servicos.index` |
| P9 | assunto ativo, mas nenhum serviço | GET `/carta-de-servicos/{slug}` | 200; mensagem "Nenhum serviço encontrado" |

### 3.3 Filtros

| # | Arrange | Act | Assert |
|---|---|---|---|
| P10 | serviço "Certidão de Birth" (com acento) | GET `?texto=certidao` | o serviço aparece (busca acento-insensitiva) |
| P11 | 2 serviços em secretarias distintas | GET `?secretaria={id}` | apenas os da secretaria indicada |
| P12 | serviço "Emissão de Nota Fiscal" | GET `?texto=nota+fiscal` | o serviço aparece (busca por frase) |
| P13 | serviço "Alvará" | GET `?texto=xyz` | nenhum resultado |
| P14 | serviço com `descricao_curta` "Prazo de 5 dias" | GET `?texto=prazo` | o serviço aparece (busca também no `descricao_curta`) |

### 3.4 Resposta AJAX

| # | Arrange | Act | Assert |
|---|---|---|---|
| P15 | 1 assunto com 1 serviço | GET `?page=1` com header `X-Requested-With: XMLHttpRequest` | status 200, `Content-Type: application/json`, corpo contém o HTML do fragmento (`lista_canais` não, mas `id="services"`) |
| P16 | idem | GET sem header AJAX | status 200, `Content-Type: text/html`, corpo é a view completa |

### 3.5 Ficha do serviço

| # | Arrange | Act | Assert |
|---|---|---|---|
| P17 | 1 serviço publicado com 3 etapas (2 ativas), 2 canais ativos, 2 documentos ativos, 1 responsável ativo | GET `/carta-de-servicos/{assunto}/{servico}` | 200; HTML contém as 3 seções; **apenas** as 2 etapas ativas; badge "Online" presente |
| P18 | idem | GET (1ª vez) | `service_letters.visualizacoes` passa de 0 para 1 |
| P19 | idem | GET (2ª vez) | `visualizacoes` = 2 |
| P20 | serviço `publicado=false` | GET da ficha | redirect para `carta-servicos.index` |
| P21 | serviço `ativo=false` | GET da ficha | redirect para `carta-servicos.index` |
| P22 | serviço com `tipo_exibicao='link_externo'` e `link_externo='https://x'` | GET da ficha | HTML contém `href="https://x"` e o texto "Acessar Serviço Online" |
| P23 | serviço com `tipo_exibicao='link_externo'` e `link_externo=null` | GET da ficha | sem link quebrado (verificação de regressão) |

### 3.6 PDF no portal

| # | Arrange | Act | Assert |
|---|---|---|---|
| P24 | PDF com `status='gerado'` e arquivo em disco | GET `/carta-de-servicos/pdf` | 200; download do arquivo |
| P25 | nenhum PDF com `status='gerado'` | GET `/carta-de-servicos/pdf` | redirect + flash "Nenhum PDF disponível." |
| P26 | PDF com registro mas arquivo ausente em disco | GET `/carta-de-servicos/pdf` | redirect + flash "Arquivo não encontrado." |

## 4. Casos de teste — CMS

### 4.1 Autorização

| # | Arrange | Act | Assert |
|---|---|---|---|
| C1 | usuário anônimo | GET `/cms/carta-de-servicos/servicos` | redirect para `login` |
| C2 | usuário com permissão `carta-de-servicos.index` | GET `/cms/carta-de-servicos/servicos` | 200 |
| C3 | usuário **sem** permissão | GET `/cms/carta-de-servicos/servicos` | redirect + flash de aviso |
| C4 | `super-admin` | GET `/cms/carta-de-servicos/servicos` | 200 (ignora permissões) |
| C5 | usuário logado sem permissão específica | GET `/cms/datatable/carta_de_servicos` | **200** (comportamento atual: rota `datatable` é "aberta" na checagem de acesso) — teste de caracterização, deve ser revisto na v7 |

### 4.2 CRUD de serviço

| # | Arrange | Act | Assert |
|---|---|---|---|
| C6 | — | POST `/cms/carta-de-servicos/servicos` sem `nome` | 422; erro em `nome` |
| C7 | — | POST com `menu_acesso_rapido='invalido'` | 422 |
| C8 | — | POST com `tipo_exibicao='invalido'` | 422 |
| C9 | — | POST com `secretaria_id=999999` | 422 |
| C10 | payload válido | POST | 302 para `carta-de-servicos.servicos.index`; flash success; linha em `service_letters` com `created_by` = usuário; `publicado_em` não nulo se `publicado=1` |
| C11 | payload com `subjects=[1,2]` | POST | 2 linhas em `service_letters_subjects_rel` |
| C12 | payload com `publicado=0` | POST | `publicado_em` **nulo** |
| C13 | serviço existente com `publicado=true`, `publicado_em` = 2026-01-01 | PUT com `publicado=1` | `publicado_em` **permanece** 2026-01-01 (E2) |
| C14 | serviço existente com `publicado=false` | PUT com `publicado=1` | `publicado_em` passa a ser `now()` |
| C15 | 2 serviços com o mesmo `nome` | POST o 2º | erro capturado; nenhum registro duplicado; mensagem genérica exibida |
| C16 | payload com `subjects=[]` (vazio) | PUT | `subjects()` fica **vazio** (`detach`) |
| C17 | payload sem o campo `subjects` | PUT | `subjects()` fica **vazio** (`detach`) |
| C18 | — | DELETE `/cms/carta-de-servicos/servicos` com `id` no body | JSON `{"success": "Serviço excluído com sucesso."}`; linha com `deleted_at` preenchido (soft delete) |
| C19 | — | POST `/cms/carta-de-servicos/servicos/delete-all` com `toDelete=[1,2]` | JSON `{"success": true}`; 2 linhas com `deleted_at` |

### 4.3 CRUD de assunto

| # | Arrange | Act | Assert |
|---|---|---|---|
| C20 | — | POST sem `nome` | 422 |
| C21 | payload com `nome='Saúde'` | POST | `slug` = `saude`; `tipo_visual` default `icone`; `icone_cor` default `#000000` |
| C22 | assunto existente | POST com `nome='Saúde'` | **erro de unicidade de slug** capturado e exibido (caracterização) |
| C23 | payload com `imagem` (arquivo válido) | POST | arquivo em `storage/app/public/service-letter/subjects/`; `imagem` = `storage/service-letter/subjects/...` |

### 4.4 CRUD de canal

| # | Arrange | Act | Assert |
|---|---|---|---|
| C24 | — | POST sem `service_letter_id` | 422 |
| C25 | — | POST com `tipo='invalido'` | 422 |
| C26 | payload `tipo='telefone'`, `telefone='(00) 00000-0000'` | POST | JSON success; `ativo=false` (checkbox não enviado) |
| C27 | idem + `ativo=1` no body | POST | `ativo=true` |
| C28 | canal existente | PUT com `tipo='online'`, `url='https://x'` | `url` persistido; `telefone` anterior **mantido** (comportamento atual — E5 em risco) |
| C29 | canal existente | DELETE `/cms/carta-de-servicos/canais/{id}` | JSON success; soft delete |

### 4.5 CRUD de etapa

| # | Arrange | Act | Assert |
|---|---|---|---|
| C30 | — | POST sem `numero` | 422 |
| C31 | — | POST sem `titulo` | 422 |
| C32 | payload `numero=1`, `titulo='Solicitação'` | POST | JSON success; `ativo` = true |
| C33 | etapa existente | PUT com `numero=2` | `numero` atualizado |
| C34 | 2 etapas | POST `/cms/carta-de-servicos/etapas/order` com `sortable_data=[{id:1,order:1},{id:2,order:2}]` | `ordem` de ambas atualizada conforme o payload |
| C35 | etapa existente | DELETE | JSON success |

### 4.6 CRUD de documento

| # | Arrange | Act | Assert |
|---|---|---|---|
| C36 | — | POST sem `nome` | 422 |
| C37 | payload sem `obrigatorio` | POST | `obrigatorio=false` (o model converte `has()` em false) — atenção: o **default do banco** é `true`, mas o controller sempre envia o valor explícito |
| C38 | payload com `obrigatorio=1` | POST | `obrigatorio=true` |

### 4.7 CRUD de anexo

| # | Arrange | Act | Assert |
|---|---|---|---|
| C39 | — | POST sem `arquivo` | 422 |
| C40 | arquivo de 11 MB | POST | 422 (limite `max:10240`) |
| C41 | arquivo de 1 KB | POST | JSON success; arquivo em `service-letter/attachments/{id}/`; `tamanho_bytes` > 0; `tipo_arquivo` preenchido |
| C42 | anexo existente | PUT com novo arquivo | `arquivo` e `tipo_arquivo` atualizados; arquivo antigo permanece em disco (caracterização) |
| C43 | anexo existente | DELETE | JSON success; **arquivo removido do disco** |

### 4.8 CRUD de responsável

| # | Arrange | Act | Assert |
|---|---|---|---|
| C44 | payload totalmente vazio (`nome`, `cargo`, `email`, `telefone` vazios) | POST | **422** com `errors` contendo "telefone" |
| C45 | payload com `nome` e `cargo`, sem `email`/`telefone` | POST | **422** com erros em `nome` e `cargo` |
| C46 | payload com `email` válido | POST | 200/JSON success |
| C47 | payload com `telefone` | POST | JSON success |
| C48 | responsável existente | PUT | JSON success; `data` reflete o registro atualizado |
| C49 | responsável inexistente | PUT `/cms/carta-de-servicos/responsaveis/999999` | 500 com `errors` (o `findOrFail` lança `ModelNotFoundException`, capturada pelo `catch (\Exception)`) — **caracterização de um comportamento a corrigir** |

### 4.9 Configurações

| # | Arrange | Act | Assert |
|---|---|---|---|
| C50 | sem registro em `service_letter_configs` | GET `/cms/carta-de-servicos/configuracoes` | 200; view sem erro (config null) |
| C51 | sem registro | PUT com `nome='X'` | 200; **1** registro criado (comportamento de `firstOrCreate([])`) |
| C52 | 1 registro | PUT com `exibir_sumario` ausente | `exibir_sumario=false` (o controller sempre envia `has()`) |
| C53 | 1 registro | PUT com `capa_arquivo_pdf` (arquivo) | arquivo em `service-letter/config/`; `capa_arquivo_pdf` = `storage/service-letter/config/...` |

### 4.10 PDFs

| # | Arrange | Act | Assert |
|---|---|---|---|
| C54 | sem config | POST `/cms/carta-de-servicos/pdfs/gerar` | redirect para `config.index` + flash de erro |
| C55 | com config, sem serviços publicados | POST `/cms/carta-de-servicos/pdfs/gerar` | redirect + flash "Não há serviços publicados para gerar o PDF." |
| C56 | com config e ≥1 serviço publicado | POST `/cms/carta-de-servicos/pdfs/gerar` | **hoje falha**: `Error: Class "App\Services\ServiceLetterPdfService" not found`. Na v7: job despachado, PDF criado, `status='gerado'`, `total_servicos` correto |
| C57 | PDF existente | GET `/cms/carta-de-servicos/pdfs/{id}/download` | 200; `Content-Disposition` com `nome_arquivo` |
| C58 | PDF existente | GET `/cms/carta-de-servicos/pdfs/{id}/visualizar` | 200; `Content-Type: application/pdf` |
| C59 | PDF existente | DELETE | JSON/redirect success; arquivo removido do disco; soft delete |

## 5. Casos de teste — Regras de negócio

| # | Cenário | Assert |
|---|---|---|
| N1 | Serviço `ativo=true`, `publicado=false` | **ausente** do portal |
| N2 | Serviço `ativo=false`, `publicado=true` | **ausente** do portal |
| N3 | Serviço `ativo=true`, `publicado=true` | **presente** no portal |
| N4 | Etapa com `ativo=false` | **ausente** da ficha pública |
| N5 | Etapas com `numero` 3, 1, 2 | exibidas em ordem **1, 2, 3** (E4) |
| N6 | `tipo_exibicao='link_externo'` | as 5 abas do CMS **não** são renderizadas |
| N7 | `tipo_exibicao='pagina'` | as 5 abas do CMS são renderizadas |
| N8 | `gratuito=true` | a ficha mostra "Serviço Gratuito" e **não** mostra o valor |
| N9 | `gratuito=false`, `valor_taxa=12.5` | a ficha mostra "R$ 12,50" |
| N10 | `acesso_online/telefone/presencial` | badges correspondentes presentes na ficha |
| N11 | `secretaria` deletada (soft delete) | o serviço continua no portal, sem secretaria |

## 6. Casos de teste — Regressão dos gaps conhecidos

Estes testes documentam o **comportamento atual** e devem ser atualizados quando a v7 corrigir o gap:

| # | Gap | Teste de caracterização |
|---|---|---|
| G1 | `service_letter_access_logs` não é gravada | após 3 visitas à ficha, a tabela tem **0** linhas |
| G2 | views SQL não filtram soft delete | após excluir um serviço, ele **ainda aparece** na listagem do CMS |
| G3 | `lista_canals` (id errado no Blade) | após criar um canal via AJAX, a linha **não** aparece na tabela |
| G4 | `ServiceLetterAccessLog::$table` divergente | `ServiceLetter::accessLogs()` lanza exceção de tabela inexistente |
| G5 | `ServiceLetterPdf` com `$timestamps=false` | `created_at` e `updated_at` ficam `NULL` |
| G6 | `down()` das views derruba nomes errados | após `migrate:rollback`, as views `listagem_*` **continuam** existindo |
| G7 | campo `responsavel_*` do serviço não exibido no portal | a ficha **não** mostra o responsável principal do formulário, só os da aba |
| G8 | seed com `menu_acesso_rapido=true` | o registro do seed tem valor booleano e falha na edição (erro de validação `in:`) |

## 7. Testes de API Resource (v7)

| # | Cenário | Assert |
|---|---|---|
| R1 | `ServiceLetterSummaryResource` | **não** contém `sobre`, `documentacoes`, `custos`, `etapas` (HTML) |
| R2 | `ServiceLetterResource` | contém `subjects`, `canais`, `documentos`, `responsaveis` quando carregados |
| R3 | Serviço `tipo_exibicao='link_externo'` | `link_externo` presente; para `pagina`, `null` |
| R4 | `valor_taxa` | serializado como string decimal ("12.50"), nunca float cru |
| R5 | Resource anônimo | não expõe `created_by`/`updated_by` |

## 8. Testes do `ServiceLetterPdfService` (v7)

| # | Cenário | Assert |
|---|---|---|
| P1 | 0 serviços | lança `BusinessException`; nenhum PDF gerado |
| P2 | 3 serviços, `exibir_sumario=true` | PDF gerado; `total_servicos=3`; `incluiu_sumario=true`; sumário presente no texto extraído |
| P3 | 3 serviços, `ordenacao_servicos='secretaria'` | ordem por `secretaria_id` |
| P4 | 3 serviços, `ordenacao_servicos='assunto'` | agrupado por assunto (comportamento novo — hoje incompleto) |
| P5 | serviço com 5 etapas | as 5 etapas aparecem, numeradas |
| P6 | serviço com 3 canais | os 3 canais aparecem com o contato correto |
| P7 | capa `tipo_capa='pdf'` | a primeira página é o PDF da capa |
| P8 | fonte/tamanho/cor customizados | refletidos no PDF |
| P9 | execução via job (`Bus::fake()`) | job despachado com a config correta |

## 9. Checklist de cobertura

| Área | Coberto |
|---|---|
| Autorização (CMS) | C1–C5 |
| CRUD de serviço | C6–C19 |
| CRUD de assunto | C20–C23 |
| CRUD de canal | C24–C29 |
| CRUD de etapa | C30–C35 |
| CRUD de documento | C36–C38 |
| CRUD de anexo | C39–C43 |
| CRUD de responsável | C44–C49 |
| Configurações | C50–C53 |
| PDFs | C54–C59 |
| Portal (listagem, filtros, ficha) | P1–P26 |
| Regras de negócio | N1–N11 |
| Regressão de gaps | G1–G8 |
| API Resources | R1–R5 |
| PDF service | P1–P9 (geração) |
