feat(recurrence): Expansions-Engine für wiederkehrende Posten
Datenbankfreie Kernberechnung auf Basis schlanker Protokolle, sodass sowohl ORM-Objekte als auch Testdatenklassen verarbeitet werden. - expand() mit voller RFC-5545-Unterstützung, Fensterbegrenzung und Overlays - expand_by_due_date() für Sichten, die nach dem tatsächlichen Zahltag gruppieren - Werktagsverschiebung über den NRW-Feiertagskalender, nominales Datum als Schlüssel - Preishistorie, Ratenzahlungen, Vertragsfristen und Rücklagenberechnung - validate_rrule() und next_dates() für Eingabeprüfung und Terminvorschau - 81 Unit-Tests, davon alle in Abschnitt 2 geforderten Fälle Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Nachweis, dass die Engine unmittelbar auf den ORM-Objekten arbeitet.
|
||||
|
||||
Die Objekte werden nur im Speicher gebaut – es wird keine Session benötigt.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from app.models import AmountVersion, Occurrence, Recurrence
|
||||
from app.models.enums import BusinessDayShift, EntryKind, OccurrenceStatus
|
||||
from app.services.recurrence import expand, monthly_reserve, notice_deadline
|
||||
|
||||
|
||||
def test_engine_arbeitet_direkt_auf_dem_ormmodell() -> None:
|
||||
recurrence = Recurrence(
|
||||
id=7,
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Netflix",
|
||||
category_id=1,
|
||||
account_id=2,
|
||||
amount=Decimal("13.99"),
|
||||
is_variable=False,
|
||||
currency="EUR",
|
||||
rrule="FREQ=MONTHLY;BYMONTHDAY=1",
|
||||
dtstart=date(2026, 1, 1),
|
||||
business_day_shift=BusinessDayShift.NEXT,
|
||||
holiday_region="DE-NW",
|
||||
reserve_enabled=False,
|
||||
is_active=True,
|
||||
)
|
||||
recurrence.amount_versions = [
|
||||
AmountVersion(amount=Decimal("9.99"), valid_from=date(2026, 1, 1)),
|
||||
AmountVersion(amount=Decimal("13.99"), valid_from=date(2026, 4, 1)),
|
||||
]
|
||||
overlay = Occurrence(
|
||||
id=99,
|
||||
recurrence_id=7,
|
||||
occurrence_date=date(2026, 2, 1),
|
||||
status=OccurrenceStatus.CONFIRMED,
|
||||
planned_amount=Decimal("9.99"),
|
||||
actual_amount=Decimal("9.99"),
|
||||
actual_date=date(2026, 2, 2),
|
||||
)
|
||||
|
||||
result = expand(recurrence, date(2026, 1, 1), date(2026, 5, 31), occurrences=[overlay])
|
||||
|
||||
assert [item.nominal_date.month for item in result] == [1, 2, 3, 4, 5]
|
||||
assert result[0].recurrence_id == 7
|
||||
assert result[0].account_id == 2
|
||||
assert result[0].due_date == date(2026, 1, 2) # 01.01. ist Feiertag
|
||||
assert [item.amount for item in result] == [
|
||||
Decimal("9.99"),
|
||||
Decimal("9.99"),
|
||||
Decimal("9.99"),
|
||||
Decimal("13.99"),
|
||||
Decimal("13.99"),
|
||||
]
|
||||
assert result[1].status is OccurrenceStatus.CONFIRMED
|
||||
assert result[1].occurrence_id == 99
|
||||
|
||||
|
||||
def test_vertrags_und_ruecklagenlogik_auf_dem_ormmodell() -> None:
|
||||
recurrence = Recurrence(
|
||||
kind=EntryKind.EXPENSE,
|
||||
title="Kfz-Versicherung",
|
||||
category_id=1,
|
||||
account_id=1,
|
||||
amount=Decimal("612.00"),
|
||||
is_variable=False,
|
||||
currency="EUR",
|
||||
rrule="FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=15",
|
||||
dtstart=date(2026, 1, 15),
|
||||
business_day_shift=BusinessDayShift.NEXT,
|
||||
holiday_region="DE-NW",
|
||||
contract_start=date(2026, 1, 1),
|
||||
contract_min_term_months=12,
|
||||
contract_notice_period_days=30,
|
||||
contract_auto_renew_months=12,
|
||||
reserve_enabled=True,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
assert monthly_reserve(recurrence, as_of=date(2026, 1, 1)) == Decimal("51.00")
|
||||
assert notice_deadline(recurrence, as_of=date(2026, 6, 1)) == date(2026, 12, 1)
|
||||
Reference in New Issue
Block a user