feat(petition): publica a petição do PL 5594/2026

A ferramenta de abaixo-assinado já existia, mas a petição do PL não tinha
caminho reproduzível fora do dump legado: produção só carregava as três
petições antigas editadas no admin. O conteúdo agora é versionado em
scripts/lib/petitionPl5594Content.mjs e provisionado por
pnpm seed:petition-pl-5594 — dry-run por padrão, create-if-missing por id,
Consent whatsapp-inscricao resolvido por chave (fail-closed) e guards de
escrita (PETITION_SEED_CONFIRM + TEQO_ENV casando o banco exato), sem
sobrescrever edição do admin.

O corpo traz os fatos do inteiro teor (autorização no registro, 5
exercícios de IR, sigilos bancário/fiscal/telefônico/de conexão, exclusão
do conteúdo das comunicações, auditoria, LGPD e divulgação pela Justiça
Eleitoral), a motivação de 18-19/10 sem acusar crime e o pedido sem
prometer aprovação. A copy hardcoded do bloco do formulário deixa de falar
em 'campanha'/'luta dos trabalhadores' e vira genérica.

Prova: unit full 475 arquivos/5238 testes verdes, lint/typecheck/format
limpos, seed aplicado e reaplicado em DB temporário (criação + no-op) e
smoke real no navegador (página 200, consent, assinatura nº 1). Sem
migration, collection, Consent novo ou mudança no fluxo de assinatura.
This commit is contained in:
Francisco Solla 2026-10-22 00:22:10 -03:00
parent 8fa6432b7b
commit 5786bc57dc
9 changed files with 521 additions and 3 deletions

View file

@ -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:<id>`). 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`.

View file

@ -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:<id>`; `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.

View file

@ -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",

View file

@ -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: [
'<p>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:</p>',
'<ul>',
'<li>informações bancárias e financeiras — contas, aplicações, investimentos, operações de crédito, empréstimos, financiamentos, participações societárias, ativos e passivos;</li>',
'<li>declarações de Imposto de Renda, rendimentos, patrimônio declarado, ganhos de capital e obrigações tributárias dos <strong>cinco exercícios fiscais anteriores</strong> ao pedido de registro;</li>',
'<li>registros telefônicos — origem, destino, data, horário, duração e faturamento —, <strong>excluído o conteúdo das comunicações</strong>;</li>',
'<li>registros de conexão e de acesso a aplicações de internet — data, horário, endereço IP e demais metadados legalmente disponíveis —, <strong>excluído o conteúdo de mensagens, arquivos, correspondências eletrônicas e comunicações privadas</strong>.</li>',
'</ul>',
'<p>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.</p>',
].join('\n'),
},
{
heading: 'Por que isso importa agora',
html: [
'<p>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.</p>',
'<p>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.</p>',
].join('\n'),
},
{
heading: 'O que o PL não faz',
html: [
'<ul>',
'<li><strong>Não abre o conteúdo das conversas.</strong> Telefonia e internet entram apenas como registros e metadados; o conteúdo de comunicações telefônicas e telemáticas permanece sujeito às garantias constitucionais e só pode ser acessado com ordem judicial quando exigida.</li>',
'<li><strong>Não permite acesso livre.</strong> O acesso fica restrito a autoridades e órgãos públicos legalmente competentes, para fiscalização eleitoral, verificação patrimonial e apuração de indícios concretos de ilícitos, respeitadas as competências e garantias legais.</li>',
'<li><strong>Não autoriza uso para outra finalidade.</strong> Toda consulta é registrada em sistema de auditoria com identificação do responsável, da finalidade, da data e das categorias de informações consultadas; as informações ficam protegidas por dever de confidencialidade.</li>',
'<li><strong>Não expõe dados de terceiros.</strong> A Justiça Eleitoral divulga ao público apenas o que a lei autoriza, resguardando dados pessoais de terceiros, credenciais de acesso e números completos de contas.</li>',
'<li><strong>Não substitui a LGPD.</strong> O tratamento das informações observa a legislação de proteção de dados pessoais e as medidas técnicas e administrativas para prevenir acessos não autorizados, vazamentos e uso indevido.</li>',
'</ul>',
].join('\n'),
},
{
heading: 'O pedido',
html: [
'<p>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.</p>',
'<p>Quem quer governar o país deve aceitar o mesmo padrão de transparência que cobra dos outros. Assine e compartilhe.</p>',
].join('\n'),
},
{
heading: 'Texto oficial e tramitação',
html: [
'<p>Leia a íntegra e acompanhe a tramitação na Câmara dos Deputados:</p>',
'<ul>',
'<li><a href="https://www.camara.leg.br/proposicoesWeb/fichadetramitacao?idProposicao=2648344">Ficha de tramitação do PL 5594/2026</a></li>',
'<li><a href="https://www.camara.leg.br/proposicoesWeb/prop_mostrarintegra?codteor=3184912">Inteiro teor do PL 5594/2026</a></li>',
'</ul>',
'<p>Status em 21/10/2026: o projeto foi apresentado à Mesa da Câmara dos Deputados; não havia tramitação posterior registrada nesta data.</p>',
].join('\n'),
},
]
export const PETITION_BODY_HTML = PETITION_BODY_SECTIONS.map(
(section) => `<h2>${section.heading}</h2>\n${section.html}`,
).join('\n')

View file

@ -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',

View file

@ -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:<id>`.
*
* 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))
})

View file

@ -204,12 +204,11 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
Participe da mobilização
</span>
<h2 className="border-none text-left text-3xl font-bold leading-tight text-foreground sm:text-4xl">
Sua assinatura fortalece a luta dos trabalhadores.
Sua assinatura fortalece esta mobilização.
</h2>
<p className="text-base leading-7 text-muted-foreground">
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.
</p>
<SignatureCounter
petitionId={petition.id}

View file

@ -42,6 +42,17 @@ describe('petition page layout', () => {
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')

View file

@ -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(/<h2>/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',
)
})
})