TS620 linhas28.386 bytes
apps/worker/src/services/detalhe.ts
SHA-256
062cefbca0751770e2243bb2c333d0fe3414d052c6d421be3fe6d737725db68e
Somente leiturafonte-acf635076da2
/**
* Listagens detalhadas por senador para subpáginas/popups da UI.
* Cada função retorna {total, pagina, porPagina, data, fonte, atualizadoEm}.
*
* Estratégia: cache filesystem-respeitando o KV-like wrapper (env.SENADO_CACHE).
* TTLs ajustados por volatilidade. Paginação sempre no servidor.
*/
import type { Env } from '../types'
const LEG57_INICIO = '2023-02-01'
interface Page {
pagina?: number
porPagina?: number
}
interface ListResult<T> {
codigo: string
total: number
pagina: number
porPagina: number
data: T[]
fonte: string
atualizadoEm: string
}
async function cachedGet<T = unknown>(
env: Env,
key: string,
url: string,
ttlSec: number,
): Promise<T | null> {
const cached = await env.SENADO_CACHE.get(key, 'json')
if (cached !== null) return cached as T
try {
const res = await fetch(url, {
headers: { Accept: 'application/json' },
signal: AbortSignal.timeout(30_000),
})
if (!res.ok) return null
const data = (await res.json()) as T
await env.SENADO_CACHE.put(key, JSON.stringify(data), { expirationTtl: ttlSec })
return data
} catch (err) {
console.warn(`[detalhe] fetch falhou: ${url}`, err)
return null
}
}
function paginate<T>(arr: T[], opts: Page): { pagina: number; porPagina: number; data: T[] } {
const porPagina = Math.min(Math.max(opts.porPagina ?? 50, 1), 200)
const pagina = Math.max(opts.pagina ?? 1, 1)
const start = (pagina - 1) * porPagina
return { pagina, porPagina, data: arr.slice(start, start + porPagina) }
}
function extractList(obj: unknown, path: string[]): unknown[] {
let cur: unknown = obj
for (const k of path) {
if (cur && typeof cur === 'object' && k in (cur as Record<string, unknown>)) {
cur = (cur as Record<string, unknown>)[k]
} else {
return []
}
}
if (Array.isArray(cur)) return cur
if (cur != null) return [cur]
return []
}
const today = () => new Date().toISOString().slice(0, 10)
const isoNow = () => new Date().toISOString()
// ─────────────────────────────────────────────────────────────────────────────
// Perfil (mandatos + filiações + profissão + formação)
// ─────────────────────────────────────────────────────────────────────────────
export interface PerfilSenador {
codigo: string
nome: string
partido?: string
uf?: string
email?: string
telefones?: string[]
fotoUrl?: string
paginaSenado?: string
endereco?: string
mandatos: Array<{
legislatura: string
inicio: string
fim?: string
participacao: string
partido?: string
suplentes?: Array<{ descricao: string; nome: string }>
}>
filiacoes: Array<{ partido: string; dataFiliacao: string; dataDesfiliacao?: string }>
profissoes: string[]
formacao: Array<{ nivel: string; curso: string; instituicao?: string }>
fonte: string
atualizadoEm: string
}
export async function obterPerfil(env: Env, codigo: string): Promise<PerfilSenador | null> {
const base = env.LEGIS_BASE_URL
const fonte = `${base}/senador/${codigo}`
const [det, mand, prof, form, fil] = await Promise.all([
cachedGet<Record<string, unknown>>(env, `legis:${codigo}:detalhe`, `${base}/senador/${codigo}.json`, 7 * 86400),
cachedGet<Record<string, unknown>>(env, `legis:${codigo}:mandatos`, `${base}/senador/${codigo}/mandatos`, 7 * 86400),
cachedGet<Record<string, unknown>>(env, `legis:${codigo}:profissao`, `${base}/senador/${codigo}/profissao`, 30 * 86400),
cachedGet<Record<string, unknown>>(env, `legis:${codigo}:formacao`, `${base}/senador/${codigo}/historicoAcademico`, 30 * 86400),
cachedGet<Record<string, unknown>>(env, `legis:${codigo}:filiacoes`, `${base}/senador/${codigo}/filiacoes`, 7 * 86400),
])
if (!det) return null
const parl = (det?.DetalheParlamentar as Record<string, unknown>)?.Parlamentar as Record<string, unknown> | undefined
const ident = parl?.IdentificacaoParlamentar as Record<string, unknown> | undefined
const dadosBasicos = parl?.DadosBasicosParlamentar as Record<string, unknown> | undefined
const telefones = extractList(parl, ['Telefones', 'Telefone'])
.map((t) => String((t as Record<string, unknown>).NumeroTelefone ?? ''))
.filter(Boolean)
const mandatos = extractList(mand, ['MandatoParlamentar', 'Parlamentar', 'Mandatos', 'Mandato']).map((m) => {
const obj = m as Record<string, unknown>
const part = obj.Partidos as Record<string, unknown> | undefined
const partidoObj = part?.Partido as Record<string, unknown> | Record<string, unknown>[] | undefined
const primeiroPartido = Array.isArray(partidoObj) ? partidoObj[0] : partidoObj
const supl = extractList(obj, ['Suplentes', 'Suplente']).map((s) => {
const sObj = s as Record<string, unknown>
return {
descricao: String(sObj.DescricaoParticipacao ?? ''),
nome: String(sObj.NomeParlamentar ?? ''),
}
})
const leg1 = obj.PrimeiraLegislaturaDoMandato as Record<string, unknown> | undefined
const leg2 = obj.SegundaLegislaturaDoMandato as Record<string, unknown> | undefined
const inicio = String(leg1?.DataInicio ?? leg2?.DataInicio ?? '')
const fim = String(leg2?.DataFim ?? leg1?.DataFim ?? '')
return {
legislatura: `${leg1?.NumeroLegislatura ?? ''}${leg2 ? `/${leg2.NumeroLegislatura}` : ''}`,
inicio,
fim: fim || undefined,
participacao: String(obj.DescricaoParticipacao ?? ''),
partido: primeiroPartido ? String(primeiroPartido.Sigla ?? '') : undefined,
suplentes: supl,
}
})
const filiacoes = extractList(fil, ['FiliacaoParlamentar', 'Parlamentar', 'Filiacoes', 'Filiacao']).map((f) => {
const obj = f as Record<string, unknown>
const partido = obj.Partido as Record<string, unknown> | undefined
return {
partido: String(partido?.SiglaPartido ?? partido?.NomePartido ?? ''),
dataFiliacao: String(obj.DataFiliacao ?? ''),
dataDesfiliacao: obj.DataDesfiliacao ? String(obj.DataDesfiliacao) : undefined,
}
})
const profissoes = extractList(prof, ['ProfissaoParlamentar', 'Parlamentar', 'Profissoes', 'Profissao']).map((p) => {
return String((p as Record<string, unknown>).NomeProfissao ?? '')
}).filter(Boolean)
const formacao = extractList(form, ['HistoricoAcademicoParlamentar', 'Parlamentar', 'HistoricosAcademicos', 'HistoricoAcademico']).map((f) => {
const obj = f as Record<string, unknown>
return {
nivel: String(obj.NomeGrauInstrucao ?? obj.GrauInstrucao ?? ''),
curso: String(obj.NomeCurso ?? ''),
instituicao: String(obj.NomeInstituicao ?? '') || undefined,
}
}).filter((f) => f.curso || f.nivel)
return {
codigo,
nome: String(ident?.NomeParlamentar ?? ''),
partido: ident?.SiglaPartidoParlamentar ? String(ident.SiglaPartidoParlamentar) : undefined,
uf: ident?.UfParlamentar ? String(ident.UfParlamentar) : undefined,
email: ident?.EmailParlamentar ? String(ident.EmailParlamentar) : undefined,
telefones: telefones.length ? telefones : undefined,
fotoUrl: ident?.UrlFotoParlamentar ? String(ident.UrlFotoParlamentar) : undefined,
paginaSenado: ident?.UrlPaginaParlamentar ? String(ident.UrlPaginaParlamentar) : undefined,
endereco: dadosBasicos?.EnderecoParlamentar ? String(dadosBasicos.EnderecoParlamentar) : undefined,
mandatos,
filiacoes,
profissoes,
formacao,
fonte,
atualizadoEm: isoNow(),
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Autorias (processo)
// ─────────────────────────────────────────────────────────────────────────────
export interface AutoriaItem {
idProcesso: number
identificacao: string
ementa: string
dataApresentacao: string
sigla: string
numero: string
ano: string
autoriaTexto?: string
primeiroAutor?: boolean
situacao?: string
ultimoLocal?: string
url?: string
}
export async function listarAutorias(env: Env, codigo: string, opts: Page & { ano?: number } = {}): Promise<ListResult<AutoriaItem>> {
const base = env.LEGIS_BASE_URL
const url = `${base}/processo?codigoParlamentarAutor=${codigo}&tramitouLegislaturaAtual=S`
const data = (await cachedGet<unknown[]>(env, `legis:${codigo}:processos:leg57`, url, 12 * 3600)) ?? []
const arr = Array.isArray(data) ? data : []
let filtrado = arr.map((p) => {
const obj = p as Record<string, unknown>
const ident = String(obj.identificacao ?? '')
const partes = ident.split(/[\s/]/)
return {
idProcesso: Number(obj.id ?? 0),
identificacao: ident,
ementa: String(obj.ementa ?? ''),
dataApresentacao: String(obj.dataApresentacao ?? '').slice(0, 10),
sigla: partes[0] ?? '',
numero: partes[1] ?? '',
ano: partes[2] ?? '',
autoriaTexto: obj.autoria ? String(obj.autoria) : undefined,
situacao: obj.descricaoSituacao ? String(obj.descricaoSituacao) : obj.statusAtual ? String(obj.statusAtual) : undefined,
ultimoLocal: obj.localAtual ? String(obj.localAtual) : undefined,
url: `https://www25.senado.leg.br/web/atividade/materias/-/materia/${obj.codigoMateria ?? obj.id}`,
} as AutoriaItem
})
if (opts.ano) {
const anoStr = String(opts.ano)
filtrado = filtrado.filter((a) => a.ano === anoStr || a.dataApresentacao.startsWith(anoStr))
}
filtrado.sort((a, b) => (b.dataApresentacao || '').localeCompare(a.dataApresentacao || ''))
const { pagina, porPagina, data: page } = paginate(filtrado, opts)
return { codigo, total: filtrado.length, pagina, porPagina, data: page, fonte: url, atualizadoEm: isoNow() }
}
// ─────────────────────────────────────────────────────────────────────────────
// Relatorias (processo/relatoria)
// ─────────────────────────────────────────────────────────────────────────────
export interface RelatoriaItem {
idProcesso: number
codigoMateria?: number
identificacao?: string
ementa: string
comissaoCodigo?: number
comissaoSigla?: string
comissaoNome?: string
papel: string
dataDesignacao: string
dataDestituicao?: string
encerramento?: string
url?: string
}
export async function listarRelatorias(env: Env, codigo: string, opts: Page & { ano?: number } = {}): Promise<ListResult<RelatoriaItem>> {
const base = env.LEGIS_BASE_URL
const url = `${base}/processo/relatoria?codigoParlamentar=${codigo}&dataInicio=${LEG57_INICIO}&dataFim=${today()}`
// chave distinta da usada por services/legis.ts (formato deprecated antigo)
const data = (await cachedGet<unknown[]>(env, `legis:${codigo}:processoRelatoria:leg57`, url, 12 * 3600)) ?? []
const arr = Array.isArray(data) ? data : []
let mapped = arr.map((r) => {
const obj = r as Record<string, unknown>
return {
idProcesso: Number(obj.idProcesso ?? 0),
codigoMateria: obj.codigoMateria ? Number(obj.codigoMateria) : undefined,
identificacao: obj.identificacaoMateria ? String(obj.identificacaoMateria) : undefined,
ementa: String(obj.ementaProcesso ?? obj.ementaMateria ?? ''),
comissaoCodigo: obj.codigoColegiado ? Number(obj.codigoColegiado) : undefined,
comissaoSigla: obj.siglaColegiado ? String(obj.siglaColegiado) : undefined,
comissaoNome: obj.nomeColegiado ? String(obj.nomeColegiado) : undefined,
papel: String(obj.descricaoTipoRelator ?? 'Relator'),
dataDesignacao: String(obj.dataDesignacao ?? '').slice(0, 10),
dataDestituicao: obj.dataDestituicao ? String(obj.dataDestituicao).slice(0, 10) : undefined,
encerramento: obj.descricaoTipoEncerramento ? String(obj.descricaoTipoEncerramento) : undefined,
url: obj.codigoMateria ? `https://www25.senado.leg.br/web/atividade/materias/-/materia/${obj.codigoMateria}` : undefined,
} as RelatoriaItem
})
if (opts.ano) {
const anoStr = String(opts.ano)
mapped = mapped.filter((r) => r.dataDesignacao.startsWith(anoStr))
}
mapped.sort((a, b) => b.dataDesignacao.localeCompare(a.dataDesignacao))
const { pagina, porPagina, data: page } = paginate(mapped, opts)
return { codigo, total: mapped.length, pagina, porPagina, data: page, fonte: url, atualizadoEm: isoNow() }
}
// ─────────────────────────────────────────────────────────────────────────────
// Votações (votação nominal por parlamentar)
// ─────────────────────────────────────────────────────────────────────────────
export interface VotacaoItem {
codigoSessao: number
codigoVotacao: number
dataSessao: string
identificacao: string
ementa: string
descricaoVotacao: string
voto: string
resultado?: string
url?: string
}
export async function listarVotacoes(env: Env, codigo: string, opts: Page & { ano?: number } = {}): Promise<ListResult<VotacaoItem>> {
const base = env.LEGIS_BASE_URL
const ano = opts.ano ?? new Date().getFullYear()
const url = `${base}/votacao?codigoParlamentar=${codigo}&dataInicio=${ano}-01-01&dataFim=${ano}-12-31`
const data = (await cachedGet<unknown[]>(env, `legis:${codigo}:votacao:${ano}`, url, 6 * 3600)) ?? []
const arr = Array.isArray(data) ? data : []
const mapped = arr.map((v) => {
const obj = v as Record<string, unknown>
// O voto individual do parlamentar vem aninhado em votos[]:
// votos[0] = { codigoParlamentar, siglaVotoParlamentar: "Sim"|"Não"|"Abstenção"|"Não votou"|... }
// Como filtramos por codigoParlamentar, há sempre 1 item nessa lista.
const votosArr = (obj.votos as Array<Record<string, unknown>>) ?? []
const meuVoto = votosArr[0] ?? {}
return {
codigoSessao: Number(obj.codigoSessao ?? 0),
codigoVotacao: Number(obj.codigoSessaoVotacao ?? obj.codigoVotacaoSve ?? 0),
dataSessao: String(obj.dataSessao ?? '').slice(0, 10),
identificacao: String(obj.identificacao ?? ''),
ementa: String(obj.ementa ?? ''),
descricaoVotacao: String(obj.descricaoVotacao ?? ''),
voto: String(meuVoto.siglaVotoParlamentar ?? meuVoto.descricaoVotoParlamentar ?? ''),
resultado: obj.resultadoVotacao ? String(obj.resultadoVotacao) : undefined,
url: obj.idProcesso ? `https://www25.senado.leg.br/web/atividade/materias/-/materia/${obj.codigoMateria ?? obj.idProcesso}` : undefined,
} as VotacaoItem
})
mapped.sort((a, b) => b.dataSessao.localeCompare(a.dataSessao))
const { pagina, porPagina, data: page } = paginate(mapped, opts)
return { codigo, total: mapped.length, pagina, porPagina, data: page, fonte: url, atualizadoEm: isoNow() }
}
// ─────────────────────────────────────────────────────────────────────────────
// Comissões (membro)
// ─────────────────────────────────────────────────────────────────────────────
export interface ComissaoItem {
codigo: string
sigla: string
nome: string
casa?: string
participacao: string
dataInicio: string
dataFim?: string
ativo: boolean
}
export async function listarComissoes(env: Env, codigo: string, opts: Page & { ativo?: boolean } = {}): Promise<ListResult<ComissaoItem>> {
const base = env.LEGIS_BASE_URL
const url = `${base}/senador/${codigo}/comissoes${opts.ativo === false ? '' : '?ativo=S'}`
const data = await cachedGet<Record<string, unknown>>(env, `legis:${codigo}:comissoes:${opts.ativo === false ? 'todas' : 'ativas'}`, url, 24 * 3600)
const arr = extractList(data, ['MembroComissaoParlamentar', 'Parlamentar', 'MembroComissoes', 'Comissao'])
const mapped: ComissaoItem[] = arr.map((c) => {
const obj = c as Record<string, unknown>
const id = obj.IdentificacaoComissao as Record<string, unknown> | undefined
return {
codigo: String(id?.CodigoComissao ?? ''),
sigla: String(id?.SiglaComissao ?? ''),
nome: String(id?.NomeComissao ?? ''),
casa: id?.SiglaCasaComissao ? String(id.SiglaCasaComissao) : undefined,
participacao: String(obj.DescricaoParticipacao ?? ''),
dataInicio: String(obj.DataInicio ?? '').slice(0, 10),
dataFim: obj.DataFim ? String(obj.DataFim).slice(0, 10) : undefined,
ativo: !obj.DataFim,
}
})
mapped.sort((a, b) => Number(b.ativo) - Number(a.ativo) || b.dataInicio.localeCompare(a.dataInicio))
const { pagina, porPagina, data: page } = paginate(mapped, opts)
return { codigo, total: mapped.length, pagina, porPagina, data: page, fonte: url, atualizadoEm: isoNow() }
}
// ─────────────────────────────────────────────────────────────────────────────
// Cargos (mesa, presidências, frentes etc)
// ─────────────────────────────────────────────────────────────────────────────
export interface CargoItem {
cargo: string
comissaoCodigo?: string
comissaoSigla?: string
comissaoNome?: string
casa?: string
dataInicio: string
dataFim?: string
ativo: boolean
}
export async function listarCargos(env: Env, codigo: string, opts: Page & { ativo?: boolean } = {}): Promise<ListResult<CargoItem>> {
const base = env.LEGIS_BASE_URL
const url = `${base}/senador/${codigo}/cargos${opts.ativo === false ? '' : '?ativo=S'}`
const data = await cachedGet<Record<string, unknown>>(env, `legis:${codigo}:cargosDet:${opts.ativo === false ? 'todos' : 'ativos'}`, url, 24 * 3600)
const arr = extractList(data, ['CargoParlamentar', 'Parlamentar', 'Cargos', 'Cargo'])
const mapped: CargoItem[] = arr.map((c) => {
const obj = c as Record<string, unknown>
const id = obj.IdentificacaoComissao as Record<string, unknown> | undefined
return {
cargo: String(obj.DescricaoCargo ?? ''),
comissaoCodigo: id?.CodigoComissao ? String(id.CodigoComissao) : undefined,
comissaoSigla: id?.SiglaComissao ? String(id.SiglaComissao) : undefined,
comissaoNome: id?.NomeComissao ? String(id.NomeComissao) : undefined,
casa: id?.SiglaCasaComissao ? String(id.SiglaCasaComissao) : undefined,
dataInicio: String(obj.DataInicio ?? '').slice(0, 10),
dataFim: obj.DataFim ? String(obj.DataFim).slice(0, 10) : undefined,
ativo: !obj.DataFim,
}
})
mapped.sort((a, b) => Number(b.ativo) - Number(a.ativo) || b.dataInicio.localeCompare(a.dataInicio))
const { pagina, porPagina, data: page } = paginate(mapped, opts)
return { codigo, total: mapped.length, pagina, porPagina, data: page, fonte: url, atualizadoEm: isoNow() }
}
// ─────────────────────────────────────────────────────────────────────────────
// Discursos / Apartes
// ─────────────────────────────────────────────────────────────────────────────
export interface DiscursoItem {
codigoPronunciamento: string
data: string
casa: string
tipoUsoPalavra: string
resumo: string
urlTextoIntegral?: string
urlTextoBinario?: string
}
export async function listarDiscursos(env: Env, codigo: string, opts: Page & { ano?: number } = {}): Promise<ListResult<DiscursoItem>> {
const base = env.LEGIS_BASE_URL
const ano = opts.ano ?? new Date().getFullYear()
const url = `${base}/senador/${codigo}/discursos?dataIni=${ano}0101&dataFim=${ano}1231`
const data = await cachedGet<Record<string, unknown>>(env, `legis:${codigo}:discursos:${ano}`, url, 6 * 3600)
const arr = extractList(data, ['DiscursosParlamentar', 'Parlamentar', 'Pronunciamentos', 'Pronunciamento'])
const mapped: DiscursoItem[] = arr.map((d) => {
const obj = d as Record<string, unknown>
const tipo = obj.TipoUsoPalavra as Record<string, unknown> | undefined
const id = String(obj.CodigoPronunciamento ?? '')
return {
codigoPronunciamento: id,
data: String(obj.DataPronunciamento ?? '').slice(0, 10),
casa: String(obj.NomeCasaPronunciamento ?? obj.SiglaCasaPronunciamento ?? ''),
tipoUsoPalavra: String(tipo?.Descricao ?? ''),
resumo: String(obj.TextoResumo ?? ''),
urlTextoIntegral: id ? `${base}/discurso/texto-integral/${id}` : undefined,
urlTextoBinario: id ? `${base}/discurso/texto-binario/${id}` : undefined,
}
})
mapped.sort((a, b) => b.data.localeCompare(a.data))
const { pagina, porPagina, data: page } = paginate(mapped, opts)
return { codigo, total: mapped.length, pagina, porPagina, data: page, fonte: url, atualizadoEm: isoNow() }
}
export async function listarApartes(env: Env, codigo: string, opts: Page & { ano?: number } = {}): Promise<ListResult<DiscursoItem>> {
const base = env.LEGIS_BASE_URL
const ano = opts.ano ?? new Date().getFullYear()
const url = `${base}/senador/${codigo}/apartes?dataIni=${ano}0101&dataFim=${ano}1231`
const data = await cachedGet<Record<string, unknown>>(env, `legis:${codigo}:apartes:${ano}`, url, 6 * 3600)
const arr = extractList(data, ['ApartesParlamentar', 'Parlamentar', 'Apartes', 'Aparte'])
const mapped: DiscursoItem[] = arr.map((d) => {
const obj = d as Record<string, unknown>
const tipo = obj.TipoUsoPalavra as Record<string, unknown> | undefined
const id = String(obj.CodigoPronunciamento ?? '')
return {
codigoPronunciamento: id,
data: String(obj.DataPronunciamento ?? '').slice(0, 10),
casa: String(obj.NomeCasaPronunciamento ?? obj.SiglaCasaPronunciamento ?? ''),
tipoUsoPalavra: String(tipo?.Descricao ?? 'Aparte'),
resumo: String(obj.TextoResumo ?? ''),
urlTextoIntegral: id ? `${base}/discurso/texto-integral/${id}` : undefined,
}
})
mapped.sort((a, b) => b.data.localeCompare(a.data))
const { pagina, porPagina, data: page } = paginate(mapped, opts)
return { codigo, total: mapped.length, pagina, porPagina, data: page, fonte: url, atualizadoEm: isoNow() }
}
// ─────────────────────────────────────────────────────────────────────────────
// Despesas detalhadas (CEAP) — transação a transação
// ─────────────────────────────────────────────────────────────────────────────
export interface DespesaItem {
id: number
ano: number
mes: number
data?: string
tipoDocumento: string
tipoDespesa: string
cpfCnpj: string
fornecedor: string
documento?: string
detalhamento?: string
valor: number
}
export interface DespesasResumo {
totalAno: number
totalTransacoes: number
porTipo: Array<{ tipo: string; total: number; transacoes: number }>
porMes: Array<{ mes: number; total: number }>
topFornecedores: Array<{ nome: string; cpfCnpj: string; total: number; transacoes: number }>
}
export async function listarDespesasDetalhadas(
env: Env,
codigo: string,
ano: number,
opts: Page & { tipo?: string; fornecedor?: string } = {},
): Promise<ListResult<DespesaItem> & { resumo: DespesasResumo }> {
const base = env.ADM_BASE_URL
const url = `${base}/api/v1/senadores/despesas_ceaps/${ano}`
const isAnoCorrente = ano >= new Date().getFullYear()
const ttl = isAnoCorrente ? 6 * 3600 : 30 * 86400
const data = await cachedGet<unknown>(env, `adm:ceapDetalhe:${ano}`, url, ttl)
// Envelope `{ data: [...] }` (formato novo do ADM) ou array puro. Falha de
// upstream precisa propagar — devolver [] mostraria "sem despesas" ao usuário.
const arr = Array.isArray(data)
? data
: Array.isArray((data as { data?: unknown } | null)?.data)
? ((data as { data: unknown[] }).data)
: null
if (arr === null) {
throw new Error(`ADM indisponível: sem lista de despesas em ${url}`)
}
const codNum = Number(codigo)
const mine = arr.filter((d) => Number((d as Record<string, unknown>).codSenador) === codNum)
const itens: DespesaItem[] = mine.map((d) => {
const obj = d as Record<string, unknown>
return {
id: Number(obj.id ?? 0),
ano: Number(obj.ano ?? ano),
mes: Number(obj.mes ?? 0),
data: obj.data ? String(obj.data).slice(0, 10) : undefined,
tipoDocumento: String(obj.tipoDocumento ?? ''),
tipoDespesa: String(obj.tipoDespesa ?? ''),
cpfCnpj: String(obj.cpfCnpj ?? ''),
fornecedor: String(obj.fornecedor ?? ''),
documento: obj.documento ? String(obj.documento) : undefined,
detalhamento: obj.detalhamento ? String(obj.detalhamento) : undefined,
valor: Number(obj.valorReembolsado ?? 0),
}
})
// Resumo (sobre TODOS os itens do ano, não só a página)
const porTipoMap = new Map<string, { total: number; transacoes: number }>()
const porMesMap = new Map<number, number>()
const porForn = new Map<string, { nome: string; cpfCnpj: string; total: number; transacoes: number }>()
let totalAno = 0
for (const it of itens) {
totalAno += it.valor
const tipoEntry = porTipoMap.get(it.tipoDespesa) ?? { total: 0, transacoes: 0 }
tipoEntry.total += it.valor
tipoEntry.transacoes += 1
porTipoMap.set(it.tipoDespesa, tipoEntry)
porMesMap.set(it.mes, (porMesMap.get(it.mes) ?? 0) + it.valor)
const key = it.cpfCnpj || it.fornecedor
const fornEntry = porForn.get(key) ?? { nome: it.fornecedor, cpfCnpj: it.cpfCnpj, total: 0, transacoes: 0 }
fornEntry.total += it.valor
fornEntry.transacoes += 1
porForn.set(key, fornEntry)
}
const resumo: DespesasResumo = {
totalAno: Math.round(totalAno * 100) / 100,
totalTransacoes: itens.length,
porTipo: [...porTipoMap.entries()]
.map(([tipo, v]) => ({ tipo, total: Math.round(v.total * 100) / 100, transacoes: v.transacoes }))
.sort((a, b) => b.total - a.total),
porMes: [...porMesMap.entries()]
.map(([mes, total]) => ({ mes, total: Math.round(total * 100) / 100 }))
.sort((a, b) => a.mes - b.mes),
topFornecedores: [...porForn.values()]
.map((f) => ({ ...f, total: Math.round(f.total * 100) / 100 }))
.sort((a, b) => b.total - a.total)
.slice(0, 20),
}
// Filtros opcionais
let filtrado = itens
if (opts.tipo) filtrado = filtrado.filter((i) => i.tipoDespesa.toLowerCase().includes(opts.tipo!.toLowerCase()))
if (opts.fornecedor) filtrado = filtrado.filter((i) => i.fornecedor.toLowerCase().includes(opts.fornecedor!.toLowerCase()))
filtrado.sort((a, b) => (b.data ?? '').localeCompare(a.data ?? '') || b.mes - a.mes)
const { pagina, porPagina, data: page } = paginate(filtrado, opts)
return {
codigo,
total: filtrado.length,
pagina,
porPagina,
data: page,
resumo,
fonte: url,
atualizadoEm: isoNow(),
}
}