# Licitações Antigas — Plano de Migração para Laravel 11/12

---

## 1. Decisões de equivalência funcional (o que NÃO pode mudar)

| # | Comportamento legado | Decisão v7 |
|---|---|---|
| E1 | Duas rotas públicas: listagem paginada e detalhe. | Manter `/licitacoes-ate-2018` e `/licitacoes-ate-2018/{id}` (mesmos nomes de rota `licitacoes.old.index` / `licitacoes.old.show`) para não quebrar links externos e o botão do módulo novo. |
| E2 | Paginação de 10 por página. | Manter 10 (ou expor como constante configurável). |
| E3 | Filtros: modalidade, secretaria, ano, texto livre, combináveis. | Manter os quatro, com validação explícita. |
| E4 | Busca acento-insensível. | Manter, mas via extensão `unaccent` do Postgres (índice GIN/trigram) em vez de `translate()` inline. |
| E5 | Corpo do aviso renderizado como HTML. | Manter HTML, mas **sanitizado** (HTMLPurifier ou `strip_tags` com allowlist) — mudança deliberada, deve ser comunicada. |
| E6 | Lista de anos distintos para o filtro. | Manter, com query Eloquent. |
| E7 | Link cruzado para `/licitacoes` ("Ir para Licitações após 2018"). | Manter. |
| E8 | Módulo somente leitura. | Manter: sem CRUD no CMS; a carga continua por importação (ou por um job de importação auditado, se o negócio permitir). |
| E9 | Detalhe inexistente mostra página de erro. | Mudar para `abort(404)` — comportamento observável muda (HTTP status), é a única exceção consciente. |

## 2. Schema de migração (Laravel moderno)

```php
Schema::create('legacy_biddings', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('legacy_modality_id')->nullable();
    $table->unsignedBigInteger('legacy_jurisdiction_id')->nullable();
    $table->string('number', 191);
    $table->string('cnpj', 191);
    $table->string('title', 250);
    $table->date('published_at');
    $table->text('body_html');
    $table->timestamps();

    $table->index('published_at');
    $table->index('legacy_modality_id');
    $table->index('legacy_jurisdiction_id');
    $table->foreign('legacy_modality_id')->references('id')->on('legacy_modalities');
    $table->foreign('legacy_jurisdiction_id')->references('id')->on('legacy_jurisdictions');
});

Schema::create('legacy_bidding_archives', function (Blueprint $table) {
    $table->id();
    $table->foreignId('legacy_bidding_id')->constrained('legacy_biddings')->cascadeOnDelete();
    $table->string('filename', 250);
    $table->string('disk', 191)->default('public');
    $table->string('path', 500);
    $table->string('original_name', 250)->nullable();
    $table->timestamps();
});

Schema::create('legacy_modalities', function (Blueprint $table) {
    $table->id();
    $table->string('name', 191);
    $table->timestamps();
});

Schema::create('legacy_jurisdictions', function (Blueprint $table) {
    $table->id();
    $table->string('title', 191);
    $table->timestamps();
});
```

Nomes novos (`legacy_*`) são permitidos: as tabelas legadas não têm FK e os
nomes atuais (`biddings_legacys`, `biddings_legacys_modalities`,
`biddings_legacys_jurisdictional`) são ambíguos e não podem ser pluralizados
com segurança.

## 3. Models

```php
class LegacyBidding extends Model
{
    protected $table = 'legacy_biddings';

    protected $fillable = ['legacy_modality_id', 'legacy_jurisdiction_id', 'number',
                           'cnpj', 'title', 'published_at', 'body_html'];

    protected $casts = ['published_at' => 'date'];

    public function archives(): HasMany
    {
        return $this->hasMany(LegacyBiddingArchive::class, 'legacy_bidding_id');
    }

    public function modality(): BelongsTo
    {
        return $this->belongsTo(LegacyModality::class, 'legacy_modality_id');
    }

    public function jurisdiction(): BelongsTo
    {
        return $this->belongsTo(LegacyJurisdiction::class, 'legacy_jurisdiction_id');
    }
}
```

Mudanças em relação ao legado:
- `data` → `published_at` (cast `date`); o accessor `getDataAttribute` que
  formatava `d-m-Y` é **removido** — a formatação passa a ser responsibility
  da view (`$bidding->published_at->format('d/m/Y')`), evitando dupla
  formatação.
- `informacoes` → `body_html` (nome explícito de que é HTML).
- Relações `belongsTo` reais para modalidade e secretaria (o legado só tinha IDs).
- `arquivo` e `title` (colunas inexistentes no `$fillable`) simplesmente não
  existem no novo schema.

## 4. Form Request

```php
class LegacyBiddingIndexRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'tipo_modalidade' => ['nullable', 'integer', 'exists:legacy_modalities,id'],
            'ids_secretarias' => ['nullable', 'integer', 'exists:legacy_jurisdictions,id'],
            'ano' => ['nullable', 'integer', 'between:1900,2100'],
            'informacoes' => ['nullable', 'string', 'max:255'],
        ];
    }
}
```

O nome dos parâmetros de query **deve** ser preservado (`tipo_modalidade`,
`ids_secretarias`, `ano`, `informacoes`) para não quebrar links salvos de
cidadãos.

## 5. Controller

```php
class LegacyBiddingController extends Controller
{
    public function index(LegacyBiddingIndexRequest $request): View
    {
        $query = LegacyBidding::query()
            ->with(['modality', 'jurisdiction'])
            ->when($request->tipo_modalidade, fn ($q) => $q->where('legacy_modality_id', $request->tipo_modalidade))
            ->when($request->ids_secretarias, fn ($q) => $q->where('legacy_jurisdiction_id', $request->ids_secretarias))
            ->when($request->ano, fn ($q) => $q->whereYear('published_at', $request->ano))
            ->when($request->informacoes, fn ($q) => $q->where('body_html', 'ilike', '%' . $request->informacoes . '%'))
            ->orderByDesc('published_at');

        return view('portal.legacy-biddings.index', [
            'biddings' => $query->paginate(10)->withQueryString(),
            'modalities' => LegacyModality::pluck('name', 'id'),
            'jurisdictions' => LegacyJurisdiction::pluck('title', 'id'),
            'years' => LegacyBidding::selectRaw('EXTRACT(YEAR FROM published_at) AS ano')
                ->distinct()->orderByDesc('ano')->pluck('ano'),
        ]);
    }

    public function show(LegacyBidding $legacyBidding): View
    {
        return view('portal.legacy-biddings.show', compact('legacyBidding'));
    }
}
```

- Route model binding substitui `find($id)` e resolve o 404 automaticamente.
- `withQueryString()` preserva os filtros na paginação (o legado usava
  `appends(Request::except('page'))`).
- `eager load` apenas de `modality`/`jurisdiction` (o legado carregava
  `archives` na listagem sem usá-las).

## 6. Views

- Blade único em `resources/views/portal/legacy-biddings/{index,show,filtro}.blade.php`,
  **sem replicação por layout**. A variação de layout é resolvida por
  componente (`<x-portal.layout>`) ou por herança de um layout único com
  slots — elimina a duplicação de 18 arquivos (armadilha A11).
- Componente `<x-legacy-bidding.attachments :bidding="$bidding" />` para o
  cartão de anexos.
- Sanitização do HTML: `{!! \App\Support\Html::sanitize($legacyBidding->body_html) !!}`.

## 7. Rotas sugeridas

```php
Route::any('licitacoes-ate-2018', LegacyBiddingController::class.'@index')->name('licitacoes.old.index');
Route::get('licitacoes-ate-2018/{legacyBidding}', LegacyBiddingController::class.'@show')
    ->name('licitacoes.old.show')
    ->whereNumber('legacyBidding');
```

Endpoint JSON opcional (não existe no legado):

```php
Route::get('api/portal/licitacoes-antigas', LegacyBiddingController::class.'@apiIndex')
    ->name('api.licitacoes-antigas.index');
```

## 8. Policies e autorização

Módulo público: **sem** policy obrigatória. Se o CMS ganhar edição de legado
(hipótese futura), criar `LegacyBiddingPolicy` com `viewAny`/`update` para
operadores com permissão `licitacoes-antigas`.

## 9. Storage, cache e fila

- **Storage**: anexos em disco `public` (ou `S3`), pasta `legacy-biddings/{id}/`,
  com `Storage::exists()` no lugar do `file_exist(url(...))` quebrado.
- **Cache**: a listagem é estável; cachear por página+filtros por alguns
  minutos (`Cache::remember`) reduz carga. O cache de `modulos` (`rememberForever`)
  já existe no legado e deve ser reimplementado com invalidação por evento.
- **Fila**: a importação do acervo legado pode ser um job `ImportLegacyBiddings`
  (chunks de 500, `upsert` por chave natural `number + published_at`).
- **Índice de busca**: `to_tsvector`/`pg_trgm` sobre `body_html` para a busca
  textual em vez do `ilike` sequencial.

## 10. Riscos

| Risco | Mitigação |
|---|---|
| Perder o acervo na migração (tabela vazia na base de referência) | Importação idempotente + conferência de contagem/faixa de datas por ano. |
| Mudança do comportamento da busca (acento, `%`, case) | Testes de equivalência com os mesmos termos do legado. |
| Quebra de links externos para `/licitacoes-ate-2018` | Manter URI e nome de rota. |
| Sanitização do HTML alterando o visual dos avisos antigos | Comparar renderização lado a lado em amostra. |
| Dependência de arquivos fora do repositório (`/storage/licitacoes_old`) | Migrar anexos para o storage da v7 e validar hash. |
