Files
CCMA/tests/test_dates.py
T
Marcel PeterkauandClaude Sonnet 5 2484a1631d Add individually agreed membership fees (contribution overrides)
Members are sometimes given a fee that deviates from the regular
schedule -- e.g. a reduced rate for students -- for a specific period.
Add a per-member "Beitrag" tab where such deviations can be recorded
with a month-granular date range (Ab/Bis), a mandatory reason, and
either a fixed annual amount or a percentage discount off whichever
base rate is in effect at the time.

Data model: ContributionData gets a contribution_overrides list, each
entry validated (month format, Bis >= Ab, non-overlapping ranges per
member, reason required) and CRUD'd through the repository
(record/update/delete/get_contribution_override), consistent with how
donations already work.

Integration: contribution_claims.py now computes each membership-fee
claim's amount month by month instead of a single rate for the whole
billing period, picking up whichever override (if any) covers each
individual month. That handles an override starting or ending mid
period correctly (e.g. a semiannual payer whose discount begins in
March) without changing behavior for members without overrides.
Already-created claims are never recalculated retroactively, matching
how changes to the global contribution rates already behave.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 00:57:51 +02:00

98 lines
3.6 KiB
Python

from datetime import date
import pytest
from ccma.domain.dates import (
DateValidationError,
age_label,
calculate_age,
format_date_for_display,
format_month_for_display,
normalize_date_input,
parse_date_input,
parse_iso_date,
parse_month_input,
validate_birth_date,
validate_member_dates,
)
def test_iso_dates_are_strict_and_real() -> None:
assert parse_iso_date("2024-02-29", "Datum") == date(2024, 2, 29)
for value in ("29.02.2024", "2024-2-29", "2023-02-29", "irgendwas"):
with pytest.raises(DateValidationError):
parse_iso_date(value, "Datum")
def test_date_input_accepts_german_and_iso_formats() -> None:
expected = date(2024, 2, 29)
assert parse_date_input("29.02.2024", "Datum") == expected
assert parse_date_input("2024-02-29", "Datum") == expected
assert normalize_date_input("29.02.2024", "Datum") == "2024-02-29"
def test_date_display_uses_system_pattern(monkeypatch) -> None:
monkeypatch.setattr("ccma.domain.dates.system_date_pattern", lambda: "%d.%m.%Y")
assert format_date_for_display("2024-02-29") == "29.02.2024"
monkeypatch.setattr("ccma.domain.dates.system_date_pattern", lambda: "%Y-%m-%d")
assert format_date_for_display("2024-02-29") == "2024-02-29"
def test_invalid_date_remains_visible_for_correction() -> None:
assert format_date_for_display("31/12/2000") == "31/12/2000"
def test_birth_date_checks_future_and_plausibility() -> None:
today = date(2026, 6, 21)
assert validate_birth_date("2000-06-22", today=today) == date(2000, 6, 22)
with pytest.raises(DateValidationError, match="Zukunft"):
validate_birth_date("2026-06-22", today=today)
with pytest.raises(DateValidationError, match="120"):
validate_birth_date("1900-01-01", today=today)
def test_member_dates_must_be_chronological() -> None:
with pytest.raises(DateValidationError, match="Aufnahmebeschluss"):
validate_member_dates(
birth_date="2000-01-01",
accepted_at="2020-01-02",
membership_started_at="2020-01-01",
today=date(2026, 6, 21),
)
with pytest.raises(DateValidationError, match="Austrittsdatum"):
validate_member_dates(
birth_date="2000-01-01",
accepted_at="2020-01-01",
membership_started_at="2020-02-01",
membership_ended_at="2020-01-31",
today=date(2026, 6, 21),
)
def test_month_input_accepts_german_and_iso_formats() -> None:
assert parse_month_input("03.2026", "Ab") == "2026-03"
assert parse_month_input("2026-03", "Ab") == "2026-03"
assert parse_month_input("", "Ab") == ""
with pytest.raises(DateValidationError, match="erforderlich"):
parse_month_input("", "Ab", allow_empty=False)
with pytest.raises(DateValidationError):
parse_month_input("13.2026", "Ab")
with pytest.raises(DateValidationError):
parse_month_input("2026-03-15", "Ab")
def test_month_display_uses_system_pattern(monkeypatch) -> None:
monkeypatch.setattr("ccma.domain.dates.system_date_pattern", lambda: "%d.%m.%Y")
assert format_month_for_display("2026-03") == "03.2026"
monkeypatch.setattr("ccma.domain.dates.system_date_pattern", lambda: "%Y-%m-%d")
assert format_month_for_display("2026-03") == "2026-03"
def test_age_calculation_and_label() -> None:
today = date(2026, 6, 21)
assert calculate_age(date(2000, 6, 21), today) == 26
assert calculate_age(date(2000, 6, 22), today) == 25
assert age_label("2000-06-21", today=today) == "Alter: 26 Jahre"
assert age_label("nein", today=today) == "UNGÜLTIGES DATUM"