# Arquitetura do módulo Licitações

## 1. Visão de conjunto

O módulo é um CRUD editorial em quatro tabelas, dividido em duas superfícies
mutuamente exclusivas: o **CMS** (autenticado, `/cms/v2/licitacoes`) e o
**portal** (público, `/licitacoes`). Não existe API JSON própria; o único
endpoint que devolve JSON é o de criação inline de órgão
(`POST /cms/v2/licitacoes/orgaos/create-inline`) e o de reordenação de anexos.

O módulo pertence ao grupo `Publicacoes` e é descoberto automaticamente pelo
`V2ServiceProvider` a partir de `V2/Modules/{Grupo}/{Modulo}` (fato da
plataforma; o provider do módulo apenas implementa `getConfig()`).

```
V2/Modules/Publicacoes/Licitacoes/
├── ModuleProvider.php                 # título, ícone, menu, posição
├── Controllers/
│   ├── Admin/LicitacaoCmsController.php
│   ├── Admin/LicitacaoOrgaoController.php
│   └── Portal/LicitacaoPortalController.php
├── DataSources/LicitacaoDataSource.php
├── Migrations/                        # 10 migrations (2026-07-17 → 2026-08-31)
├── Models/                            # Licitacao, LicitacaoAnexo, LicitacaoOrgao, LicitacaoStatus
├── Requests/LicitacaoRequest.php
├── Routes/{cms,web}.php
├── Services/LicitacaoAttachmentService.php
└── Views/{cms,portal}/                # 10 blades
```

## 2. Fluxo de requisição

### 2.1 CMS — listagem

```
GET /cms/v2/licitacoes?search=&sort=created_at&dir=desc
  → middleware v2.authenticate
  → middleware v2.permission:licitacoes,read
  → LicitacaoCmsController@index
      se !v2_can('licitacoes','read') → abort(403)
      valida sort/dir contra allowlist
      Licitacao::search($term)->with(['status','orgaos','anexos'])->orderBy(...)->paginate(20)
      CmsActionLogger::info('licitacoes','index', ...)
  → view v2-licitacoes::cms.index
      → v2-cms::layouts.app
      → partials filter-card + table + status-badge
```

Rotas e controllers: `V2/Modules/Publicacoes/Licitacoes/Routes/cms.php:6-42` e
`V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoCmsController.php:40-66`. O middleware
`v2.permission:licitacoes,read` é a primeira barreira; a checagem `v2_can` no
controller é redundante para o caminho HTTP, mas necessária para testes que
chamam a action diretamente.

### 2.2 CMS — mutações

```
POST /cms/v2/licitacoes
  → v2.authenticate → v2.permission:licitacoes,read → v2.permission:licitacoes,write
  → LicitacaoCmsController@store(LicitacaoRequest)
      FormRequest::authorize() → v2_can('licitacoes','write')
      $data = validated(); $data['id'] = v2_uuid()
      Licitacao::create($data)
      $licitacao->orgaos()->sync($request->input('orgao_ids', []))
      V2AuditLogger::activity($licitacao,'created',...)
      CmsActionLogger::info('licitacoes','store',...)
  → 302 para v2.licitacoes.index com flash 'success'
```

As demais mutações seguem o mesmo desenho: checagem `v2_can`, operação
Eloquent, evento de auditoria e redirect. Exceção: `reorderAttachments`
responde `{"success": true}`; `createInline` responde o órgão em JSON.

### 2.3 Portal

```
GET /licitacoes
  → LicitacaoPortalController@index
      Licitacao::active()->with(['status','orgaos','anexos'])->ordered()->get()
      LicitacaoOrgao::active()->get()
  → view v2-licitacoes::portal.index
      → v2-portal::app-layout

GET /licitacao/{slug}
  → LicitacaoPortalController@show($slug)
      Licitacao::active()->with([...])->bySlug($slug)->firstOrFail()
      ContentRichEditorRendererService::toHtml($l->object)
      SEOService::setTitle(...) (try/catch)
  → view v2-licitacoes::portal.show
```

O portal não usa paginação nem filtro de servidor: o controller materializa
todas as linhas ativas e a view esconde as linhas via JavaScript.

## 3. Classes e papéis

| Classe / arquivo | Papel |
|---|---|
| `V2\Modules\Publicacoes\Licitacoes\ModuleProvider` | declara título, descrição, ícone `bi bi-file-earmark-text`, posição 20, grupo “Atos Oficiais”, rota `v2.licitacoes.index` e padrão de menu ativo |
| `Controllers\Admin\LicitacaoCmsController` | CRUD principal, toggle, arquivo/restauração/force delete, upload e ordenação de anexos |
| `Controllers\Admin\LicitacaoOrgaoController` | CRUD de órgãos, toggle, restore e criação inline |
| `Controllers\Portal\LicitacaoPortalController` | listagem e detalhe públicos |
| `Requests\LicitacaoRequest` | autorização e validação do formulário principal |
| `Models\Licitacao` | regras de identidade, slug, mutators, escopos e relações |
| `Models\LicitacaoAnexo` | metadados de arquivo, URL via `v2_storage_url()` e apresentação |
| `Models\LicitacaoOrgao` | cadastro de órgãos e escopo `active()` |
| `Models\LicitacaoStatus` | situação e cor Bootstrap derivada do nome |
| `Services\LicitacaoAttachmentService` | allowlist MIME, magic bytes, storage, update, reorder e delete |
| `DataSources\LicitacaoDataSource` | métricas de Estatísticas por leitura direta das tabelas |
| `Views\cms\*` | backoffice (index, archived, create, edit, form, anexos, órgãos) |
| `Views\portal\*` | listagem e detalhe públicos |

### 3.1 Construtor do controller principal

```php
public function __construct(
    V2AuditLogger $audit,
    LicitacaoAttachmentService $attachmentService,
    CmsActionLogger $cmsLog
) {
    $this->audit = $audit;
    $this->attachmentService = $attachmentService;
    $this->cmsLog = $cmsLog;
}
```

(`V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoCmsController.php:27-37`)

A injeção é feita pelo container; não há ServiceProvider do módulo registrando
bindings. `LicitacaoAttachmentService` é resolvido por autowiring e recebe
`V2AuditLogger`.

## 4. Modelo de autorização

Existem três camadas, das quais duas são efetivas no HTTP:

1. **Middleware de grupo** (`V2/Modules/Publicacoes/Licitacoes/Routes/cms.php:7-8`):
   `['v2.authenticate', 'v2.permission:licitacoes,read']`.
2. **Middleware por rota** para escrita:
   `v2.permission:licitacoes,write` nas rotas de create/store/edit/update/
   archive/restore/force-delete/toggle e em todas as rotas de anexos/órgãos.
3. **Checagem no controller**: `v2_can('licitacoes', 'read'|'write')` no topo de
   cada action.

O helper `v2_can` resolve o usuário via Sentinel, aplica bypass de
`config('v2.support.email')` e delega ao `PermissionService`. O serviço aceita
os níveis `read` e `write`; `write` implica `read`, o inverso não
(`V2/Core/Services/PermissionService.php:69-84`). A chave `licitacoes` é
aliasada para a canônica `Licitacoes` (`:35`).

## 5. Dependências

### 5.1 Internas da V2

| Dependência | Uso | Acoplamento |
|---|---|---|
| `V2\Core\Controllers\BaseController` | classe-base dos três controllers | baixo |
| `V2\Core\Services\V2AuditLogger` | `activity()` nas mutações | médio |
| `V2\Core\Logging\CmsActionLogger` | eventos de navegação do CMS | baixo |
| `V2\Core\Services\PermissionService` | resolução indireta via `v2_can` | médio |
| `V2\Services\ContentRichEditor\Services\ContentRichEditorRendererService` | renderiza `object` no portal | alto (formato do conteúdo) |
| `V2\Services\SEO\Services\SEOService` | título do detalhe | baixo (try/catch) |
| `V2\Services\Storage\StorageService` | URL de anexo via `v2_storage_url()` | alto (caminho do arquivo) |
| `V2\Modules\Infraestrutura\Estatisticas\DataSources\DataSourceContract` | contrato do datasource | baixo |
| `V2\Modules\Infraestrutura\Estatisticas\DataSources\FilterOptionsContract` | opções de filtro | baixo |

### 5.2 Do framework e do legado

- Eloquent + `SoftDeletes` (`Licitacao`, `LicitacaoAnexo`, `LicitacaoOrgao`).
- `Illuminate\Http\UploadedFile` e `Storage` (disco `public`).
- `Illuminate\Foundation\Http\FormRequest`.
- `cartalyst/sentinel` indiretamente, via `v2_can` e `v2.authenticate`.
- Layouts do Core: `v2-cms::layouts.app`, `v2-cms::partials.*`,
  `v2-portal::app-layout`.
- Serviço de editor rico: componente
  `v2-service-contentricheditor::components.forms.content-rich-editor`.

### 5.3 Consumidores externos (o módulo é usado por)

| Consumidor | Forma | Risco |
|---|---|---|
| Importador legado | `LicitacoesLegacyImporter` grava direto em `v2_licitacoes` e `v2_licitacao_status` | alto: conhece o schema por dentro |
| Estatísticas | `LicitacaoDataSource` lê `v2_licitacoes`/`v2_licitacao_status` via query builder | médio: quebra se colunas mudarem |
| Command de mock | `EstatisticasMockCommand` insere lotes em `v2_licitacoes` | médio |
| Homepage | atalho “Editais” aponta para `/licitacoes` | baixo |
| Analytics | rotas são mapeadas para rótulos em `AnalyticsCmsController` | baixo |

## 6. Diagrama de estados do registro

```
                 create
                   │
                   ▼
              ┌─────────┐   toggle-status   ┌─────────┐
              │ ativo   │ ◄────────────────►│ inativo │
              │(portal) │                   │(oculto) │
              └────┬────┘                   └────┬────┘
                   │ delete (soft)               │ delete (soft)
                   ▼                             ▼
              ┌──────────────────────────────────────┐
              │ arquivada (deleted_at preenchido)     │
              │  restore → volta ao estado anterior   │
              │  forceDelete → some do banco          │
              └──────────────────────────────────────┘
```

`is_active` e `deleted_at` são ortogonais: uma licitação arquivada mantém o
valor de `is_active` que tinha antes. `status_id` é rótulo editorial e não
participa de nenhuma transição automática.

## 7. Pontos de extensão existentes

- **Órgãos**: cadastro independente; `sync()` no formulário permite criar,
  associar e desassociar sem editar a licitação em outra tela.
- **Anexos**: tabela própria com `order`; o portal ordena novamente por
  `sortBy('order')`, apesar de a relação já vir ordenada.
- **Status**: tabela editável apenas por migration/seed; não há tela de CRUD.
- **SEO**: apenas título no detalhe; não há `description`, canonical, OG ou
  JSON-LD próprios.
- **Datas**: três campos livres; não há campo “data de abertura” unificado nem
  cálculo de vigência.

## 8. Observações de arquitetura

- O módulo **não** tem ServiceProvider próprio nem bindings; toda a fiação é
  por convenção do Core.
- O controller principal concentra 14 actions e três responsabilidades
  (licitação, anexo, órgão somente leitura para o formulário). No V7, cada
  operação deve virar um controller invocável (padrão do módulo Notícias) e as
  regras devem ir para Actions.
- Não há camada de repositório. As consultas estão nos controllers e nos
  escopos do model.
- A view de índice consulta `LicitacaoOrgao::orderBy(...)` e
  `LicitacaoStatus::orderBy(...)` diretamente (N+2 queries por render),
  `V2/Modules/Publicacoes/Licitacoes/Views/cms/index.blade.php:6-7`.
- O módulo não possui rotas de API; não há exportação CSV/JSON nem endpoint
  de dados abertos.
