Skip to content

feat(enterprise-import): reconcile de emails em sessão persistida#19

Open
daniloleonecarneiro wants to merge 2 commits into
etusdigital:mainfrom
daniloleonecarneiro:feat/email-reconcile-session
Open

feat(enterprise-import): reconcile de emails em sessão persistida#19
daniloleonecarneiro wants to merge 2 commits into
etusdigital:mainfrom
daniloleonecarneiro:feat/email-reconcile-session

Conversation

@daniloleonecarneiro

@daniloleonecarneiro daniloleonecarneiro commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Reescreve o fluxo super-admin de conciliação de emails mascarados (import Enterprise) para operar em lotes sobre uma sessão server-side, viável para exports grandes (centenas de milhares de contatos, com milhares de colisões de máscara):

  • Sessão persistida (email_reconcile_sessions/_items, migration 1783000000000): CSV parseado e matched uma vez; depois tudo é incremental — apply-auto em chunks, fila de ambíguos paginada, resolve em lote (best-name / skip-remaining), progresso derivado dos status.
  • Match em camadas por created_at: o import preserva o timestamp da origem e o export CSV carrega o mesmo campo, então (máscara, created_at) age como chave natural. Tier de minuto exato (segundos/ms ignorados, tolerância a offset fixo de timezone) > mesma data > desempate por similaridade de nome (Jaccard, insensível a pontuação/ordem).
  • Colunas obrigatórias validadas nos headers antes de processar (email, created_at e sinal de nome — coluna name ou par first_name/last_name); formulário de upload trava as obrigatórias e permite ignorar opcionais.
  • Email como chave única por conta em todo o fluxo: dedup de auto-picks disputando a mesma linha, pré-checagem amigável no apply (em vez do erro cru do índice único), candidatos já usados desabilitados na UI e estratégias automáticas nunca consomem email já aplicado (item fica pendente para decisão manual).
  • Visibilidade: tabela "itens da sessão" (quem casou com o quê) com busca por nome/email e filtros de tipo/status; busca também na fila de ambíguos; badges de concordância de data; toggle de seleção.
  • Cadeia de limites de upload alinhada: nginx client_max_body_size 64m + proxy_read_timeout 300s no /api, body-parser 64mb escopado em /imports, Joi 50MB.
  • Leituras de contato sem máscara de exibição (commit separado): detalhe/listagem retornam o email armazenado — a máscara do email real é idêntica ao placeholder importado, então contatos reconciliados pareciam continuar mascarados e o endereço utilizável não chegava ao fluxo de envios. Supressões seguem mascaradas.

Test plan

  • email-reconcile.service.spec.ts: 26 testes cobrindo colunas obrigatórias (ambas as formas de nome), desambiguação por created_at (minuto/data/timezone), CSV parcial vs colisões de máscara, dedup de linha disputada, guarda de email em uso e o apply legado.
  • Validado e2e no stack docker vivo com um export real: sessão criada, match automático correto por minuto, apply com re-derivação de hashed_email (listener @BeforeUpdate), auditoria registrada, busca e tabela de itens exercitadas contra a sessão viva.
  • tsc de backend e frontend nos baselines; locales pt-BR/en-US/es-ES válidos.

@daniloleonecarneiro daniloleonecarneiro force-pushed the feat/email-reconcile-session branch from a4cc4bc to f9fa3ab Compare July 13, 2026 15:49
… match por created_at

Reescreve o fluxo super-admin de conciliação de emails mascarados
(export Enterprise) para operar em lotes sobre uma sessão server-side,
viável para bases reais (350k contatos, CSV 20MB, dezenas de milhares
de colisões de máscara):

- Sessão persistida (email_reconcile_sessions/_items): CSV parseado e
  matched UMA vez; depois tudo é incremental — apply-auto em chunks,
  fila de ambíguos paginada com busca, resolve em lote (best-name /
  skip-remaining) e progresso derivado dos status dos itens.
- Match em camadas: created_at como sinal forte (tier minuto exato
  tolerando offset fixo de timezone, tier mesma data) antes do
  desempate por similaridade de nome (Jaccard, pontuação/ordem
  insensível); import preserva o created_at da origem, então o par
  (máscara, created_at) funciona como chave natural.
- Colunas obrigatórias (email, created_at e sinal de nome — coluna
  name OU par first_name/last_name) validadas nos headers antes de
  processar; formulário de upload trava as obrigatórias e permite
  ignorar opcionais.
- Email tratado como chave única por conta em todo o fluxo: dedup de
  auto-picks disputando a mesma linha, pré-checagem amigável no apply
  (em vez do erro cru do índice único), candidatos já usados
  desabilitados na UI e estratégias automáticas nunca consomem email
  já aplicado (item permanece pendente para decisão manual).
- Tabela "itens da sessão" (quem casou com o quê) com busca por
  nome/email e filtros de tipo/status; badges de concordância de data
  nos candidatos; toggle de seleção nos ambíguos.
- Cadeia de limites de upload alinhada (nginx 64m/300s no /api,
  body-parser 64mb escopado em /imports, Joi 50MB) para o CSV viajar
  embutido no JSON.
…scarar

As rotas de leitura (detalhe por id/uuid, listagem paginada e findAll)
reescreviam o email com maskEmail() na resposta. Como a máscara do email
real é idêntica ao placeholder importado do Enterprise, contatos
reconciliados pareciam continuar mascarados — e consumidores da API não
tinham como obter o endereço utilizável para o fluxo de envios.

Agora o email retorna como está no banco: reconciliado = endereço real;
pendente de reconcile = placeholder (que é o valor armazenado mesmo).
O campo derivado maskedEmail segue disponível no payload e a lista de
supressões permanece mascarada.
@daniloleonecarneiro daniloleonecarneiro force-pushed the feat/email-reconcile-session branch from f9fa3ab to fd0e2c6 Compare July 13, 2026 15:58
@daniloleonecarneiro daniloleonecarneiro changed the title feat(enterprise-import): reconcile de emails em sessão persistida com match por created_at feat(enterprise-import): reconcile de emails em sessão persistida Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant