# Ouvidoria — Modelagem

> Fonte: migrations em `v6/database/migrations/` e schema real do banco `prefeituras_v5` (container `v6_6f22e1bd-postgres-1`), consultado apenas com `SELECT`/`\d`.

## 1. Panorama das tabelas

| # | Tabela | Propósito |
|---|---|---|
| 1 | `ombudsman_requests` | A manifestação do cidadão |
| 2 | `ombudsman_answers` | Resposta (ou indeferimento) do operador |
| 3 | `ombudsman_subjects` | Assunto da manifestação (vinculado a um setor) |
| 4 | `ombudsman_sectors` | Setor / Órgão responsável (exclusivo da v6) |
| 5 | `ombudsman_manifestation_types` | Tipo de chamado (Denúncia, Reclamação, …) |
| 6 | `ombudsman_response_methods` | Forma de resposta (E-mail, Telefone, …) |
| 7 | `ombudsman_origins` | Origem do chamado (Telefone, Site, …) |
| 8 | `ombudsman_canned_responses` | Respostas prontas (modelo de texto) |
| 9 | `ombudsman_request_logs` | Histórico de tramitação (exclusivo da v6) |
| — | `statuses` | Tabela **compartilhada** com outros módulos — 7 status |

## 2. `ombudsman_requests`

Criada por `v6/database/migrations/2025_03_31_182643_create_ombudsman_requests_table.php`; alterada por `2026_02_04_154432_update_ombudsman_requests_table.php` e `2026_09_15_100000_add_mask_description_to_ombudsman_requests_table.php`.

| Coluna | Tipo | Null | Default | Origem |
|---|---|---|---|---|
| `id` | integer (serial) | não | seq | PK |
| `name` | varchar(191) | sim | — | migration inicial |
| `email` | varchar(191) | sim | — | migration inicial |
| `phone` | varchar(191) | sim | — | migration inicial |
| `type_id` | integer | **não** | — | FK → `ombudsman_manifestation_types.id` ON DELETE CASCADE |
| `subject_id` | integer | **não** | — | FK → `ombudsman_subjects.id` ON DELETE CASCADE |
| `response_method_id` | integer | **não** | — | FK → `ombudsman_response_methods.id` ON DELETE CASCADE |
| `origin_id` | integer | **não** | — | FK → `ombudsman_origins.id` ON DELETE CASCADE |
| `protocol` | varchar(191) | **não** | — | **UNIQUE** |
| `anonymous` | boolean | não | `false` | migration inicial |
| `description` | text | **não** | — | mensagem do cidadão |
| `number` | varchar(191) | sim | — | endereço |
| `neighborhood` | varchar(191) | sim | — | endereço |
| `complement` | varchar(191) | sim | — | endereço |
| `address` | varchar(191) | sim | — | endereço |
| `reference_point` | varchar(191) | sim | — | endereço |
| `user_option` | varchar(191) | sim | — | — |
| `status_id` | integer | **não** | — | FK → `statuses.id` |
| `ip` | varchar(191) | **não** | — | `$_SERVER['REMOTE_ADDR']` |
| `browser` | varchar(191) | **não** | — | User-Agent |
| `created_at`/`updated_at`/`deleted_at` | timestamp(0) | sim | — | migration inicial |
| `modality` | varchar(191) | sim | — | `2026_02_04` — `identificado`/`sigiloso`/`anonimo` |
| `token` | varchar(191) | sim | — | `2026_02_04` — hash do token de contestação |
| `answer_contestation` | text | sim | — | `2026_02_04` — 1ª contestação |
| `answer_contestation_date` | timestamp(0) | sim | — | `2026_02_04` |
| `answer_contestation_2` | text | sim | — | `2026_02_04` — 2ª contestação |
| `answer_contestation_2_date` | timestamp(0) | sim | — | `2026_02_04` |
| `mask_description` | text | sim | — | `2026_09_15` — versão mascarada (LGPD) |

**Casts do model** (`v6/app/Http/Model/Prefecture/OmbudsmanRequest.php`): **nenhum**. Todos os booleanos e datas são tratados como strings do Postgres pelo driver. `modality` e `status_id` não são convertidos.

**Accessors:**
- `getMaskedProtocolAttribute()` → `$this->maskProtocol($this->protocol)`
- `getMaskedNameAttribute()` → `'Anônimo'` se `anonymous`, senão `$this->maskName($this->name, $this->modality)`

> **Divergência verificada:** o model **não** usa `SoftDeletes`, mas a tabela tem `deleted_at` e as views SQL filtram `r.deleted_at IS NULL`. Registros com `deleted_at` preenchido ficam invisíveis nas listagens, mas **continuam** aparecendo nos escopos do model (`getByBetweenDate`, `byDateBetweenMain`) — que não filtram `deleted_at`. Ver `08-GAPS-E-DIVIDAS.md`.

## 3. `ombudsman_answers`

`v6/database/migrations/2025_04_28_141003_create_ombudsman_answers_table.php`; alterada por `2026_09_21_093024_add_mask_answer_to_ombudsman_answers_table.php`.

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | integer | não | seq |
| `answer` | text (`longText`) | **não** | — | texto da resposta ou da justificativa de indeferimento |
| `archives_id` | integer | sim | — | FK → `archives.id` |
| `ombudsman_request_id` | integer | **não** | — | FK → `ombudsman_requests.id` |
| `created_at`/`updated_at`/`deleted_at` | timestamp | sim | — |
| `mask_answer` | text | sim | — | versão mascarada (LGPD) |

> **Risco estrutural verificado:** a coluna `archives_id` (singular) permite apenas **um** arquivo por resposta, mas o controller anexa **vários** arquivos usando `$answer->archives()->attach($insertArchive->id)` — relação `belongsToMany` que está **comentada** no model (`v6/app/Http/Model/Prefecture/OmbudsmanAnswer.php`, método `archives()`). A tabela pivô `ombudsman_answer_archives` **existe na v5** (`v5/database/migrations/2026_08_06_162636_create_ombudsman_answer_archives_table.php`) mas **não existe na v6** (verificado em `pg_tables` e em `v6/database/migrations/`). Ver `08-GAPS-E-DIVIDAS.md`.

## 4. `ombudsman_subjects`

`v6/database/migrations/2025_03_31_172538_create_ombudsman_subjects_table.php`; alterada por `2026_07_22_100000_create_ombudsman_sectors_and_migrate_subjects.php`.

| Coluna | Tipo | Null | Default | Notas |
|---|---|---|---|---|
| `id` | integer | não | seq | — |
| `title` | varchar(191) | **não** | — | — |
| `secretary_id` | bigint | **sim** | — | FK → `contents.id` ON DELETE **SET NULL** (era NOT NULL + CASCADE; loosened em 2026-07-22) |
| `email` | varchar(191) | **não** | — | destinatário do aviso de novo chamado |
| `responsible` | varchar(191) | **não** | — | nome do responsável |
| `created_at`/`updated_at`/`deleted_at` | timestamp | sim | — | soft delete |
| `sector_id` | integer | sim | — | FK → `ombudsman_sectors.id` ON DELETE **RESTRICT** (adicionado em 2026-07-22) |

> **Nota de modelagem:** `OmbudsmanSubject::$fillable` inclui `secretary_id`, mas a migration de 2026-07-22 **tornou a coluna nullable e trocou o ON DELETE** — o `secretary_id` deixou de ser obrigatório, mas segue gravado. O accessor `sector_name` prioriza `sector`, cai para `secretary` e, por fim, devolve `'Outros'`.

## 5. `ombudsman_sectors` (exclusivo da v6)

`v6/database/migrations/2026_07_22_100000_create_ombudsman_sectors_and_migrate_subjects.php`

| Coluna | Tipo | Null | Default | Notas |
|---|---|---|---|---|
| `id` | integer | não | seq | — |
| `title` | varchar(191) | **não** | — | — |
| `content_id` | integer | sim | — | FK → `contents.id` ON DELETE SET NULL — "Referência à secretaria original em contents" |
| `active` | boolean | não | `true` | — |
| `created_at`/`updated_at`/`deleted_at` | timestamp | sim | — | soft delete |

O model `OmbudsmanSector` declara `protected $table = 'ombudsman_sectors';` e `protected $casts = ['active' => 'boolean'];`.

## 6. Tabelas de apoio

### 6.1 `ombudsman_manifestation_types`

`v6/database/migrations/2025_03_31_172524_create_ombudsman_manifestation_types_table.php`

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | integer | não | seq |
| `title` | varchar(191) | **não** | — |
| `status` | boolean | não | `true` | (ativado/desativado) |
| `created_at`/`updated_at`/`deleted_at` | timestamp | sim | — |

### 6.2 `ombudsman_response_methods`

`v6/database/migrations/2025_03_31_172612_create_ombudsman_response_methods_table.php`

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | integer | não | seq |
| `title` | varchar(191) | **não** | — |
| `created_at`/`updated_at`/`deleted_at` | timestamp | sim | — |

### 6.3 `ombudsman_origins`

`v6/database/migrations/2025_03_31_172643_create_ombudsman_origins_table.php`

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | integer | não | seq |
| `title` | varchar(191) | **não** | — |
| `created_at`/`updated_at`/`deleted_at` | timestamp | sim | — |

> O model `OmbudsmanOrigin` tem `protected $fillable = ['title', 'description'];` — mas a tabela **não tem** coluna `description`. Gravar `description` causa erro de coluna inexistente. Ver `08-GAPS-E-DIVIDAS.md`.

### 6.4 `ombudsman_canned_responses`

`v6/database/migrations/2025_03_31_172602_create_ombudsman_canned_responses_table.php`

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | integer | não | seq |
| `title` | varchar(191) | **não** | — |
| `message` | text | **não** | — |
| `created_at`/`updated_at`/`deleted_at` | timestamp | sim | — |

## 7. `ombudsman_request_logs` (exclusivo da v6)

`v6/database/migrations/2026_02_24_163925_create_ombudsman_request_logs_table.php`

| Coluna | Tipo | Null | Default | FK |
|---|---|---|---|---|
| `id` | integer | não | seq | — |
| `ombudsman_request_id` | integer | **não** | — | → `ombudsman_requests.id` CASCADE |
| `user_id` | integer | **não** | — | → `users.id` CASCADE |
| `sector_from_id` | integer | sim | — | → **`contents.id`** SET NULL |
| `sector_to_id` | integer | sim | — | → **`contents.id`** SET NULL |
| `subject_from_id` | integer | sim | — | → `ombudsman_subjects.id` SET NULL |
| `subject_to_id` | integer | sim | — | → `ombudsman_subjects.id` SET NULL |
| `type` | varchar(255) | **não** | — | CHECK: `forward` \| `status_change` \| `closed` \| `reopened` \| `comment` |
| `observation` | text | sim | — | — |
| `created_at`/`updated_at` | timestamp | sim | — | — |

> **Divergência model↔banco verificada:** as FKs `sector_from_id`/`sector_to_id` referenciam **`contents(id)`**, mas o model `OmbudsmanRequestLogs::sectorFrom()/sectorTo()` declara `belongsTo(OmbudsmanSector::class, 'sector_from_id')` — que resolveria para `ombudsman_sectors.id`. A view `chamados_ouvidoria` e a view de-answer exibem os títulos de setor via `contents`. O único gravador atual (`OmbudsmanRequestController@enviar_chamado`) preenche `sector_from_id`/`sector_to_id` com **`$ombudsmanRequest->subject->sector_id`** e **`$ombudsmanRequest->subject->sector_id`** — que são ids de `ombudsman_sectors`, gravados em colunas que apontam para `contents`. Ver `08-GAPS-E-DIVIDAS.md`.

## 8. Tabela `statuses` (compartilhada)

Consultada via `SELECT id, name FROM statuses ORDER BY id`:

| id | name | Uso na Ouvidoria |
|---|---|---|
| 1 | Em Tramitação | status inicial de todo chamado novo |
| 2 | Atendido | resposta enviada (`type=1`) |
| 3 | Indeferido | indeferimento (`type=0`) |
| 4 | Prorrogado | `prorrogar_chamado` |
| 5 | Aberta | não usado pelo módulo |
| 6 | Finalizada | não usado pelo módulo |
| 7 | Cancelada | não usado pelo módulo |

O model `Status` (`v6/app/Http/Model/Prefecture/Status.php`) usa `SoftDeletes` e `PresentableTrait` (`ContentPresenter`).

## 9. Views SQL

Criadas/recriadas por 6 migrations (a de respostas prontas tem nome sem "ombudsman"). Confirmadas em `pg_views` — 7 views com "ouvidoria" no nome.

### 9.1 `chamados_ouvidoria`

Origem: `2025_04_01_155609_create_view_ombudsman_requests.php` → recriada em `2026_02_06_083955_add_modality_to_ombudsman_requests_view.php` → recriada em `2026_07_22_100000_create_ombudsman_sectors_and_migrate_subjects.php` (versão vigente):

```sql
CREATE OR REPLACE VIEW public.chamados_ouvidoria AS
SELECT row_number() OVER (ORDER BY r.created_at DESC) AS value,
    r.id AS ID,
    t.title AS tipo_chamado,
    s.title AS assunto,
    COALESCE(s.sector_id, s.secretary_id) AS secretaria_responsavel,
    rm.title AS forma_de_resposta,
    o.title AS origem,
    r.protocol AS protocolo,
    COALESCE(sec.title, c.title, '-') AS setor,
    CASE WHEN r.anonymous = true THEN 'sim' ELSE 'nao' END AS anonimo,
    LEFT(r.description, 100) AS descricao_resumida,
    r.address AS endereco,
    st.name AS status,
    r.created_at AS data_criacao,
    CASE
        WHEN r.modality = 'sigiloso' THEN 'Sigiloso'
        WHEN r.modality = 'anonimo' THEN 'Anônimo'
        ELSE 'Identificado'
    END AS modalidade
FROM ombudsman_requests r
JOIN ombudsman_manifestation_types t ON r.type_id = t.id
JOIN ombudsman_subjects s ON r.subject_id = s.id
JOIN ombudsman_response_methods rm ON r.response_method_id = rm.id
JOIN ombudsman_origins o ON r.origin_id = o.id
JOIN statuses st ON r.status_id = st.id
LEFT JOIN ombudsman_sectors sec ON s.sector_id = sec.id AND sec.deleted_at IS NULL
LEFT JOIN contents c ON s.secretary_id = c.id AND c.deleted_at IS NULL
WHERE r.deleted_at IS NULL
ORDER BY r.created_at DESC;
```

Consumida por `v6/app/Http/Controllers/Prefecture/DatatableController.php:404-408` (`get_chamados_ouvidoria`).

> **Risco verificado:** `COALESCE(s.sector_id, s.secretary_id)` na coluna `secretaria_responsavel` — mistura um id de `ombudsman_sectors` com um id de `contents` na **mesma** coluna. Como a coluna só é exibida (não usada em join), não quebra a consulta, mas é semanticamente incorreta.

### 9.2 `assuntos_ouvidoria`

Versão vigente (após `2026_07_22`):

```sql
CREATE OR REPLACE VIEW assuntos_ouvidoria AS
SELECT
    row_number() OVER (ORDER BY os.created_at DESC) AS value,
    os.id AS id,
    os.title AS titulo_assunto,
    COALESCE(sec.title, c.title, '-') AS secretaria_vinculada
FROM ombudsman_subjects os
LEFT JOIN ombudsman_sectors sec ON os.sector_id = sec.id AND sec.deleted_at IS NULL
LEFT JOIN contents c ON os.secretary_id = c.id AND c.deleted_at IS NULL
WHERE os.deleted_at IS NULL
ORDER BY os.created_at DESC;
```

Consumida por `DatatableController@get_assuntos` (linha 409-413).

### 9.3 `ouvidoria_setores_orgaos`

Criada em `2026_07_22_100000`:

```sql
CREATE OR REPLACE VIEW public.ouvidoria_setores_orgaos AS
SELECT
    row_number() OVER (ORDER BY s.created_at DESC) AS value,
    s.id AS id,
    s.title AS titulo,
    CASE WHEN s.active THEN 'Ativo' ELSE 'Inativo' END AS status
FROM ombudsman_sectors s
WHERE s.deleted_at IS NULL
ORDER BY s.created_at DESC;
```

### 9.4 `ouvidoria_tipos_chamado`

`2025_04_01_155617_create_view_ombudsman_manifestation_types.php`:

```sql
CREATE OR REPLACE VIEW public.ouvidoria_tipos_chamado AS
SELECT row_number() OVER (ORDER BY created_at DESC) AS value,
    id, title AS titulo, status, created_at AS data_criacao, updated_at AS data_atualizacao
FROM ombudsman_manifestation_types
WHERE deleted_at IS NULL
ORDER BY created_at DESC;
```

### 9.5 `ouvidoria_formas_resposta`

`2025_04_01_155631_create_view_ombudsman_response_methods.php`:

```sql
CREATE OR REPLACE VIEW public.ouvidoria_formas_resposta AS
SELECT row_number() OVER (ORDER BY created_at DESC) AS value,
    id AS id, title AS titulo, created_at AS data_criacao, updated_at AS data_atualizacao
FROM ombudsman_response_methods
WHERE deleted_at IS NULL
ORDER BY created_at DESC;
```

### 9.6 `ouvidoria_origens_chamado`

`2025_04_01_155637_create_view_ombudsman_origins.php`:

```sql
CREATE OR REPLACE VIEW public.ouvidoria_origens_chamado AS
SELECT row_number() OVER (ORDER BY created_at DESC) AS value,
    id, title AS titulo, created_at AS data_criacao, updated_at AS data_atualizacao
FROM ombudsman_origins
WHERE deleted_at IS NULL
ORDER BY created_at DESC;
```

### 9.7 `ouvidoria_respostas_prontas`

`v6/database/migrations/2025_04_03_234114_create_view_canned_response.php` (nome do arquivo **não** contém "ombudsman"):

```sql
CREATE OR REPLACE VIEW ouvidoria_respostas_prontas AS
SELECT row_number() OVER (ORDER BY created_at DESC) AS value,
    id, title AS titulo, message AS mensagem,
    created_at AS data_criacao, updated_at AS data_atualizacao
FROM ombudsman_canned_responses
WHERE deleted_at IS NULL
ORDER BY created_at DESC;
```

## 10. Mapeamento DataTable → view

| Rota | Método no `DatatableController` | View | Linhas |
|---|---|---|---|
| `/cms/datatable/chamados-da-ouvidoria` | `get_chamados_ouvidoria` | `chamados_ouvidoria` | 404-408 |
| `/cms/datatable/assuntos` | `get_assuntos` | `assuntos_ouvidoria` | 409-413 |
| `/cms/datatable/setores-orgaos` | `get_ouvidoria_setores_orgaos` | `ouvidoria_setores_orgaos` | 415-419 |
| `/cms/datatable/tipos-de-chamado` | `get_ouvidoria_tipos_chamado` | `ouvidoria_tipos_chamado` | 425-429 |
| `/cms/datatable/formas-de-resposta` | `get_ouvidoria_formas_resposta` | `ouvidoria_formas_resposta` | 435-439 |
| `/cms/datatable/respostas-prontas` | `get_ouvidoria_respostas_prontas` | `ouvidoria_respostas_prontas` | 420-424 |
| `/cms/datatable/origens-de-chamado` | `get_ouvidoria_origens_chamado` | `ouvidoria_origens_chamado` | 430-434 |

> **Observação:** `struct_views($data, $permission)` recebe um **terceiro argumento distinto do slug da rota**: `get_assuntos` usa `'assuntos'`, `get_ouvidoria_setores_orgaos` usa `'assuntos'` (não `'setores-orgaos'`), `get_chamados_ouvidoria` usa `'chamados_ouvidoria'`. Isso determina as permissões de editar/excluir exibidas no DataTable.

## 11. Migrations do módulo (19 arquivos)

| # | Arquivo (`v6/database/migrations/`) | Efeito |
|---|---|---|
| 1 | `2025_03_31_172524_create_ombudsman_manifestation_types_table.php` | cria `ombudsman_manifestation_types` |
| 2 | `2025_03_31_172538_create_ombudsman_subjects_table.php` | cria `ombudsman_subjects` |
| 3 | `2025_03_31_172602_create_ombudsman_canned_responses_table.php` | cria `ombudsman_canned_responses` |
| 4 | `2025_03_31_172612_create_ombudsman_response_methods_table.php` | cria `ombudsman_response_methods` |
| 5 | `2025_03_31_172643_create_ombudsman_origins_table.php` | cria `ombudsman_origins` |
| 6 | `2025_03_31_182643_create_ombudsman_requests_table.php` | cria `ombudsman_requests` |
| 7 | `2025_04_01_155609_create_view_ombudsman_requests.php` | cria a view `chamados_ouvidoria` (1ª versão) |
| 8 | `2025_04_01_155617_create_view_ombudsman_manifestation_types.php` | cria `ouvidoria_tipos_chamado` |
| 9 | `2025_04_01_155624_create_view_ombudsman_subjects.php` | cria `assuntos_ouvidoria` (1ª versão) |
| 10 | `2025_04_01_155631_create_view_ombudsman_response_methods.php` | cria `ouvidoria_formas_resposta` |
| 11 | `2025_04_01_155637_create_view_ombudsman_origins.php` | cria `ouvidoria_origens_chamado` |
| 12 | `2025_04_03_234114_create_view_canned_response.php` | cria `ouvidoria_respostas_prontas` |
| 13 | `2025_04_28_141003_create_ombudsman_answers_table.php` | cria `ombudsman_answers` |
| 14 | `2026_02_04_154432_update_ombudsman_requests_table.php` | + `modality`, `token`, contestações |
| 15 | `2026_02_06_083955_add_modality_to_ombudsman_requests_view.php` | recria `chamados_ouvidoria` com `modalidade` |
| 16 | `2026_02_24_163925_create_ombudsman_request_logs_table.php` | cria `ombudsman_request_logs` |
| 17 | `2026_07_22_100000_create_ombudsman_sectors_and_migrate_subjects.php` | cria `ombudsman_sectors`; altera `ombudsman_subjects`; recria 2 views; cria `ouvidoria_setores_orgaos` |
| 18 | `2026_09_15_100000_add_mask_description_to_ombudsman_requests_table.php` | + `mask_description` |
| 19 | `2026_09_21_093024_add_mask_answer_to_ombudsman_answers_table.php` | + `mask_answer` |

> **Ponto de atenção na migration 17:** ela executa `DROP VIEW IF EXISTS public.assuntos_ouvidoria CASCADE` e `DROP VIEW IF EXISTS public.chamados_ouvidoria CASCADE` **antes** de alterar `ombudsman_subjects` — porque as views antigas faziam `INNER JOIN contents` em `secretary_id`, que era NOT NULL. O `down()` restaura o estado original (secretary_id NOT NULL + FK CASCADE) e recria a view `chamados_ouvidoria` **sem** as colunas `setor` e `modalidade`.

## 12. Seeds

| Arquivo (`v6/database/seeds/`) | Efeito |
|---|---|
| `OmbudsmanOptionsSeeder.php` | Popula origens (Telefone, Site, Presencial, E-mail, Aplicativo Móvel, Redes Sociais), assunto "Outros", tipos (Denúncia, Reclamação, Solicitação, Sugestão, Elogio, Pedido de Informação), formas de resposta (E-mail, Telefone, Correspondência, Não deseja resposta) e 3 respostas prontas. Chama `OmbudsmanSectorSeeder`. |
| `OmbudsmanSectorSeeder.php` | Importa as secretarias (`category_contents.slug = 'secretarias'`) para `ombudsman_sectors`, **preservando os IDs**; copia `secretary_id → sector_id` em `ombudsman_subjects`; importa também os ids já usados em `secretary_id` que não estejam na categoria. |
| `AddEmailDenunciaOuvidoriaSeeder.php` | Ajusta o e-mail do assunto de denúncia |
| `OuvidoriaPaginasLinksSeeder.php` | Links das páginas institucionais |
| `UpdateRequestToMaskRequestInOmbudsmanRequest.php` | Backfill de `mask_description` |
| `UpdateAnswerToMaskAnswerInOmbudsmanAnswer.php` | Backfill de `mask_answer` |

## 13. Registro do módulo

| Tabela | id | slug | vínculo |
|---|---|---|---|
| `modulos` | 56 | `ouvidoria` | — |
| `resources` | 76 | `ouvidoria` | `modulo_id = 56` |

> Nenhum papel do banco possui chaves `ouvidoria.*` ou `ombudsman.*` no JSON de `roles.permissions` (verificado). Na prática, só `super-admin` tem acesso ao CMS do módulo.
