Enables @payloadcms/plugin-import-export on signature (flattened contact/petition columns) and contact (native fields) so operations can download filtered lists for external ESP/SMS workflows.
14 KiB
Exportar CSV das assinaturas e contatos (admin Payload)
Status: entregue 2026-07-23
Atualizado em: 2026-07-23
Item do roadmap: docs/roadmap.md (Admin Payload — export CSV de signature e contact)
Impeccable: B — encaixe na list view admin via plugin oficial (sem rota /campanha nova)
Appetite: ~0,5–1 dia eng; plugin @3.82.0 alinhado ao core (sem bump) + flatten de contact/petition em signature + migration das collections do plugin
Responsável: —
Revisão 2026-07-23: implementação com @payloadcms/plugin-import-export@3.82.0 (peer dep bate com Payload 3.82 — bump desnecessário). API 3.82 usa toCSV nos campos (não hooks.beforeExport da doc ≥3.85). Migration 20260723_025513_add_import_export_plugin; helpers em src/utilities/signatureExport.ts. Pós-entrega: export CSV também em contact (campos nativos, sem flatten) via adminCsvExportCollection() em payload.config.ts.
Design (Impeccable)
Âncoras: admin Payload nativo (grupo Abaixo-assinados); não Field Desk / data-theme='campaign'. Labels pt-BR nas strings visíveis do plugin quando configuráveis.
Na implementação (implement-roadmap-item): craft compacto → critique → polish (smoke no /admin/collections/signature: filtrar petição → Exportar → CSV abrível). Sem shape longo.
Brief compacto:
- Persona / contexto: operação/marketing no
/adminprecisa da lista de assinantes (e-mail/celular) para disparo externo sobre o abaixo-assinado. - Job principal: baixar um CSV filtrável das assinaturas com PII de contato já achatada, sem sair do Payload.
- Estratégia de cor: Restrained (chrome do admin Payload; sem paleta nova).
- Edit where you see: N/A (só leitura/export; mutação de assinaturas não é o job).
- Anti-goals: import em massa de assinaturas; botão custom fora do plugin; disparo de e-mail/SMS dentro do Teqo; segundo design system.
Dados → decisão → apresentação
- Vou apresentar dados? Sim, superfície neste item (arquivo CSV baixado no admin).
- Decisões desbloqueadas:
- Operação/marketing: “estes assinantes entram no disparo de e-mail/SMS desta petição?”
- Operação: “filtro por abaixo-assinado X antes de exportar — conjunto certo?”
- Forma escolhida: tabela CSV (degrau tabela/lista) — por quê: ferramentas externas de e-mail/SMS consomem CSV/planilha; é o contrato operacional. Rejeitado: dashboard/KPI de assinaturas nesta fatia; chart; export só JSON; página pública de download.
- Profile: categórico + PII textual; granularidade = 1 linha por
signature; tamanho típico = milhares (não centenas de mil); absoluto (lista nominal, não %). - Anti-goals de dado: sem vanity “total de leads” como substituto do CSV; sem incluir campos internos irrelevantes ao disparo (
consentrichText, locks); sem vazar para quem não éusersadmin.
Contexto
Assinaturas de abaixo-assinados vivem na collection signature (src/collections/Signature.ts): join contact + petition + consent (+ comment opcional). O PII útil para disparo (nome, e-mail, celular, UF/cidade) está em contact (src/collections/Contact.ts), não nas colunas da assinatura. Hoje o admin lista relacionamentos por ID/título, sem botão de export.
O time pediu (2026-07-22) um botão no admin Payload para exportar CSV das assinaturas, a ser usado em disparos de e-mail e SMS fora deste app. O usuário suspeitou corretamente de feature nativa: Payload oferece @payloadcms/plugin-import-export (CSV/JSON na list view; saiu de beta no release 3.85). O app está em Payload 3.82.0 — a implementação deve alinhar a versão do plugin ao core (preferir bump ≥3.85 se o pacote estável exigir).
Este app não dispara e-mail/SMS em massa (roadmap: fora de escopo WhatsApp Business / disparo em massa; Res. TSE 23.610). O produto só entrega o arquivo; o canal externo fica com a operação.
Objetivos
- Na list view de Assinaturas (
/admin/collections/signature): controles de export CSV (download direto). - Na list view de Contatos (
/admin/collections/contact): export CSV com campos nativos (name,email,phone,gender,state,city,postalCode,createdAt) — sem flatten. - CSV de assinaturas com colunas achatadas de
contactúteis a ESP/SMS (no mínimo nome, e-mail, telefone; preferir também UF/cidade, id/título da petição,createdAt, comentário). - Respeitar filtros da list view (exportar só o conjunto filtrado — tipicamente um
petitionem assinaturas). - Só export em
signatureecontact(import: false); não habilitar o plugin em collections irrelevantes. - Guardrails: access só para sessão
usersdo admin; migration para collectionsexports/importsque o plugin cria;push: false; sem novoConsentkey (titular já consentiu na assinatura); sem jobs queue nesta fatia se evitável (disableJobsQueue: true).
Decisões travadas
- Usar o plugin oficial
@payloadcms/plugin-import-export, não endpoint/botão caseiro. Já resolve UI admin, preview, CSV, depth de relationships e (opcional) fila. Rejeitado: customadmin.components+ route handler CSV (duplica o que o plugin faz); CLI-only (pnpmscript) — fora do fluxo “botão no admin”; plugin community antigomadaxen86/payload-plugin-import-export— preferir o oficial alinhado ao core. - Escopo v1 =
signature+contact, comimport: falseeexport.format: 'csv'. Import de assinaturas reinventaria opt-in/Consent e risco de duplicata Contact.contactexporta campos planos;signatureachata relationships viatoCSV. Rejeitado: plugin em todas as collections; import habilitado “por se”; JSON como formato default (CSV é o pedido operacional). - Achatamento de
contact/petitionemsignatureviatoCSVdo plugin (relationship → colunascontact_name,contact_email,contact_phone, …). Rejeitado: exportar só IDs de relacionamento (inútil para ESP); denormalizar PII na rowsignature(migration cara, duplicação). - v1 síncrono (
disableJobsQueue: true) — o projeto ainda não temjobs.autoRun/runner; fila deixaria exports em “pending”. Rejeitado: montar cron/jobs só para este botão nesta fatia. Gatilho para fila: volume que estoure timeout de request ou pedido explícito de save-to-exports. - Sem disparo in-app. CSV é handoff para ferramenta externa. Rejeitado: Resend em massa no Teqo; integração Twilio/SMS; “campanha” de e-mail no Payload.
- i18n e naming: identificadores em inglês (
importExportPlugin, hooksbeforeExport); labels admin em pt-BR (“Assinaturas”, “Exportar”).
Questões em aberto
- Bump de Payload 3.82 → ≥3.85 junto com o plugin? Resolvido 2026-07-23:
@payloadcms/plugin-import-export@3.82.0instalado sem bump — peer deppayload@3.82.0confirmado no npm. - Conjunto exato de colunas do CSV? Opções: mínimo (nome/e-mail/telefone) | operacional (mínimo + petition id/title + createdAt + city/state + comment). Recomendação: operacional — um export serve e-mail e SMS sem segundo passo. Confirmar com quem dispara se e-mail vazio deve aparecer como linha (telefone é required no Contact; e-mail é opcional).
- Visibilidade das collections
exports/importsno sidebar? Opções: ocultas (default do plugin) | grupo admin “Abaixo-assinados” / “Ferramentas”. Recomendação: manter ocultas no v1 (disableSave: truese só download direto); abrir sidebar só se operação pedir histórico de arquivos salvos.
Abordagem proposta
flowchart LR
Admin["/admin Assinaturas<br/>filtros petition"]
Plugin["importExportPlugin<br/>signature export CSV"]
Hooks["beforeExport<br/>flatten contact"]
CSV["Download CSV"]
Ext["ESP / SMS externo"]
Admin --> Plugin
Plugin --> Hooks
Hooks --> CSV
CSV --> Ext
Componentes:
importExportPluginemsrc/payload.config.ts:adminCsvExportCollection('signature' | 'contact')comexport: { format: 'csv', disableJobsQueue: true, disableSave: true },import: false. Access nas collections geradas viaoverrideExportCollectionrestrito ausersautenticados (espelhar padrão admin).- Hooks de campo / collection em
src/collections/Signature.ts(ou helpersrc/utilities/signatureExport.tsse o arquivo da collection ficar gordo):custom['plugin-import-export'].hooks.beforeExportno relationshipcontact(e, se útil,petition) para colunas planas;consentcomdisabled: trueno custom do plugin (não exportar texto jurídico versionado). - Depth / populate: garantir que o export carregue
contactepetitionpopulados o suficiente para o flatten (seguir default do plugin; ajustar se IDs crus aparecerem no preview). - Bump de pacotes (se necessário):
payload+@payloadcms/*alinhados;pnpm generate:types/generate:importmap. - Migration: o plugin adiciona collections
exports/imports(uploads) —pnpm migrate:create add_import_export_plugin(nome final na implementação) epnpm migratelocal. Sem alterar schema designature/contact. - Testes: unit do flatten (dado
contactpopulado → colunas esperadas); smoke manual admin (filtro por petição → CSV). Int opcional se houver harness de plugin; não bloquear appetite com e2e Playwright do admin.
Dependências
- Nenhuma dura de outro plano de trilha A/B/C/D.
- Suave: RBAC em
users(bloqueador AGENTS #1) — hoje todo adminusersjá tem acesso pleno; quando houver roles, reaplicar access no override das collections do plugin. - Suave: textos finais de Consent / privacidade (Onda 0) — não bloqueiam o botão; o titular já optou na assinatura. Operação deve usar o CSV só para fins compatíveis com o consentimento coletado (assumido — validar com jurídico se o texto atual cobre comunicação sobre o abaixo-assinado).
Não escopo
- Disparo de e-mail/SMS dentro do Teqo (fora de escopo do roadmap / Res. TSE).
- Export de
subscription,supporter,leadership(itens separados se pedido). - Import CSV de assinaturas / reimport.
- CAPI / Pixel (já em pixel-meta-abaixo-assinado.md).
- Jobs queue / cron / save histórico em
exportscom blob (adiado abaixo). - UI em
/campanhapara assinaturas públicas.
Rabbit holes
- Habilitar
import: true“só para reprocessar”. Explode Consent, dedup de telefone e joins. Mitigação:import: falsetravado neste plano. - Plugin em todas as collections “já que instalou”. Expõe PII de campanha sem pedido. Mitigação: whitelist só
signatureecontact. - Montar
jobs.autoRun+ runner Vercel só para export. Fora do appetite. Mitigação:disableJobsQueue: true; gatilho abaixo. - Planilha tipo CRM no admin (colunas editáveis, bulk). Não é export. Mitigação: CSV download only.
Adiado com gatilho
- Jobs queue + save em
exports(Vercel Blob). Revisitar quando: export síncrono estourar timeout / operação pedir arquivo versionado no admin. - Access fino por role de
users. Revisitar quando: item “RBAC emusers” do Admin Payload for implementado. - Export de
subscription(base WhatsApp/site). Revisitar quando: operação pedir lista transversal fora de petição específica. defaultPopulatenas collections relacionadas (contact,petition,consent). Revisitar quando: export síncrono ficar lento ou estourar timeout — evitar carregarpetition.body/consent.textnodepth:1default do plugin.contactFromRelationshipcompartilhado comsupporterViewModels. Revisitar quando: 3º call site de “contact populado de relationship”.- Constante
importExportConsentDisabledpara fieldsconsent. Revisitar quando: export desubscription(2º campoconsentcomdisabled: true).
Já resolvido no simplify (não reabrir)
- Consolidar flatten em
signatureContactToCSV/signaturePetitionToCSV(sem wrappersflatten*exportados). - Testes via
runToCSVnos handlerstoCSVde produção. - Nullable consistente (
?? '') nos campos de contato do export de assinaturas. withImportExportAdminAccess+importExportAdminOnly(access DRY nas collectionsexports/imports).adminCsvExportCollection()(config DRY entresignatureecontact).isPopulatedRelationship<Contact>direto (semisPopulatedContactredundante).
Explicitamente fora (triage pós-entrega 2026-07-23)
export.limitno plugin — sem evidência de abuso em v1; reabrir com profiling.- Populate de
consentno depth do plugin — excluído do CSV; custo marginal do batch do plugin. - Factory genérica
plugin-import-exportnos 3 fields deSignature— YAGNI (1 collection). - Módulo
importExportPlugin.tsseparado dopayload.config— organizacional; parcialmente coberto poradminCsvExportCollection. - Variante
isPopulatedRelationshippara id string (Petition) — 1 consumidor; guard local emsignatureExport.tssuficiente. - Colunas
contact_*compartilhadas entre export decontactesignature— schemas intencionalmente diferentes (namevscontact_name). - Usar
canManageContactsno access do plugin — semântica errada; manteveisPayloadAdmin.
Referências
docs/roadmap.md(Admin Payload; Site público — abaixo-assinados)- Payload Import Export Plugin — config
export/import,disableJobsQueue,beforeExport src/collections/Signature.ts— collection alvosrc/collections/Contact.ts— campos a achatarsrc/app/(frontend)/actions/submitPetitionSignature.ts— origem dos dados (Contact + Signature + Subscription na mesma TX)src/payload.config.ts— registro do plugindocs/plans/pixel-meta-abaixo-assinado.md— precedente recente da vertical abaixo-assinados no admin- AGENTS.md — naming;
push: false+ migrations; LGPD/Consent (sem nova key); Known Gap #1 RBAC admin