Update member UI and related app changes

This commit is contained in:
Marcel Peterkau
2026-06-26 21:57:11 +02:00
parent 833075f0dc
commit 30b6d253b2
18 changed files with 490 additions and 84 deletions
+4
View File
@@ -16,6 +16,8 @@ def test_paths_and_automation_settings_round_trip(tmp_path, monkeypatch) -> None
anniversary_days_before=21,
anniversary_days_after=5,
anniversary_intervals="30D;2M;1Y;10Y",
retroactive_claims=True,
optional_member_fields=("email", "phone", "nickname"),
window_geometry="1200x800-1800+40",
window_state="maximized",
monitor_bounds=(-1920, 0, 1920, 1080),
@@ -28,6 +30,8 @@ def test_paths_and_automation_settings_round_trip(tmp_path, monkeypatch) -> None
assert raw["schema_version"] == 1
assert raw["monitor_bounds"] == [-1920, 0, 1920, 1080]
assert raw["splash_minimum_seconds"] == 0
assert raw["retroactive_claims"] is True
assert raw["optional_member_fields"] == ["nickname", "email", "phone"]
def test_splash_minimum_defaults_to_five_and_is_clamped(tmp_path, monkeypatch) -> None:
+25
View File
@@ -113,3 +113,28 @@ def test_housekeeper_reports_invalid_member_dates(tmp_path) -> None:
assert len(invalid) == 1
assert invalid[0].member_id == member.member_id
assert "Geburtsdatum" in invalid[0].detail
def test_housekeeper_can_treat_selected_member_fields_as_optional(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Optional", last_name="Fields")
member.status = "active"
repository.save_member(member)
settings = HousekeeperSettings.from_values(
birthday_days_before=0,
birthday_days_after=0,
anniversary_days_before=0,
anniversary_days_after=0,
anniversary_intervals="1Y",
optional_member_fields=("nickname", "email", "phone", "birth_date"),
)
findings = Housekeeper(repository, settings).run(today=date(2026, 6, 21))
codes = {finding.code for finding in findings}
assert "missing_birth_date" not in codes
assert "missing_member_field:nickname" not in codes
assert "missing_member_field:email" not in codes
assert "missing_member_field:phone" not in codes
assert "missing_member_field:street" in codes
+5 -1
View File
@@ -18,6 +18,7 @@ def test_repository_creates_transparent_member_record(tmp_path) -> None:
member = repository.create_member(
first_name="Ada",
last_name="Lovelace",
nickname="Enchantress",
email="ada@example.org",
birth_date="1990-12-10",
member_number="0042",
@@ -32,6 +33,7 @@ def test_repository_creates_transparent_member_record(tmp_path) -> None:
raw = json.loads((member_dir / "member.json").read_text(encoding="utf-8"))
assert raw["person"]["first_name"] == "Ada"
assert raw["person"]["nickname"] == "Enchantress"
assert raw["schema_version"] == 1
@@ -41,12 +43,13 @@ def test_search_matches_name_email_number_and_german_birth_date(tmp_path) -> Non
member = repository.create_member(
first_name="Jörg",
last_name="Müller",
nickname="Jogi",
email="joerg.mueller@example.org",
birth_date="1990-04-23",
member_number="C3-007",
)
for query in ("Jorg Muller", "mueller@example.org", "C3-007", "23.04.1990"):
for query in ("Jorg Muller", "Jogi", "mueller@example.org", "C3-007", "23.04.1990"):
assert [result.member_id for result in repository.search(query)] == [member.member_id]
@@ -138,6 +141,7 @@ def test_automatic_member_numbers_are_sequential_and_preview_does_not_consume(tm
first = repository.create_member(first_name="First", last_name="Member")
second = repository.create_member(first_name="Second", last_name="Member")
assert first.member_number == "CCMA-0001"
assert second.member_number == "CCMA-0002"
assert repository.preview_member_number() == "CCMA-0003"
+78
View File
@@ -91,6 +91,84 @@ def test_housekeeper_claim_actions_are_idempotent(tmp_path) -> None:
assert state["last_completed_run"] == "2026-04-15:000002"
def test_housekeeper_creates_membership_claims_retroactively_since_entry(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Retro", last_name="Claims", birth_date="1990-01-01")
member.status = "active"
member.accepted_at = "2024-04-15"
member.membership_started_at = "2024-04-15"
repository.save_member(member)
settings = housekeeper_module.HousekeeperSettings.from_values(
birthday_days_before=0,
birthday_days_after=0,
anniversary_days_before=0,
anniversary_days_after=0,
anniversary_intervals="1Y",
retroactive_claims=True,
)
Housekeeper(repository, settings).run(today=date(2026, 6, 21))
claims = repository.get_contributions(member.member_id).claims
claims_by_key = {claim["claim_key"]: claim for claim in claims}
assert set(claims_by_key) == {
"admission-fee",
"membership-fee:2024:annual",
"membership-fee:2025:annual",
"membership-fee:2026:annual",
}
assert claims_by_key["membership-fee:2024:annual"]["amount"] == "150.00"
def test_housekeeper_uses_pre_2022_contribution_amounts_for_legacy_years(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Legacy", last_name="Rates", birth_date="1990-01-01")
member.status = "active"
member.accepted_at = "2021-04-15"
member.membership_started_at = "2021-04-15"
repository.save_member(member)
settings = housekeeper_module.HousekeeperSettings.from_values(
birthday_days_before=0,
birthday_days_after=0,
anniversary_days_before=0,
anniversary_days_after=0,
anniversary_intervals="1Y",
retroactive_claims=True,
)
Housekeeper(repository, settings).run(today=date(2022, 6, 21))
claims = repository.get_contributions(member.member_id).claims
claims_by_key = {claim["claim_key"]: claim for claim in claims}
assert claims_by_key["membership-fee:2021:annual"]["amount"] == "90.00"
assert claims_by_key["membership-fee:2022:annual"]["amount"] == "150.00"
def test_housekeeper_does_not_create_retroactive_membership_claims_by_default(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Current", last_name="Only", birth_date="1990-01-01")
member.status = "active"
member.accepted_at = "2024-04-15"
member.membership_started_at = "2024-04-15"
repository.save_member(member)
Housekeeper(repository).run(today=date(2026, 6, 21))
claim_keys = {claim["claim_key"] for claim in repository.get_contributions(member.member_id).claims}
assert claim_keys == {
"admission-fee",
"membership-fee:2026:annual",
}
def test_housekeeper_resolves_tasks_not_seen_in_current_run(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
+43
View File
@@ -96,3 +96,46 @@ def test_german_ui_labels_round_trip_to_english_storage_keys() -> None:
assert storage_key(CLAIM_ITEM_TYPE_LABELS, "Dienstleistung") == "service"
assert display_label(MEMBERSHIP_STATUS_LABELS, "active") == "AKTIV"
assert storage_key(MEMBERSHIP_STATUS_LABELS, "EHRENMITGLIED") == "honorary"
def test_member_table_filter_only_keeps_selected_status() -> None:
from ccma.domain.models import Member
from ccma.ui.work_tabs import _filter_members, _selected_status_filter
members = [
Member("1", "0001", "Ada", "Lovelace", status="active"),
Member("2", "0002", "Grace", "Hopper", status="application"),
Member("3", "0003", "Linus", "Example", status="active"),
]
assert _selected_status_filter("Alle") == "all"
assert _selected_status_filter("AKTIV") == "active"
assert [member.member_id for member in _filter_members(members, "active")] == ["1", "3"]
assert [member.member_id for member in _filter_members(members, "all")] == ["1", "2", "3"]
def test_member_table_sort_uses_display_values() -> None:
from ccma.domain.models import Member
from ccma.ui.work_tabs import _sort_members
members = [
Member("1", "0002", "Grace", "Hopper", status="application"),
Member("2", "0001", "Ada", "Lovelace", status="active"),
Member("3", "0003", "Linus", "Example", status="honorary"),
]
assert [member.member_id for member in _sort_members(members, "number", False)] == ["2", "1", "3"]
assert [member.member_id for member in _sort_members(members, "first_name", False)] == ["2", "1", "3"]
assert [member.member_id for member in _sort_members(members, "last_name", False)] == ["3", "1", "2"]
assert [member.member_id for member in _sort_members(members, "status", False)] == ["2", "1", "3"]
def test_claim_table_sort_uses_due_date_by_raw_value() -> None:
from ccma.domain.models import ContributionData
from ccma.ui.member_tab import _claim_sort_value
data = ContributionData()
older = {"title": "Alt", "due_date": "2024-01-31", "amount": "75.00"}
newer = {"title": "Neu", "due_date": "2025-07-31", "amount": "50.00"}
assert _claim_sort_value(data, older, "due") < _claim_sort_value(data, newer, "due")