feat: Szenario-Einträge je Stichtag mit Zeitraum und Art Neuer Betrag (A14-T2)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 13:00:18 +02:00
co-authored by Claude Opus 5.5
parent b0a1ba80a1
commit aaf05b9209
7 changed files with 144 additions and 25 deletions
+22
View File
@@ -172,3 +172,25 @@ def test_category_rows_nur_kosten_der_kategorien():
assert [r.name for r in category_rows(rs, {"Wohnen", "Versicherung"})] == ["Miete", "Police", "Strom"]
assert category_rows(rs, set()) == []
assert category_rows(rs, {FREI}) == []
def test_stichtag_ist_faelligkeitstag_im_monat():
r = PlainRecurring(1, "R1", D("-200.00"), "monthly", 14, None, None, None)
mods = [PlainModifier("recurring", 1, "percent", D("50"), start_date=date(2030, 10, 16))]
assert by_name(rows(month=date(2030, 10, 1), recurring=[r], modifiers=mods)) == {"R1": D("-200.00")}
assert by_name(rows(month=date(2030, 11, 1), recurring=[r], modifiers=mods)) == {"R1": D("-100.00")}
def test_stichtag_monatsende_geklemmt():
# Fällig am 31.: im Februar ist der Stichtag der 28.02. (liegt im Zeitraum).
r = PlainRecurring(1, "R1", D("-100.00"), "monthly", 31, None, None, None)
mods = [PlainModifier("recurring", 1, "remove", D("0"), end_date=date(2030, 2, 28))]
assert rows(month=date(2030, 2, 1), recurring=[r], modifiers=mods) == []
assert by_name(rows(month=date(2030, 3, 1), recurring=[r], modifiers=mods)) == {"R1": D("-100.00")}
def test_szenario_zahlung_ohne_recurring_id():
r = PlainRecurring(None, "S", D("-30.00"), "monthly", 1, None, None, 1)
rs = rows(recurring=[r])
assert by_name(rs) == {"S": D("-30.00")}
assert (rs[0].recurring_id, rs[0].category) == (None, "Wohnen")
+7
View File
@@ -27,3 +27,10 @@ def test_monthly_leap_year_clamping():
def test_quarterly_clamping():
got = occurrences("quarterly", 31, date(2026, 1, 1), date(2026, 12, 31))
assert got == [date(2026, 1, 31), date(2026, 4, 30), date(2026, 7, 31), date(2026, 10, 31)]
def test_due_in_month_klemmt_auf_monatsende():
from app.engine.recurrence import due_in_month
assert due_in_month(date(2030, 2, 1), 31) == date(2030, 2, 28)
assert due_in_month(date(2032, 2, 10), 30) == date(2032, 2, 29)
assert due_in_month(date(2030, 10, 1), 14) == date(2030, 10, 14)
+67
View File
@@ -105,3 +105,70 @@ def test_ende_modifier_aendert_betraege_nicht():
m = PlainModifier("recurring", 1, "ende", Decimal("50"), date(2026, 12, 31))
flows = build_cashflows([r], [], [], [], [m], date(2026, 7, 1), date(2026, 8, 31))
assert all(a == Decimal("-2000.00") for _, a in flows)
D = Decimal
def _rec(amount="-100.00", due_day=14, id=1, cat=None):
return PlainRecurring(id, "R", D(amount), "monthly", due_day, None, None, cat)
def test_zeitraum_grenzen_inklusiv():
mods = [PlainModifier("recurring", 1, "percent", D("50"),
end_date=date(2030, 3, 14), start_date=date(2030, 2, 14))]
flows = build_cashflows([_rec()], [], [], [], mods, date(2030, 1, 1), date(2030, 4, 30))
assert flows == [(date(2030, 1, 14), D("-100.00")),
(date(2030, 2, 14), D("-50.00")),
(date(2030, 3, 14), D("-50.00")),
(date(2030, 4, 14), D("-100.00"))]
def test_ab_stichtag_erst_folgemonat():
# Fällig am 14., gültig ab 16.10. -> Oktober voll, ab November halbiert.
mods = [PlainModifier("recurring", 1, "percent", D("50"), start_date=date(2030, 10, 16))]
flows = build_cashflows([_rec("-200.00")], [], [], [], mods,
date(2030, 10, 1), date(2030, 12, 31))
assert flows == [(date(2030, 10, 14), D("-200.00")),
(date(2030, 11, 14), D("-100.00")),
(date(2030, 12, 14), D("-100.00"))]
def test_neuer_betrag_dann_prozent():
mods = [PlainModifier("recurring", 1, "set", D("-80.00"), start_date=date(2030, 2, 1)),
PlainModifier("recurring", 1, "percent", D("25"))]
flows = build_cashflows([_rec(due_day=1)], [], [], [], mods,
date(2030, 1, 1), date(2030, 2, 28))
assert flows == [(date(2030, 1, 1), D("-75.00")), (date(2030, 2, 1), D("-60.00"))]
def test_entfaellt_nur_im_zeitraum():
mods = [PlainModifier("recurring", 1, "remove", D("0"),
end_date=date(2030, 2, 28), start_date=date(2030, 2, 1))]
flows = build_cashflows([_rec(due_day=1)], [], [], [], mods,
date(2030, 1, 1), date(2030, 3, 31))
assert [d for d, _ in flows] == [date(2030, 1, 1), date(2030, 3, 1)]
def test_szenario_zahlung_ohne_id_nur_von_kategorie_getroffen():
rec = [PlainRecurring(None, "S", D("-100.00"), "monthly", 1, None, None, 7)]
mods = [PlainModifier("recurring", 1, "remove", D("0")),
PlainModifier("category", 7, "percent", D("10"))]
flows = build_cashflows(rec, [], [], [], mods, date(2030, 1, 1), date(2030, 1, 31))
assert flows == [(date(2030, 1, 1), D("-90.00"))]
def test_einmalposten_stichtag_ist_faelligkeit():
planned = [PlainPlanned("E", D("-100.00"), date(2030, 1, 31), 7),
PlainPlanned("F", D("-100.00"), date(2030, 2, 1), 7)]
mods = [PlainModifier("category", 7, "percent", D("50"), start_date=date(2030, 2, 1))]
flows = build_cashflows([], planned, [], [], mods, date(2030, 1, 1), date(2030, 2, 28))
assert flows == [(date(2030, 1, 31), D("-100.00")), (date(2030, 2, 1), D("-50.00"))]
def test_ende_unveraendert_mit_zeitraum_feldern():
# "ende" wirkt weiter nur über effective_end, nie als Betragsänderung.
mods = [PlainModifier("recurring", 1, "ende", D("0"), date(2030, 2, 15))]
flows = build_cashflows([_rec(due_day=1)], [], [], [], mods,
date(2030, 1, 1), date(2030, 4, 30))
assert flows == [(date(2030, 1, 1), D("-100.00")), (date(2030, 2, 1), D("-100.00"))]
+2 -2
View File
@@ -12,8 +12,8 @@ from app.services.projection_service import load_scenario_inputs
def test_modifikator_helfer_oeffentlich():
mods = [PlainModifier("recurring", 1, "percent", Decimal("10")),
PlainModifier("recurring", 1, "ende", Decimal("0"), date(2030, 6, 30))]
assert modified_amount(Decimal("-100.00"), None, 1, mods) == Decimal("-90.00")
assert modified_amount(Decimal("-100.00"), None, 2, mods) == Decimal("-100.00")
assert modified_amount(Decimal("-100.00"), None, 1, mods, on=date(2030, 1, 1)) == Decimal("-90.00")
assert modified_amount(Decimal("-100.00"), None, 2, mods, on=date(2030, 1, 1)) == Decimal("-100.00")
r = PlainRecurring(1, "R", Decimal("-100.00"), "monthly", 1, None,
date(2030, 12, 31), None)
assert effective_end(r, mods) == date(2030, 6, 30)