diff --git a/backend/app/services/budget.py b/backend/app/services/budget.py index 3bc6640..8c35f2f 100644 --- a/backend/app/services/budget.py +++ b/backend/app/services/budget.py @@ -56,10 +56,16 @@ def period_bounds(label: str, start_day: int) -> tuple[date, date]: def period_of(d: date, start_day: int) -> str: - """Label des Budget-Monats, in den ein Datum fällt.""" + """Label des Budget-Monats, in den ein Datum fällt. + + Der Starttag wird auf den letzten Tag des Monats gekappt – einen 31. + Februar gibt es nicht. Ohne dieselbe Kappung wie in ``period_bounds`` fiele + der 28. Februar bei Starttag 31 in einen Monat, der ihn gar nicht enthält: + Die Ausgabe waere in keiner Auswertung aufgetaucht. + """ if start_day <= 1: return f"{d.year:04d}-{d.month:02d}" - if d.day >= start_day: + if d >= _clamp_day(d.year, d.month, start_day): nxt = date(d.year, d.month, 1) + relativedelta(months=1) return f"{nxt.year:04d}-{nxt.month:02d}" return f"{d.year:04d}-{d.month:02d}" diff --git a/backend/tests/test_budget.py b/backend/tests/test_budget.py index 8aa4301..37f5132 100644 --- a/backend/tests/test_budget.py +++ b/backend/tests/test_budget.py @@ -126,3 +126,44 @@ def test_planned_income(admin): assert line["planned_cents"] == 300000 assert line["received_cents"] == 300000 assert v["totals"]["planned_income"] >= 300000 + + +def test_jeder_tag_liegt_in_genau_seinem_budget_monat(): + """period_of und period_bounds muessen dasselbe sagen. + + Der Starttag wird auf den letzten Tag des Monats gekappt – einen 31. + Februar gibt es nicht. Ohne dieselbe Kappung auf beiden Seiten fiel der + 28. Februar bei Starttag 31 in einen Monat, der ihn nicht enthaelt: Die + Ausgabe tauchte in keiner Auswertung auf. + """ + from datetime import date, timedelta + + from app.services.budget import period_bounds, period_of + + fehler = [] + for start_day in range(1, 32): + tag = date(2024, 1, 1) # Schaltjahr eingeschlossen + while tag < date(2028, 1, 1): + von, bis = period_bounds(period_of(tag, start_day), start_day) + if not von <= tag <= bis: + fehler.append((start_day, tag.isoformat())) + tag += timedelta(days=1) + assert not fehler, f"Tage ausserhalb ihres Budget-Monats: {fehler[:5]}" + + +def test_budget_monate_stossen_lueckenlos_aneinander(): + """Kein Tag darf zwischen zwei Monate fallen und keiner in beide.""" + from datetime import timedelta + + from app.services.budget import period_bounds, shift_label + + for start_day in (1, 15, 25, 28, 29, 31): + label = "2026-01" + for _ in range(24): + _, bis = period_bounds(label, start_day) + naechstes = shift_label(label, 1) + von_naechstes, _ = period_bounds(naechstes, start_day) + assert von_naechstes == bis + timedelta(days=1), ( + f"Starttag {start_day}: {label} endet {bis}, " + f"{naechstes} beginnt {von_naechstes}") + label = naechstes