Pular para o conteúdo principal

Boletim Diário — Header do Email e Invariante de grupoId

O email do Boletim Diário de Emplacamento abre com um bloco de saudação personalizado por destinatário (TEC-369), renderizado acima do banner e das tabelas:

  • Título: Olá, <NOME_DO_DESTINATÁRIO>
  • Descrição: "Você acaba de receber o relatório com os dados resumidos do Emplacamento <Associação>. Para saber mais informações acesse aqui." — com "aqui" apontando para a página do relatório no site.

Esta página documenta como o header é gerado e injetado no pipeline do cron, e a invariante de IDs que faz a página de destino do link funcionar.

Geração do header

O HTML do email é montado por template de string puro (sem React) em apps/web/app/[locale]/home/[account]/reports/daily-report/_lib/server/generate-email-html.ts, respeitando as restrições de clientes de email: layout só com <table>, estilos inline e URLs absolutas.

O header é opcional — controlado pelo terceiro parâmetro de generateEmailHtml:

export interface EmailGreetingOptions {
/** Nome exibido em "Olá, <nome>" */
recipientName: string;
/** URL absoluta do destino do link "aqui" */
reportUrl: string;
}

generateEmailHtml(assoc, sections, greeting?: EmailGreetingOptions);

Sem o parâmetro greeting, o email é gerado exatamente como antes do TEC-369 — nenhum bloco de saudação é renderizado.

Escape de HTML

recipientName e o nome da associação passam por escapeHtml antes da interpolação. Nomes vindos do Admin DB nunca são injetados crus no HTML do email.

Injeção no cron

O cron (apps/web/app/api/cron/boletim-diario/_lib/execute-boletim-diario.ts) monta o greeting por destinatário no doSend():

const html = generateEmailHtml(assoc, sections, {
recipientName: recipient.name,
reportUrl: getReportPageUrl(assoc.slug),
});

O cache de dados do boletim é por filtro, não por destinatário — os dados são buscados uma vez e só o HTML é regenerado por pessoa, então a personalização não degrada o caching.

getReportPageUrl(slug) vive em _lib/association-config.ts e resolve para {NEXT_PUBLIC_SITE_URL}/home/{slug}/reports/daily-report.

Destino do link é autenticado

O link "aqui" aponta para a página protegida por login do Supabase — não usa o fluxo de token do vona-emplacamento legado (/api/reports/daily-report?token=...). Destinatário sem sessão cai na tela de login. Se o acesso sem login for requisito, o link precisa migrar para o fluxo de token, o que exige antes a validação HMAC pendente da TEC-117.

Preview em desenvolvimento

A rota de dev GET /[locale]/daily-report-preview/email-html?assoc=<slug> renderiza o email com um greeting mockado, sem enviar nada.

Para um teste ponta a ponta, dispare o cron manualmente:

curl -X POST http://localhost:3000/api/cron/boletim-diario

Em localhost dev sem CRON_SECRET o bypass é permitido. Use BOLETIM_RECIPIENTS_OVERRIDE para redirecionar a entrega para o seu email — a identidade do destinatário (e portanto o nome no "Olá, ...") continua sendo a real, o que valida a interpolação. Os emails caem no Mailpit (http://127.0.0.1:54324).

Invariante: accounts.id == Control_GrupoId

As páginas de relatório (daily-report, por-marca, por-territorio) usam workspace.account.id como grupoId nas queries do Data Warehouse. Isso só funciona porque vale a invariante de sistema:

accounts.id (Supabase) == Control_GrupoId (Admin DB / DW da Nebula)

Por isso o seed.sql local cria as contas das associações com os UUIDs reais dos grupos:

Contaaccounts.id == grupoId
ABRACAF9bf82171-ce0b-4b61-85e3-b6e25fbdcbf0
ABCNa6ac52f6-947f-4f79-8e0c-60657f662eb6
ABRAREf81a800a-0a43-4b98-b5ab-fb7b6374e25f

Os mesmos UUIDs estão hardcoded em association-config.ts (grupoId) para uso do cron, que roda sem sessão de usuário.

Sintoma de quebra da invariante

Se uma conta for criada com um id que não existe como grupo_id no DW, as páginas de relatório renderizam apenas a linha "Total Ranking" zerada e a data cai no dia atual (o MAX(data_emplacamento) retorna nulo). Foi exatamente esse o bug corrigido no seed — os IDs antigos (aa000001-…, bb000002-…, cc000003-…) eram fictícios. Após alterar IDs do seed, rode pnpm supabase:web:reset.

Testes

  • _lib/__tests__/generate-email-html.test.ts — interpolação do nome, link "aqui" com URL correta, posição do header antes do banner, escape de HTML e omissão quando greeting não é passado.
  • api/cron/boletim-diario/_lib/__tests__/execute-boletim-diario.test.ts — garante que o cron repassa recipientName e reportUrl corretos por associação.