From 4843f2e1303d9c578aef68660a27af830b588cc7725efeeeb62e28ee3d03c8da Mon Sep 17 00:00:00 2001 From: wlfb Date: Mon, 20 Jul 2026 12:43:01 +0200 Subject: [PATCH] feat: PATCH-Endpunkte fuer Szenario-Modifikatoren und -Einmalzahlungen Co-Authored-By: Claude Fable 5 --- finance/app/routers/scenarios.py | 30 ++++++++++++++++++++ finance/tests/test_crud_api.py | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/finance/app/routers/scenarios.py b/finance/app/routers/scenarios.py index 6951ed8..70db717 100644 --- a/finance/app/routers/scenarios.py +++ b/finance/app/routers/scenarios.py @@ -181,6 +181,21 @@ def delete_modifier(scenario_id: int, mod_id: int, session.commit() +@router.patch("/{scenario_id}/modifiers/{mod_id}", response_model=ModifierOut) +def patch_modifier(scenario_id: int, mod_id: int, data: ModifierIn, + session: Session = Depends(get_session)): + # Full-Body-Update: das Edit-Formular sendet immer alle Felder des Typs. + _get_scenario(session, scenario_id) + modifier = session.get(ScenarioModifier, mod_id) + if modifier is None or modifier.scenario_id != scenario_id: + raise HTTPException(404, "Modifikator nicht gefunden") + for key, value in data.model_dump().items(): + setattr(modifier, key, value) + session.commit() + session.refresh(modifier) + return ModifierOut.model_validate(modifier) + + class ScenarioPlannedIn(BaseModel): name: str amount: Decimal @@ -225,6 +240,21 @@ def delete_scenario_planned(scenario_id: int, item_id: int, session.commit() +@router.patch("/{scenario_id}/planned/{item_id}", response_model=ScenarioPlannedOut) +def patch_scenario_planned(scenario_id: int, item_id: int, data: ScenarioPlannedIn, + session: Session = Depends(get_session)): + # Full-Body-Update, Muster patch_modifier. + _get_scenario(session, scenario_id) + item = session.get(ScenarioPlannedItem, item_id) + if item is None or item.scenario_id != scenario_id: + raise HTTPException(404, "Einmalzahlung nicht gefunden") + for key, value in data.model_dump().items(): + setattr(item, key, value) + session.commit() + session.refresh(item) + return ScenarioPlannedOut.model_validate(item) + + @router.post("/{scenario_id}/project", response_model=ProjectionOut) def project_scenario(scenario_id: int, horizon_days: int | None = None, start_date: date | None = None, diff --git a/finance/tests/test_crud_api.py b/finance/tests/test_crud_api.py index 2016eb8..31c1830 100644 --- a/finance/tests/test_crud_api.py +++ b/finance/tests/test_crud_api.py @@ -229,3 +229,51 @@ def test_scenario_planned_crud_und_cleanup(client, db): assert client.delete(f"/api/scenarios/{sc['id']}", headers=H).status_code == 204 from app.models.tables import ScenarioPlannedItem assert db.query(ScenarioPlannedItem).count() == 0 + + +def test_modifier_patch(client, db): + sc = client.post("/api/scenarios", headers=H, json={"name": "ModPatch"}).json() + m = client.post(f"/api/scenarios/{sc['id']}/modifiers", headers=H, json={ + "target_type": "recurring", "target_id": 1, "kind": "percent", + "value": "10.00"}).json() + # Wert aendern + r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={ + "target_type": "recurring", "target_id": 1, "kind": "percent", "value": "25.00"}) + assert r.status_code == 200 and r.json()["value"] == "25.00" + # Art auf ende wechseln (mit Datum); value wird vom Validator genullt + r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={ + "target_type": "recurring", "target_id": 1, "kind": "ende", + "end_date": "2026-08-31"}) + assert r.status_code == 200 + assert r.json()["kind"] == "ende" and r.json()["end_date"] == "2026-08-31" + # ende ohne Datum -> 422 + r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={ + "target_type": "recurring", "target_id": 1, "kind": "ende"}) + assert r.status_code == 422 + # zurueck auf remove: end_date wird genullt + r = client.patch(f"/api/scenarios/{sc['id']}/modifiers/{m['id']}", headers=H, json={ + "target_type": "recurring", "target_id": 1, "kind": "remove", + "end_date": "2026-08-31"}) + assert r.status_code == 200 and r.json()["end_date"] is None + # fremdes Szenario -> 404 + other = client.post("/api/scenarios", headers=H, json={"name": "ModPatch2"}).json() + r = client.patch(f"/api/scenarios/{other['id']}/modifiers/{m['id']}", headers=H, json={ + "target_type": "recurring", "target_id": 1, "kind": "remove"}) + assert r.status_code == 404 + + +def test_scenario_planned_patch(client, db): + sc = client.post("/api/scenarios", headers=H, json={"name": "SpiPatch"}).json() + p = client.post(f"/api/scenarios/{sc['id']}/planned", headers=H, json={ + "name": "Zufluss", "amount": "5000.00", "due": "2026-07-30"}).json() + r = client.patch(f"/api/scenarios/{sc['id']}/planned/{p['id']}", headers=H, json={ + "name": "Zufluss geaendert", "amount": "6000.00", "due": "2026-08-15"}) + assert r.status_code == 200 + body = r.json() + assert body["name"] == "Zufluss geaendert" + assert body["amount"] == "6000.00" and body["due"] == "2026-08-15" + # fremdes Szenario -> 404 + other = client.post("/api/scenarios", headers=H, json={"name": "SpiPatch2"}).json() + r = client.patch(f"/api/scenarios/{other['id']}/planned/{p['id']}", headers=H, json={ + "name": "x", "amount": "1.00", "due": "2026-08-15"}) + assert r.status_code == 404