"""Integrationstests der Fälligkeiten-Overlays und der Saldoberechnung.""" from httpx import AsyncClient async def abo(client: AsyncClient, seeded: dict, **overrides) -> dict: payload = { "kind": "expense", "title": "Netflix", "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() async def test_bestaetigen_ohne_betrag_uebernimmt_das_soll( auth_client: AsyncClient, seeded: dict ) -> None: posten = await abo(auth_client, seeded) body = ( await auth_client.post( "/api/occurrences/confirm", json={"recurrence_id": posten["id"], "occurrence_date": "2026-02-15"}, ) ).json() assert body["actual_amount"] == "13.99" assert body["effective_amount"] == "13.99" assert body["status"] == "confirmed" async def test_auslassen_nimmt_die_faelligkeit_aus_der_rechnung( auth_client: AsyncClient, seeded: dict ) -> None: posten = await abo(auth_client, seeded) body = ( await auth_client.post( "/api/occurrences/skip", json={ "recurrence_id": posten["id"], "occurrence_date": "2026-02-15", "note": "Monat geschenkt", }, ) ).json() assert body["status"] == "skipped" assert body["effective_amount"] == "0.00" report = (await auth_client.get("/api/reports/month", params={"month": "2026-02-01"})).json() assert report["planned"]["expenses"] == "0.00" assert report["skipped_count"] == 1 async def test_zuruecksetzen_entfernt_die_abweichung( auth_client: AsyncClient, seeded: dict ) -> None: posten = await abo(auth_client, seeded) await auth_client.post( "/api/occurrences/confirm", json={ "recurrence_id": posten["id"], "occurrence_date": "2026-02-15", "actual_amount": "20.00", }, ) zurueck = ( await auth_client.post( "/api/occurrences/reset", json={"recurrence_id": posten["id"], "occurrence_date": "2026-02-15"}, ) ).json() assert zurueck["status"] == "planned" assert zurueck["actual_amount"] is None assert zurueck["occurrence_id"] is None async def test_bestaetigung_an_einem_termin_ohne_faelligkeit( auth_client: AsyncClient, seeded: dict ) -> None: posten = await abo(auth_client, seeded) antwort = await auth_client.post( "/api/occurrences/confirm", json={"recurrence_id": posten["id"], "occurrence_date": "2026-02-16"}, ) assert antwort.status_code == 422 assert antwort.json()["code"] == "occurrence_not_due" async def test_nominales_datum_bleibt_schluessel_trotz_verschiebung( auth_client: AsyncClient, seeded: dict ) -> None: """Der 01.02.2026 ist ein Sonntag; der Zahltag rutscht auf den 02.02.""" posten = await abo( auth_client, seeded, rrule="FREQ=MONTHLY;BYMONTHDAY=1", dtstart="2026-02-01", business_day_shift="next", ) faelligkeiten = ( await auth_client.get("/api/occurrences", params={"from": "2026-02-01", "to": "2026-02-28"}) ).json() assert faelligkeiten[0]["nominal_date"] == "2026-02-01" assert faelligkeiten[0]["due_date"] == "2026-02-02" # Bestätigt wird über das nominale Datum. bestaetigt = await auth_client.post( "/api/occurrences/confirm", json={"recurrence_id": posten["id"], "occurrence_date": "2026-02-01"}, ) assert bestaetigt.status_code == 200 assert bestaetigt.json()["due_date"] == "2026-02-02" async def test_verschobene_faelligkeit_zaehlt_im_zahlmonat( auth_client: AsyncClient, seeded: dict ) -> None: """Der 31.05.2026 ist ein Sonntag – die Zahlung gehört in den Juni.""" await abo( auth_client, seeded, rrule="FREQ=MONTHLY;BYMONTHDAY=-1", dtstart="2026-01-31", business_day_shift="next", ) mai = ( await auth_client.get("/api/occurrences", params={"from": "2026-05-01", "to": "2026-05-31"}) ).json() juni = ( await auth_client.get("/api/occurrences", params={"from": "2026-06-01", "to": "2026-06-30"}) ).json() assert [item["nominal_date"] for item in mai] == [] assert [item["nominal_date"] for item in juni] == ["2026-05-31", "2026-06-30"] # Nach nominalem Datum gefiltert sieht es anders aus. nominal = ( await auth_client.get( "/api/occurrences", params={"from": "2026-05-01", "to": "2026-05-31", "by_due_date": False}, ) ).json() assert [item["nominal_date"] for item in nominal] == ["2026-05-31"] async def test_faelligkeiten_lassen_sich_filtern(auth_client: AsyncClient, seeded: dict) -> None: await abo(auth_client, seeded) await abo( auth_client, seeded, title="Gehalt", kind="income", category_id=seeded["gehalt"], amount="3000.00", rrule="FREQ=MONTHLY;BYMONTHDAY=28", dtstart="2026-01-28", ) einkuenfte = ( await auth_client.get( "/api/occurrences", params={"from": "2026-02-01", "to": "2026-02-28", "kind": "income"}, ) ).json() assert [item["recurrence_title"] for item in einkuenfte] == ["Gehalt"] alle = ( await auth_client.get("/api/occurrences", params={"from": "2026-02-01", "to": "2026-02-28"}) ).json() assert len(alle) == 2 # Sortiert nach tatsächlichem Zahltag. assert [item["effective_date"] for item in alle] == ["2026-02-15", "2026-02-28"] async def test_deaktivierte_posten_erscheinen_nicht(auth_client: AsyncClient, seeded: dict) -> None: posten = await abo(auth_client, seeded) await auth_client.patch(f"/api/recurrences/{posten['id']}", json={"is_active": False}) ohne = ( await auth_client.get("/api/occurrences", params={"from": "2026-02-01", "to": "2026-02-28"}) ).json() mit = ( await auth_client.get( "/api/occurrences", params={"from": "2026-02-01", "to": "2026-02-28", "include_inactive": True}, ) ).json() assert ohne == [] assert len(mit) == 1 # --- Kontosalden --------------------------------------------------------------- async def test_saldo_beruecksichtigt_buchungen_und_bestaetigungen( auth_client: AsyncClient, seeded: dict ) -> None: konto = seeded["account_id"] posten = await abo(auth_client, seeded) await auth_client.post( "/api/transactions", json={ "kind": "expense", "title": "Wocheneinkauf", "category_id": seeded["lebensmittel"], "account_id": konto, "amount": "84.30", "booking_date": "2026-02-05", }, ) await auth_client.post( "/api/occurrences/confirm", json={ "recurrence_id": posten["id"], "occurrence_date": "2026-02-15", "actual_amount": "13.99", }, ) saldo = ( await auth_client.get(f"/api/accounts/{konto}/balance", params={"as_of": "2026-02-28"}) ).json() assert saldo["opening_balance"] == "1000.00" assert saldo["booked_transactions"] == "-84.30" assert saldo["booked_occurrences"] == "-13.99" assert saldo["balance"] == "901.71" async def test_saldo_zaehlt_nur_bis_zum_stichtag(auth_client: AsyncClient, seeded: dict) -> None: konto = seeded["account_id"] posten = await abo(auth_client, seeded) await auth_client.post( "/api/occurrences/confirm", json={"recurrence_id": posten["id"], "occurrence_date": "2026-02-15"}, ) davor = ( await auth_client.get(f"/api/accounts/{konto}/balance", params={"as_of": "2026-02-14"}) ).json() danach = ( await auth_client.get(f"/api/accounts/{konto}/balance", params={"as_of": "2026-02-15"}) ).json() assert davor["balance"] == "1000.00" assert danach["balance"] == "986.01" async def test_saldo_folgt_dem_istdatum(auth_client: AsyncClient, seeded: dict) -> None: """Wird eine Zahlung später erfasst, zählt sie erst ab dem Ist-Datum.""" konto = seeded["account_id"] posten = await abo(auth_client, seeded) await auth_client.post( "/api/occurrences/confirm", json={ "recurrence_id": posten["id"], "occurrence_date": "2026-02-15", "actual_date": "2026-03-02", }, ) februar = ( await auth_client.get(f"/api/accounts/{konto}/balance", params={"as_of": "2026-02-28"}) ).json() maerz = ( await auth_client.get(f"/api/accounts/{konto}/balance", params={"as_of": "2026-03-31"}) ).json() assert februar["balance"] == "1000.00" assert maerz["balance"] == "986.01" async def test_geplante_faelligkeiten_veraendern_den_saldo_nicht( auth_client: AsyncClient, seeded: dict ) -> None: konto = seeded["account_id"] await abo(auth_client, seeded) saldo = ( await auth_client.get(f"/api/accounts/{konto}/balance", params={"as_of": "2026-12-31"}) ).json() assert saldo["balance"] == "1000.00" async def test_abweichendes_konto_bei_der_bestaetigung( auth_client: AsyncClient, seeded: dict ) -> None: zweitkonto = ( await auth_client.post( "/api/accounts", json={ "name": "Kreditkarte", "type": "credit_card", "opening_balance": "0.00", "opening_balance_date": "2026-01-01", }, ) ).json() posten = await abo(auth_client, seeded) await auth_client.post( "/api/occurrences/confirm", json={ "recurrence_id": posten["id"], "occurrence_date": "2026-02-15", "account_id": zweitkonto["id"], }, ) giro = ( await auth_client.get( f"/api/accounts/{seeded['account_id']}/balance", params={"as_of": "2026-12-31"} ) ).json() karte = ( await auth_client.get( f"/api/accounts/{zweitkonto['id']}/balance", params={"as_of": "2026-12-31"} ) ).json() assert giro["balance"] == "1000.00" assert karte["balance"] == "-13.99"