Files
Substrate/get-de-lobbyregister
svemagieandClaude Sonnet 4.6 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

198 lines
6.2 KiB
TypeScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bun
/**
* Get DE Lobbyregister Data
*
* Fetches all registrations from the Bundestag Lobbyregister API and produces:
* - Data/DE-Lobby-Transparency/top-50-spenders.csv
* - Data/DE-Lobby-Transparency/sector-summary.csv
*
* API: https://www.lobbyregister.bundestag.de/sucheDetailJson
* Source: https://github.com/bundesAPI/bundestag-lobbyregister-api
*/
// Bun on macOS lacks the Bundestag CA in its bundled cert store; curl uses the system
// keychain. 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 API_URL =
"https://www.lobbyregister.bundestag.de/sucheDetailJson?q=&sort=FINANCIALEXPENSES_DESC";
const OUT_DIR = join(__dirname, "Data/DE-Lobby-Transparency");
interface LegalForm {
code: string;
de: string;
en: string;
}
interface FieldOfInterest {
code: string;
de: string;
en: string;
}
interface FinancialExpensesEuro {
from: number;
to: number;
}
interface FinancialExpenses {
relatedFiscalYearStart?: string;
relatedFiscalYearEnd?: string;
financialExpensesEuro?: FinancialExpensesEuro;
}
interface EmployeesInLobbying {
employeeFTE?: number;
}
interface LobbyistIdentity {
name?: string;
legalForm?: LegalForm;
}
interface ActivitiesAndInterests {
fieldsOfInterest?: FieldOfInterest[];
}
interface LobbyEntry {
registerNumber: string;
lobbyistIdentity?: LobbyistIdentity;
financialExpenses?: FinancialExpenses;
employeesInvolvedInLobbying?: EmployeesInLobbying;
activitiesAndInterests?: ActivitiesAndInterests;
}
interface ApiResponse {
resultCount: number;
results: LobbyEntry[];
}
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;
}
// The Lobbyregister taxonomy uses "|" to separate parent|child codes.
// Top-level sectors have no "|" in their code.
const isTopLevel = (f: FieldOfInterest) => !f.code.includes("|");
function primaryTopLevelSector(foi: FieldOfInterest[] | undefined): string {
if (!foi || foi.length === 0) return "";
return (foi.find(isTopLevel) ?? foi[0])?.de ?? "";
}
async function main() {
console.log("Fetching Lobbyregister data…");
const res = await fetch(API_URL);
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const data = (await res.json()) as ApiResponse;
const entries = data.results ?? [];
if (entries.length < data.resultCount) {
console.warn(`Warning: received ${entries.length} of ${data.resultCount} reported registrations`);
}
console.log(`Fetched ${entries.length} registrations (API total: ${data.resultCount})`);
mkdirSync(OUT_DIR, { recursive: true });
// ── Top 50 Spenders ─────────────────────────────────────────────────────
const withFinancials = entries.filter(
(e) => e.financialExpenses?.financialExpensesEuro
);
const top50Header =
"rank,register_number,name,legal_form_de,expenses_from_eur,expenses_to_eur,employees_fte,primary_sector_de";
const top50Rows = withFinancials.slice(0, 50).map((e, i) => {
const fin = e.financialExpenses!.financialExpensesEuro!;
const identity = e.lobbyistIdentity ?? {};
const emp = e.employeesInvolvedInLobbying?.employeeFTE;
const sector = primaryTopLevelSector(
e.activitiesAndInterests?.fieldsOfInterest
);
return [
i + 1,
csvEscape(e.registerNumber),
csvEscape(identity.name),
csvEscape(identity.legalForm?.de),
fin.from,
fin.to,
emp !== undefined ? emp : "",
csvEscape(sector),
].join(",");
});
const top50Path = join(OUT_DIR, "top-50-spenders.csv");
writeFileSync(top50Path, [top50Header, ...top50Rows].join("\n") + "\n");
console.log(`Wrote ${top50Path}`);
// ── Sector Summary + Totals (single pass) ───────────────────────────────
const sectorMap = new Map<
string,
{ de: string; en: string; count: number; totalMinEur: number }
>();
let totalMin = 0;
let totalMax = 0;
for (const e of entries) {
const fin = e.financialExpenses?.financialExpensesEuro;
if (fin) {
totalMin += fin.from;
totalMax += fin.to;
}
const foi = e.activitiesAndInterests?.fieldsOfInterest ?? [];
const topLevelSectors = foi.filter(isTopLevel);
if (topLevelSectors.length === 0) continue;
const finMin = fin?.from ?? 0;
for (const f of topLevelSectors) {
const existing = sectorMap.get(f.code);
if (existing) {
existing.count += 1;
existing.totalMinEur += finMin;
} else {
sectorMap.set(f.code, { de: f.de, en: f.en, count: 1, totalMinEur: finMin });
}
}
}
const sortedSectors = [...sectorMap.entries()].sort(
(a, b) => b[1].count - a[1].count
);
const sectorHeader = "sector_code,sector_de,sector_en,registrant_count,total_min_eur";
const sectorRows = sortedSectors.map(([code, s]) =>
[
csvEscape(code),
csvEscape(s.de),
csvEscape(s.en),
s.count,
s.totalMinEur,
].join(",")
);
const sectorPath = join(OUT_DIR, "sector-summary.csv");
writeFileSync(sectorPath, [sectorHeader, ...sectorRows].join("\n") + "\n");
console.log(`Wrote ${sectorPath}`);
console.log(`\n── Summary ──────────────────────────────`);
console.log(`Total registrations: ${entries.length}`);
console.log(`With financial data: ${withFinancials.length}`);
console.log(`Total expenditure range: ${(totalMin / 1e9).toFixed(2)}B ${(totalMax / 1e9).toFixed(2)}B EUR`);
console.log(`Top spender: ${withFinancials[0]?.lobbyistIdentity?.name ?? "N/A"}`);
console.log(`Top sector (by count): ${sortedSectors[0]?.[1].de ?? "N/A"} (${sortedSectors[0]?.[1].count} registrants)`);
}
main().catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});