mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
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.
152 lines
14 KiB
Markdown
152 lines
14 KiB
Markdown
# Exportar CSV das assinaturas e contatos (admin Payload)
|
||
|
||
Status: entregue 2026-07-23
|
||
Atualizado em: 2026-07-23
|
||
Item do roadmap: [docs/roadmap.md](../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 `/admin` precisa 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 (`consent` richText, locks); sem vazar para quem não é `users` admin.
|
||
|
||
## 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`](https://payloadcms.com/docs/plugins/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 `petition` em assinaturas).
|
||
- **Só export** em `signature` e `contact` (`import: false`); não habilitar o plugin em collections irrelevantes.
|
||
- Guardrails: access só para sessão `users` do admin; migration para collections `exports`/`imports` que o plugin cria; `push: false`; sem novo `Consent` key (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:** custom `admin.components` + route handler CSV (duplica o que o plugin faz); CLI-only (`pnpm` script) — fora do fluxo “botão no admin”; plugin community antigo `madaxen86/payload-plugin-import-export` — preferir o oficial alinhado ao core.
|
||
- **Escopo v1 = `signature` + `contact`, com `import: false` e `export.format: 'csv'`.** Import de assinaturas reinventaria opt-in/Consent e risco de duplicata Contact. `contact` exporta campos planos; `signature` achata relationships via `toCSV`. **Rejeitado:** plugin em todas as collections; import habilitado “por se”; JSON como formato default (CSV é o pedido operacional).
|
||
- **Achatamento de `contact`/`petition` em `signature` via `toCSV` do plugin** (relationship → colunas `contact_name`, `contact_email`, `contact_phone`, …). **Rejeitado:** exportar só IDs de relacionamento (inútil para ESP); denormalizar PII na row `signature` (migration cara, duplicação).
|
||
- **v1 síncrono (`disableJobsQueue: true`)** — o projeto ainda não tem `jobs.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`, hooks `beforeExport`); 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.0` instalado sem bump — peer dep `payload@3.82.0` confirmado 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`/`imports` no sidebar?** **Opções:** ocultas (default do plugin) | grupo admin “Abaixo-assinados” / “Ferramentas”. **Recomendação:** manter ocultas no v1 (`disableSave: true` se só download direto); abrir sidebar só se operação pedir histórico de arquivos salvos.
|
||
|
||
## Abordagem proposta
|
||
|
||
```mermaid
|
||
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:
|
||
|
||
- **`importExportPlugin`** em `src/payload.config.ts`: `adminCsvExportCollection('signature' | 'contact')` com `export: { format: 'csv', disableJobsQueue: true, disableSave: true }`, `import: false`. Access nas collections geradas via `overrideExportCollection` restrito a `users` autenticados (espelhar padrão admin).
|
||
- **Hooks de campo / collection** em `src/collections/Signature.ts` (ou helper `src/utilities/signatureExport.ts` se o arquivo da collection ficar gordo): `custom['plugin-import-export'].hooks.beforeExport` no relationship `contact` (e, se útil, `petition`) para colunas planas; `consent` com `disabled: true` no custom do plugin (não exportar texto jurídico versionado).
|
||
- **Depth / populate:** garantir que o export carregue `contact` e `petition` populados 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) e `pnpm migrate` local. Sem alterar schema de `signature`/`contact`.
|
||
- **Testes:** unit do flatten (dado `contact` populado → 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 admin `users` já 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](pixel-meta-abaixo-assinado.md)).
|
||
- Jobs queue / cron / save histórico em `exports` com blob (adiado abaixo).
|
||
- UI em `/campanha` para assinaturas públicas.
|
||
|
||
## Rabbit holes
|
||
|
||
- **Habilitar `import: true` “só para reprocessar”.** Explode Consent, dedup de telefone e joins. **Mitigação:** `import: false` travado neste plano.
|
||
- **Plugin em todas as collections “já que instalou”.** Expõe PII de campanha sem pedido. **Mitigação:** whitelist só `signature` e `contact`.
|
||
- **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 em `users`” do Admin Payload for implementado.
|
||
- **Export de `subscription` (base WhatsApp/site).** Revisitar quando: operação pedir lista transversal fora de petição específica.
|
||
- **`defaultPopulate` nas collections relacionadas (`contact`, `petition`, `consent`).** Revisitar quando: export síncrono ficar lento ou estourar timeout — evitar carregar `petition.body` / `consent.text` no `depth:1` default do plugin.
|
||
- **`contactFromRelationship` compartilhado com `supporterViewModels`.** Revisitar quando: 3º call site de “contact populado de relationship”.
|
||
- **Constante `importExportConsentDisabled` para fields `consent`.** Revisitar quando: export de `subscription` (2º campo `consent` com `disabled: true`).
|
||
|
||
## Já resolvido no simplify (não reabrir)
|
||
|
||
- Consolidar flatten em `signatureContactToCSV` / `signaturePetitionToCSV` (sem wrappers `flatten*` exportados).
|
||
- Testes via `runToCSV` nos handlers `toCSV` de produção.
|
||
- Nullable consistente (`?? ''`) nos campos de contato do export de assinaturas.
|
||
- `withImportExportAdminAccess` + `importExportAdminOnly` (access DRY nas collections `exports`/`imports`).
|
||
- `adminCsvExportCollection()` (config DRY entre `signature` e `contact`).
|
||
- `isPopulatedRelationship<Contact>` direto (sem `isPopulatedContact` redundante).
|
||
|
||
## Explicitamente fora (triage pós-entrega 2026-07-23)
|
||
|
||
- **`export.limit` no plugin** — sem evidência de abuso em v1; reabrir com profiling.
|
||
- **Populate de `consent` no depth do plugin** — excluído do CSV; custo marginal do batch do plugin.
|
||
- **Factory genérica `plugin-import-export` nos 3 fields de `Signature`** — YAGNI (1 collection).
|
||
- **Módulo `importExportPlugin.ts` separado do `payload.config`** — organizacional; parcialmente coberto por `adminCsvExportCollection`.
|
||
- **Variante `isPopulatedRelationship` para id string (`Petition`)** — 1 consumidor; guard local em `signatureExport.ts` suficiente.
|
||
- **Colunas `contact_*` compartilhadas entre export de `contact` e `signature`** — schemas intencionalmente diferentes (`name` vs `contact_name`).
|
||
- **Usar `canManageContacts` no access do plugin** — semântica errada; manteve `isPayloadAdmin`.
|
||
|
||
## Referências
|
||
|
||
- `docs/roadmap.md` (Admin Payload; Site público — abaixo-assinados)
|
||
- [Payload Import Export Plugin](https://payloadcms.com/docs/plugins/import-export) — config `export`/`import`, `disableJobsQueue`, `beforeExport`
|
||
- `src/collections/Signature.ts` — collection alvo
|
||
- `src/collections/Contact.ts` — campos a achatar
|
||
- `src/app/(frontend)/actions/submitPetitionSignature.ts` — origem dos dados (Contact + Signature + Subscription na mesma TX)
|
||
- `src/payload.config.ts` — registro do plugin
|
||
- `docs/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
|