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_frequency | E-mail chega | Melhor para |
|---|---|---|
daily | A cada manha apos o fechamento do dia anterior (hora local da jurisdicao) | Contatos focados em operacao |
weekly | Manha de segunda, cobrindo a Seg-Dom anterior | Oficiais principais de compliance |
monthly | Primeiro do mes, cobrindo o mes civil anterior | Revisores 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:
- Percorre linhas
city_contactsondeportal_access = true. - 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
- Daily:
- Para cada contato que esta na hora, chama
buildComplianceReport({jurisdictionId, period, date})para computar o payload. - Chama
buildDigestEmail(report, contact.locale)para formatar assunto + HTML + texto. - Entrega o e-mail ao
complianceDigestSenderpluggavel -- conectado a infra de e-mail transacional existente em producao. - Atualiza
last_digest_sent_atna linha do contato. - Persiste o payload em
city_compliance_reportscom 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
| Secao | Conteudo |
|---|---|
| Assunto | Levy Compliance Digest -- Boulder, CO -- Daily Report for 2026-05-17 |
| Cabecalho | Nome do operador, nome da jurisdicao, periodo |
| Scoreboard de compliance | Pass/fail por condicao com valor atual vs limiar |
| Snapshot de hoje | Viagens, veiculos implantados, reclamacoes abertas/fechadas |
| Resumo de enforcement | Comandos de limite de velocidade enviados, bloqueios emitidos, horas em slow zones |
| Questoes abertas | Condicoes falhando, reclamacoes abertas acima do SLA |
| Rodape | Link 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 = trueedigest_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:
| Coluna | Conteudo |
|---|---|
id | UUID |
jurisdiction_id | FK |
report_type | daily / weekly / monthly |
period_start / period_end | Janela de tempo coberta |
computed_at | Quando o relatorio foi construido |
payload | Snapshot JSON completo |
pdf_url | Para relatorios mensais |
delivered_to_contact_id | A 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
- Portal da Cidade e Magic-Link Auth -- onde contatos fazem login para ver os mesmos dados ao vivo.
- Relatorios de Condicoes de Licenca -- o que o scoreboard dentro do digest esta medindo.