# Ouvidoria — Plano de Implementação (Fases 2 e 3)

> Complemento de `06-MIGRACAO-MODERNA.md` (schema, models, Form Requests).

## 5. Policies / autorização

```php
// app/Policies/OmbudsmanRequestPolicy.php
class OmbudsmanRequestPolicy
{
    public function viewAny(User $user): bool { return $user->can('ouvidoria.index'); }
    public function view(User $user, OmbudsmanRequest $r): bool { return $user->can('ouvidoria.index'); }
    public function answer(User $user, OmbudsmanRequest $r): bool { return $user->can('ouvidoria.edit'); }
    public function defer(User $user, OmbudsmanRequest $r): bool { return $user->can('ouvidoria.edit'); }
    public function forward(User $user, OmbudsmanRequest $r): bool { return $user->can('ouvidoria.edit'); }
    public function destroy(User $user, OmbudsmanRequest $r): bool { return $user->can('ouvidoria.delete'); }
}
```

- A permissão do middleware legado `ouvidoria.edit` (para `responder_chamado`/`enviar_resposta`) deve ser **preservada** — é a única com nome próprio do módulo.
- Criar as permissões `ouvidoria.index`, `ouvidoria.create`, `ouvidoria.edit`, `ouvidoria.delete`, `ouvidoria.export` no seeder de papéis (hoje **não existem**).

## 6. Endpoints REST/JSON

### 6.1 Portal (público, com rate limiting e captcha)

| Método | URI | Descrição |
|---|---|---|
| GET | `/ouvidoria` | Página + formulário (HTML) |
| POST | `/ouvidoria` | Cria a manifestação (JSON ou redirect) — **substitui `Route::any`** |
| GET | `/api/ouvidoria/consulta/{protocol}` | Consulta por protocolo (com `token` opcional) |
| POST | `/api/ouvidoria/consulta/{protocol}/contestacao` | Registra contestação |
| GET | `/relatorio-estatistico-ouvidoria` | Relatório (HTML) |
| GET | `/api/ouvidoria/relatorio?ano=2026` | Indicadores em JSON |
| GET | `/api/ouvidoria/relatorio/exportacao?ano=2026&formato=xls\|csv\|pdf\|txt` | Exportação |

### 6.2 CMS (autenticado)

| Método | URI | Descrição |
|---|---|---|
| GET | `/admin/api/ouvidoria/chamados` | Listagem paginada (substitui a view SQL + DataTable) |
| GET | `/admin/api/ouvidoria/chamados/{id}` | Detalhe **mascarado** por padrão; completo com `?full=1` e permissão `ouvidoria.sensitive` |
| POST | `/admin/api/ouvidoria/chamados/{id}/respostas` | Responder |
| POST | `/admin/api/ouvidoria/chamados/{id}/indeferimentos` | Indeferir |
| POST | `/admin/api/ouvidoria/chamados/{id}/prorrogacoes` | Prorrogar |
| POST | `/admin/api/ouvidoria/chamados/{id}/encaminhamentos` | Encaminhar |
| GET | `/admin/api/ouvidoria/chamados/{id}/historico` | Trilha de tramitação |
| CRUD | `/admin/api/ouvidoria/{assuntos,setores,tipos,formas-resposta,origens,respostas-prontas}` | Tabelas de apoio |
| GET | `/admin/api/ouvidoria/estatisticas` | Indicadores para o Dashboard |

### 6.3 API pública de dados abertos (opcional)

Como o portal já publica o relatório estatístico, expor `GET /api/ouvidoria/estatisticas` com **apenas** indicadores agregados (sem dados pessoais) é uma evolução natural e alinhada à Lei de Acesso à Informação.

## 7. API Resources

```php
// app/Http/Resources/OmbudsmanRequestResource.php
class OmbudsmanRequestResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'protocol' => $this->masked_protocol,          // SEMPRE mascarado
            'modalidade' => $this->modality->value,
            'anonimo' => $this->anonymous,
            'solicitante' => $this->masked_name,           // SEMPRE mascarado
            'descricao' => $this->mask_description,        // SEMPRE mascarado
            'status' => [
                'id' => $this->status_id->value,
                'nome' => $this->status_id->label(),
            ],
            'tipo' => $this->whenLoaded('type', fn () => $this->type->title),
            'assunto' => $this->whenLoaded('subject', fn () => $this->subject->title),
            'setor' => $this->whenLoaded('subject.sector', fn () => $this->subject->sector->title),
            'forma_resposta' => $this->whenLoaded('responseMethod', fn () => $this->responseMethod->title),
            'origem' => $this->whenLoaded('origin', fn () => $this->origin->title),
            'criado_em' => $this->created_at?->toIso8601String(),
            'respostas' => OmbudsmanAnswerResource::collection($this->whenLoaded('answers')),
            'historico' => OmbudsmanRequestLogResource::collection($this->whenLoaded('logs')),
        ];
    }
}
```

> **Regra de ouro:** o Resource **público** nunca expõe `name`, `email`, `phone`, `ip`, `browser`, `token` nem `description` em claro. Se o CMS precisar dos dados completos, criar um `OmbudsmanRequestFullResource` protegido por `ouvidoria.sensitive`.

Resources sugeridos: `OmbudsmanRequestResource`, `OmbudsmanRequestFullResource`, `OmbudsmanAnswerResource`, `OmbudsmanRequestLogResource`, `OmbudsmanSubjectResource`, `OmbudsmanSectorResource`, `OmbudsmanTypeResource`, `OmbudsmanOriginResource`, `OmbudsmanResponseMethodResource`, `OmbudsmanCannedResponseResource`, `OmbudsmanStatisticsResource`.

## 8. Recuperação da funcionalidade perdida na v6 (Relatório Estatístico de Ouvidoria)

A v5 permitia cadastrar **relatórios anuais** em PDF no CMS (`relatorios_estatisticos_ouvidoria`: `ano`, `descricao`, `arquivo`) e exibi-los no portal. Para recuperar:

```php
Schema::create('ombudsman_reports', function (Blueprint $table) {
    $table->id();
    $table->string('year', 10);
    $table->string('description')->nullable();
    $table->string('file_path', 500)->nullable();
    $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
    $table->timestamps();
    $table->softDeletes();
    $table->unique('year');
});
```

| Item | Implementação |
|---|---|
| Model | `OmbudsmanReport` (`year`, `description`, `file_path`) |
| Request | `StoreOmbudsmanReportRequest`: `year` required\|string\|max:10; `description` required\|string\|max:255; `file` nullable\|file\|mimes:pdf\|max:20480 |
| Policy | `create`/`update`/`delete` sob `ouvidoria.relatorio.*` |
| Rotas CMS | `resource('/admin/api/ouvidoria/relatorios', OmbudsmanReportController::class)` |
| Rotas portal | `GET /relatorio-estatistico-ouvidoria/{ano}/arquivo` — serve o PDF cadastrado, **se existir**; caso contrário, cai no relatório dinâmico |
| Migração de dados | `INSERT INTO ombudsman_reports SELECT ano, descricao, arquivo FROM relatorios_estatisticos_ouvidoria WHERE deleted_at IS NULL` |

> **Decisão de produto pendente:** o relatório **dinâmico** (calculado de `ombudsman_requests`) e o relatório **institucional** (PDF cadastrado) coexistem. Recomenda-se: o portal mostra o dinâmico por padrão e oferece o institucional como download adicional.

## 9. Fila, cache e storage

### 9.1 Fila

| Job | Descrição |
|---|---|
| `SendOmbudsmanNotification` | Envio do e-mail ao cidadão, ao setor e no reenvio — **fora da transação** (hoje o `Mail::send` está dentro do `DB::transaction` do `store`, o que segura a conexão durante o I/O) |
| `MaskOmbudsmanText` | Mascaramento via API externa — **assíncrono**, com fallback determinístico (`maskTextDeterministic`) quando a API falhar |
| `GenerateOmbudsmanStatistics` | Pré-cálculo dos indicadores por ano (opcional, para o relatório público) |

> **Ponto crítico:** `maskTextWithAi` depende de uma API externa (ver `05-REGRAS-E-PORTABILIDADE.md` §1.6). Na v7, o job deve ter **fallback** para `maskTextDeterministic()` (que é local e determinístico) para nunca perder o mascaramento.

### 9.2 Cache

| Chave | TTL |
|---|---|
| `ouvidoria:tipos-ativos`, `ouvidoria:origens`, `ouvidoria:formas-resposta` | 1 h |
| `ouvidoria:assuntos-por-setor` | 30 min |
| `ouvidoria:estatisticas:{ano}` | 1 h (invalidate ao mudar um status) |
| `ouvidoria:config:{ano}` (prazo em dias) | 1 h |

A configuração `configurations.response_time` deve virar uma chave de cache em vez de consulta por requisição.

### 9.3 Storage

| Uso | Disco |
|---|---|
| Anexos das respostas | `private` + rota de download autenticada (hoje são públicos em `storage/app/public/answer/`) |
| Páginas estáticas (PDFs da Ouvidoria) | `public` (herdado do módulo de páginas estáticas) |

## 10. Views Blade modernas

| Atual | Moderno |
|---|---|
| `portal/.../ouvidoria/form-contact.blade.php` (6 modelos) | **Componente único** `<x-ouvidoria-form>` + `<x-portal-layout :template="$template">`; eliminar as 6 cópias |
| `consulta-solicitacao-ouvidoria.blade.php` (6 modelos) | Componente `<x-ouvidoria-consulta>` + Alpine.js para o AJAX |
| `relatorio-estatistico-ouvidoria/{index,js}.blade.php` (6 modelos) | Componente `<x-ouvidoria-estatisticas>`; gráfico com Chart.js (já presente em `v6/public/assets/plugins/chart.js`) em vez de Google Charts |
| `prefecture/ombudsman/request/index.blade.php` | **Livewire** `OmbudsmanChamadosList` com paginação server-side e filtros |
| `prefecture/ombudsman/request/answer/create.blade.php` | **Livewire** `OmbudsmanAnswerForm` com wysiwyg, respostas prontas e anexos |
| Modais `modal_show`/`modal_send` | **Livewire modal** ou Alpine + componente Blade |
| 6 CRUDs de apoio idênticos | **Componente genérico** `<x-crud-table>` parametrizado por resource |
| `prefecture/ombudsman/{index,edit,form}.blade.php` (órfãs) | **Remover** (copiadas de Feriados Municipais) |

**Ganho de manutenibilidade:** as ~90 views atuais (6 modelos × 4 telas + 13 do CMS + 9 e-mails + templates de teste) reduzem para ~10 componentes.

## 11. Riscos e decisões de equivalência funcional

| # | Risco / decisão | Impacto | Mitigação |
|---|---|---|---|
| R1 | **Anexos em respostas quebrados** (`archives()` comentado, pivô inexistente) | Alto | Recriar o pivô e o método; migrar os dados da v5 |
| R2 | **`update()` altera o primeiro registro da tabela** | Alto | Reescrever com filtro por `id` + validação |
| R3 | **7 controllers sem `check.route.permission`** | Alto (segurança) | Corrigir os construtores ou usar Policies |
| R4 | **Modelo do portal valida `modality` como `integer` contra strings** | Alto (funcional) | `Rule::enum(OmbudsmanModality::class)` |
| R5 | **Captcha não validado** | Alto (segurança) | Reativar `captcha` |
| R6 | **Consulta por protocolo expõe dados em claro** | Alto (LGPD) | Resource público sempre mascarado + exigir `token` para ver a descrição |
| R7 | **`dd($e)` em produção** | Alto (segurança/UX) | Remover; log estruturado + mensagem amigável |
| R8 | **Limite de 20 dias hardcoded** | Médio | Mover para `configurations` (`ouvidoria_prazo_dias`) e usar em código **e** relatório |
| R9 | **Rota `Route::any` no `store`** | Médio | Separar `GET` (página) e `POST` (criação) |
| R10 | **Colisão de protocolo no mesmo segundo** | Médio | `Str::uuid()` ou sufixo aleatório; ou `OUV-{dmY-His}-{rand}` |
| R11 | **`modality` comparison com `'Anônimo'` (acento)** | Médio | Usar o enum |
| R12 | **Escopos do model não filtram `deleted_at`** | Médio | Adicionar `SoftDeletes` aos models que têm a coluna |
| R13 | **`sector_from_id` gravado com id de setor, FK para `contents`** | Médio | Corrigir as FKs e migrar os dados |
| R14 | **Relatório Estatístico de Ouvidoria (CMS) perdido** | Médio | Reimplementar (§8) |
| R15 | **`OmbudsmanPortalRequest` com `authorize()=false`** | Baixo | Remover a classe |
| R16 | **Views órfãs** (`ombudsman/{index,edit,form}`, `teste/*`) | Baixo | Remover |
| R17 | **`ip` e `browser` no e-mail ao setor** | Médio (LGPD) | Não enviar ao setor; ou enviar mascarado |
| R18 | **Dependência externa de mascaramento** | Alto (disponibilidade) | Fallback determinístico + job assíncrono |
| R19 | **Status ids hardcoded (1-4)** | Médio | Enum `OmbudsmanStatus` com `from()` |
| R20 | **Coluna `statuses` compartilhada** | Baixo | Manter como está; não migrar para enum isolado |

## 12. Estimativa de esforço

| Frente | Itens |
|---|---|
| Schema + models + factories | 9 tabelas, 9 models, 9 factories, 2 enums |
| Requests + Policies | 8 requests, 1 policy (chamado) + 6 policies simples |
| API + Resources | 11 resources, ~25 endpoints |
| Views | 3 componentes de portal, 2 Livewire de CMS, 1 componente CRUD genérico |
| Services | `OmbudsmanMaskingService` (com fallback), `OmbudsmanStatisticsService` |
| Jobs | 3 |
| Testes | ~70 casos (ver `07-TESTES.md`) |
| Migração de dados | 1 comando idempotente + relatório de divergências |
| Recuperação do relatório estatístico | 1 model, 1 request, 1 controller, 1 view |
