# Serviços da V2 — como estão implementados

> 15 pacotes em `V2/Services/`. São auto-descobertos pelo provider do Core
> (`V2/Core/Providers/V2ServiceProvider.php:174-201`): cada serviço pode entregar migrações, views
> (`v2-service-<slug>::`), rotas e comandos. Exceção: SEO, ExcelImporter e FormularioDinamico têm
> `ServiceProvider` próprio. Esta página cobre **como funciona** e **o que o Public V7 ainda não tem**;
> o plano de aproveitamento está em [`06-APROVEITAMENTO-V7.md`](06-APROVEITAMENTO-V7.md).

| Serviço | Função | Persistência | Contraparte no V7 | Veredito |
|---|---|---|---|---|
| Search | busca pública + “search-to-edit” do CMS | `v2_public_search_index`, `v2_cms_links_index` (tsvector + GIN) | **não existe** (backlog) | portar desenho |
| ExcelImporter | carga de planilha com preview e log | `import_excel_logs` (V1) + arquivo | **não existe** (backlog) | portar pipeline |
| LandingSectionsEditor | seções/blocos da home | `v2_layout_base_configs.payload` jsonb | **não existe** (F4/widgets) | portar contrato |
| FormularioDinamico | formulários versionados com score | 4 tabelas, `answers` jsonb + GIN | **não existe** | portar motor |
| ContentRichEditor | AST TipTap + render + conversores | nenhuma tabela (coluna do módulo) | `src/services/RichText` | comparar/portar extras |
| Media | upload e variantes | storage (quem persiste é Gallery) | `src/services/Media` | comparar |
| Gallery | catálogo de mídia | `v2_gallery_media` | `src/services/Gallery` | comparar |
| SEO | metas, schema, sitemap, visitas | `metadata['seo']` jsonb; `v2_seo_page_visits` | `src/services/Seo` | comparar |
| Storage | abstração local/S3 | disco | Flysystem (`s3`) | padrão já superado |
| DigitalSignature | A1 → PAdES + carimbo | `v2_digital_certificates`, `v2_signature_logs` | **não existe** (backlog) | portar especificação |
| MailService | envio + histórico + config | `v2_system_mail_configs`, `v2_mail_logs` | **não existe** (backlog) | adaptar (sem Swift) |
| ActivityLogger | timeline por model | `activity_log` (Spatie) | `core_audit_logs` | já superado |
| Webhook | ingestão assinada + sumário | 5 tabelas, jsonb | **não existe** | adaptar desenho |
| FormularioDinamico | (acima) | | | |

---

## 1. Search

**Como funciona.** Duas tabelas independentes:

- `v2_public_search_index` — `title`, `slug`, `module`, `entity_type`, `entity_id`, `summary_fragment`,
  `search_deep_content`, `tags` (json), `search_vector` `tsvector` **mantido por trigger** com pesos
  A=título, B=resumo, C=conteúdo, índice GIN
  (`V2/Services/Search/Migrations/2026_06_14_000000_create_search_indexes_tables.php:29-70`).
- `v2_cms_links_index` — rótulo, módulo, `edit_url`, `searchable_text`, índice GIN em expressão.

**Consulta pública** (`V2/Services/Search/Services/SearchService.php:21-95`):

```php
DB::table('v2_public_search_index')
    ->select([...,
        DB::raw("ts_headline('portuguese', summary_fragment, plainto_tsquery('portuguese', ?)) as headline"),
        DB::raw("ts_rank(search_vector, plainto_tsquery('portuguese', ?)) as rank")])
    ->whereRaw("search_vector OPERATOR(pg_catalog.@@) plainto_tsquery('portuguese', ?)")
    ->setBindings([$term, $term, $term])
    ->orderByDesc('rank')->orderByDesc('created_at')->limit(20);
```

**Indexação** é explícita no model, não por evento genérico: `News::syncToSearch()`
(`V2/Modules/Publicacoes/Noticias/Models/News.php:518`) chama `NewsSearchSender`, que monta o DTO e
faz `INSERT ... ON CONFLICT (entity_type, entity_id) DO UPDATE` (`PublicIndexerService.php:22-63`).
Falha só loga. O comando de reindexação percorre em lotes de 100
(`Search/Console/Commands/ReindexPublicSearch.php`), mas referencia módulos que não existem nesta
árvore (`Associados`, `UnidadeRegional`) e os ignora.

**Pontos fortes.** Índice desnormalizado (busca não toca as tabelas de conteúdo), UPSERT idempotente,
`ts_headline` para destaque, separação público × CMS, reindex comandado.

**Limites.** URL pública resolvida por `switch ($module)` hardcoded (`SearchService.php:67-88`) —
exatamente o acoplamento que a ADR-028 §8 proíbe; `entity_id` é `uuid` (módulo com id inteiro quebra
no upsert); `search_deep_content` das notícias é só resumo+categoria, não o corpo; sem paginação,
sinônimos, locale ou boost por módulo; sincronização síncrona no request do usuário.

## 2. ExcelImporter

**Como funciona.** Pipeline em três atos:

1. **Preview** (`Services/ImportExcelService.php:43-150`): detecta encoding de CSV
   (`UTF-8`/`ISO-8859-1`/`Windows-1252`) e converte; persiste o arquivo em `imports/Y/m`; cria
   `import_excel_logs` em transação isolada; valida todas as linhas e devolve um relatório
   (“Poka-Yoke”) com `has_errors`, contagens e aviso de encoding.
2. **Confirmação**: o cliente devolve as linhas normalizadas; `processImportExcelData()` responde
   `application/x-ndjson` em chunks de 25, com uma transação por linha e progresso incremental
   (`:177-244`).
3. **Fechamento**: `finishImportLog()` grava `processed_at`, contagens e auditoria.

**Schema declarativo** (`ImportExcelSchema` + `ImportExcelSchemaColumn`): colunas, aliases de
cabeçalho (mapeamento tolerante), normalização por tipo (CNPJ, data serial, moeda), chave de
unicidade e validadores por linha; duplicata intra-arquivo resolve “última vence”. Há gerador de
template XLSX (`ImportExcelTemplateGenerator`) e um “transpiler” Excel→CSV/HTML acessível
(`ExcelTranspilerService`).

**Limites.** O passo 2 confia nas linhas devolvidas pelo **cliente** (integridade depende do front);
`ini_set('memory_limit','512M')` e `set_time_limit(300)` dentro do request; transação por linha é
lenta em planilhas grandes; a unicidade não consulta o banco antes de gravar; o nome do arquivo usa
`time()` (colisão no mesmo segundo).

## 3. LandingSectionsEditor (+ StructuralLandingEditor)

**Como funciona.** O serviço é o **motor de UI**: `WidgetRegistry` resolve classes de widget por
tipo; cada widget implementa `WidgetContract` (icon/title/rules/sanitize/formView/renderView/
`getAttachmentIds`/`prepare`); 22+ `ValidationSchemas` descrevem os blocos (`hero`, `cards-links`,
`carousel`, `grid`, `command-line`, …); `prepareSections()` coleta **todos** os IDs de mídia das
seções e faz **um único `whereIn`** antes de renderizar, evitando N+1
(`Services/LandingSectionsEditorRendererService.php:118-198`).

A persistência fica no módulo `Support/StructuralLandingEditor`: `v2_layout_base_configs.payload`
(jsonb) por área; home como lista versionada v3 `{type, parameters, hide}` com whitelist, defaults e
ordem (`Services/HomepageSectionsService.php:18-22,194-241`); `v2_structural_portal_contents` guarda
HTML por área; `v2_sle_assets` guarda snippets.

**Limites.** `BlockRenderer` compila e executa Blade vindo do banco (`BlockRenderer.php:143-230`) —
RCE se o banco/suporte for comprometido; áreas são HTML em `text` (ADR-017 proíbe); `strip_tags` não
é sanitização; gate por e-mail fixo; `html-free` em iframe `srcdoc`; preview em cache com token sem
vínculo ao usuário.

## 4. FormularioDinamico

**Como funciona.** Modelo de **versão imutável**: `v2_formulario_dinamico_formularios` (slug) →
`_versoes` (`schema`, `scoring_config`) → `_campos` (`weight`, `options`, `rules`);
`_submissoes` guarda `answers` jsonb com **GIN `jsonb_path_ops`**, `score`, `max_score`,
`result_level`, `result_token` unique, `consent_at`, `ip_hash` (HMAC-SHA256).
Serviços pequenos: `ValidationService`, `SubmissionService`, `ScoringService` (faixas percentuais ou
absolutas), `DiagnosisService`, `ExportService` (CSV/agregações), `RendererService`, e 9 `FieldTypes`.

**Limites.** O `result_token` **não é assinado**, apesar do comentário; `distribution` carrega todas
as submissões na memória; render é string por tipo (escape depende de cada `FieldType`); i18n
ausente no motor.

## 5. ContentRichEditor

**Como funciona.** `ContentASTParser` com **26 classes de nó** e 7 de marca registradas por closure
(`Parser/ContentASTParser.php:123+`), renderizadores `HtmlStrategy`/`MarkdownStrategy`, endpoints de
upload/busca/import DOCX e **live preview** em iframe com `postMessage`. Conversores
`DocxToTiptapConverter` (ZipArchive, `word/document.xml`, numbering, imagens→Media) e
`HtmlToTiptapConverter` (descarta script/style/head/iframe/form; normaliza `<del>+<ins>` em
retificação).

**Comparação com o V7.** O V7 tem parser único com `match` e um conjunto menor de nós; o V2 tem mais
nós (tabela interativa, passos, CTA, carrossel, retificação, YouTube) e os dois conversores. O V2
carrega **TipTap de `https://esm.sh`** (`Assets/js/_bundle-header.js:3-14`), usa estilo inline e
`contentRichHtmlFree` — três coisas que o V7 rejeita (ADR-002/022/028).

## 6. Media e Gallery

**Media.** `MediaInputer::processUpload()` (`Services/MediaInputer.php:138`) roteia por MIME e devolve
`{path, disk, mime_type, size, type, variants, metadata}`. Variantes: WebP 360/720/1200/1920 via
Imagick q75; PDF ganha capa WebP e contagem de páginas; planilha vira CSV + HTML; áudio é validado.
`<picture>` usa `<source media="(min-width:Npx)">` — sem `srcset/sizes` e sem `width/height`.

**Gallery.** `v2_gallery_media` (`increments`, `owner_module`, `is_shared`, `variants` json,
`metadata` jsonb, `softDeletes`, índices parciais `deleted_at IS NULL`), com
upload/seedFromPath/list/find/update/archive/restore/rotate/delete e auditoria por operação.

**O que o V7 tem a mais:** AVIF via libvips, `srcset/sizes` e `width/height`, `media.stream` (chave de
objeto em vez de URL no conteúdo), limites por tipo, DTO/Jobs/batches e testes. **O que o V2 tem a
mais:** variantes/visualização de PDF, CSV/HTML de planilha e fallback S3→local.

## 7. SEO, Storage, ActivityLogger

- **SEO** (`SEOService`): metas e schema em `metadata['seo']`; `robots()`/`sitemap()` em XML manual;
  visitas em `v2_seo_page_visits` com buffer de 50 no cache e separação bot/humano; sanitizador de
  snippets por allowlist de tags/hosts (`SeoScriptSanitizer`). O sitemap só inclui itens **com visita
  nos últimos 30 dias** — conteúdo novo pode nunca entrar.
- **Storage**: contrato de 11 métodos com adaptadores S3/local; `ensureLocalPath` baixa para
  `/tmp/lotep_s3_downloads`; **falha de S3 degrada silenciosamente para o disco local** (dados
  divididos entre backends, sem sinal).
- **ActivityLogger**: fachada sobre Spatie `activity_log` (`subject_type` = FQCN → quebra ao mover
  namespace); a migração do serviço é *no-op* quando a tabela legada existe. O V7 já substituiu por
  `AuditWriter` + BLAKE3 (ADR-035).

## 8. DigitalSignature

**Como funciona.** Importa PFX (senha) → extrai X.509 e chave, cifra a chave privada com AES-256-CBC
(e IV ao lado, derivação a partir de `APP_KEY`) em `v2_digital_certificates`; assina PDF com
TCPDF+FPDI em PAdES e desenha carimbo visual; `v2_signature_logs` guarda SHA-256 do PDF, usuário e
data. **Não há TSA, OCSP/CRL nem verificação pública** (é dívida declarada no próprio serviço).

## 9. MailService

`send()` recebe `profile/cc/bcc/attachments/is_html/use_template`, troca o **mailer global**
(`Mail::setSwiftMailer`) e grava corpo completo em `v2_mail_logs` (PII). Config em
`v2_system_mail_configs` com senha cifrada. `Swift_SmtpTransport` **não existe no Laravel 12**; no V7
o caminho é `Mail`/`Notification` nativo + SPF/DKIM/DMARC (backlog `email-deliverability`).

## 10. Webhook

Ingestão por token: `v2_webhook_tokens` (`token_hash` + `salt`), campanhas com schema, payload cru e
compilado em jsonb, sumário consolidado e transições auditadas
(`received → validated → integrated`). **Limites:** o middleware percorre **todos** os tokens ativos e
calcula HMAC por linha (O(N)); sem timestamp/nonce (replay), sem rate limit e sem unicidade de
`payload_md5`.
