# Licitações — Regras de Negócio e Portabilidade

> Todas as afirmações são rastreáveis aos arquivos citados. Divergências v5 -> v6
> estão resumidas aqui e detalhadas em `08-GAPS-E-DIVIDAS.md`.

---

## 1. Regras de negócio não óbvias

### 1.1 Identidade da licitação

O “número” da licitação **não** é único sozinho. A unicidade é verificada em
`BiddingController@store` e `@update`:

```php
$biddings = Bidding::whereHas('contents', function ($query) use ($request) {
    $query->whereIn('contents_id', $request->jurisdicionado);
})->where('number', $request->number)
  ->where('bidding_modalities_id', $request->get('bidding_modalities_id'))
  ->get();
```

Ou seja: **mesmo número + mesma modalidade + mesmos secretarias jurisdicionados**
= conflito. Na edição, a própria licitação é excluída do conflito via
`->where('id', '!=', $id)`.

Consequência: o mesmo número em modalidades diferentes **é permitido**, e o banco
não tem nenhum índice único que garanta isso.

### 1.2 Situações válidas

O formulário só oferece `Status::where('status_category_id', 2)`, isto é:

| id | Situação |
|---|---|
| 5 | Aberta |
| 6 | Finalizada |
| 7 | Cancelada |

Os demais status (1 Em Tramitação, 2 Atendido, 3 Indeferido, 4 Prorrogado) são da
categoria 1 (ouvidoria) e não aparecem.

### 1.3 Publicação do valor

`show_value` é obrigatório e binário (Sim/Não). Quando falso:
- a listagem do portal mostra “Indisponível” (`portal/components/licitacoes/tabela.blade.php`);
- a view de detalhe mostra “Indisponível” (`licitacao/show.blade.php`);
- a exportação (`v6/resources/views/exports/biddings.blade.php`) mostra “Indisponível”.

### 1.4 “Show artístico” (`is_show`)

- Coluna `biddings.is_show` (boolean, nullable) adicionada em 2023-03-01.
- No CMS aparece como checkbox com o **rótulo herdado “Show Artístico”**
  (`v6/resources/views/prefecture/biddings/form.blade.php`), por copy/paste de outro módulo.
- No portal habilita o filtro “Apenas Shows” (`show=1` → `where('is_show', 1)`).
- A listagem **não** é filtrada por `is_show` por padrão: uma licitação com
  `is_show = false` continua aparecendo normalmente.

### 1.5 Exclusão bloqueada por propostas

```php
if (Proposal::where('biddings_id', $id)->count() == 0) { ... }
```

A exclusão individual é recusada com
`{"erro": "A licitação não pode ser excluída porque já possui proposta(s)."}` e HTTP 500.
A exclusão em lote (`deleteByLote`) **pula** as licitações com propostas e informa
quais foram excluídas; devolve HTTP 500 com
`{"erro": "Não é possivel deletar nenhuma licitação pois todas estão vinculadas a propostas."}`
se nenhuma puder ser removida.

### 1.6 Arquivos do edital

- Campo no form: `archive[][0]` (arquivo) e `archive[][description]` (texto).
- Gravação: `storage/licitacao/{id}/arquivos/{nome}` (caminho montado em
  `BiddingController@extractedToArchive`).
- Nome do arquivo: `uniqid('', false) . str_random(5) . '.' . extensão`, em minúsculas
  (`FileTrait::getFileName`).
- Cada arquivo vira um registro em `archives` (com `title` = nome, `type` = mime,
  `extension`, `path`) e é ligado à licitação por `biddings_has_archives`.
- Arquivos com `isValid() == false` geram `flash('Arquivo inválido!')->error()`.
- Não há validação de mime/tamanho (a regra de mimes do `ContractRequest` está
  comentada e o `BiddingRequest` não tem regra de arquivo).

### 1.7 Contratos vinculados

- Um contrato pode apontar para uma **proposta** (`contracts.proposals_id`) e/ou
  diretamente para uma **licitação** (`contracts.bidding_id`, FK `ON DELETE SET NULL`
  criada em 2025-03-07).
- A tela de detalhe da licitação mostra contratos de duas origens:
  `licitacao/contratos.blade.php` (via propostas) e
  `portal/layouts/contratos/contratos-sem-proposta.blade.php` (via `bidding_id`).
- A exclusão de uma licitação **não** apaga os contratos: a FK é `SET NULL`.

### 1.8 Datas

- `publication_edital_date` e `competition_date` são opcionais desde a migration
  de 2019-10-30 (o bloco de obrigatoriedade no `BiddingRequest` está comentado).
- O `BiddingRepositoryDefaultImpl::structureUpInsert()` converte `d/m/Y` → `Y-m-d`
  e `d/m/Y H:i` → `Y-m-d H:i` via `AppTrait::toUSDataFormat()` /
  `toUSDataHourFormat()`.
- `toUSDataHourFormat()` faz `explode("/", $valor)` e depois
  `explode(" ", $array[2])` — **quebra** se a data vier sem hora
  (`Undefined offset`).
- `toUSDataFormat()` valida a data via `is_date()`; inválida chama `tError()`.

### 1.9 Valor monetário

- O campo `value` chega como string mascarada (`000.000.000.000,00`) e é convertido
  por `AppTrait::traitRequest()` (`case 'value': toUSCurrencyFormat($value)`).
- Na leitura, `Bidding::getvalueAttribute()` devolve `toBRCurrencyFormat($value)`.
- O filtro de valor do portal usa `whereBetween` com os extremos do slider
  (`min(value)`, `max(value)` calculados no controller).

## 2. Acoplamentos

### 2.1 Dependências de outros módulos

| Módulo | Ponto de acoplamento |
|---|---|
| Ouvidoria / Requisições (`statuses`) | `Status::where('status_category_id', 2)` — a lista de situações do módulo é compartilhada com a category de status 2. |
| Conteúdo / Secretarias (`contents`, `category_contents`) | As “secretarias jurisdicionadas” são conteúdos da categoria de slug `secretarias`. |
| Unidades Gestoras (`management_units`) | Unidade gestora da licitação. |
| Propostas (`proposals`, `proponents`) | Propostas, proponentes e o bloqueio de exclusão. |
| Contratos (`contracts`) | Contratos vinculados à proposta e/ou à licitação. |
| Arquivos (`archives`) | Storage de arquivos (edital e anexos). |
| Dashboard (`v6/app/Services/Dashboard/Widgets/LicitacoesWidget.php`) | Widget que conta licitações (`Bidding::count()`). |
| Layout / Temas (`configurations.layout`) | Define qual pasta `portal/layouts/*` renderiza as telas. |

### 2.2 O que este módulo expõe para os demais

- Tabela `biddings` (com soft delete) e view `licitacao`.
- Rotas `licitacao.index` / `licitacoes.show` / `contratos` / `exportacao` no portal.
- Recurso de permissão `licitacao` e módulo `licitacoes` (id 21 em `modulos`).

## 3. Armadilhas (anti-patterns) encontrados

| # | Onde | Problema |
|---|---|---|
| 1 | `v6/app/Http/Controllers/Prefecture/Bidding/BiddingController.php` (`ajaxDeleteFiles`) | `$archive = $this->archiveRepository->getModel()->find($request->get('licitacao'));` — busca o **arquivo pelo id da licitação**. O arquivo removido do disco é o de `archives.id == biddings.id`, não o `archive_id` enviado. |
| 2 | `v6/app/Repositories/Repository.php` (`upInsert`) | Em erro executa `dd($th)`, que define `http_response_code(500)`, faz o dump e executa `exit(1)`. O `try/catch` do controller nunca é alcançado para erros de repositório; o operador vê um dump, não a mensagem amigável. |
| 3 | `v6/app/Repositories/Repository.php` (`dataTrait`) | Usa `$request->request->all()`: só o corpo do POST. Requisições JSON não alimentariam o repositório. |
| 4 | `BiddingController@deleteByLote` | `Proposal::get()->pluck('biddings_id')` carrega **todas** as propostas em memória. |
| 5 | `BiddingController@deleteByLote` | Usa `destroyAll()` da base, que **não** desanexa as secretarias (`contents()->detach()`), ao contrário de `destroy()`. |
| 6 | `v6/app/Http/Filter/FilterBiddings.php` | `whereRaw` com `translate(lower(object), ...)` e `transliterator_transliterate` — SQL com PostgreSQL específico, não portável. |
| 7 | `FilterBiddings` | O filtro `contents_id` (órgão) é renderizado no formulário, mas **não é aplicado** na query. |
| 8 | `Portal\BiddingsController@index` | `Route::currentRouteName() == 'exportacao'` — **rota dentro do controller**. |
| 9 | `Portal\BiddingsController@index` | `$res->...->get()` antes da paginação: todas as licitações filtradas são carregadas em memória. |
| 10 | `Portal\BiddingsController@index` | `json_encode($licitacoes)` embutido na view: payload gigante na página. |
| 11 | `Bidding::scopeRelationsBidding` | Carrega `proposals.contracts`, `proposals.proponents` **e** `propostas.proponents` (relação duplicada) — trabalho extra. |
| 12 | `Portal\BiddingsController@gerarTxt` | Percorre `$model->propostas` (relação **não** eager-loaded) e, dentro do foreach, `$model->contents` — N+1. |
| 13 | `Portal\ContractController@formatValue` | `explode('-', $value)` sem garantir 2 partes → `Undefined offset: 1` quando o filtro não tem faixa. |
| 14 | `v6/app/Http/Controllers/Prefecture/ContractController.php` | `DB::select()` cru para montar os selects de propostas e licitações (SQL concatenado, não bound). |
| 15 | `v6/app/Http/Controllers/Prefecture/DatatableController.php` | Lê `DB::table('licitacao')` — a **view SQL**, não a tabela `biddings`. |
| 16 | `view licitacao` | `INNER JOIN` em modalidades/tipos/status: licitações com FK órfã desaparecem do CMS. |
| 17 | `view licitacao` | `to_char(...::timestamp with time zone ...)` pode deslocar a data em um dia conforme o fuso do servidor. |
| 18 | `v6/routes/basepro.php:129-130` | Rota `ajax-delete-files-licitacoes` **duplicada** com o mesmo nome. |
| 19 | `v6/routes/web.php:147` | `licitacao-ajax-detalhe/{id}` → `ajaxShowDetail`, **método inexistente** em `Portal\BiddingsController` (rota morta, HTTP 500). |
| 20 | `v6/routes/basepro.php:184` | `Route::resource` gera `licitacao.show`, mas `BiddingController` **não tem** `show()` — HTTP 500 se acessado. |
| 21 | `v6/resources/views/prefecture/biddings-types/index.blade.php` | `route('licitacao-anterior', $id)` — nome de rota inexistente (`licitacao-anterior` é slug de recurso). |
| 22 | `v6/resources/views/prefecture/biddings-types/create.blade.php` | `route('licitacao-anterior')` no link “Voltar” — mesma falha. |
| 23 | `v6/resources/views/prefecture/biddings/form.blade.php` | `verificaModalidades()` esconde `#dt_certame`/`#dt_edital`, ids que **não existem** no formulário (efeito nulo). |
| 24 | `v6/app/Http/Model/Prefecture/BiddingType.php` | `biddings()` aponta para a coluna `bidding_type_id`, que não existe (a real é `bidding_types_id`). |
| 25 | `v6/app/Http/Model/Prefecture/Bidding.php` | `biddingJurisdictionals()` é `belongsTo` com chaves invertidas (`'id'`, `'biddings_id'`); funciona por acaso e mostra **um** órgão em vez de todos. |
| 26 | `v6/app/Http/Model/Prefecture/Bidding.php` | `propostas()` duplica `proposals()` — duas relações para a mesma tabela, com grafia diferente. |
| 27 | `v6/app/Http/Model/Prefecture/Bidding.php` | `managementUnits()`, `statuses()`, `modalities()` declaradas como `hasOne` (FK no lado inverso) — funcionam, mas a intenção é `belongsTo`. |
| 28 | `v6/app/Http/Model/Prefecture/BiddingHasArchive.php` | `hasMany` com chaves invertidas e `Bidding::cLass` (typo de capitalização). |
| 29 | `v6/app/Http/Requests/BiddingRequest.php` | `$uniqueNumber = '';` — a regra `unique` está comentada; unicidade só em controller. |
| 30 | `v6/app/Http/Requests/BiddingRequest.php` | `$codigo = Request::segment(3);` atribuído e nunca usado. |
| 31 | `v6/app/Http/Controllers/Prefecture/Bidding/BiddingController.php` | `store()` em erro redireciona para `licitacao.index` **perdendo os dados digitados**; `update()` volta com `withInput()`. |
| 32 | `v6/app/Http/Model/Prefecture/Bidding.php` | `getPublicationEditalDateAttribute()` etc. formatam a data **na leitura**, misturando formato de exibição com dado bruto (a view do portal ainda aplica `preg_replace('/-/i','/', ...)`). |
| 33 | `v6/database/seeds/BiddingModalityTableSeeder.php` | `DB::table('bidding_modalities')->delete()` — apaga todas as modalidades cadastradas pelo operador. |
| 34 | `v6/app/Http/Controllers/Portal/BiddingsController.php` | `Participant` (`Person`) é carregado **inteiro** (`Person::whereNotNull('is_proponent')->get(...)`) só para popular um select. |
| 35 | `v6/app/Http/Controllers/Prefecture/Bidding/BiddingTypeController.php` / `BiddingModalityController.php` | Usam `has_access(['licitacoes.*'])` (plural), mas o recurso cadastrado é `licitacao` (singular): telas sempre redirecionam para o dashboard. |

## 4. Código duplicado entre v5 e v6

Comparação arquivo a arquivo (`diff`):

| Arquivo | Situação |
|---|---|
| `v5/app/Http/Controllers/Prefecture/Bidding/BiddingController.php` vs `v6/...` | **idêntico** |
| `v5/app/Http/Controllers/Prefecture/Bidding/BiddingTypeController.php` vs `v6/...` | **idêntico** |
| `v5/app/Http/Controllers/Prefecture/Bidding/BiddingModalityController.php` vs `v6/...` | **idêntico** |
| `v5/app/Http/Model/Prefecture/Bidding.php` vs `v6/...` | **idêntico** |
| `v5/app/Http/Model/Prefecture/BiddingType.php`, `BiddingModality.php`, `BiddingHasArchive.php`, `BiddingJurisdictionals.php` | **idênticos** |
| `v5/app/Http/Requests/BiddingRequest.php`, `BiddingTypeRequest.php`, `BiddingModalityRequest.php` | **idênticos** |
| `v5/app/Repositories/Eloquent/Prefecture/Bidding/BiddingRepositoryDefaultImpl.php` vs `v6/...` | **idêntico** |
| `v5/app/Http/Filter/FilterBiddings.php` vs `v6/...` | **idêntico** |
| `v5/app/Exports/BiddingsExport.php` vs `v6/...` | **idêntico** |
| `v5/database/seeds/BiddingTypeTableSeeder.php`, `BiddingModalityTableSeeder.php`, `NewBiddingModalities.php` | **idênticos** |
| `v5/app/Http/Controllers/Portal/BiddingsController.php` vs `v6/...` | **divergente** (ver item 5.1) |
| `v5/app/Http/Controllers/Portal/ContractController.php` vs `v6/...` | **divergente** (ver item 5.2) |
| `v5/app/Http/Model/Prefecture/Contract.php` vs `v6/...` | **divergente** (ver item 5.2) |

## 5. Onde a v6 divergiu da v5

### 5.1 Ordenação da listagem pública de licitações

`v5/app/Http/Controllers/Portal/BiddingsController.php` ordenava extraindo dígitos
do número:

```php
$res->orderByRaw("case when number ~ '/[0-9]' then substring(number from '/([0-9]+)')::int else 0 end desc, ...")
```

`v6/app/Http/Controllers/Portal/BiddingsController.php` substituiu por:

```php
$res->orderByRaw('homologation_date IS NULL, homologation_date DESC, publication_edital_date DESC')->get();
```

Ou seja, na v6 a listagem passa a priorizar o que foi **homologado** (licitações
sem homologação ao final) e, dentro disso, o edital mais recente. A regra da v5
privilegia o maior número — mais intuitivo para o usuário, mas frágil com
números não numéricos.

### 5.2 Contratos: coluna `agent` e ordenação

| Aspecto | v5 | v6 |
|---|---|---|
| Coluna `contracts.agent` | existe (`v5/database/migrations/2025_06_07_004403_add_agent_column_to_contracts_table.php`) | **não existe** (nem migration nem coluna) |
| `Contract::$fillable` | inclui `'agent'` | não inclui |
| Campo no formulário do CMS | existe — “Fiscal do Contrato” (`v5/resources/views/prefecture/contracts/form.blade.php`) | removido |
| Persistência | `ContractController@store`/`@update` gravam `'agent' => $request->get('agent')` | removido |
| Filtro `agent` no portal | `where('agent','ilike','%...%')` + campo no filtro (`v5/resources/views/portal/layouts/modelo1/licitacao/contratos/filtro.blade.php`) | removido |
| Ordenação | `CAST(NULLIF(substring(number from '/([0-9]{4})'), '') AS INTEGER) DESC NULLS LAST` + id desc | `orderBy('publication_date','desc')` |
| Route model | — | mesmo controller, mesmo conjunto de rotas |

**Impacto:** a v6 perdeu a identificação do **Fiscal do Contrato** — função
obrigatória na fiscalização de contratos administrativos (Lei 8.666/93, art. 67).
Não se trata de campo não utilizado: na v5 o campo era preenchido no formulário
do CMS, gravado e filtrável no portal. A remoção na v6 é uma **perda funcional**
que precisa ser confirmada com o negócio antes da migração.

### 5.3 Demais diferenças (não ligadas ao módulo)

`v5/routes/web.php` e `v5/routes/basepro.php` diferem em vários outros módulos
(VIAP, Emendas Parlamentares, Dashboard, Selos, Requerimentos, Carta de Serviços,
Pauta do Dia). As rotas de licitações em si são as mesmas nos dois arquivos
(`v5/routes/web.php:119-121`, `149-151`, `166`).

## 6. Riscos de portabilidade para a v7 (Laravel 11/12)

| Tema | Risco |
|---|---|
| View SQL `licitacao` | Depende de `row_number()`, `to_char` e `::timestamp with time zone`. Em PostgreSQL moderno continua válida, mas é preciso recriar a view no deploy e lembrar que ela **não** acompanha alterações de schema automaticamente (a migration usa `CREATE OR REPLACE VIEW`). |
| `translate(lower(object), ...)` | Função `translate` é do PostgreSQL; em MySQL/SQLite quebra. Manter PostgreSQL ou reescrever a busca em PHP/SQL portátil. |
| `ilike` | Específico de PostgreSQL (usado nos filtros de número/processo). |
| `DB::transaction` + `updateOrCreate` | Substituir por `firstOrCreate`/`updateOrCreate` com validação de unicidade real. |
| `dd()` no repositório | Eliminar; devolver exceptions tipadas. |
| Accessors que formatam data/valor | Separar em casts (`'date'`, `'datetime'`) e formatadores de apresentação. |
| `hasOne` usado como `belongsTo` | Corrigir para `belongsTo`/`hasMany` ao reescrever os models. |
| Sessão/`$_SESSION` | Não há uso direto de `$_SESSION` no módulo; a autenticação é via `Sentinel`. |
| Rotas dentro de controllers | `Route::currentRouteName()` deve virar um parâmetro (`/exportacao?type=...` já resolve) ou uma rota dedicada `/licitacoes/export`. |
