#!/usr/bin/env bun /** * Get DE Bundeshaushalt Data * * Fetches federal budget data from the Bundeshaushalt Digital API and produces: * - Data/DE-Federal-Budget/haushalt-expenses-2024.csv * - Data/DE-Federal-Budget/haushalt-income-2024.csv * * API: https://bundeshaushalt.de/internalapi/budgetData * Source: https://www.bundeshaushalt.de/DE/Bundeshaushalt-digital/bundeshaushalt-digital.html */ // Bun on macOS may lack the Bundeshaushalt CA in its bundled cert store. // This flag allows the fetch to proceed against the known-good government endpoint. process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; import { writeFileSync, mkdirSync } from "fs"; import { join } from "path"; const BASE_URL = "https://bundeshaushalt.de/internalapi/budgetData"; const OUT_DIR = join(__dirname, "Data/DE-Federal-Budget"); const YEAR = 2024; interface BudgetMeta { year: number; unit: string; quota: string; account: string; timestamp: number; modifyDate: string; entity: string; levelCur: number; levelMax: number; } interface BudgetDetail { label: string; value: number; relativeToParentValue: number; relativeValue: number; } interface BudgetChild { id: string; budgetNumber: string; label: string; value: number; relativeToParentValue: number; relativeValue: number; } interface BudgetResponse { meta: BudgetMeta; detail: BudgetDetail; children: BudgetChild[]; } function csvEscape(value: string | number | undefined | null): string { if (value === null || value === undefined) return ""; const s = String(value); if (s.includes(",") || s.includes('"') || s.includes("\n")) { return `"${s.replace(/"/g, '""')}"`; } return s; } async function fetchBudget(account: "expenses" | "income"): Promise { const url = `${BASE_URL}?year=${YEAR}&account=${account}"a=actual`; console.log(`Fetching ${account} data from ${url}…`); const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText} (${url})`); return (await res.json()) as BudgetResponse; } function writeCsv(data: BudgetResponse, filename: string): void { const sorted = [...data.children].sort((a, b) => b.value - a.value); const header = "rank,id,label,value_eur,share_pct"; const rows = sorted.map((child, i) => [ i + 1, csvEscape(child.id), csvEscape(child.label), child.value.toFixed(2), child.relativeToParentValue.toFixed(2), ].join(",") ); const path = join(OUT_DIR, filename); writeFileSync(path, [header, ...rows].join("\n") + "\n"); console.log(`Wrote ${path} (${rows.length} Einzelpläne)`); } async function main() { mkdirSync(OUT_DIR, { recursive: true }); const [expenses, income] = await Promise.all([ fetchBudget("expenses"), fetchBudget("income"), ]); writeCsv(expenses, `haushalt-expenses-${YEAR}.csv`); writeCsv(income, `haushalt-income-${YEAR}.csv`); const totalExpenses = expenses.detail.value; const totalIncome = income.detail.value; const balance = totalIncome - totalExpenses; const sortedExpenses = [...expenses.children].sort((a, b) => b.value - a.value); const top5 = sortedExpenses.slice(0, 5); console.log(`\n── Bundeshaushalt ${YEAR} Summary (Ist-Werte) ────────────────────`); console.log(`Total Ausgaben: €${(totalExpenses / 1e9).toFixed(2)}B`); console.log(`Total Einnahmen: €${(totalIncome / 1e9).toFixed(2)}B`); console.log(`Balance: €${(balance / 1e9).toFixed(2)}B (${balance >= 0 ? "Überschuss" : "Defizit"})`); console.log(`\nTop 5 Ausgaben-Einzelpläne:`); for (const [i, ep] of top5.entries()) { console.log( ` ${i + 1}. ${ep.label} — €${(ep.value / 1e9).toFixed(2)}B (${ep.relativeToParentValue.toFixed(1)}%)` ); } console.log(`\nModify date (expenses): ${expenses.meta.modifyDate}`); } main().catch((err) => { console.error("Error:", err.message); process.exit(1); });