Iniciativa Cidadã Independente · Transparência Parlamentar
TS207 linhas10.936 bytes

apps/worker/src/services/premiacao-email.ts

SHA-256

6306c2e701e05e2187b57d8744bf2e8fc89cd6e35ce6dd78df7642c9ce47b326

Somente leiturafonte-0531042d9f3e
import type { Env } from '../types'
import type { Certificado, DadosCertificado } from './premiacao-edicoes'
import { listarCertificados, registrarEnvio } from './premiacao-edicoes'
import { URL_VERIFICACAO, justificativa, nomeArquivo, pdfDoCertificado } from './certificado'

/**
 * E-mail de entrega dos certificados ao gabinete.
 *
 * Um e-mail por senador, com todos os certificados dele na edição anexados.
 * Remetente e assinatura são só institucionais; o texto evita concordância de
 * gênero fora da saudação, e a saudação usa a forma oficial de tratamento.
 */

const FROM_EMAIL = 'Observatório do Senado <[email protected]>'
const REPLY_TO = '[email protected]'
const SITE = 'https://observasenado.org'

function esc(s: string): string {
  return s.replace(/[&<>"']/g, (c) =>
    ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]!,
  )
}

/** "Excelentíssima Senhora Senadora Fulana" / "Excelentíssimo Senhor Senador Fulano". */
function saudacao(d: DadosCertificado): string {
  const t = d.senador.tratamento
  if (t === 'Senadora') return `Excelentíssima Senhora Senadora ${d.senador.nome}`
  if (t === 'Senador') return `Excelentíssimo Senhor Senador ${d.senador.nome}`
  return `Ao Gabinete de ${t} ${d.senador.nome}`
}

export interface OpcoesEmail {
  /** Parágrafo opcional escrito pelo admin, ex.: data da divulgação pública. */
  mensagem?: string
  /** Marca o e-mail como reenvio. */
  reenvio?: boolean
  /** Envio de teste para o admin: assunto com [TESTE] e aviso no topo. */
  teste?: boolean
}

export function montarEmail(
  certs: Certificado[],
  opts: OpcoesEmail = {},
): { assunto: string; html: string; texto: string } {
  const d = certs[0].dados
  const varios = certs.length > 1
  const edicao = `Prêmio Observatório do Senado — ${d.edicao.titulo}`
  const assunto =
    (opts.teste ? '[TESTE] ' : '') +
    (opts.reenvio ? 'Reenvio: ' : '') +
    `${edicao}: ${varios ? 'certificados' : 'certificado'} de reconhecimento`

  const categoriasHtml = certs
    .map((c) => `
      <tr><td style="padding:14px 0;border-top:1px solid #e2dfd8">
        <div style="font-family:Georgia,serif;font-size:17px;color:#8a6a22;font-weight:bold;letter-spacing:.04em">${esc(c.dados.categoria.titulo)}</div>
        <div style="font-size:13px;color:#3d4247;line-height:1.6;margin-top:4px">${esc(justificativa(c.dados))}</div>
        <div style="font-size:12px;color:#5b6166;margin-top:6px">Código de verificação:
          <a href="${URL_VERIFICACAO}?codigo=${encodeURIComponent(c.codigo)}" style="color:#0F3D5C;font-weight:bold;letter-spacing:.06em">${esc(c.codigo)}</a></div>
      </td></tr>`)
    .join('')

  const periodoTxt = d.edicao.tipo === 'legislatura'
    ? 'ao longo da 57ª Legislatura'
    : `no ano legislativo de ${d.edicao.titulo.replace(/\D/g, '')}`

  const mensagem = opts.mensagem?.trim()
  const html = `<!doctype html>
<html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(assunto)}</title></head>
<body style="margin:0;padding:24px 12px;background:#f7f6f3;font-family:system-ui,-apple-system,'Segoe UI',Arial,sans-serif;color:#1a1a1a">
  <table role="presentation" cellpadding="0" cellspacing="0" border="0" align="center" width="620" style="max-width:620px;width:100%;background:#ffffff;border:1px solid #e2dfd8;border-radius:4px">
    <tr><td style="padding:22px 32px;background:#0F3D5C;border-bottom:3px solid #C9A24B">
      <div style="font-family:Georgia,serif;font-size:18px;color:#ffffff;letter-spacing:.03em">Observatório do Senado</div>
      <div style="font-size:11px;color:#C9A24B;text-transform:uppercase;letter-spacing:.14em;margin-top:4px">${esc(edicao.replace('Prêmio Observatório do Senado — ', 'Prêmio · '))}</div>
    </td></tr>
    ${opts.teste ? `<tr><td style="padding:10px 32px;background:#fff6d8;font-size:12px;color:#7a5b00">Envio de teste. Os anexos são os certificados reais desta edição.</td></tr>` : ''}
    <tr><td style="padding:30px 32px 8px;font-size:15px;line-height:1.7">
      <p style="margin:0 0 18px">${esc(saudacao(d))},</p>
      <p style="margin:0 0 14px">O Observatório do Senado tem a satisfação de comunicar que Vossa Excelência obteve o primeiro lugar ${varios ? `em ${certs.length} categorias` : 'na categoria abaixo'} do <strong>${esc(edicao)}</strong>, que reconhece o desempenho parlamentar ${periodoTxt}.</p>
      ${opts.reenvio ? `<p style="margin:0 0 14px">Reenviamos, a pedido ou por precaução, ${varios ? 'os certificados já emitidos' : 'o certificado já emitido'}. ${varios ? 'Os códigos de verificação continuam os mesmos.' : 'O código de verificação continua o mesmo.'}</p>` : ''}
    </td></tr>
    <tr><td style="padding:0 32px">
      <table role="presentation" width="100%" cellpadding="0" cellspacing="0">${categoriasHtml}</table>
    </td></tr>
    <tr><td style="padding:18px 32px 8px;font-size:14px;line-height:1.7;color:#1a1a1a">
      <p style="margin:0 0 14px">O prêmio não tem inscrição, indicação nem votação. O resultado decorre exclusivamente dos dados abertos do Senado Federal, aplicados ao Índice de Desempenho Senatorial (IDS), cuja <a href="${SITE}/metodologia" style="color:#0F3D5C">metodologia</a> e cujo <a href="${SITE}/codigo" style="color:#0F3D5C">código-fonte</a> são públicos e podem ser conferidos por qualquer pessoa.</p>
      <p style="margin:0 0 14px">${varios ? 'Seguem anexos os certificados' : 'Segue anexo o certificado'} em PDF. ${varios ? 'Cada um traz' : 'Ele traz'} um código e um QR code que permitem conferir a autenticidade em <a href="${URL_VERIFICACAO}" style="color:#0F3D5C">observasenado.org/premiacao/verificar</a>. O gabinete pode divulgar ${varios ? 'os certificados' : 'o certificado'} livremente.</p>
      ${mensagem ? `<p style="margin:0 0 14px">${esc(mensagem).replace(/\n/g, '<br>')}</p>` : ''}
      <p style="margin:22px 0 0">Respeitosamente,</p>
      <p style="margin:6px 0 0;font-family:Georgia,serif;font-size:16px;color:#0F3D5C"><strong>Observatório do Senado</strong></p>
      <p style="margin:2px 0 0;font-size:12px;color:#5b6166">Iniciativa cidadã independente, sem vínculo partidário</p>
    </td></tr>
    <tr><td style="padding:22px 32px 26px">
      <hr style="border:none;border-top:1px solid #e2dfd8;margin:0 0 12px">
      <p style="margin:0;font-size:11px;color:#8a8e93;line-height:1.6">Dúvidas ou correções: responda a este e-mail ou escreva para <a href="mailto:${REPLY_TO}" style="color:#5b6166">${REPLY_TO}</a>. <a href="${SITE}" style="color:#5b6166">observasenado.org</a></p>
    </td></tr>
  </table>
</body></html>`

  const texto = [
    `${saudacao(d)},`,
    '',
    `O Observatório do Senado tem a satisfação de comunicar que Vossa Excelência obteve o primeiro lugar ${varios ? `em ${certs.length} categorias` : 'na categoria abaixo'} do ${edicao}, que reconhece o desempenho parlamentar ${periodoTxt}.`,
    ...(opts.reenvio ? ['', `Reenviamos ${varios ? 'os certificados já emitidos; os códigos' : 'o certificado já emitido; o código'} de verificação ${varios ? 'continuam os mesmos' : 'continua o mesmo'}.`] : []),
    '',
    ...certs.flatMap((c) => [
      `• ${c.dados.categoria.titulo}`,
      `  ${justificativa(c.dados)}`,
      `  Código de verificação: ${c.codigo} (${URL_VERIFICACAO}?codigo=${c.codigo})`,
      '',
    ]),
    'O prêmio não tem inscrição, indicação nem votação. O resultado decorre exclusivamente dos dados abertos do Senado Federal, aplicados ao Índice de Desempenho Senatorial (IDS), com metodologia e código-fonte públicos em observasenado.org.',
    '',
    `${varios ? 'Seguem anexos os certificados' : 'Segue anexo o certificado'} em PDF, com código e QR code para conferência da autenticidade em observasenado.org/premiacao/verificar. O gabinete pode divulgá-${varios ? 'los' : 'lo'} livremente.`,
    ...(mensagem ? ['', mensagem] : []),
    '',
    'Respeitosamente,',
    'Observatório do Senado',
    'Iniciativa cidadã independente, sem vínculo partidário',
    REPLY_TO,
  ].join('\n')

  return { assunto, html, texto }
}

/** Certificados de um senador na edição, sem os revogados. */
async function certificadosDoSenador(
  env: Env,
  edicaoId: string,
  senadorCod: string,
): Promise<Certificado[]> {
  return (await listarCertificados(env, edicaoId)).filter(
    (c) => c.senadorCod === senadorCod && !c.revogadoEm,
  )
}

export async function previaEmail(
  env: Env,
  edicaoId: string,
  senadorCod: string,
  opts: OpcoesEmail = {},
) {
  const certs = await certificadosDoSenador(env, edicaoId, senadorCod)
  if (certs.length === 0) return null
  return { ...montarEmail(certs, opts), para: certs[0].email, anexos: certs.map((c) => nomeArquivo(c.dados)) }
}

export interface ResultadoEnvio {
  senadorCod: string
  para: string
  ok: boolean
  certificados: string[]
  erro?: string
}

/**
 * Envia os certificados do senador. Com `paraTeste`, manda ao endereço do
 * admin e não marca nada como enviado.
 */
export async function enviarCertificados(
  env: Env,
  edicaoId: string,
  senadorCod: string,
  opts: OpcoesEmail & { paraTeste?: string } = {},
): Promise<ResultadoEnvio> {
  if (!env.RESEND_API_KEY) throw new Error('RESEND_API_KEY não configurada')
  const certs = await certificadosDoSenador(env, edicaoId, senadorCod)
  if (certs.length === 0) throw new Error('nenhum certificado válido para este senador')
  const para = opts.paraTeste ?? certs[0].email
  if (!para) throw new Error('defina o e-mail de envio antes')

  const anexos = []
  for (const c of certs) {
    const pdf = await pdfDoCertificado(env, c.codigo)
    if (!pdf) throw new Error(`PDF do certificado ${c.codigo} indisponível`)
    anexos.push({ filename: nomeArquivo(c.dados), content: Buffer.from(pdf).toString('base64') })
  }

  const teste = Boolean(opts.paraTeste)
  const reenvio = !teste && (opts.reenvio ?? certs.some((c) => c.enviadoEm))
  const { assunto, html, texto } = montarEmail(certs, { ...opts, teste, reenvio })
  const codigos = certs.map((c) => c.codigo)

  let resultado: { ok: boolean; erro?: string }
  try {
    const r = await fetch('https://api.resend.com/emails', {
      method: 'POST',
      headers: { Authorization: `Bearer ${env.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        from: FROM_EMAIL,
        to: [para],
        reply_to: REPLY_TO,
        subject: assunto,
        html,
        text: texto,
        attachments: anexos,
      }),
    })
    resultado = r.ok ? { ok: true } : { ok: false, erro: `Resend ${r.status}: ${(await r.text()).slice(0, 200)}` }
  } catch (err) {
    resultado = { ok: false, erro: String(err) }
  }

  if (!teste) await registrarEnvio(env, codigos, resultado)
  return { senadorCod, para, ok: resultado.ok, certificados: codigos, erro: resultado.erro }
}