# Licitações — Estratégia de Testes

> Estado atual no repositório: **não há** teste específico do módulo de licitações.
> A única suíte que toca a tabela `biddings` é
> `v6/tests/Feature/DashboardLicitacoesWidgetTest.php` (widget do dashboard).
> Ambiente de teste existente: `v6/tests/DashboardTestCase.php` (base abstrata) e
> `v6/tests/Unit/` (ver `v6/phpunit.xml`).

---

## 1. Estratégia

| Camada | Ferramenta sugerida | Foco |
|---|---|---|
| Feature (HTTP) | PHPUnit + `RefreshDatabase` | Rotas do CMS e do portal, validação, permissões, uploads, exclusão. |
| Unit | PHPUnit | Regras de unicidade, conversão de datas, filtro de valores, formatadores. |
| End-to-end manual | — | conference CMS ↔ portal ↔ exportação. |

Comandos (a partir de `v6/`):

```bash
# suíte completa
./vendor/bin/phpunit

# apenas o módulo
./vendor/bin/phpunit --filter Bidding

# arquivo específico
./vendor/bin/phpunit tests/Feature/Bidding/CmsBiddingCrudTest.php
```

Configuração de banco de teste: `v6/phpunit.xml` (conexão de testes) e
`v6/.env.testing` quando existir. O banco de desenvolvimento é
`prefeituras_v5` em PostgreSQL — **nunca** executar testes contra ele.

## 2. Fixtures / factories necessárias

Factories a criar (não existem hoje):

```php
// database/factories/BiddingFactory.php
Bidding::factory()->create([
    'number'             => '001/2026',
    'publication_edital_date' => now(),
    'competition_date'   => now()->addDays(7),
    'homologation_date'  => null,
    'object'             => 'Objeto do teste',
    'process_number'     => 'PROC-2026-001',
    'value'              => 1000.00,
    'show_value'         => true,
    'management_units_id' => ManagementUnit::factory(),
    'bidding_modalities_id' => BiddingModality::factory(),
    'bidding_types_id'   => BiddingType::factory(),
    'statuses_id'        => 5,          // Aberta
    'local'              => 'Sala de Reuniões',
    'is_show'            => false,
]);
```

Factories auxiliares: `BiddingTypeFactory`, `BiddingModalityFactory`,
`ManagementUnitFactory`, `StatusFactory` (com `status_category_id = 2`),
`ContentFactory` (categoria `secretarias`), `ProposalFactory`,
`ContractFactory`, `ArchiveFactory`.

Estado inicial do banco para os testes (seed mínimo):

```php
BiddingType::create(['name' => 'Compras e Serviços']);
BiddingModality::create(['name' => 'Pregão Eletrônico']);
ManagementUnit::create(['name' => 'Secretaria de Obras']);
Status::create(['name' => 'Aberta', 'status_category_id' => 2]);
$secretarias = CategoryContent::create(['name' => 'Secretarias', 'slug' => 'secretarias']);
Content::create(['title' => 'Secretaria de Obras', 'category_contents_id' => $secretarias->id]);
```

## 3. Casos de teste — CMS (cadastro e edição)

| # | Caso | Arrange | Act | Assert |
|---|---|---|---|---|
| C1 | Cadastro válido | usuário `super-admin`, modalidade/tipo/unidade/situação cadastrados | `POST /cms/licitacao` com todos os campos | 302 para `/cms/licitacao`; registro em `biddings` com `show_value`, `value` e `is_show` corretos; mensagem “Licitação cadastrada com sucesso.” |
| C2 | Número duplicado (mesma modalidade + mesmas secretarias) | licitação existente com o mesmo `number` | `POST /cms/licitacao` | 302 de volta; mensagem “O Número da Licitação digitado já está em uso.”; **nenhum** registro novo |
| C3 | Mesmo número, modalidade diferente | licitação existente com `number` X na modalidade A | `POST` com `number` X na modalidade B | 302; **novo** registro criado |
| C4 | `number` com 16 caracteres | — | `POST` com `number` de 16 chars | 302 de volta com erro em `number` (máx. 15) |
| C5 | `jurisdicionado` vazio | — | `POST` sem `jurisdicionado` | erro de validação em `jurisdicionado` |
| C6 | `show_value` ausente | — | `POST` sem `show_value` | erro de validação em `show_value` |
| C7 | `object` ausente | — | `POST` sem `object` | erro de validação em `object` |
| C8 | `value` ausente | — | `POST` sem `value` | erro de validação em `value` |
| C9 | Datas ausentes aceitas | — | `POST` sem `publication_edital_date` nem `competition_date` | **criado com sucesso** (equivalência funcional: são opcionais) |
| C10 | Upload de arquivo | `Storage::fake('public')` | `POST` com `archive[][0]` e `archive[][description]` | registro em `archives`; linha em `biddings_has_archives`; arquivo presente em `storage/licitacao/{id}/arquivos/` |
| C11 | Arquivo inválido | `UploadedFile::fake()->create('x.pdf', 0)` | `POST` com arquivo inválido | mensagem “Arquivo inválido!”; licitação **não** criada |
| C12 | Edição troca as secretarias | licitação com 2 secretarias | `PUT /cms/licitacao/{id}` com 1 secretaria | `bidding_jurisdictionals` reflete apenas a nova secretaria (`sync`) |
| C13 | Edição mantém o número | licitação existente | `PUT` com o mesmo `number` | **sem** mensagem de duplicidade (o `id` é excluído da checagem) |
| C14 | Exclusão sem propostas | — | `DELETE /cms/licitacao/{id}` | JSON `{success: 'Licitação deletada com sucesso.'}`, HTTP 200; registro removido (soft delete); secretarias desanexadas |
| C15 | Exclusão com propostas | propostas vinculadas | `DELETE` | JSON `{erro: 'A licitação não pode ser excluída porque já possui proposta(s).'}`, HTTP 500; registro preservado |
| C16 | Exclusão em lote parcial | 3 licitações, 1 com propostas | `POST /cms/ajax/licitacao/delete-por-lote` com `toDelete[]` das 3 | 2 removidas; a com propostas permanece; mensagem lista os códigos/números remanescentes |
| C17 | Exclusão em lote total bloqueado | todas com propostas | `POST` | `{erro: 'Não é possivel deletar nenhuma licitação pois todas estão vinculadas a propostas.'}`, HTTP 500 |
| C18 | Permissão negada | usuário sem `licitacao.create` | `POST /cms/licitacao` | 302 para a rota anterior + “Você não tem autorização para acessar esta funcionalidade!” |
| C19 | Exclusão de arquivo | arquivo vinculado | `POST /cms/ajax/ajax-delete-files-licitacoes` | resposta `[true]`; registro em `archives` removido; **arquivo correto** removido do disco (regressão do bug do id) |
| C20 | Log de auditoria | — | `POST /cms/licitacao` | entrada em `activity_log` com “Cadastro de Licitação” e o detalhe com número, valor e autor |
| C21 | CRUD de tipo de licitação | — | `POST /cms/tipo-licitacao` | registro em `bidding_types`; nome com 121 caracteres é rejeitado |
| C22 | CRUD de modalidade | — | `POST /cms/modalidade-licitacao` | registro em `bidding_modalities` |

## 4. Casos de teste — Portal (público)

| # | Caso | Arrange | Act | Assert |
|---|---|---|---|---|
| P1 | Listagem pública | 3 licitações | `GET /licitacoes` | HTTP 200; view `portal.layouts.{layout}.licitacao.index` renderizada; 3 linhas |
| P2 | Paginação | 15 licitações | `GET /licitacoes` | 10 por página; `LengthAwarePaginator` com `path` = `url('licitacoes')` |
| P3 | Filtro por número | — | `GET /licitacoes?number=001` | só as licitações cujo número contém “001” |
| P4 | Filtro por período | — | `GET /licitacoes?dt_inicial=2026-01-01&dt_final=2026-12-31` | só as licitações no intervalo (`publication_edital_date`) |
| P5 | Filtro por objeto com acento | objeto “Manutenção de vias públicas” | `GET /licitacoes?object=vias publicas` | retorna a licitação (tradução/`translate` funciona) |
| P6 | Filtro por faixa de valor | valores 100 e 5000 | `GET /licitacoes?value=100-5000` | só as licitações dentro da faixa |
| P7 | Filtro por participante | proposta com proponente X | `GET /licitacoes?proponent={id}` | só as licitações com proposta do proponente X |
| P8 | Filtro “Apenas Shows” | 1 com `is_show = true`, 1 com `false` | `GET /licitacoes?show=1` | apenas a de `is_show = true` |
| P9 | Filtro por órgão (regressão) | 2 secretarias | `GET /licitacoes?contents_id={id}` | **apenas** as licitações do órgão (o legado ignora este filtro) |
| P10 | Ordenação | lic-homologada, lic-sem-homologação, lic-homologada-mais-antiga | `GET /licitacoes` | homologadas primeiro; entre elas, `publication_edital_date` desc; não homologadas por último |
| P11 | Detalhe | — | `GET /licitacao/{id}` | HTTP 200; situação, número, modalidade, objeto, valor e arquivos visíveis |
| P12 | Detalhe com `show_value = false` | — | `GET /licitacao/{id}` | exibe “Indisponível” no lugar do valor |
| P13 | Detalhe inexistente | id inexistente | `GET /licitacao/999999` | renderiza `admin.errors.conteudo-nao-encontrado` (sem exceção) |
| P14 | Detalhe com propostas e contratos | — | `GET /licitacao/{id}` | tabela de propostas e bloco de contratos renderizados |
| P15 | Exportação xlsx | — | `GET /exportacao?type=xls` | resposta com `Content-Disposition` de `licitacoes.xlsx` |
| P16 | Exportação csv | — | `GET /exportacao?type=csv` | arquivo `licitacoes.csv` |
| P17 | Exportação pdf | — | `GET /exportacao?type=pdf` | `licitacoes.pdf` (dompdf) |
| P18 | Exportação txt | — | `GET /exportacao?type=txt` | texto com os cabeçalhos “Licitação \| Publicação \| …” e uma linha por licitação |
| P19 | Exportação respeita os filtros | — | `GET /exportacao?number=001&type=csv` | só as licitações do filtro no arquivo |
| P20 | Resposta AJAX | — | `GET /licitacoes` com `X-Requested-With: XMLHttpRequest` | JSON com a coleção (sem HTML) |
| P21 | Lista de contratos | — | `GET /contratos` | HTTP 200; ordenação por `publication_date` desc |
| P22 | Filtro de contratos por licitação | — | `GET /contratos?bidding_number=001` | só os contratos da licitação indicada |
| P23 | Detalhe de contrato | — | `GET /contrato/{id}` | HTTP 200 com os dados do contrato |
| P24 | Contrato inexistente | id inexistente | `GET /contrato/999999` | flash “Contrato não encontrado.” + redirect de volta |
| P25 | Rota morta | — | `GET /licitacao-ajax-detalhe/1` | na v7 a rota deve **não existir** (404); na v6 retorna 500 — registrar como bug |

## 5. Casos de teste — View SQL e DataTable do CMS

| # | Caso | Arrange | Act | Assert |
|---|---|---|---|---|
| V1 | View reflete os dados | licitação com modalidade, tipo e situação | `SELECT * FROM licitacao WHERE id = {id}` | `numero`, `valor`, `modalidade`, `tipo`, `situacao`, `data_publicacao` preenchidos |
| V2 | Soft delete exclui da view | licitação excluída | `SELECT` na view | linha ausente |
| V3 | `row_number` é a numeração da linha | 2 licitações | `SELECT value FROM licitacao ORDER BY value` | 1 e 2 (a coluna **não** é o valor monetário) |
| V4 | Data formatada em `DD/MM/YYYY` | `publication_edital_date = 2026-03-05` | `SELECT data_publicacao` | `05/03/2026` |
| V5 | FK órfã não some da view (regressão) | `bidding_types_id` apontando para registro inexistente | `SELECT` na view | a licitação continua presente (com `LEFT JOIN`) — no legado some |
| V6 | DataTable do CMS | 2 licitações | `GET /cms/datatable/licitacao` | JSON com as colunas `numero`, `valor`, `modalidade`, `data_publicacao`, `tipo`, `situacao`, `id` + `edit`/`delete` |
| V7 | Permissões no DataTable | usuário sem `licitacao.edit` | `GET /cms/datatable/licitacao` | `edit = false` e `delete = false` nas linhas |

## 6. Casos de teste — Regras de negócio (unitário)

| # | Caso | Assert |
|---|---|---|
| U1 | Conversão `d/m/Y` → `Y-m-d` | `toUSDataFormat('05/03/2026') === '2026-03-05'` |
| U2 | Conversão `d/m/Y H:i` → `Y-m-d H:i` | `toUSDataHourFormat('05/03/2026 14:30') === '2026-03-05 14:30'` |
| U3 | Valor BR → double | `1.234.567,89` → `1234567.89` |
| U4 | Formatação BR na leitura | `toBRCurrencyFormat(1234.5)` → `1.234,50` |
| U5 | `SelectRule` reprova `'0'` | `passes('x','0') === false`; `passes('x','1') === true` |
| U6 | Cor da situação no presenter | `getStyleColorTd()` → `text-success` (5), `text-danger` (6 e 7), `text-secondary` (outros) |
| U7 | `getStyleHasProposal` | título “Não há propostas” quando não há contrato nas propostas; “Mais informações” quando há |
| U8 | Extensão/nome do arquivo | `getFileName()` gera nome único, minúsculo, com a extensão original |

## 7. Cenários manuais de aceite

1. **Publicação completa**: cadastrar uma licitação com edital, abrir `/licitacoes`,
   filtrar por ela, abrir o detalhe, baixar o edital e exportar em xls.
2. **Valor protegido**: cadastrar com “Publicar valor estimado? Não” e conferir
   “Indisponível” na listagem, no detalhe e no arquivo exportado.
3. **Show artístico**: marcar `is_show` e filtrar por “Apenas Shows”.
4. **Propostas**: vincular propostas a uma licitação e tentar excluí-la
   (deve ser recusado).
5. **Lote**: excluir em lote uma mistura de licitações com e sem propostas.
6. **Modalidades**: cadastrar modalidade nova e usá-la em uma licitação.
7. **Permissões**: remover a permissão `licitacao.create` do papel e tentar
   cadastrar (deve ser redirecionado com aviso).

## 8. O que não deve ser testado (regressões conhecidas a fixar)

| Item | Comportamento legado que **não** deve ser reproduzido |
|---|---|
| `ajaxDeleteFiles` | Apagar o arquivo errado (bug do id). O teste deve exigir que o arquivo `archive_id` seja removido do disco. |
| `Route::resource` `licitacao.show` | Retornar 500. O teste deve exigir 404 (rota removida) ou uma tela de leitura real. |
| `licitacao-ajax-detalhe/{id}` | Retornar 500. |
| `route('licitacao-anterior')` | Lançar `RouteNotFoundException` nas telas de tipos de licitação. |
| `has_access(['licitacoes.*'])` | Redirecionar ao dashboard para usuários não `super-admin`. |
| Filtro `contents_id` | Ser ignorado. |
| `value` em `double precision` | Perder precisão em centavos. |
| `dd()` no repositório | Encerrar a requisição com dump em vez de mensagem de negócio. |

## 9. Regressão existente a preservar

`v6/tests/Feature/DashboardLicitacoesWidgetTest.php` garante que o widget do
dashboard (`v6/app/Services/Dashboard/Widgets/LicitacoesWidget.php`) conte
**todas** as licitações não excluídas logicamente, sem filtro por `is_show`,
`statuses_id` ou data — e que o widget **não** introduza `where` adicional.
Esse comportamento deve ser mantido na v7 (o widget não faz parte do módulo de
licitações, mas depende da tabela `biddings`).
