Files
Substrate/get-de-haushalt
svemagie e0e879d917 feat: add DE bundesAPI integrations — Lobbyregister, SMARD, Haushalt, DIP
Four new data sources and fetch scripts via bundesAPI community project:

- DS-00011 Lobbyregister: 6,799 registrants, €0.86–0.91B declared lobbying (FY2024)
- DS-00012 SMARD Strommarkt: 60.2% renewable 2024, Wind Onshore #1 (107 TWh)
- DS-00013 Bundeshaushalt: €474.75B Ist-Wert 2024, 38.2% Soziales, 10.6% Verteidigung
- DS-00014 DIP Bundestag: 7,605 Drucksachen WP21, 12,507 Vorgänge, 83 Plenarprotokolle

Each integration: live-data fetch script (bun/TypeScript) + DATASET-TEMPLATE
markdown + CSV outputs. Scripts idempotent — re-run for current data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 12:54:02 +02:00

130 lines
3.9 KiB
Plaintext
Executable File

#!/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<BudgetResponse> {
const url = `${BASE_URL}?year=${YEAR}&account=${account}&quota=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);
});