From a071422e23494ec0b2ad6ccd272fc9a27f3d4e3d Mon Sep 17 00:00:00 2001 From: Guido Date: Wed, 21 Oct 2026 20:23:49 +0200 Subject: [PATCH] test: Die iOS-Modelle werden gegen echte Serverantworten geprueft Die Swift-Modelle sind aus den Pydantic-Schemas abgeschrieben. Ein vertippter Feldname uebersetzt anstandslos und faellt erst dem Nutzer auf, wenn eine Liste leer bleibt - das ist beim blinden Schreiben die einzige Luecke, die kein Uebersetzer schliesst. backend/scripts/erzeuge_ios_fixture.py legt eine Wegwerfdatenbank an, befuellt sie so, dass jedes Feld einmal belegt ist (Tags, Uebertrag am Stueck, verknuepfter Uebertrag, Vertragszuordnung, gebundenes Konto, jaehrliches Abo) und schreibt 15 echte Antworten nach ios/Tests/Fixtures/antworten.json. Der Swift-Test "Serverantworten" entschluesselt daraus jedes Modell. Nach jeder Schema-Aenderung neu laufen lassen und das Ergebnis mit committen. Co-Authored-By: Claude Opus 5 --- backend/scripts/erzeuge_ios_fixture.py | 139 +++ ios/Tests/Fixtures/antworten.json | 1127 ++++++++++++++++++++++++ 2 files changed, 1266 insertions(+) create mode 100644 backend/scripts/erzeuge_ios_fixture.py create mode 100644 ios/Tests/Fixtures/antworten.json diff --git a/backend/scripts/erzeuge_ios_fixture.py b/backend/scripts/erzeuge_ios_fixture.py new file mode 100644 index 0000000..ae3f71b --- /dev/null +++ b/backend/scripts/erzeuge_ios_fixture.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Echte Antworten des Backends als Prüfdatei für die iOS-Modelle. + +Aufruf aus ``backend/``:: + + .venv/bin/python -m scripts.erzeuge_ios_fixture + +Schreibt ``ios/Tests/Fixtures/antworten.json``. Der Swift-Test +``Serverantworten`` entschlüsselt daraus jedes Modell und schlägt fehl, sobald +ein Feldname nicht mehr passt. + +**Warum es das gibt.** Die iOS-App wird gegen Modelle geschrieben, die jemand +aus den Pydantic-Schemas abgeschrieben hat. Ein vertippter Feldname fällt beim +Übersetzen nicht auf -- er fällt erst dem Nutzer auf, wenn die Liste leer +bleibt. Diese Datei ist die einzige Prüfung, die das fängt. + +Nach jeder Änderung an einem Schema neu laufen lassen und das Ergebnis +mit committen. Die Datenbank ist eine Wegwerfdatenbank; es landen keine +echten Daten in der Datei. +""" + +import json +import os +import tempfile +from pathlib import Path + +# Muss vor dem Import von app stehen -- die Konfiguration wird beim Import gelesen. +os.environ["APP_DATA_DIR"] = tempfile.mkdtemp(prefix="cf-fixture-") +os.environ.setdefault("APP_ECB_FETCH_ON_START", "false") +os.environ.setdefault("APP_ADMIN_USERNAME", "admin") +os.environ.setdefault("APP_ADMIN_PASSWORD", "AdminPass1") +os.environ.setdefault("APP_SESSION_SECRET", "fixture-secret") + +from datetime import date # noqa: E402 + +from fastapi.testclient import TestClient # noqa: E402 + +from app.main import app # noqa: E402 + +ZIEL = Path(__file__).resolve().parents[2] / "ios" / "Tests" / "Fixtures" / "antworten.json" + + +def _seed(c: TestClient) -> None: + """Genug Daten, dass jedes Feld einmal belegt ist. + + Leere Listen pruefen nichts: Ein falscher Feldname in einer Zeile faellt + nur auf, wenn es eine Zeile gibt. + """ + c.put("/api/settings/budget_start_day", params={"value": "1"}) + giro = c.post("/api/accounts", json={ + "name": "Girokonto", "type": "girokonto", "currency": "EUR", + "opening_balance_cents": 120000}).json() + sparen = c.post("/api/accounts", json={ + "name": "Sparen", "type": "sparkonto", "currency": "CHF", + "opening_balance_cents": 500000, "is_locked": True}).json() + + gruppen = c.get("/api/categories").json() + ausgabe = next(g for g in gruppen if g["kind"] == "expense")["categories"][0] + einnahme = next(g for g in gruppen if g["kind"] == "income")["categories"][0] + + tag = c.post("/api/tags", json={"name": "Urlaub", "color": "#2a78d6"}).json() + abo = c.post("/api/subscriptions", json={ + "name": "Streaming", "amount_cents": 1299, "currency": "EUR", + "interval_months": 12, "category_id": ausgabe["id"]}).json() + + heute = str(date.today()) + kauf = c.post("/api/transactions", json={ + "type": "expense", "date": heute, "account_id": giro["id"], + "amount_cents": 4250, "category_id": ausgabe["id"], "payee": "Supermarkt", + "description": "Wocheneinkauf", "notes": "mit Pfand"}).json() + c.patch(f"/api/transactions/{kauf['id']}", json={"tag_ids": [tag["id"]]}) + c.post("/api/transactions", json={ + "type": "income", "date": heute, "account_id": giro["id"], + "amount_cents": 350000, "category_id": einnahme["id"], "payee": "Arbeitgeber"}) + c.post("/api/transactions", json={ + "type": "transfer", "date": heute, "account_id": giro["id"], + "to_account_id": sparen["id"], "amount_cents": 20000}) + + # Zwei getrennte Buchungen, von Hand zu einem Uebertrag verknuepft -- der + # zweite Mechanismus, den die App nachbauen muss. + raus = c.post("/api/transactions", json={ + "type": "expense", "date": heute, "account_id": giro["id"], + "amount_cents": 15000, "payee": "Ueberweisung"}).json() + rein = c.post("/api/transactions", json={ + "type": "income", "date": heute, "account_id": sparen["id"], + "amount_cents": 15000, "payee": "Gutschrift"}).json() + c.post("/api/uebertraege/verbinden", + json={"a_id": raus["id"], "b_id": rein["id"]}) + + c.put(f"/api/budget/template/category/{ausgabe['id']}", + params={"planned_cents": 50000}) + c.post(f"/api/subscriptions/{abo['id']}/buchungen/{kauf['id']}") + + +PFADE = { + "version": "/api/version", + "ichSelbst": "/api/auth/me", + "einstellungen": "/api/settings", + "konten": "/api/accounts", + "kategoriegruppen": "/api/categories", + "schlagwoerter": "/api/tags", + "buchungen": "/api/transactions", + "uebertragsverknuepfungen": "/api/transactions/links", + "budget": "/api/budget/current", + "budgetvorlage": "/api/budget/template", + "abos": "/api/subscriptions", + "kontostaende": "/api/dashboard/balances", + "budgetfortschritt": "/api/dashboard/budget-progress", + "einnahmenAusgaben": "/api/dashboard/income-expense", + "letzteBuchungen": "/api/dashboard/recent", +} + + +def main() -> int: + with TestClient(app) as c: + r = c.post("/api/auth/login", + data={"username": "admin", "password": "AdminPass1"}) + if r.status_code != 200: + print("Anmeldung fehlgeschlagen:", r.text) + return 1 + _seed(c) + + heraus = {} + for name, pfad in PFADE.items(): + antwort = c.get(pfad) + if antwort.status_code != 200: + print(f"{pfad} antwortet {antwort.status_code}: {antwort.text[:200]}") + return 1 + heraus[name] = antwort.json() + + ZIEL.parent.mkdir(parents=True, exist_ok=True) + ZIEL.write_text(json.dumps(heraus, indent=1, ensure_ascii=False) + "\n", + encoding="utf-8") + print(f"{ZIEL} geschrieben, {len(heraus)} Antworten.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ios/Tests/Fixtures/antworten.json b/ios/Tests/Fixtures/antworten.json new file mode 100644 index 0000000..f73ab28 --- /dev/null +++ b/ios/Tests/Fixtures/antworten.json @@ -0,0 +1,1127 @@ +{ + "version": { + "name": "Certain Finance", + "version": "0.1.0", + "git_sha": null + }, + "ichSelbst": { + "id": 1, + "username": "admin", + "role": "admin", + "display_name": null, + "is_active": true, + "must_change_password": false, + "must_change_password_reason": null, + "totp_enabled": false, + "created_at": "2026-10-21T18:22:28.427216", + "mfa_setup_required": false + }, + "einstellungen": [ + { + "key": "base_currency", + "value": "EUR" + }, + { + "key": "budget_start_day", + "value": "1" + }, + { + "key": "locale", + "value": "de" + }, + { + "key": "theme", + "value": "system" + }, + { + "key": "date_format", + "value": "de" + }, + { + "key": "active_currencies", + "value": "EUR,CHF,USD" + } + ], + "konten": [ + { + "id": 1, + "name": "Girokonto", + "type": "girokonto", + "currency": "EUR", + "icon": "wallet", + "color": "#3f51b5", + "opening_balance_cents": 120000, + "opening_date": null, + "credit_limit_cents": null, + "is_locked": false, + "is_archived": false, + "position": 0, + "savings_category_id": null, + "identifiers": [], + "balance_cents": 430750, + "balance_base_cents": 430750, + "bank_balance_cents": null, + "bank_balance_at": null, + "bank_balance_vergleichbar": false + }, + { + "id": 2, + "name": "Sparen", + "type": "sparkonto", + "currency": "CHF", + "icon": "wallet", + "color": "#3f51b5", + "opening_balance_cents": 500000, + "opening_date": null, + "credit_limit_cents": null, + "is_locked": true, + "is_archived": false, + "position": 1, + "savings_category_id": null, + "identifiers": [], + "balance_cents": 535000, + "balance_base_cents": 535000, + "bank_balance_cents": null, + "bank_balance_at": null, + "bank_balance_vergleichbar": false + } + ], + "kategoriegruppen": [ + { + "id": 1, + "name": "Fixkosten", + "kind": "expense", + "icon": "landmark", + "color": "#3f51b5", + "position": 0, + "is_builtin": true, + "categories": [ + { + "id": 1, + "group_id": 1, + "name": "Wohnen", + "type": "expense", + "icon": "home", + "color": "#3f51b5", + "position": 0, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 2, + "group_id": 1, + "name": "Versicherungen", + "type": "expense", + "icon": "shield", + "color": "#3f51b5", + "position": 1, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 3, + "group_id": 1, + "name": "Telefon & Internet", + "type": "expense", + "icon": "wifi", + "color": "#3f51b5", + "position": 2, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 4, + "group_id": 1, + "name": "Abos & Streaming", + "type": "expense", + "icon": "refresh-ccw", + "color": "#3f51b5", + "position": 3, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + } + ] + }, + { + "id": 2, + "name": "Lebenshaltung", + "kind": "expense", + "icon": "shopping-cart", + "color": "#2e7d55", + "position": 1, + "is_builtin": true, + "categories": [ + { + "id": 5, + "group_id": 2, + "name": "Lebensmittel", + "type": "expense", + "icon": "shopping-cart", + "color": "#2e7d55", + "position": 0, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 6, + "group_id": 2, + "name": "Restaurant", + "type": "expense", + "icon": "utensils", + "color": "#2e7d55", + "position": 1, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 7, + "group_id": 2, + "name": "Mobilität", + "type": "expense", + "icon": "car", + "color": "#2e7d55", + "position": 2, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 8, + "group_id": 2, + "name": "Gesundheit", + "type": "expense", + "icon": "heart-pulse", + "color": "#2e7d55", + "position": 3, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 9, + "group_id": 2, + "name": "Kleidung", + "type": "expense", + "icon": "shirt", + "color": "#2e7d55", + "position": 4, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + } + ] + }, + { + "id": 3, + "name": "Freizeit & Persönliches", + "kind": "expense", + "icon": "film", + "color": "#b06f14", + "position": 2, + "is_builtin": true, + "categories": [ + { + "id": 10, + "group_id": 3, + "name": "Freizeit", + "type": "expense", + "icon": "film", + "color": "#b06f14", + "position": 0, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 11, + "group_id": 3, + "name": "Reisen", + "type": "expense", + "icon": "plane", + "color": "#b06f14", + "position": 1, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 12, + "group_id": 3, + "name": "Sport & Fitness", + "type": "expense", + "icon": "dumbbell", + "color": "#b06f14", + "position": 2, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 13, + "group_id": 3, + "name": "Bildung", + "type": "expense", + "icon": "graduation-cap", + "color": "#b06f14", + "position": 3, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 14, + "group_id": 3, + "name": "Haustiere", + "type": "expense", + "icon": "paw-print", + "color": "#b06f14", + "position": 4, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 15, + "group_id": 3, + "name": "Geschenke & Spenden", + "type": "expense", + "icon": "gift", + "color": "#b06f14", + "position": 5, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + } + ] + }, + { + "id": 4, + "name": "Finanzen", + "kind": "expense", + "icon": "piggy-bank", + "color": "#2a78d6", + "position": 3, + "is_builtin": true, + "categories": [ + { + "id": 16, + "group_id": 4, + "name": "Sparen & Investments", + "type": "expense", + "icon": "piggy-bank", + "color": "#2a78d6", + "position": 0, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 17, + "group_id": 4, + "name": "Steuern & Gebühren", + "type": "expense", + "icon": "percent", + "color": "#2a78d6", + "position": 1, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + } + ] + }, + { + "id": 5, + "name": "Sonstiges", + "kind": "expense", + "icon": "more-horizontal", + "color": "#6b7480", + "position": 4, + "is_builtin": true, + "categories": [ + { + "id": 18, + "group_id": 5, + "name": "Sonstiges", + "type": "expense", + "icon": "more-horizontal", + "color": "#6b7480", + "position": 0, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + } + ] + }, + { + "id": 6, + "name": "Einnahmen", + "kind": "income", + "icon": "coins", + "color": "#2e7d55", + "position": 5, + "is_builtin": true, + "categories": [ + { + "id": 19, + "group_id": 6, + "name": "Gehalt", + "type": "income", + "icon": "banknote", + "color": "#2e7d55", + "position": 0, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 20, + "group_id": 6, + "name": "Nebeneinkünfte", + "type": "income", + "icon": "coins", + "color": "#2e7d55", + "position": 1, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + }, + { + "id": 21, + "group_id": 6, + "name": "Erstattungen", + "type": "income", + "icon": "receipt", + "color": "#2e7d55", + "position": 2, + "is_builtin": true, + "is_archived": false, + "excluded_from_stats": false, + "spending_is_good": false, + "keyword_count": 0 + } + ] + } + ], + "schlagwoerter": [ + { + "id": 1, + "name": "Urlaub", + "color": "#2a78d6" + } + ], + "buchungen": [ + { + "id": 5, + "type": "income", + "date": "2026-10-21", + "time": null, + "account_id": 2, + "to_account_id": null, + "account_currency": "CHF", + "amount_cents": 15000, + "to_amount_cents": null, + "fx_rate": null, + "amount_eur_cents": 15000, + "amount_base_cents": 15000, + "category_id": null, + "budget_treatment": null, + "budget_category_id": null, + "subscription_id": null, + "budget_period": "2026-10", + "budget_period_manuell": false, + "import_batch_id": null, + "herkunft": "manuell", + "payee": "Gutschrift", + "description": null, + "notes": null, + "tag_ids": [] + }, + { + "id": 4, + "type": "expense", + "date": "2026-10-21", + "time": null, + "account_id": 1, + "to_account_id": null, + "account_currency": "EUR", + "amount_cents": 15000, + "to_amount_cents": null, + "fx_rate": null, + "amount_eur_cents": 15000, + "amount_base_cents": 15000, + "category_id": null, + "budget_treatment": null, + "budget_category_id": null, + "subscription_id": null, + "budget_period": "2026-10", + "budget_period_manuell": false, + "import_batch_id": null, + "herkunft": "manuell", + "payee": "Ueberweisung", + "description": null, + "notes": null, + "tag_ids": [] + }, + { + "id": 3, + "type": "transfer", + "date": "2026-10-21", + "time": null, + "account_id": 1, + "to_account_id": 2, + "account_currency": "EUR", + "amount_cents": 20000, + "to_amount_cents": 20000, + "fx_rate": null, + "amount_eur_cents": 20000, + "amount_base_cents": 20000, + "category_id": null, + "budget_treatment": null, + "budget_category_id": null, + "subscription_id": null, + "budget_period": "2026-10", + "budget_period_manuell": false, + "import_batch_id": null, + "herkunft": "manuell", + "payee": null, + "description": null, + "notes": null, + "tag_ids": [] + }, + { + "id": 2, + "type": "income", + "date": "2026-10-21", + "time": null, + "account_id": 1, + "to_account_id": null, + "account_currency": "EUR", + "amount_cents": 350000, + "to_amount_cents": null, + "fx_rate": null, + "amount_eur_cents": 350000, + "amount_base_cents": 350000, + "category_id": 19, + "budget_treatment": null, + "budget_category_id": null, + "subscription_id": null, + "budget_period": "2026-10", + "budget_period_manuell": false, + "import_batch_id": null, + "herkunft": "manuell", + "payee": "Arbeitgeber", + "description": null, + "notes": null, + "tag_ids": [] + }, + { + "id": 1, + "type": "expense", + "date": "2026-10-21", + "time": null, + "account_id": 1, + "to_account_id": null, + "account_currency": "EUR", + "amount_cents": 4250, + "to_amount_cents": null, + "fx_rate": null, + "amount_eur_cents": 4250, + "amount_base_cents": 4250, + "category_id": 1, + "budget_treatment": null, + "budget_category_id": null, + "subscription_id": 1, + "budget_period": "2026-10", + "budget_period_manuell": false, + "import_batch_id": null, + "herkunft": "manuell", + "payee": "Supermarkt", + "description": "Wocheneinkauf", + "notes": "mit Pfand", + "tag_ids": [ + 1 + ] + } + ], + "uebertragsverknuepfungen": { + "4": { + "link_id": 1, + "other_id": 5, + "richtung": "raus", + "other_account_id": 2, + "automatisch": false + }, + "5": { + "link_id": 1, + "other_id": 4, + "richtung": "rein", + "other_account_id": 1, + "automatisch": false + } + }, + "budget": { + "label": "2026-10", + "start": "2026-10-01", + "end": "2026-10-31", + "customized": false, + "base_currency": "EUR", + "prev": "2026-09", + "next": "2026-11", + "groups": [ + { + "id": 1, + "name": "Fixkosten", + "icon": "landmark", + "color": "#3f51b5", + "planned_cents": 50000, + "actual_cents": 4250, + "over": false, + "good": false, + "aus_vertraegen_cents": 108, + "categories": [ + { + "category_id": 1, + "name": "Wohnen", + "icon": "home", + "color": "#3f51b5", + "planned_cents": 50000, + "actual_cents": 4250, + "delta_cents": 45750, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 108, + "vertraege_anzahl": 1, + "good": false + }, + { + "category_id": 2, + "name": "Versicherungen", + "icon": "shield", + "color": "#3f51b5", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 3, + "name": "Telefon & Internet", + "icon": "wifi", + "color": "#3f51b5", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 4, + "name": "Abos & Streaming", + "icon": "refresh-ccw", + "color": "#3f51b5", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + } + ] + }, + { + "id": 2, + "name": "Lebenshaltung", + "icon": "shopping-cart", + "color": "#2e7d55", + "planned_cents": 0, + "actual_cents": 0, + "over": false, + "good": false, + "aus_vertraegen_cents": 0, + "categories": [ + { + "category_id": 5, + "name": "Lebensmittel", + "icon": "shopping-cart", + "color": "#2e7d55", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 6, + "name": "Restaurant", + "icon": "utensils", + "color": "#2e7d55", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 7, + "name": "Mobilität", + "icon": "car", + "color": "#2e7d55", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 8, + "name": "Gesundheit", + "icon": "heart-pulse", + "color": "#2e7d55", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 9, + "name": "Kleidung", + "icon": "shirt", + "color": "#2e7d55", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + } + ] + }, + { + "id": 3, + "name": "Freizeit & Persönliches", + "icon": "film", + "color": "#b06f14", + "planned_cents": 0, + "actual_cents": 0, + "over": false, + "good": false, + "aus_vertraegen_cents": 0, + "categories": [ + { + "category_id": 10, + "name": "Freizeit", + "icon": "film", + "color": "#b06f14", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 11, + "name": "Reisen", + "icon": "plane", + "color": "#b06f14", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 12, + "name": "Sport & Fitness", + "icon": "dumbbell", + "color": "#b06f14", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 13, + "name": "Bildung", + "icon": "graduation-cap", + "color": "#b06f14", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 14, + "name": "Haustiere", + "icon": "paw-print", + "color": "#b06f14", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 15, + "name": "Geschenke & Spenden", + "icon": "gift", + "color": "#b06f14", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + } + ] + }, + { + "id": 4, + "name": "Finanzen", + "icon": "piggy-bank", + "color": "#2a78d6", + "planned_cents": 0, + "actual_cents": 0, + "over": false, + "good": false, + "aus_vertraegen_cents": 0, + "categories": [ + { + "category_id": 16, + "name": "Sparen & Investments", + "icon": "piggy-bank", + "color": "#2a78d6", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + }, + { + "category_id": 17, + "name": "Steuern & Gebühren", + "icon": "percent", + "color": "#2a78d6", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + } + ] + }, + { + "id": 5, + "name": "Sonstiges", + "icon": "more-horizontal", + "color": "#6b7480", + "planned_cents": 0, + "actual_cents": 0, + "over": false, + "good": false, + "aus_vertraegen_cents": 0, + "categories": [ + { + "category_id": 18, + "name": "Sonstiges", + "icon": "more-horizontal", + "color": "#6b7480", + "planned_cents": 0, + "actual_cents": 0, + "delta_cents": 0, + "over": false, + "modus": "fest", + "zuschlag_cents": 0, + "aus_vertraegen_cents": 0, + "vertraege_anzahl": 0, + "good": false + } + ] + } + ], + "income": [], + "vertraege": { + "ohne_kategorie_cents": 0, + "ohne_kategorie_anzahl": 0, + "ohne_kurs": [] + }, + "totals": { + "planned_expense": 50000, + "actual_expense": 4250, + "planned_income": 0, + "received_income": 0 + } + }, + "budgetvorlage": { + "lines": [ + { + "category_id": 1, + "planned_cents": 50000, + "modus": "fest", + "zuschlag_cents": 0 + } + ], + "income": [] + }, + "abos": { + "subscriptions": [ + { + "id": 1, + "name": "Streaming", + "icon": "refresh-ccw", + "color": "#6b7480", + "amount_cents": 1299, + "currency": "EUR", + "interval_months": 12, + "account_id": null, + "category_id": 1, + "next_due": "2027-10-21", + "payee_pattern": "Supermarkt", + "savings_account_id": null, + "is_active": true, + "cancelled_at": null, + "notes": null, + "position": 0, + "monatlich_cents": 108, + "monatlich_base_cents": 108, + "tage_bis_faellig": 365, + "has_logo": false, + "logo_version": null + } + ], + "summary": { + "currency": "EUR", + "monatlich_cents": 108, + "jaehrlich_cents": 1296, + "anzahl_aktiv": 1, + "ohne_kurs": [] + } + }, + "kontostaende": { + "accounts": [ + { + "id": 1, + "name": "Girokonto", + "icon": "wallet", + "color": "#3f51b5", + "currency": "EUR", + "balance_cents": 430750, + "balance_base_cents": 430750, + "archived": false, + "locked": false + }, + { + "id": 2, + "name": "Sparen", + "icon": "wallet", + "color": "#3f51b5", + "currency": "CHF", + "balance_cents": 535000, + "balance_base_cents": 535000, + "archived": false, + "locked": true + } + ], + "total": 430750, + "gebunden": 535000, + "currency": "EUR" + }, + "budgetfortschritt": { + "rows": [ + { + "name": "Fixkosten", + "color": "#3f51b5", + "planned": 50000, + "actual": 4250, + "over": false, + "good": false + } + ], + "currency": "EUR", + "label": "2026-10", + "group_by": "group" + }, + "einnahmenAusgaben": { + "income": 350000, + "expense": 4250, + "net": 345750, + "currency": "EUR", + "label": "2026-10" + }, + "letzteBuchungen": { + "transactions": [ + { + "id": 5, + "date": "2026-10-21", + "type": "income", + "title": "Gutschrift", + "icon": "more-horizontal", + "color": "#6b7480", + "sign": "pos", + "amount_cents": 15000, + "currency": "CHF", + "amount_base_cents": 15000 + }, + { + "id": 4, + "date": "2026-10-21", + "type": "expense", + "title": "Ueberweisung", + "icon": "more-horizontal", + "color": "#6b7480", + "sign": "neg", + "amount_cents": 15000, + "currency": "EUR", + "amount_base_cents": 15000 + }, + { + "id": 3, + "date": "2026-10-21", + "type": "transfer", + "title": "Girokonto → Sparen", + "icon": "arrow-left-right", + "color": "#6b7480", + "sign": null, + "amount_cents": 20000, + "currency": "EUR", + "amount_base_cents": 20000 + }, + { + "id": 2, + "date": "2026-10-21", + "type": "income", + "title": "Gehalt", + "icon": "banknote", + "color": "#2e7d55", + "sign": "pos", + "amount_cents": 350000, + "currency": "EUR", + "amount_base_cents": 350000 + }, + { + "id": 1, + "date": "2026-10-21", + "type": "expense", + "title": "Wohnen", + "icon": "home", + "color": "#3f51b5", + "sign": "neg", + "amount_cents": 4250, + "currency": "EUR", + "amount_base_cents": 4250 + } + ], + "currency": "EUR" + } +}