diff --git a/docs/changelog/2026-10-22-s48-peticao-pl-5594.md b/docs/changelog/2026-10-22-s48-peticao-pl-5594.md new file mode 100644 index 00000000..b5c966d9 --- /dev/null +++ b/docs/changelog/2026-10-22-s48-peticao-pl-5594.md @@ -0,0 +1 @@ +**S48 — Abaixo-assinado do PL 5594/2026 (transparência dos presidenciáveis) (2026-10-22):** a petição `pl-5594-2026` passa a existir de forma reproduzível: o conteúdo (título, subtítulo, 5 blocos do corpo, links oficiais da Câmara e o pedido) é versionado em `scripts/lib/petitionPl5594Content.mjs` e provisionado por `pnpm seed:petition-pl-5594`, um seed dry-run por padrão que resolve o Consent existente `whatsapp-inscricao` **por chave** (fail-closed) e cria a petição `enabled`/`published` apenas quando o id não existe — nunca sobrescreve edição feita no admin. `--apply` em alvo não-local/produção exige `PETITION_SEED_CONFIRM=1` e `TEQO_ENV` casando o banco exato (`teqo_staging`/`teqo_1313`; dev local comprovadamente local segue a política do `assertWriteConfirm`), e o runbook de produção é `TEQO_ENV=production PETITION_SEED_CONFIRM=1 pnpm seed:petition-pl-5594 --apply` seguido de restart/redeploy (o seed roda fora do runtime Next e não busta `document_petition:`). O corpo cobre o que o PL propõe (autorização no registro, 5 exercícios de IR, dados bancários/fiscais/telefônicos/de conexão), por que importa (debate de 18/10 e protocolo em 19/10), o que o PL **não** faz (conteúdo das comunicações, acesso livre, finalidade estranha, dados de terceiros, LGPD), o pedido e o status oficial em 21/10 — sem acusar crime nem prometer aprovação. A copy hardcoded do bloco do formulário da página de petição trocou "nesta campanha"/"luta dos trabalhadores" pelos literais genéricos "Sua assinatura fortalece esta mobilização." / "Os dados serão usados apenas para registrar sua participação e a comunicação de conteúdos relacionados." (vale para as 3 petições existentes; autorizado no plano de intenção). Sem migration, sem collection/field novo, sem Consent novo; `Contact`+`Signature`+`Subscription` e a transação existentes não mudaram. Testes: unit novo de invariantes do conteúdo (identidade, 5 blocos, fatos oficiais, links, guardrail editorial) + pin da copy na página; smoke real em navegador (página 200, consent renderizado, assinatura nº 1 e compartilhamento). Sem e2e novo (a suíte não tem e2e de petição e o fluxo não mudou). Dívida registrada em #1447 (fora de escopo): o JSON-LD da petição serializa sem escapar `<`, diferente do artigo — conteúdo é admin-only, mas o escape deve entrar numa próxima passada. Impl plan: `docs/plans/abaixo-assinado-pl-5594-2026-impl.md`. diff --git a/docs/plans/abaixo-assinado-pl-5594-2026-impl.md b/docs/plans/abaixo-assinado-pl-5594-2026-impl.md new file mode 100644 index 00000000..104da86d --- /dev/null +++ b/docs/plans/abaixo-assinado-pl-5594-2026-impl.md @@ -0,0 +1,126 @@ +# Impl: Abaixo-assinado do PL 5594/2026 — transparência dos presidenciáveis (S48) + +Status: aprovado +Atualizado em: 2026-10-21 +Issue: #1443 +Intenção: docs/plans/abaixo-assinado-pl-5594-2026.md +Appetite restante: herdado — ~0,5 dia eng, sem corte + +## Leitura da intenção + +- **Outcome:** a petição `pl-5594-2026` existe `enabled`, com título/subtítulo/corpo/consent, página 200 em `/abaixo-assinado/pl-5594-2026`, assinável e compartilhável, com contador e SEO atual. +- **O que NÃO negociar:** sem cadastro paralelo a `Contact`; sem acusar crime nem prometer aprovação; sem Consent novo (reusar `whatsapp-inscricao`); sem migration/collection nova; `id` é a URL pública e não muda; sem PII em log. +- **O que reavaliar:** a hipótese "conteúdo criado no admin" não é repetível/auditável — vira seed versionado; "sem código novo" vira seed + copy + testes (sem schema). + +## Abordagem recomendada + +```mermaid +flowchart LR + A[petitionPl5594Content.mjs] --> B[seed-petition-pl-5594.mjs] + B -->|getConsentByKey| C[(Consent whatsapp-inscricao)] + B -->|create-if-missing| D[(petition pl-5594-2026)] + D --> E[/abaixo-assinado/pl-5594-2026/] + E --> F[getCachedDocumentById + SignatureCounter + PetitionForm] + G[copy literals na page] --> E +``` + +**Opções consideradas:** A) criar a petição só no admin; B) migration de dados; C) seed idempotente versionado + ajuste de copy + testes unitários. +**Recomendação:** C — o conteúdo nasce no repo, é revisável/testável, reproduzível em worktree/staging/produção e não toca schema. +**Rejeitadas:** A porque não é repetível/auditável e some do repo; B porque a intenção proíbe migration e conteúdo editorial não pertence a migration. + +### Componentes / mudanças + +- **`scripts/lib/petitionPl5594Content.mjs`**: literais (id, título, subtítulo, `form.title` "Assine o abaixo-assinado"/`form.subtitle` "Preencha seus dados para apoiar o PL 5594/2026.", consent key), 5 blocos HTML e links oficiais; fonte única para seed e testes. +- **`scripts/seed-petition-pl-5594.mjs`**: dry-run por padrão; `--apply`; create-if-missing por `id`; consent via `getConsentByKey` (fail-closed); HTML→Lexical com `editorConfigFactory.default` + JSDOM (padrão de `seed-posts.mjs`). +- **`package.json`**: `"seed:petition-pl-5594": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-petition-pl-5594.mjs"`. +- **`src/app/(frontend)/abaixo-assinado/[id]/page.tsx:204-213`**: só copy (D2); canonical/OG/JSON-LD existentes, sem SEO novo. +- **Testes**: novo `tests/unit/petitionPl5594Content.unit.spec.ts`; ajuste de `tests/unit/petition-page-layout.unit.spec.ts`. +- **Changelog:** `docs/changelog/2026-10-22-s48-peticao-pl-5594.md` (copy global + passo de seed em produção). +- **Migration:** nenhuma; nenhuma collection/field/access alterado. +- **Access / Consent:** access inalterado; Consent existente `whatsapp-inscricao` resolvido por key (`overrideAccess` intencional), nunca por id hardcoded; ausência → seed aborta. +- **UI:** Impeccable A — nenhuma UI nova; reusa page, `PetitionForm` e `SignatureCounter`; sem shape/craft/critique. + +### Dados → forma + +- Contador absoluto já existente (`SignatureCounter` + `getSignatureCount`); sem percentual, ranking ou meta. Nenhuma forma nova. + +## Decisões de engenharia + +**D1 — Provisionamento do conteúdo.** +Opções: A) criar só no admin; B) migration de dados; C) seed dedicado versionado, dry-run e create-if-missing. +Recomendação: C — idempotente por `id`, auditável, testável em worktree/staging; dry-run default, `--apply`, guards `PETITION_SEED_CONFIRM=1` + `assertEnvironmentDatabaseTarget`, npm script `seed:petition-pl-5594`; se existir, reporta `_status`/`enabled` e não sobrescreve (admin vira dono). +Rejeitadas: A porque não é repetível nem revisável no repo; B porque viola a intenção e mistura editorial com schema; upsert/sobrescrita porque apagaria ajuste feito no admin. + +**D2 — Copy do bloco do formulário.** +Opções: A) novos campos no schema; B) editar as 3 strings hardcoded; C) manter. +Recomendação: B — headline "Sua assinatura fortalece esta mobilização."; parágrafo "Os dados serão usados para registrar sua participação e a comunicação de conteúdos relacionados."; eyebrow "Participe da mobilização" intacto. Afeta as 3 petições existentes, autorizado na intenção; registrar no changelog. +Rejeitadas: A porque exige migration/field para copy e não há demanda de admin; C porque "campanha"/"trabalhadores" contradizem os literais de produto. + +**D3 — Corpo em 5 blocos.** +Opções: A) Lexical JSON à mão; B) HTML versionado convertido por `convertHTMLToLexical`; C) texto corrido. +Recomendação: B — HTML legível/revisável, reusa o padrão de `seed-posts.mjs`; h2 nos 5 blocos (o que é; por que agora; o que o PL não faz; o pedido; links oficiais), listas e links para ficha/inteiro teor. +Rejeitadas: A porque é verboso e frágil de revisar; C porque perde h2/listas/links exigidos. +Fatos: ementa oficial (transparência patrimonial, fiscal e financeira; altera a Lei 9.504/1997); autorização expressa no registro (Pres/VP); 5 exercícios fiscais anteriores de IR; dados bancários/financeiros; registros telefônicos e de conexão/acesso sem o conteúdo das comunicações; acesso exclusivo por autoridades competentes (fiscalização eleitoral, verificação patrimonial, apuração de indícios concretos); auditoria (responsável/finalidade/data/categorias); confidencialidade e vedação de finalidade estranha; conteúdo só com ordem judicial; LGPD (Art. 7); Justiça Eleitoral divulga o autorizado resguardando terceiros (Art. 3 §2º); complementação com contraditório (Art. 5); vigência após adaptação (Art. 8). +Guardrails: motivação 18/10 cita o desafio de Lula a Flávio Bolsonaro e o protocolo de Solla em 19/10 (sem tramitação posterior em 21/10), sem afirmar crime/ilícito/recusa não comprovada; pedido de tramitação/aprovação sem prometer resultado; `form.title`/`form.subtitle` são copy do seed (a intenção não os fixa). + +**D4 — Testes.** +Opções: A) unit do conteúdo + pin da copy; B) acrescentar int; C) e2e novo. +Recomendação: A — invariantes puros (id/título/subtítulo/consent key/5 blocos/fatos/links; ausência de "crime", "ilícito", "corrupção", "desvio", "será aprovado") + pin da copy com whitespace normalizado (JSX quebra linha); fluxo de assinatura intocado. +Rejeitadas: B porque o write path/transação não muda e o int existente já cobre; C porque não há interação nova, a suíte não tem e2e de petição e o custo/flakiness não se paga. + +**D5 — Aplicação em produção e cache.** +Opções: A) deploy aplica o seed; B) passo manual pós-merge documentado; C) não aplicar. +Recomendação: B — no checkout do homeserver (deps de dev, como nos demais seeds), com o env do stack carregado: `TEQO_ENV=production PETITION_SEED_CONFIRM=1 pnpm seed:petition-pl-5594 --apply`; registrar no changelog/PR. +Rejeitadas: A porque deploy não deve publicar conteúdo editorial e o guard exige intenção explícita; C porque o aceite exige a página em produção. +Limitação: o seed roda fora do processo do Next e não busta `document_petition:`; `findByID` lança 404 (não há entrada negativa no `unstable_cache`), mas uma visita anterior ao slug pode ter cacheado a resposta 404 na rota. Após o seed, reiniciar/redeployar o container (ou salvar a petição uma vez no admin) antes de divulgar; sem endpoint novo. + +**D6 — Escopo negativo.** +Opções: A) incluir agora; B) cortar e registrar. +Recomendação: B — e2e novo, CTA home/footer (S47), OG nova, consent novo e conserto do escape do JSON-LD ficam fora; a dívida do JSON-LD (`page.tsx:150-153`) vai para o changelog/issue. +Rejeitadas: A porque estoura o appetite e mistura concerns. + +## Fases verificáveis + +1. **Fase 1 — conteúdo + seed (tracer, ~0,25 dia):** content module + seed + npm script; dry-run no worktree resolve consent e mostra o plano; `--apply` cria `pl-5594-2026` `enabled`/`published`; segunda execução é no-op. +2. **Fase 2 — copy (UI, ~0,05 dia):** editar as 3 strings; nenhum componente novo; conferir `PetitionForm` data-driven. +3. **Fase 3 — testes (~0,1 dia):** unit novo e pin da copy; rodar os dois specs. +4. **Fase 4 — verificação local (~0,05 dia):** com o DB do worktree, `GET /abaixo-assinado/pl-5594-2026` → 200; conferir título, 5 h2, links oficiais, contador e consent; uma assinatura manual de fumaça com dados fictícios (sem teste commitado). Sem e2e novo: o fluxo não mudou e a suíte não tem precedente de petição. +5. **Fase 5 — gates e entrega (~0,05 dia):** `pnpm gate:fast`, changelog, PR com o passo de produção; `pnpm push`. + +## Rabbit holes / Não escopo (engenharia) + +- Não editar schema/collection/Consent; não criar migration; não "consertar" o SEO existente nem o escape do JSON-LD neste PR. +- Não transformar o seed em upsert, importador genérico de petições ou runner de deploy. +- Não criar e2e/listing/meta de assinaturas/ranking; não adicionar imagem OG ou CTA na home (S47). +- Não duplicar leitura de petição: reusar `getCachedDocumentById`, `getSignatureCount`, `getConsentByKey`. + +## Débitos da sessão (triage) + +- **Registrado:** escape de `<` no JSON-LD da petição — Issue #1447 (`S48-FOLLOWUP-JSONLD`, plano `docs/plans/json-ld-escape-peticao-impl.md`). +- **Deferido com gatilho:** int test do write path do seed (HTML→Lexical + `_status`/`enabled`/`form.consent`) — reabrir na próxima edição de conteúdo/seed ou upgrade do Payload no `create`; hoje os invariantes do conteúdo estão em unit e o create foi provado em DB temporário. +- **Descartado:** rename do npm script (`seed:face-*` é precedente) e reparo gramatical de literal fixado pela intenção. + +## Riscos e mitigação + +- **`id` é URL pública e PK:** definido uma vez no seed; create-if-missing nunca altera; revisão de conteúdo não mexe no id. +- **Consent ausente/errado:** resolução por key fail-closed; sem id hardcoded; dry-run mostra o alvo antes de escrever. +- **Seed rodado no ambiente errado:** `assertWriteConfirm` (`PETITION_SEED_CONFIRM`) + `assertEnvironmentDatabaseTarget` (`TEQO_ENV` casando `teqo_1313`/`teqo_staging`); write só com `--apply`. +- **Cache negativo do 404:** documentado em D5; operador reinicia/redeploya ou salva no admin após o seed. +- **Copy afeta 3 petições:** mudança intencional e autorizada; changelog registra; rollback é reverter o commit. +- **Divergência editorial pós-seed:** o seed não sobrescreve; o admin é dono; testes pinam o conteúdo versionado, não o DB. + +## Aceite de engenharia + +- [ ] Petição `pl-5594-2026` criada `enabled`/`published`, consent `whatsapp-inscricao` resolvido por key, sem migration/collection nova. +- [ ] Corpo com 5 blocos e fatos oficiais; sem acusação de crime e sem promessa de aprovação. +- [ ] Copy nova na page; eyebrow intacto; `PetitionForm` continua data-driven. +- [ ] Seed dry-run por padrão, create-if-missing, guards e npm script; segunda execução no-op. +- [ ] Testes de domínio: unit novo/atualizado verde; sem int porque o write path não muda; `pnpm gate:fast` limpo. +- [ ] Verificação local: página 200, corpo/consent/contador renderizados, assinatura de fumaça OK. +- [ ] Changelog + passo de seed em produção documentados no PR. +- [ ] Invariantes AGENTS/engineering-standards: sem PII em log; access/LGPD inalterados; sem migration. +- [ ] Aceite de produto da intenção ainda coberto. + +## Self-score decision-quality + +5/5 — todas as decisões caras têm rejeitadas (D1–D6); appetite herdado sem corte; rabbit holes nomeados; depth check reusa `getConsentByKey`, `getCachedDocumentById`, `getSignatureCount`, `seed-loader` e o padrão de `seed-posts`; o outcome de produto permanece intacto. diff --git a/package.json b/package.json index 1532259e..9c290839 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "db:seed:posts": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-posts.mjs", "db:seed:tse": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-tse-results.mjs", "db:seed:consent": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-consent.mjs", + "seed:petition-pl-5594": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-petition-pl-5594.mjs", "db:seed:projecao": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/import-projecao.mjs", "media:recover": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/recover-media.mjs", "reels:ingest": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/reels-ingest.mjs", diff --git a/scripts/lib/petitionPl5594Content.mjs b/scripts/lib/petitionPl5594Content.mjs new file mode 100644 index 00000000..b776ff2e --- /dev/null +++ b/scripts/lib/petitionPl5594Content.mjs @@ -0,0 +1,93 @@ +/** + * S48 — the PL 5594/2026 petition (transparency of presidential candidates). + * + * The petition is editorial content, but production only carries the three + * legacy petitions restored from the old platform: there is no admin-free path + * to reproduce it in a worktree, staging or a fresh database. This module + * versions the content as the single source the seed writes and the unit + * invariants check (`tests/unit/petitionPl5594Content.unit.spec.ts`). + * + * Facts come from the official bill text (Câmara, codteor=3184912) and the + * product plan `docs/plans/abaixo-assinado-pl-5594-2026.md`. Guardrails: the + * motivation mentions the 18/10/2026 debate without accusing anyone of a + * crime, and the request asks for support without promising approval. + * + * The body is HTML (readable/reviewable) and the seed converts it to Lexical + * with the project editor config, the same round-trip as `seed-posts.mjs`. + */ +import { WHATSAPP_SUBSCRIPTION_CONSENT_KEY } from '../../src/lib/campaignConsentKeys.ts' + +export const PETITION_ID = 'pl-5594-2026' + +export const PETITION_TITLE = 'Quem quer ser presidente deve abrir os sigilos' + +export const PETITION_SUBTITLE = + 'Apoie o PL 5594/2026: candidatos à Presidência e à Vice-Presidência devem autorizar o acesso aos sigilos bancário, fiscal, telefônico e de internet.' + +export const PETITION_FORM_TITLE = 'Assine o abaixo-assinado' + +export const PETITION_FORM_SUBTITLE = 'Preencha seus dados para apoiar o PL 5594/2026.' + +/** + * The existing petition Consent (`form.consent` of the three production + * petitions). Resolved by stable key — never by a hardcoded id; the seed is + * fail-closed when the key is missing. + */ +export const PETITION_CONSENT_KEY = WHATSAPP_SUBSCRIPTION_CONSENT_KEY + +export const PETITION_BODY_SECTIONS = [ + { + heading: 'O que o PL 5594/2026 propõe', + html: [ + '

O projeto de lei do deputado Jorge Solla (PT/BA), apresentado em 19/10/2026, torna a transparência uma condição do registro da candidatura à Presidência e à Vice-Presidência da República. Quem disputa o cargo passa a apresentar, no pedido de registro, uma declaração expressa, individual, específica e informada de autorização de acesso a:

', + '', + '

A ementa oficial resume: o PL dispõe sobre a transparência patrimonial, fiscal e financeira dos candidatos à Presidência da República, estabelece mecanismos de autorização de acesso a informações protegidas por sigilo e altera a Lei nº 9.504, de 30 de setembro de 1997.

', + ].join('\n'), + }, + { + heading: 'Por que isso importa agora', + html: [ + '

Em 18/10/2026, durante o debate eleitoral, Lula desafiou Flávio Bolsonaro a abrir seus sigilos. No dia seguinte, Solla protocolou o PL 5594/2026 na Câmara dos Deputados. O projeto não trata de um caso isolado: cria uma regra geral de transparência para quem disputa a Presidência.

', + '

Hoje o eleitor recebe declarações de bens, mas não tem instrumentos equivalentes para verificar a coerência entre patrimônio, rendimentos e movimentação financeira dos candidatos. O PL busca fechar essa lacuna com critérios objetivos e fiscalização por órgãos competentes.

', + ].join('\n'), + }, + { + heading: 'O que o PL não faz', + html: [ + '', + ].join('\n'), + }, + { + heading: 'O pedido', + html: [ + '

Assine para pedir que a Câmara dos Deputados dê tramitação e aprove o PL 5594/2026. A transparência não substitui a apuração de responsabilidades nem presume irregularidade: ela dá ao eleitor e aos órgãos de fiscalização os meios para verificar.

', + '

Quem quer governar o país deve aceitar o mesmo padrão de transparência que cobra dos outros. Assine e compartilhe.

', + ].join('\n'), + }, + { + heading: 'Texto oficial e tramitação', + html: [ + '

Leia a íntegra e acompanhe a tramitação na Câmara dos Deputados:

', + '', + '

Status em 21/10/2026: o projeto foi apresentado à Mesa da Câmara dos Deputados; não havia tramitação posterior registrada nesta data.

', + ].join('\n'), + }, +] + +export const PETITION_BODY_HTML = PETITION_BODY_SECTIONS.map( + (section) => `

${section.heading}

\n${section.html}`, +).join('\n') diff --git a/scripts/lib/test-affected-core.mjs b/scripts/lib/test-affected-core.mjs index 16e3e166..a1a19bbb 100644 --- a/scripts/lib/test-affected-core.mjs +++ b/scripts/lib/test-affected-core.mjs @@ -102,6 +102,7 @@ export const SCRIPTS_SPEC_PINNED = [ 'scripts/lib/mediaBinaries.mjs', 'scripts/lib/mediaEndpoint.mjs', 'scripts/lib/opsGlobal.mjs', + 'scripts/lib/petitionPl5594Content.mjs', 'scripts/lib/plansOnlyClosesGuard.mjs', 'scripts/lib/playwright-e2e-args.mjs', 'scripts/lib/portalTransparenciaEmendas.mjs', diff --git a/scripts/seed-petition-pl-5594.mjs b/scripts/seed-petition-pl-5594.mjs new file mode 100644 index 00000000..b0f3cf69 --- /dev/null +++ b/scripts/seed-petition-pl-5594.mjs @@ -0,0 +1,203 @@ +/** + * S48 — provisions the PL 5594/2026 petition (`pl-5594-2026`). + * + * The petition is editorial content: production historically only had the three + * petitions restored from the old platform (edited in the admin), so a fresh + * database has no reproducible path to the new one. This seed writes the content + * versioned in `scripts/lib/petitionPl5594Content.mjs`. + * + * Contract: + * - dry-run (default): resolves the target/Consent and reports the plan; no write + * - `--apply`: creates the petition only when the id is missing. An existing row + * is never overwritten — after the first write the admin owns the content. + * - the Consent is the existing `whatsapp-inscricao` document, resolved by + * stable key (`getConsentByKey`), fail-closed when absent. + * - `--apply` outside a provably local dev database requires + * `PETITION_SEED_CONFIRM=1` and a `TEQO_ENV` matching the exact database + * (staging → `teqo_staging`, production → `teqo_1313`). + * + * After a production seed the page may still serve a cached 404 until the server + * restarts/redeploys (or the petition is saved once in the admin): the seed runs + * outside the Next runtime and cannot bust `document_petition:`. + * + * Usage: + * pnpm seed:petition-pl-5594 + * pnpm seed:petition-pl-5594 --apply + * TEQO_ENV=production PETITION_SEED_CONFIRM=1 pnpm seed:petition-pl-5594 --apply + */ +import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical' +import { JSDOM } from 'jsdom' +import { getPayload } from 'payload' + +import { + PETITION_BODY_HTML, + PETITION_BODY_SECTIONS, + PETITION_CONSENT_KEY, + PETITION_FORM_SUBTITLE, + PETITION_FORM_TITLE, + PETITION_ID, + PETITION_SUBTITLE, + PETITION_TITLE, +} from './lib/petitionPl5594Content.mjs' + +import { + assertEnvironmentDatabaseTarget, + assertWriteConfirm, + databaseName, + databaseTarget, + dieWithLabel, + loadCliEnv, + requiresWriteConfirm, + TEQO_ENV_DATABASE_BY_ENV, +} from './lib/cli.mjs' + +loadCliEnv() + +const die = dieWithLabel('seed:petition-pl-5594') +const WRITE_CONFIRM_FLAG = 'PETITION_SEED_CONFIRM' + +const HELP = ` +Uso: pnpm seed:petition-pl-5594 [--apply] + +Cria a petição "${PETITION_ID}" (abaixo-assinado do PL 5594/2026), ativa e +publicada, com o Consent existente "${PETITION_CONSENT_KEY}" resolvido por chave. + +Modos: + (sem flag) plano/dry-run — alvo, Consent e estado atual; sem escrita + --apply cria a petição se o id não existir (nunca sobrescreve) + +Ambiente: + DATABASE_URL alvo (obrigatório em todos os modos) + +--apply em alvo não-local/produção exige ${WRITE_CONFIRM_FLAG}=1 e TEQO_ENV +casando o banco exato (staging=teqo_staging | production=teqo_1313). +` + +/** + * `draft: true` is what makes the lookup honest: a draft-only row with the same + * id (the admin create view generates one during render) would otherwise be + * invisible and the create would die on a duplicate key instead of no-op. + */ +const findPetition = async (payload, id) => { + const result = await payload.find({ + collection: 'petition', + where: { id: { equals: id } }, + draft: true, + limit: 1, + depth: 0, + pagination: false, + }) + return result.docs[0] ?? null +} + +async function main() { + const argv = process.argv.slice(2) + if (argv.includes('--help') || argv.includes('-h')) { + console.log(HELP) + process.exit(0) + } + const unknown = argv.find((arg) => arg !== '--apply') + if (unknown) + die(`argumento desconhecido: ${unknown} — uso: pnpm seed:petition-pl-5594 [--apply].`) + + const apply = argv.includes('--apply') + if (!process.env.DATABASE_URL) die('DATABASE_URL não definida; recusando continuar.') + + console.log( + `[seed:petition-pl-5594] alvo: ${databaseTarget()} | modo: ${apply ? 'apply' : 'plan'}`, + ) + + if (apply) { + assertWriteConfirm({ + label: 'seed:petition-pl-5594', + flag: WRITE_CONFIRM_FLAG, + command: 'pnpm seed:petition-pl-5594 --apply', + }) + // A worktree dev database is provably local and follows the write-confirm + // policy (no flag). But the homeserver proxy rewrites the host to localhost, + // so a local host carrying a declared target name (teqo_1313/teqo_staging) + // must still declare the exact environment — the name is the discriminator. + const targetName = databaseName(process.env.DATABASE_URL) + const declaredTargets = Object.values(TEQO_ENV_DATABASE_BY_ENV) + if (requiresWriteConfirm() || declaredTargets.includes(targetName)) { + assertEnvironmentDatabaseTarget() + } + } + + const config = (await import('../src/payload.config.ts')).default + const payload = await getPayload({ config }) + + const existing = await findPetition(payload, PETITION_ID) + if (existing) { + console.log( + `[seed:petition-pl-5594] ${PETITION_ID}: existe — enabled=${Boolean(existing.enabled)} ` + + `_status=${existing._status ?? '(sem drafts)'}; não sobrescreve (o admin é dono).`, + ) + if (!existing.enabled || existing._status !== 'published') { + console.log( + '[seed:petition-pl-5594] atenção: a petição existe mas não está ativa/publicada; ' + + 'ajuste no admin (o seed nunca sobrescreve).', + ) + } + process.exit(0) + } + + const { getConsentByKey } = await import('../src/utilities/campaignConsent.ts') + const consent = await getConsentByKey(payload, PETITION_CONSENT_KEY) + if (!consent) { + die( + `Consent "${PETITION_CONSENT_KEY}" ausente — a petição não pode ser criada sem ele ` + + '(fail-closed). Rode `pnpm db:seed:minimal` no ambiente local ou confira o Consent ' + + 'de produção antes de aplicar.', + ) + } + console.log( + `[seed:petition-pl-5594] Consent "${PETITION_CONSENT_KEY}": #${consent.id} (resolvido por chave)`, + ) + + console.log( + `[seed:petition-pl-5594] ${PETITION_ID}: ausente | ${PETITION_BODY_SECTIONS.length} bloco(s) de corpo | ` + + `título "${PETITION_TITLE}"`, + ) + + if (!apply) { + console.log('\n[seed:petition-pl-5594] dry-run — use --apply para criar a petição.') + process.exit(0) + } + + const editorConfig = await editorConfigFactory.default({ config: payload.config }) + const body = convertHTMLToLexical({ editorConfig, html: PETITION_BODY_HTML, JSDOM }) + const created = await payload.create({ + collection: 'petition', + data: { + id: PETITION_ID, + title: PETITION_TITLE, + subtitle: PETITION_SUBTITLE, + enabled: true, + body, + form: { + title: PETITION_FORM_TITLE, + subtitle: PETITION_FORM_SUBTITLE, + consent: consent.id, + }, + _status: 'published', + }, + depth: 0, + // Operator CLI: the create must write the petition even though the + // collection's create access is admin-only (same intent as the seeds). + overrideAccess: true, + }) + console.log( + `[seed:petition-pl-5594] ${PETITION_ID}: criada (#${created.id}) ` + + `enabled=${Boolean(created.enabled)} _status=${created._status ?? '(sem drafts)'}.`, + ) + console.log( + '[seed:petition-pl-5594] Em produção, reinicie/redeploye o container (ou salve a petição uma vez ' + + 'no admin) antes de divulgar: o seed não busta o cache document_petition da rota.', + ) + process.exit(0) +} + +main().catch((error) => { + die(error?.message || String(error)) +}) diff --git a/src/app/(frontend)/abaixo-assinado/[id]/page.tsx b/src/app/(frontend)/abaixo-assinado/[id]/page.tsx index 73c55872..29846d57 100644 --- a/src/app/(frontend)/abaixo-assinado/[id]/page.tsx +++ b/src/app/(frontend)/abaixo-assinado/[id]/page.tsx @@ -204,12 +204,11 @@ export default async function Page({ params }: { params: Promise<{ id: string }> Participe da mobilização

- Sua assinatura fortalece a luta dos trabalhadores. + Sua assinatura fortalece esta mobilização.

Preencha o formulário para somar apoio público à proposta. Os dados serão usados - apenas para registrar sua participação nesta campanha, e comunicação de conteúdos - relacionados. + apenas para registrar sua participação e a comunicação de conteúdos relacionados.

{ expect(formSource).not.toContain('convertLexicalToHTML') }) + it('uses the generic mobilization copy instead of campaign/trabalhadores', () => { + const normalizedPage = pageSource.replace(/\s+/g, ' ') + + expect(normalizedPage).toContain('Sua assinatura fortalece esta mobilização.') + expect(normalizedPage).toContain( + 'Os dados serão usados apenas para registrar sua participação e a comunicação de conteúdos relacionados.', + ) + expect(normalizedPage).not.toContain('fortalece a luta dos trabalhadores') + expect(normalizedPage).not.toContain('participação nesta campanha') + }) + it('uses semantic field color variables for shared form controls', () => { for (const source of [inputSource, textareaSource, nativeSelectSource]) { expect(source).toContain('--field-background') diff --git a/tests/unit/petitionPl5594Content.unit.spec.ts b/tests/unit/petitionPl5594Content.unit.spec.ts new file mode 100644 index 00000000..75c9bc85 --- /dev/null +++ b/tests/unit/petitionPl5594Content.unit.spec.ts @@ -0,0 +1,83 @@ +// @vitest-environment node + +import { describe, expect, it } from 'vitest' + +import { + PETITION_BODY_HTML, + PETITION_BODY_SECTIONS, + PETITION_CONSENT_KEY, + PETITION_FORM_SUBTITLE, + PETITION_FORM_TITLE, + PETITION_ID, + PETITION_SUBTITLE, + PETITION_TITLE, +} from '../../scripts/lib/petitionPl5594Content.mjs' + +// S48 — the PL 5594/2026 petition content is editorial but versioned: the seed +// writes it to a fresh/staging/production database, so these invariants are the +// guard against a silent editorial drift. + +const plainText = PETITION_BODY_HTML.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ') + +describe('petitionPl5594Content', () => { + it('pins the public identity (id is the URL and the signature FK)', () => { + expect(PETITION_ID).toBe('pl-5594-2026') + expect(PETITION_TITLE).toBe('Quem quer ser presidente deve abrir os sigilos') + expect(PETITION_SUBTITLE).toBe( + 'Apoie o PL 5594/2026: candidatos à Presidência e à Vice-Presidência devem autorizar o acesso aos sigilos bancário, fiscal, telefônico e de internet.', + ) + expect(PETITION_FORM_TITLE).toBe('Assine o abaixo-assinado') + expect(PETITION_FORM_SUBTITLE).toBe('Preencha seus dados para apoiar o PL 5594/2026.') + }) + + it('reuses the existing petition Consent by stable key, never by id', () => { + expect(PETITION_CONSENT_KEY).toBe('whatsapp-inscricao') + }) + + it('has the five planned sections with an h2 each', () => { + expect(PETITION_BODY_SECTIONS.map((section) => section.heading)).toEqual([ + 'O que o PL 5594/2026 propõe', + 'Por que isso importa agora', + 'O que o PL não faz', + 'O pedido', + 'Texto oficial e tramitação', + ]) + expect(PETITION_BODY_HTML.match(/

/g)).toHaveLength(5) + for (const section of PETITION_BODY_SECTIONS) { + expect(section.html.trim().length).toBeGreaterThan(0) + } + }) + + it('carries the official facts of the bill', () => { + expect(plainText).toContain('cinco exercícios fiscais anteriores') + expect(plainText).toContain('excluído o conteúdo das comunicações') + expect(plainText).toContain('excluído o conteúdo de mensagens, arquivos') + expect(plainText).toContain('autoridades e órgãos públicos legalmente competentes') + expect(plainText).toContain('sistema de auditoria') + expect(plainText).toContain('proteção de dados pessoais') + expect(plainText).toContain('Justiça Eleitoral') + expect(PETITION_BODY_HTML).toContain( + 'https://www.camara.leg.br/proposicoesWeb/fichadetramitacao?idProposicao=2648344', + ) + expect(PETITION_BODY_HTML).toContain( + 'https://www.camara.leg.br/proposicoesWeb/prop_mostrarintegra?codteor=3184912', + ) + }) + + it('mentions the 18/10 debate without accusing anyone or promising approval', () => { + expect(plainText).toContain('18/10/2026') + expect(plainText).toContain('Lula desafiou Flávio Bolsonaro a abrir seus sigilos') + expect(plainText).toContain('19/10/2026') + + // The law's own access purpose says "indícios concretos de ilícitos"; that + // is a legal competence, not an accusation. The guard is against naming a + // crime/irregularity as fact and against promising the bill's approval. + expect(plainText).not.toMatch(/\b(crime|corrupção|desvio|lavagem)\b/i) + expect(plainText).not.toMatch( + /será aprovad|vai ser aprovad|vamos aprovar|aprovação garantida|garantir a aprovação/i, + ) + expect(plainText).toContain( + 'não substitui a apuração de responsabilidades nem presume irregularidade', + ) + }) +})