# Terceirizados — Modelagem de Dados

> Módulo **existente apenas na `v5`**. Tabela `outsourceds`, banco PostgreSQL 10 da v5
> (`prefeituras_v5`).

---

## 1. Tabela `outsourceds`

Criada por `v5/database/migrations/2026_05_26_110000_create_outsourceds_table.php`.

Schema real no banco v5 (`\d outsourceds`):

```
 Table "public.outsourceds"
    Column     |              Type              | Nullable |                 Default
---------------+--------------------------------+----------+-----------------------------------------
 id            | integer                        | not null | nextval('outsourceds_id_seq'::regclass)
 year          | integer                        | not null |
 month         | integer                        | not null |
 employee_name | character varying(191)         | not null |
 company       | character varying(191)         | not null |
 role          | character varying(191)         | not null |
 file_path     | text                           |          |
 is_show       | boolean                        | not null | true
 created_at    | timestamp(0) without time zone |          |
 updated_at    | timestamp(0) without time zone |          |
 deleted_at    | timestamp(0) without time zone |          |
Indexes:
    "outsourceds_pkey" PRIMARY KEY, btree (id)
```

| Coluna | Tipo | Null | Default | Origem | Observações |
|---|---|---|---|---|---|
| `id` | `serial` / `integer` | não | `nextval('outsourceds_id_seq')` | `$blueprint->increments('id')` | PK. |
| `year` | `integer` | não | — | `$blueprint->integer('year')` | Ano de referência da folha. Validado entre 2000 e `date('Y') + 1`. |
| `month` | `integer` | não | — | `$blueprint->integer('month')` | Mês de referência (1–12). Validado. |
| `employee_name` | `varchar(191)` | não | — | `$blueprint->string('employee_name')` | `varchar(191)` porque `v5/app/Providers/AppServiceProvider.php:24` define `Schema::defaultStringLength(191)` — não 255. |
| `company` | `varchar(191)` | não | — | `$blueprint->string('company')` | Nome da empresa contratada. |
| `role` | `varchar(191)` | não | — | `$blueprint->string('role')` | Função exercida. |
| `file_path` | `text` | **sim** | `NULL` | `$blueprint->string('file_path')` → alterada para `text` | Ver §2. |
| `is_show` | `boolean` | não | `true` | `$blueprint->boolean('is_show')->default(true)` | Flag de exibição no portal; o CMS não expõe controle dela. |
| `created_at` / `updated_at` | `timestamp(0)` | sim | `NULL` | `$blueprint->timestamps()` | Laravel 5.5 usa `timestamp` sem timezone. |
| `deleted_at` | `timestamp(0)` | sim | `NULL` | `$blueprint->softDeletes()` | *Soft delete*. |

> Observação de modelagem: a migration original declarava `string('file_path')` (sem
> `nullable()`), mas a migration de 28/05/2026 passou a `text` **nullable**, aligning o
> schema com o uso real (registro sem anexo).

### Índices

| Índice | Tipo | Coluna | Observação |
|---|---|---|---|
| `outsourceds_pkey` | `btree` (PRIMARY KEY) | `id` | Único índice da tabela. |

**Não existem índices secundários** — nem em `year`, `month`, `is_show` nem em
`employee_name`/`company`. Impacto: os filtros do portal (`ilike` com `%termo%`) e a
ordenação `year desc, month desc` fazem varredura sequencial. Em tabelas grandes isso
deve ser endereçado na v7 (ver `06-MIGRACAO-MODERNA.md`).

### Views SQL

O módulo **não cria nem consome** views SQL. O `Prefecture\DatatableController`
consulta a tabela diretamente com `DB::table('outsourceds')`.

## 2. A mudança de `file_path` para `text`

`v5/database/migrations/2026_05_28_120000_change_file_path_to_text_in_outsourceds_table.php`:

```php
class ChangeFilePathToTextInOutsourcedsTable extends Migration
{
    public function up()
    {
        Schema::table('outsourceds', function (Blueprint $table) {
            $table->text('file_path')->nullable()->change();
        });
    }

    public function down()
    {
        Schema::table('outsourceds', function (Blueprint $table) {
            $table->string('file_path')->nullable()->change();
        });
    }
}
```

Por que a mudança foi necessária:

| Aspecto | Antes (`varchar(255)`) | Depois (`text`) |
|---|---|---|
| Capacidade | 255 caracteres | Ilimitado (1 GB) |
| Formato real do conteúdo | JSON `[{"path":"terceirizados/5f3a1b2c3d4e5.pdf","description":"Contrato"}]` — ~100+ caracteres por anexo | Cabe JSON com vários anexos e descrições longas |
| Risco | Truncamento silencioso do JSON → `json_decode` retorna `null` → o controller cai no fallback e trata o JSON truncado como "caminho único" | Nenhum |

O `down()` volta para `string`, o que **reintroduz** o risco de truncamento — não deve ser
executado em produção com dados multi-anexo.

### Formato do conteúdo de `file_path`

O campo **não é um caminho**: é um JSON (array de objetos) gravado pelo controller CMS.

```json
[
  {"path": "terceirizados/5f3a1b2c3d4e5.pdf", "description": "Contrato"},
  {"path": "terceirizados/9a8b7c6d5e4f3.pdf", "description": "Aditivo 01"}
]
```

Regras de leitura (implementadas de forma **duplicada** em três lugares):

| Local | Trecho | Comportamento |
|---|---|---|
| `v5/app/Http/Controllers/Prefecture/OutsourcedController.php` (`update`) | `json_decode($outsourced->file_path, true) ?: [['path' => $outsourced->file_path, 'description' => '']]` | Se o JSON é inválido, trata o valor bruto como **um único caminho**. |
| `v5/app/Http/Controllers/Prefecture/OutsourcedController.php` (`destroyArquivo`) | mesmo fallback | Idem. |
| `v5/resources/views/prefecture/outsourceds/form.blade.php` | `json_decode(...)` + `array_map` normalizando string → `['path' => ..., 'description' => '']` | Idem. |
| `v5/resources/views/portal/layouts/modelo1/terceirizados/show.blade.php` (e modelos 2–6) | idem | Idem. |

O `array_map` normaliza **itens escalares** (formato legado "caminho único") para
`['path' => ..., 'description' => '']`. É a prova de que o formato evoluiu de
`varchar` (caminho) para JSON (lista de anexos) sem migração de dados.

## 3. Model `App\Prefecture\Outsourced`

`v5/app/Http/Model/Prefecture/Outsourced.php`:

```php
class Outsourced extends Model
{
    use AppTrait, SoftDeletes, LogsActivity;

    protected $table = 'outsourceds';

    protected $fillable = [
        'year', 'month', 'employee_name', 'company', 'role', 'file_path', 'is_show'
    ];

    protected static $logFillable = true;
    protected static $logName = 'outsourced';

    public function getDescriptionForEvent(string $eventName): string
    {
        return "O Terceirizado {$this->employee_name} foi {$eventName}";
    }
}
```

| Aspecto | Definição | Efeito |
|---|---|---|
| Tabela | `outsourceds` | Fixa o nome (o plural irregular do Eloquent seria `outsourceds` por acaso). |
| `SoftDeletes` | trait | `deleted_at`; registros excluídos somem de Eloquent, mas o `DatatableController` filtra `whereNull('deleted_at')` manualmente porque usa `DB::table`. |
| `LogsActivity` | `spatie/laravel-activitylog` | Grava created/updated/deleted em `activity_log` com `logName = 'outsourced'` e descrição "O Terceirizado {nome} foi {evento}". |
| `AppTrait` | `v5/app/Http/Traits/AppTrait.php` | Apenas utilitários genéricos (sanitização, `traitRequest`); não altera o comportamento do model. |
| `fillable` | 7 campos | Inclui `file_path` e `is_show` — o controller CMS escreve neles diretamente via `$data`. |
| Sem casts | — | `year`/`month` chegam como `int` do PostgreSQL; `is_show` como `bool`; `file_path` como `string`. Não há accessor para o JSON de anexos. |
| Sem relações | — | Nenhum `belongsTo`/`hasMany`: a tabela não tem FK. |

> O namespace declarado (`App\Prefecture`) diverge do diretório
> (`app/Http/Model/Prefecture/`). Funciona porque `v5/composer.json` mapeia `app/` por
> **classmap** e por `psr-4` `App\` → `app/`; é o mesmo padrão dos demais models da
> plataforma.

## 4. Relacionamentos

| Relacionamento | Existe? | Observação |
|---|---|---|
| `outsourceds` → outras tabelas (FK) | **Não** | Nenhuma chave estrangeira; `year`/`month` são atributos livres (competência), não referência a `calendars`. |
| Tabela pivot | **Não** | — |
| `belongsTo`/`hasMany` no model | **Não** | Nenhuma relação Eloquent declarada. |
| Ligação implícita com `menu_orders`/`resources` | por **slug** (`'terceirizados'`), não por FK | Ver §6. |

## 5. Ciclo de vida dos dados

| Operação | Efeito no banco | Efeito no storage |
|---|---|---|
| `store` | `INSERT` com `is_show = true` | Cria N arquivos em `storage/app/public/terceirizados/` |
| `update` | `UPDATE` das colunas enviadas | Anexa novos arquivos; **preserva** os anteriores |
| `destroyArquivo` | `UPDATE` de `file_path` (JSON sem o item) | **Não apaga** o arquivo físico → órfão |
| `destroy` | `UPDATE deleted_at` (*soft delete*) | Mantém os arquivos |
| `destroyAll` | `UPDATE deleted_at` em lote (`whereIn('id', ...)`) | Mantém os arquivos |

## 6. Tabelas de apoio (fora do módulo)

| Tabela | Papel | Estado no banco v5 |
|---|---|---|
| `resources` | Permissões. A linha `id 14 / slug terceirizados / name Terceirizados` habilita `terceirizados.index|create|edit|delete`. | Presente (id 14). |
| `menu_orders` | Entrada do menu do CMS + contador `clicks`. | Presente (id 34, `clicks = 0`). |
| `configurations` | Chave `layout` (template ativo do portal) e chave `estado` (usada pelo seeder). | — |
| `activity_log` | Trilha de auditoria do `spatie/laravel-activitylog`. | — |
| `static_pages`, `contents`, `links` | Criadas pelo seeder para o card "Terceirizados" na Transparência Fiscal. | — |

## 7. Migrations

| Migration | Efeito |
|---|---|
| `v5/database/migrations/2026_05_26_110000_create_outsourceds_table.php` | Cria `outsourceds` com `file_path` como `string` (sem `nullable`). |
| `v5/database/migrations/2026_05_26_113000_insert_terceirizados_into_menu_and_resources.php` | Insere `menu_orders` (slug `terceirizados`) e `resources` (slug `terceirizados`), de forma idempotente (`->exists()` antes do `insert`). O `down()` remove as duas linhas. |
| `v5/database/migrations/2026_05_28_120000_change_file_path_to_text_in_outsourceds_table.php` | Altera `file_path` para `text nullable`. |

A migration `2026_05_26_113000` insere em `resources` sem `modulo_id` (a v5 **não possui**
a tabela `modulos`) e sem `status_create`, coerente com o schema v5:

```php
DB::table('resources')->insert([
    'name' => 'Terceirizados',
    'slug' => 'terceirizados',
    'description' => 'Módulo de Serviços Terceirizados',
    'created_at' => now(),
    'updated_at' => now(),
]);
```

## 8. Seeds

| Seed | Efeito |
|---|---|
| `v5/database/seeds/AddTerceirizadosInTransparenciaFiscalSeeder.php` | Se `configurations.key = estado` for `PB`, cria `static_pages` (slug `terceirizados`), `contents` (na categoria `transparencia-fiscal`) e `links` apontando para `https://www.<cidade>.pb.gov.br/terceirizados`. Idempotente por checagem de `slug`. |

**Não existe seed com dados de terceirizados.** A tabela `outsourceds` está **vazia**
(0 linhas) no banco v5 — o módulo foi entregue pronto para uso, sem carga inicial.

## 9. Estado atual dos dados (v5) e ausências (v6)

| Verificação | v5 | v6 |
|---|---|---|
| Tabela `outsourceds` | existe (0 linhas) | **não existe** |
| `resources` com slug `terceirizados` | id 14 | **ausente** |
| `menu_orders` com slug `terceirizados` | id 34 | **ausente** |
| Migrations do módulo | 3 | **ausentes** |

Detalhamento em `08-GAPS-E-DIVIDAS.md`.
