mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
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:
parent
8fa6432b7b
commit
5786bc57dc
9 changed files with 521 additions and 3 deletions
1
docs/changelog/2026-10-22-s48-peticao-pl-5594.md
Normal file
1
docs/changelog/2026-10-22-s48-peticao-pl-5594.md
Normal 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`.
|
||||
126
docs/plans/abaixo-assinado-pl-5594-2026-impl.md
Normal file
126
docs/plans/abaixo-assinado-pl-5594-2026-impl.md
Normal 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.
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
93
scripts/lib/petitionPl5594Content.mjs
Normal file
93
scripts/lib/petitionPl5594Content.mjs
Normal 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')
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
203
scripts/seed-petition-pl-5594.mjs
Normal file
203
scripts/seed-petition-pl-5594.mjs
Normal 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))
|
||||
})
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
83
tests/unit/petitionPl5594Content.unit.spec.ts
Normal file
83
tests/unit/petitionPl5594Content.unit.spec.ts
Normal 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',
|
||||
)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue