Files
certain/backend/scripts/erzeuge_ios_fixture.py
T
GuidoandClaude Opus 5 adbc3e0aa4 fix: Ein Uebertragsvorschlag erscheint nur noch einmal
Jedes Paar steht in der Kandidatenliste zweimal, einmal je Richtung. Was
verknuepft wird, faellt ueber "verbraucht" heraus - ein blosser Vorschlag aber
nicht. Also bot /uebertraege/vorschlaege dieselbe Zahlung zweimal zum
Verbinden an, und wer das zweite anklickte, bekam "bereits verknuepft" fuer
etwas, das er gerade selbst verknuepft hatte.

Betrifft auch die Weboberflaeche: Uebertragsvorschlaege.jsx zeigt die Liste
unveraendert an und hatte denselben Doppeleintrag.

Gefunden beim Bauen der Arbeitsliste fuer die iOS-App - die Pruefdatei brachte
den Vorschlag doppelt zurueck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-21 21:28:00 +02:00

174 lines
7.3 KiB
Python

#!/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']}")
# --- Arbeitsliste -----------------------------------------------------
# Ein Eingangseintrag entsteht nur ueber die Schnellerfassung; von Hand
# legt man keinen an. Also eine Marke ausstellen und damit buchen.
marke = c.post("/api/inbox/tokens",
json={"name": "Fixture", "default_account_id": giro["id"]}).json()
kopf = {"X-Certain-Token": marke["token"]}
c.post("/api/quick/booking", headers=kopf,
json={"amount": "12.90", "payee": "Baeckerei", "date": heute,
"external_id": "fixture-1"})
# Zwei gleich aussehende Buchungen: gleiches Konto, gleicher Tag,
# gleicher krummer Betrag. Genau das meldet /dubletten.
for _ in range(2):
c.post("/api/transactions", json={
"type": "expense", "date": heute, "account_id": giro["id"],
"amount_cents": 1043, "payee": "Kiosk", "category_id": ausgabe["id"]})
# Ein Abgang und ein Zugang ueber verschiedene Konten, gleicher Betrag,
# unverknuepft - daraus macht /uebertraege/vorschlaege einen Vorschlag.
# Beide Konten muessen dieselbe Waehrung haben: 75 CHF und 75 EUR sind
# nicht derselbe Betrag, nur dieselbe Zahl (siehe _kandidaten).
zweitgiro = c.post("/api/accounts", json={
"name": "Zweitkonto", "type": "girokonto", "currency": "EUR",
"opening_balance_cents": 0}).json()
c.post("/api/transactions", json={
"type": "expense", "date": heute, "account_id": giro["id"],
"amount_cents": 7500, "payee": "Dauerauftrag"})
c.post("/api/transactions", json={
"type": "income", "date": heute, "account_id": zweitgiro["id"],
"amount_cents": 7500, "payee": "Dauerauftrag"})
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",
"eingang": "/api/inbox",
"dubletten": "/api/transactions/dubletten",
"uebertragsvorschlaege": "/api/uebertraege/vorschlaege",
}
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())