Iniciativa Cidadã Independente · Transparência Parlamentar
TS175 linhas5.241 bytes

apps/worker/src/routes/premiacao.ts

SHA-256

eacccb865326ed5fac767e43f4f50ad74cdf6d45cb64648b515a090536ef3480

Somente leiturafonte-eb8a54281800
import { Hono } from 'hono'
import type { Env } from '../types'
import {
  apurarPremiacao,
  getSnapshotFixado,
  setSnapshotFixado,
  PremiacaoIndisponivel,
  MIN_LINHAS_SNAPSHOT,
  MESES_MIN_VITORIA,
  REGIOES,
  DIVULGAR_RESULTADO,
} from '../services/premiacao'
import type { Premiacao } from '../services/premiacao'
import { listSnapshots } from '../services/ranking'

const premiacao = new Hono<{ Bindings: Env }>()

function exigeAdmin(c: { req: { header: (k: string) => string | undefined }; env: Env }) {
  const secret = c.req.header('X-Admin-Secret')
  return Boolean(secret) && secret === c.env.ADMIN_SECRET
}

/**
 * Versão pública enquanto o resultado não é divulgado: categorias e tamanho
 * da coorte, sem vencedor, pódio nem snapshot que identifique a apuração.
 */
function semResultado(p: Premiacao) {
  return {
    edicao: p.edicao,
    legislatura: p.legislatura,
    divulgado: false,
    coorte: p.coorte,
    categorias: p.categorias.map((cat) => ({
      id: cat.id,
      eixo: cat.eixo,
      titulo: cat.titulo,
      descricao: cat.descricao,
      criterio: cat.criterio,
      peso: cat.peso,
      regiao: cat.regiao,
      uf: cat.uf,
      ufs: cat.ufs,
      elegiveis: cat.elegiveis,
    })),
  }
}

premiacao.get('/', async (c) => {
  try {
    const resultado = await apurarPremiacao(c.env)
    if (!DIVULGAR_RESULTADO) {
      // O admin vê a apuração completa; a resposta não pode ir para cache compartilhado.
      if (exigeAdmin(c)) {
        return c.json({ ...resultado, divulgado: false }, 200, {
          'Cache-Control': 'private, no-store',
          Vary: 'X-Admin-Secret',
        })
      }
      return c.json(semResultado(resultado), 200, {
        'Cache-Control': 'public, max-age=3600, stale-while-revalidate=600',
        Vary: 'X-Admin-Secret',
      })
    }
    // Resultado congelado não muda até alguém repinar: pode cachear por mais tempo.
    const maxAge = resultado.congelado ? 86400 : 3600
    return c.json({ ...resultado, divulgado: true }, 200, {
      'Cache-Control': `public, max-age=${maxAge}, stale-while-revalidate=600`,
    })
  } catch (err) {
    if (err instanceof PremiacaoIndisponivel) {
      // 503: a apuração recusa em vez de publicar prêmio sobre dado parcial.
      return c.json(
        { error: 'premiacao indisponivel', motivo: err.motivo, detalhe: err.detalhe },
        503,
      )
    }
    console.error('[premiacao] erro:', err)
    return c.json({ error: 'apuracao falhou', detalhe: String(err) }, 500)
  }
})

/** Metadados estáveis: regiões, piso de sanidade e snapshot em uso. */
premiacao.get('/meta', async (c) => {
  const fixado = await getSnapshotFixado(c.env)
  return c.json(
    {
      edicao: 'leg57',
      legislatura: 57,
      divulgado: DIVULGAR_RESULTADO,
      congelado: fixado !== null,
      snapshot: fixado,
      mesesMinimosVitoria: MESES_MIN_VITORIA,
      minLinhasSnapshot: MIN_LINHAS_SNAPSHOT,
      regioes: Object.entries(REGIOES).map(([id, r]) => ({
        id,
        nome: r.nome,
        ufs: r.ufs,
      })),
    },
    200,
    { 'Cache-Control': 'public, max-age=3600' },
  )
})

/** Admin: lista snapshots disponíveis para congelar. */
premiacao.get('/snapshots', async (c) => {
  if (!exigeAdmin(c)) return c.json({ error: 'unauthorized' }, 401)
  const snapshots = await listSnapshots(c.env)
  const fixado = await getSnapshotFixado(c.env)
  return c.json({
    fixado,
    minLinhasSnapshot: MIN_LINHAS_SNAPSHOT,
    snapshots: snapshots.map((s) => ({
      ...s,
      apto: s.total >= MIN_LINHAS_SNAPSHOT,
      emUso: s.computedAt === fixado,
    })),
  })
})

/**
 * Admin: fixa o snapshot da apuração.
 *
 * Sem `computed_at`, congela o snapshot mais recente que passe no piso de
 * sanidade. Recusa snapshots inexistentes ou truncados — é exatamente o caso
 * que o piso existe para barrar.
 */
premiacao.post('/congelar', async (c) => {
  if (!exigeAdmin(c)) return c.json({ error: 'unauthorized' }, 401)

  const snapshots = await listSnapshots(c.env)
  if (snapshots.length === 0) {
    return c.json({ error: 'nenhum snapshot disponível' }, 503)
  }

  const pedido = c.req.query('computed_at')
  const alvo = pedido
    ? snapshots.find((s) => s.computedAt === pedido)
    : snapshots.find((s) => s.total >= MIN_LINHAS_SNAPSHOT)

  if (!alvo) {
    return c.json(
      {
        error: pedido
          ? `snapshot ${pedido} não existe`
          : `nenhum snapshot com ao menos ${MIN_LINHAS_SNAPSHOT} linhas`,
        disponiveis: snapshots.slice(0, 10),
      },
      404,
    )
  }

  if (alvo.total < MIN_LINHAS_SNAPSHOT) {
    return c.json(
      {
        error: 'snapshot incompleto',
        detalhe: `${alvo.computedAt} tem ${alvo.total} linhas, mínimo ${MIN_LINHAS_SNAPSHOT}`,
      },
      422,
    )
  }

  await setSnapshotFixado(c.env, alvo.computedAt)
  return c.json({ ok: true, congelado: true, snapshot: alvo.computedAt, total: alvo.total })
})

/** Admin: volta a apurar sobre o snapshot mais recente. */
premiacao.post('/descongelar', async (c) => {
  if (!exigeAdmin(c)) return c.json({ error: 'unauthorized' }, 401)
  await setSnapshotFixado(c.env, null)
  return c.json({ ok: true, congelado: false })
})

export { premiacao }