From 7a0cdbc04eddf3a34e1e2461d481c356a88c6ea7 Mon Sep 17 00:00:00 2001 From: Marcel Peterkau Date: Thu, 30 Jul 2026 19:35:34 +0200 Subject: [PATCH] feat: add application date to members --- src/ccma/assets/CHANGELOG.json | 1 + src/ccma/domain/models.py | 3 +++ src/ccma/storage/repository.py | 7 +++++++ src/ccma/ui/dialogs.py | 2 ++ src/ccma/ui/member_tab.py | 2 ++ tests/test_repository.py | 14 ++++++++++++++ 6 files changed, 29 insertions(+) diff --git a/src/ccma/assets/CHANGELOG.json b/src/ccma/assets/CHANGELOG.json index 0c8f51c..d90f8c1 100644 --- a/src/ccma/assets/CHANGELOG.json +++ b/src/ccma/assets/CHANGELOG.json @@ -3,6 +3,7 @@ "version": "0.2.0", "date": "upcoming", "changes": [ + "Mitgliedsanträge und Mitgliederakten enthalten jetzt ein separates Feld für das Antragsdatum.", "Eigentum und aktueller Gewahrsam von Assets werden getrennt dokumentiert; neben Vereinseigentum kann nun auch Eigentum von Mitgliedern oder externen Personen zentral erfasst werden.", "Mitgliedseigentum wird per UUID mit der Mitgliedsakte verknüpft und erscheint dort gemeinsam mit den vom Verein erhaltenen Assets in getrennt verständlichen Ansichten.", "Standort, Übergabedatum, Zustand und Wert eines Assets können für Übergabe- und Versicherungsnachweise festgehalten werden; Eigentumsänderungen erscheinen zusätzlich in der Chronik.", diff --git a/src/ccma/domain/models.py b/src/ccma/domain/models.py index b512eb3..0391b81 100644 --- a/src/ccma/domain/models.py +++ b/src/ccma/domain/models.py @@ -88,6 +88,7 @@ class Member: mandate_active: bool = False mandate_revoked_at: str = "" status: str = "application" + application_date: str = "" accepted_at: str = "" membership_started_at: str = "" membership_ended_at: str = "" @@ -134,6 +135,7 @@ class Member: }, "membership": { "status": self.status, + "application_date": self.application_date, "accepted_at": self.accepted_at, "started_at": self.membership_started_at, "ended_at": self.membership_ended_at, @@ -178,6 +180,7 @@ class Member: mandate_active=bool(banking.get("mandate_active", False)), mandate_revoked_at=str(banking.get("mandate_revoked_at", "")), status=str(membership.get("status", "application")), + application_date=str(membership.get("application_date", "")), accepted_at=str(membership.get("accepted_at", "")), membership_started_at=str(membership.get("started_at", "")), membership_ended_at=str(membership.get("ended_at", "")), diff --git a/src/ccma/storage/repository.py b/src/ccma/storage/repository.py index 98ade57..bd08120 100644 --- a/src/ccma/storage/repository.py +++ b/src/ccma/storage/repository.py @@ -294,6 +294,7 @@ class MemberRepository: email: str = "", phone: str = "", birth_date: str = "", + application_date: str = "", member_number: str = "", ) -> Member: with self._member_number_lock(): @@ -304,6 +305,7 @@ class MemberRepository: email=email, phone=phone, birth_date=birth_date, + application_date=application_date, member_number=member_number, ) @@ -316,12 +318,14 @@ class MemberRepository: email: str, phone: str, birth_date: str, + application_date: str, member_number: str, ) -> Member: if not first_name.strip() or not last_name.strip(): raise RepositoryError("Vorname und Nachname sind erforderlich.") try: birth_date = normalize_date_input(birth_date, "Geburtsdatum") + application_date = normalize_date_input(application_date, "Antragsdatum") validate_member_dates(birth_date=birth_date) except DateValidationError as exc: raise RepositoryError(str(exc)) from exc @@ -348,6 +352,7 @@ class MemberRepository: email=email.strip(), phone=phone.strip(), birth_date=birth_date, + application_date=application_date, ) write_json_atomic(directory / "member.json", member.to_dict()) write_json_atomic(directory / "contributions.json", ContributionData().to_dict()) @@ -364,6 +369,7 @@ class MemberRepository: existing = self.get_member(member.member_id) try: member.birth_date = normalize_date_input(member.birth_date, "Geburtsdatum") + member.application_date = normalize_date_input(member.application_date, "Antragsdatum") member.accepted_at = normalize_date_input(member.accepted_at, "Aufnahmebeschluss") member.membership_started_at = normalize_date_input(member.membership_started_at, "Mitglied seit") member.membership_ended_at = normalize_date_input(member.membership_ended_at, "Austrittsdatum") @@ -1889,6 +1895,7 @@ class MemberRepository: "email": "E-Mail-Adresse", "phone": "Telefonnummer", "birth_date": "Geburtsdatum", + "application_date": "Antragsdatum", "membership_ended_at": "Austrittsdatum", "status": "Status", "payment_frequency": "Zahlungsweise", diff --git a/src/ccma/ui/dialogs.py b/src/ccma/ui/dialogs.py index fa74a5a..c2c5ae4 100644 --- a/src/ccma/ui/dialogs.py +++ b/src/ccma/ui/dialogs.py @@ -41,6 +41,7 @@ class NewMemberDialog(tk.Toplevel): "email", "phone", "birth_date", + "application_date", "member_number", ) } @@ -63,6 +64,7 @@ class NewMemberDialog(tk.Toplevel): ("E-Mail-Adresse", "email"), ("Telefonnummer", "phone"), (f"Geburtsdatum ({date_input_hint()})", "birth_date"), + (f"Antragsdatum ({date_input_hint()})", "application_date"), ] if self.number_policy["mode"] == "manual": fields.append(("Mitgliedsnummer *", "member_number")) diff --git a/src/ccma/ui/member_tab.py b/src/ccma/ui/member_tab.py index a7f5cc9..0e43bc5 100644 --- a/src/ccma/ui/member_tab.py +++ b/src/ccma/ui/member_tab.py @@ -170,6 +170,7 @@ class MemberTab(ttk.Frame): ("E-Mail-Adresse", "email"), ("Telefonnummer", "phone"), (f"Geburtsdatum ({date_input_hint()})", "birth_date"), + (f"Antragsdatum ({date_input_hint()})", "application_date"), (f"Aufnahmebeschluss ({date_input_hint()})", "accepted_at"), (f"Mitglied seit ({date_input_hint()})", "membership_started_at"), (f"Austrittsdatum ({date_input_hint()})", "membership_ended_at"), @@ -464,6 +465,7 @@ class MemberTab(ttk.Frame): ) date_fields = { "birth_date", + "application_date", "accepted_at", "membership_started_at", "membership_ended_at", diff --git a/tests/test_repository.py b/tests/test_repository.py index 9113779..55fa331 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -25,6 +25,7 @@ def test_repository_creates_transparent_member_record(tmp_path) -> None: nickname="Enchantress", email="ada@example.org", birth_date="1990-12-10", + application_date="30.07.2026", member_number="0042", ) @@ -38,6 +39,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["membership"]["application_date"] == "2026-07-30" assert raw["schema_version"] == 1 assert raw["membership"]["ended_at"] == "" assert raw["content_hash"] @@ -121,6 +123,18 @@ def test_repository_accepts_local_date_input_and_rejects_invalid_dates(tmp_path) repository.create_member(first_name="Invalid", last_name="Date", birth_date="31.02.2000") +def test_existing_member_without_application_date_stays_empty(tmp_path) -> None: + repository = MemberRepository(tmp_path) + repository.initialize() + member = repository.create_member(first_name="Legacy", last_name="Member") + path = repository.members_root / member.member_id / "member.json" + raw = json.loads(path.read_text(encoding="utf-8")) + raw["membership"].pop("application_date") + path.write_text(json.dumps(raw), encoding="utf-8") + + assert repository.get_member(member.member_id).application_date == "" + + def test_repository_reports_empty_contributions_file(tmp_path) -> None: repository = MemberRepository(tmp_path) repository.initialize()