intermediate
digest
email
cadencia

E-mails de Digest e Cadencia

Como o cron compliance-digest horario percorre city_contacts, escolhe quem esta na hora, monta o payload do e-mail e atualiza last_digest_sent_at.

Equipe Levy FleetsMay 18, 202610 min read

E-mails de Digest e Cadencia

Contatos municipais escolhem com que frequencia querem um digest de compliance -- diario, semanal ou mensal. Um cron horario percorre cada linha de city_contacts, verifica se sua janela de cadencia expirou e (se sim) monta e envia o digest.

Opcoes de cadencia

digest_frequencyE-mail chegaMelhor para
dailyA cada manha apos o fechamento do dia anterior (hora local da jurisdicao)Contatos focados em operacao
weeklyManha de segunda, cobrindo a Seg-Dom anteriorOficiais principais de compliance
monthlyPrimeiro do mes, cobrindo o mes civil anteriorRevisores de licenca, auditores

Cadencia e por contato, nao por jurisdicao. Padrao comum: oficial principal de compliance recebe semanal, um analista ops recebe diario, um auditor recebe mensal.

O cron horario

/api/cron/compliance-digest roda a cada hora (0 * * * * em vercel.json). Ele:

  1. Percorre linhas city_contacts onde portal_access = true.
  2. Para cada linha, verifica a janela de cadencia:
    • Daily: last_digest_sent_at < (hoje 00:00 hora local da jurisdicao)
    • Weekly: last_digest_sent_at < (esta segunda 00:00 hora local da jurisdicao) e e segunda agora
    • Monthly: last_digest_sent_at < (1o deste mes 00:00) e e o primeiro agora
  3. Para cada contato que esta na hora, chama buildComplianceReport({jurisdictionId, period, date}) para computar o payload.
  4. Chama buildDigestEmail(report, contact.locale) para formatar assunto + HTML + texto.
  5. Entrega o e-mail ao complianceDigestSender pluggavel -- conectado a infra de e-mail transacional existente em producao.
  6. Atualiza last_digest_sent_at na linha do contato.
  7. Persiste o payload em city_compliance_reports com a linha de auditoria.

Se qualquer passo lanca, o contato e logado mas o cron avanca -- um contato falhado nao bloqueia o resto do batch.

O sender pluggavel

src/lib/compliance/digest-email.ts exporta setComplianceDigestSender(fn). O padrao e um sender no-op que loga no console; producao o define para a infra de e-mail do operator-app na inicializacao. Essa indirecao permite testes rodarem o pipeline de digest sem enviar e-mails.

Forma do e-mail

SecaoConteudo
AssuntoLevy Compliance Digest -- Boulder, CO -- Daily Report for 2026-05-17
CabecalhoNome do operador, nome da jurisdicao, periodo
Scoreboard de compliancePass/fail por condicao com valor atual vs limiar
Snapshot de hojeViagens, veiculos implantados, reclamacoes abertas/fechadas
Resumo de enforcementComandos de limite de velocidade enviados, bloqueios emitidos, horas em slow zones
Questoes abertasCondicoes falhando, reclamacoes abertas acima do SLA
RodapeLink para o portal da cidade, contato para suporte

Para digests mensais anexamos um PDF de uma pagina. Diario e semanal sao apenas HTML + texto.

Escape HTML

buildDigestEmail faz escape HTML em cada string controlada pelo usuario (nome de jurisdicao, titulos de reclamacao, numeros de veiculo). Testado em digest-email.test.ts para ser seguro contra tags <script> injetadas em nomes de politica -- mesmo que uma cidade publique uma politica com caracteres estranhos no nome, o digest renderiza corretamente sem quebrar o layout.

Localizacao

O e-mail e renderizado em city_contacts.locale (padrao en). Lancamento suporta en e es. Locales adicionais voltam para ingles com uma nota no rodape ("Este relatorio esta disponivel apenas em ingles").

Os rotulos de pass/fail e nomes de condicoes do scoreboard de compliance usam o locale do contato; o nome do operador e o nome da cidade passam inalterados.

last_digest_sent_at e idempotencia

Se o cron tenta novamente (por exemplo, um deploy no meio do ciclo, um timeout do Vercel), last_digest_sent_at e a protecao contra envios duplicados. O cron o atualiza dentro da mesma transacao que o insert em city_compliance_reports, entao ou ambos acontecem ou nenhum.

Se voce precisar forcar um reenvio (um contato diz nao ter recebido o digest de segunda), defina last_digest_sent_at para um valor anterior via a acao "Reenviar digest" do dashboard. O proximo run de cron pega o contato como devido e re-entrega.

Desabilitando digests temporariamente

Para pausar digests para um contato sem remove-lo:

  • Acesso apenas ao portal: defina portal_access = true e digest_frequency = null. O contato ainda pode fazer login no portal mas nao recebe e-mail.
  • Ambos pausados: defina portal_access = false. Auth de magic link falha; nenhum digest enviado.

Relatorios persistidos

Cada run de digest escreve uma linha em city_compliance_reports:

ColunaConteudo
idUUID
jurisdiction_idFK
report_typedaily / weekly / monthly
period_start / period_endJanela de tempo coberta
computed_atQuando o relatorio foi construido
payloadSnapshot JSON completo
pdf_urlPara relatorios mensais
delivered_to_contact_idA linha city_contacts que o recebeu

Esta tabela e a trilha de auditoria. Se um auditor municipal pergunta "voces enviaram um digest em 14 de abril?", a linha em city_compliance_reports e a resposta. Relatorios sao retidos indefinidamente.

Perguntas comuns

"Meu contato diz que nao recebeu o e-mail." Verifique spam, depois verifique city_compliance_reports para a linha esperada. Se a linha existe e delivered_to_contact_id corresponde, o e-mail foi despachado -- investigue no provedor de e-mail. Se a linha nao existe, o cron ou pulou o contato (janela de cadencia ainda nao expirou) ou bateu em um erro (Sentry).

"O mesmo contato pode receber diario e mensal?" Nao -- digest_frequency e um valor unico. Crie duas linhas de contato para o mesmo e-mail se quiser ambas cadencias.

"Posso colocar alguem em CC no digest?" Nao via a linha de contato. Crie uma linha city_contacts separada para o destinatario CC. Cada um recebe seu proprio acesso por magic link e seu proprio last_digest_sent_at.

Proximos passos