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.
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.
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:
| Conta | accounts.id == grupoId |
|---|---|
| ABRACAF | 9bf82171-ce0b-4b61-85e3-b6e25fbdcbf0 |
| ABCN | a6ac52f6-947f-4f79-8e0c-60657f662eb6 |
| ABRARE | f81a800a-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.
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 quandogreetingnão é passado.api/cron/boletim-diario/_lib/__tests__/execute-boletim-diario.test.ts— garante que o cron repassarecipientNameereportUrlcorretos por associação.