"""Integrationstests: Posten anlegen, Fälligkeiten abrufen, bestätigen, Monatsreport.""" from httpx import AsyncClient async def anlegen(client: AsyncClient, seeded: dict, **overrides) -> dict: """Legt ein Abo an; einzelne Felder lassen sich überschreiben.""" payload = { "kind": "expense", "title": "Netflix Standard", "category_id": seeded["streaming"], "account_id": seeded["account_id"], "amount": "13.99", "rrule": "FREQ=MONTHLY;BYMONTHDAY=15", "dtstart": "2026-01-15", "business_day_shift": "none", } payload.update(overrides) antwort = await client.post("/api/recurrences", json=payload) assert antwort.status_code == 201, antwort.text return antwort.json() # --- Der geforderte Durchstich ------------------------------------------------- async def test_durchstich_anlegen_abrufen_bestaetigen_report( auth_client: AsyncClient, seeded: dict ) -> None: """Recurrence anlegen → Fälligkeiten abrufen → mit abweichendem Betrag bestätigen → der Monatsreport weist das Ist aus.""" posten = await anlegen(auth_client, seeded) assert posten["amount"] == "13.99" assert len(posten["amount_versions"]) == 1 assert posten["annual_burden"] == "167.88" # zwölf Monate à 13,99 # Fälligkeiten des ersten Quartals abrufen. faelligkeiten = ( await auth_client.get("/api/occurrences", params={"from": "2026-01-01", "to": "2026-03-31"}) ).json() assert [item["nominal_date"] for item in faelligkeiten] == [ "2026-01-15", "2026-02-15", "2026-03-15", ] assert all(item["status"] == "planned" for item in faelligkeiten) assert all(item["occurrence_id"] is None for item in faelligkeiten) # Februar mit abweichendem Betrag und Datum bestätigen. bestaetigt = await auth_client.post( "/api/occurrences/confirm", json={ "recurrence_id": posten["id"], "occurrence_date": "2026-02-15", "actual_amount": "15.49", "actual_date": "2026-02-17", "note": "Preis erhöht", }, ) assert bestaetigt.status_code == 200 body = bestaetigt.json() assert body["status"] == "confirmed" assert body["amount"] == "13.99" # Soll bleibt stehen assert body["actual_amount"] == "15.49" assert body["effective_amount"] == "15.49" assert body["effective_date"] == "2026-02-17" assert body["occurrence_id"] is not None # Der Monatsreport weist Soll und Ist getrennt aus. report = (await auth_client.get("/api/reports/month", params={"month": "2026-02-01"})).json() assert report["month"] == "2026-02-01" assert report["planned"]["expenses"] == "13.99" assert report["actual"]["expenses"] == "15.49" assert report["confirmed_count"] == 1 assert report["open_count"] == 0 # Im Januar ist nichts bestätigt – dort bleibt das Ist leer. januar = (await auth_client.get("/api/reports/month", params={"month": "2026-01-01"})).json() assert januar["planned"]["expenses"] == "13.99" assert januar["actual"]["expenses"] == "0.00" assert januar["open_count"] == 1 async def test_report_verrechnet_einkuenfte_und_fixkosten( auth_client: AsyncClient, seeded: dict ) -> None: await anlegen( auth_client, seeded, title="Gehalt", kind="income", category_id=seeded["gehalt"], amount="3200.00", rrule="FREQ=MONTHLY;BYMONTHDAY=28", dtstart="2026-01-28", ) await anlegen( auth_client, seeded, title="Miete", category_id=seeded["miete"], amount="950.00", rrule="FREQ=MONTHLY;BYMONTHDAY=1", dtstart="2026-01-01", ) await anlegen(auth_client, seeded) # Netflix – Abos zählen laut Seed als Fixkosten await anlegen( auth_client, seeded, title="Wocheneinkauf", category_id=seeded["lebensmittel"], amount="120.00", rrule="FREQ=MONTHLY;BYMONTHDAY=5", dtstart="2026-01-05", is_variable=True, ) report = (await auth_client.get("/api/reports/month", params={"month": "2026-03-10"})).json() assert report["planned"]["income"] == "3200.00" assert report["planned"]["expenses"] == "1083.99" assert report["planned"]["balance"] == "2116.01" # Miete und Netflix sind Fixkostenkategorien, Lebensmittel nicht. assert report["fixed_costs"] == "963.99" assert report["variable_costs"] == "120.00" assert report["available_after_fixed"] == "2236.01" async def test_report_vergleicht_mit_dem_vormonat(auth_client: AsyncClient, seeded: dict) -> None: posten = await anlegen(auth_client, seeded, amount="10.00") await auth_client.post( f"/api/recurrences/{posten['id']}/amount-versions", json={"amount": "20.00", "valid_from": "2026-03-01"}, ) report = (await auth_client.get("/api/reports/month", params={"month": "2026-03-01"})).json() assert report["planned"]["expenses"] == "20.00" assert report["previous_planned"]["expenses"] == "10.00" assert report["delta_to_previous"]["expenses"] == "10.00" assert report["delta_to_previous"]["balance"] == "-10.00" # --- Anlegen und Prüfen -------------------------------------------------------- async def test_ungueltige_rrule_wird_abgewiesen(auth_client: AsyncClient, seeded: dict) -> None: antwort = await auth_client.post( "/api/recurrences", json={ "kind": "expense", "title": "Kaputt", "category_id": seeded["streaming"], "account_id": seeded["account_id"], "amount": "5.00", "rrule": "FREQ=QUARTERLY", "dtstart": "2026-01-01", }, ) assert antwort.status_code == 422 assert antwort.json()["code"] == "validation_error" async def test_dtstart_in_der_rrule_wird_abgewiesen(auth_client: AsyncClient, seeded: dict) -> None: antwort = await auth_client.post( "/api/recurrences", json={ "kind": "expense", "title": "Mit DTSTART", "category_id": seeded["streaming"], "account_id": seeded["account_id"], "amount": "5.00", "rrule": "DTSTART=20260101;FREQ=MONTHLY", "dtstart": "2026-01-01", }, ) assert antwort.status_code == 422 async def test_falsche_kategorierichtung_wird_abgewiesen( auth_client: AsyncClient, seeded: dict ) -> None: antwort = await auth_client.post( "/api/recurrences", json={ "kind": "income", "title": "Gehalt in Ausgabenkategorie", "category_id": seeded["streaming"], "account_id": seeded["account_id"], "amount": "3000.00", "rrule": "FREQ=MONTHLY", "dtstart": "2026-01-01", }, ) assert antwort.status_code == 422 assert antwort.json()["code"] == "category_kind_mismatch" async def test_negativer_betrag_wird_abgewiesen(auth_client: AsyncClient, seeded: dict) -> None: antwort = await auth_client.post( "/api/recurrences", json={ "kind": "expense", "title": "Negativ", "category_id": seeded["streaming"], "account_id": seeded["account_id"], "amount": "-5.00", "rrule": "FREQ=MONTHLY", "dtstart": "2026-01-01", }, ) assert antwort.status_code == 422 # --- Preisversionen ------------------------------------------------------------ async def test_preisversion_wirkt_ab_dem_stichtag(auth_client: AsyncClient, seeded: dict) -> None: posten = await anlegen(auth_client, seeded, amount="9.99") await auth_client.post( f"/api/recurrences/{posten['id']}/amount-versions", json={"amount": "13.99", "valid_from": "2026-04-01", "note": "Preiserhöhung"}, ) vorschau = ( await auth_client.get( f"/api/recurrences/{posten['id']}/preview", params={"from": "2026-01-01", "to": "2026-06-30"}, ) ).json() betraege = {item["nominal_date"]: item["amount"] for item in vorschau} assert betraege["2026-03-15"] == "9.99" assert betraege["2026-04-15"] == "13.99" # Der Basisbetrag folgt der jüngsten Version. detail = (await auth_client.get(f"/api/recurrences/{posten['id']}")).json() assert detail["amount"] == "13.99" assert len(detail["amount_versions"]) == 2 async def test_doppelte_preisversion_wird_abgewiesen( auth_client: AsyncClient, seeded: dict ) -> None: posten = await anlegen(auth_client, seeded) antwort = await auth_client.post( f"/api/recurrences/{posten['id']}/amount-versions", json={"amount": "20.00", "valid_from": "2026-01-15"}, ) assert antwort.status_code == 409 # --- Vorschau, Raten, Verträge ------------------------------------------------- async def test_vorschau_liefert_die_naechsten_termine( auth_client: AsyncClient, seeded: dict ) -> None: posten = await anlegen(auth_client, seeded) vorschau = ( await auth_client.get( f"/api/recurrences/{posten['id']}/preview", params={"from": "2026-01-01", "to": "2026-03-31"}, ) ).json() assert [item["nominal_date"] for item in vorschau] == [ "2026-01-15", "2026-02-15", "2026-03-15", ] assert vorschau[0]["recurrence_title"] == "Netflix Standard" async def test_kredit_zeigt_restschuld_und_raten(auth_client: AsyncClient, seeded: dict) -> None: posten = await anlegen( auth_client, seeded, title="Autokredit", category_id=seeded["kredite"], amount="250.00", rrule="FREQ=MONTHLY;BYMONTHDAY=1", dtstart="2026-01-01", installments_total=36, principal_amount="9000.00", ) assert posten["installments"]["total"] == 36 assert posten["installments"]["final_due_date"] == "2028-12-01" vorschau = ( await auth_client.get( f"/api/recurrences/{posten['id']}/preview", params={"from": "2026-01-01", "to": "2032-12-31"}, ) ).json() assert len(vorschau) == 36 assert vorschau[0]["installment_number"] == 1 assert vorschau[-1]["installment_number"] == 36 async def test_vertrag_kuendigen_beendet_die_serie(auth_client: AsyncClient, seeded: dict) -> None: posten = await anlegen( auth_client, seeded, title="Handyvertrag", rrule="FREQ=MONTHLY;BYMONTHDAY=1", dtstart="2026-03-01", contract_start="2026-03-01", contract_min_term_months=24, contract_notice_period_days=90, contract_auto_renew_months=12, ) assert posten["contract_term"]["term_end"] == "2028-02-29" assert posten["contract_term"]["notice_deadline"] == "2027-12-01" assert posten["contract_term"]["renews_on"] == "2028-03-01" gekuendigt = (await auth_client.post(f"/api/recurrences/{posten['id']}/cancel")).json() assert gekuendigt["contract_cancelled_at"] == "2028-02-29" assert gekuendigt["contract_term"]["is_cancelled"] is True vorschau = ( await auth_client.get( f"/api/recurrences/{posten['id']}/preview", params={"from": "2026-01-01", "to": "2030-12-31"}, ) ).json() assert vorschau[-1]["nominal_date"] == "2028-02-01" async def test_ruecklage_wird_ausgewiesen(auth_client: AsyncClient, seeded: dict) -> None: posten = await anlegen( auth_client, seeded, title="Kfz-Versicherung", amount="612.00", rrule="FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=15", dtstart="2026-01-15", reserve_enabled=True, ) assert posten["monthly_reserve"] == "51.00" assert posten["annual_burden"] == "612.00"