from __future__ import annotations from dataclasses import dataclass, field from datetime import date, datetime from decimal import Decimal from typing import Any def _iso_now() -> str: return datetime.now().astimezone().isoformat(timespec="seconds") MEMBERSHIP_STATUS_LABELS = { "application": "ANTRAG", "accepted_pending_payment": "ANGENOMMEN / ZAHLUNG OFFEN", "active": "AKTIV", "suspended_contribution": "RUHEND / BEITRAG", "resigned_end_of_year": "AUSTRITT ZUM JAHRESENDE", "honorary": "EHRENMITGLIED", "ended": "BEENDET", } ASSET_STATUS_LABELS = { "available": "VERFUEGBAR", "issued": "AUSGEGEBEN", "lost": "VERLOREN", "retired": "AUSGEMUSTERT", } HOUSEKEEPER_MEMBER_FIELD_LABELS = { "nickname": "Nickname", "email": "E-Mail-Adresse", "phone": "Telefonnummer", "birth_date": "Geburtsdatum", "street": "Straße und Hausnummer", "postal_code": "Postleitzahl", "city": "Ort", "country": "Land", "accepted_at": "Aufnahmebeschluss", "membership_started_at": "Mitglied seit", } DEFAULT_OPTIONAL_MEMBER_FIELDS = tuple( field for field in HOUSEKEEPER_MEMBER_FIELD_LABELS if field != "birth_date" ) def normalize_optional_member_fields(values: Any) -> tuple[str, ...]: if not isinstance(values, (list, tuple, set, frozenset)): return () selected = {str(value).strip() for value in values} return tuple(field for field in HOUSEKEEPER_MEMBER_FIELD_LABELS if field in selected) @dataclass(slots=True) class Member: member_id: str member_number: str first_name: str last_name: str nickname: str = "" email: str = "" phone: str = "" birth_date: str = "" street: str = "" address_addition: str = "" postal_code: str = "" city: str = "" country: str = "Deutschland" account_holder: str = "" iban: str = "" bic: str = "" mandate_reference: str = "" mandate_signed_at: str = "" mandate_active: bool = False mandate_revoked_at: str = "" status: str = "application" accepted_at: str = "" membership_started_at: str = "" payment_frequency: str = "annual" contribution_rule_id: str = "standard-2022" honorary: bool = False notes: str = "" created_at: str = field(default_factory=_iso_now) updated_at: str = field(default_factory=_iso_now) schema_version: int = 1 @property def display_name(self) -> str: return " ".join(part for part in (self.first_name, self.last_name) if part).strip() def to_dict(self) -> dict[str, Any]: return { "schema_version": self.schema_version, "member_id": self.member_id, "member_number": self.member_number, "person": { "first_name": self.first_name, "last_name": self.last_name, "nickname": self.nickname, "birth_date": self.birth_date, "email": self.email, "phone": self.phone, }, "address": { "street": self.street, "addition": self.address_addition, "postal_code": self.postal_code, "city": self.city, "country": self.country, }, "banking": { "account_holder": self.account_holder, "iban": self.iban, "bic": self.bic, "mandate_reference": self.mandate_reference, "mandate_signed_at": self.mandate_signed_at, "mandate_active": self.mandate_active, "mandate_revoked_at": self.mandate_revoked_at, }, "membership": { "status": self.status, "accepted_at": self.accepted_at, "started_at": self.membership_started_at, "honorary": self.honorary, }, "contribution_profile": { "rule_id": self.contribution_rule_id, "payment_frequency": self.payment_frequency, }, "notes": self.notes, "created_at": self.created_at, "updated_at": self.updated_at, } @classmethod def from_dict(cls, data: dict[str, Any]) -> Member: person = data.get("person") or {} membership = data.get("membership") or {} contribution = data.get("contribution_profile") or {} address = data.get("address") or {} banking = data.get("banking") or {} return cls( schema_version=int(data.get("schema_version", 1)), member_id=str(data["member_id"]), member_number=str(data.get("member_number", "")), first_name=str(person.get("first_name", "")), last_name=str(person.get("last_name", "")), nickname=str(person.get("nickname", "")), email=str(person.get("email", "")), phone=str(person.get("phone", "")), birth_date=str(person.get("birth_date", "")), street=str(address.get("street", "")), address_addition=str(address.get("addition", "")), postal_code=str(address.get("postal_code", "")), city=str(address.get("city", "")), country=str(address.get("country", "Deutschland")), account_holder=str(banking.get("account_holder", "")), iban=str(banking.get("iban", "")), bic=str(banking.get("bic", "")), mandate_reference=str(banking.get("mandate_reference", "")), mandate_signed_at=str(banking.get("mandate_signed_at", "")), mandate_active=bool(banking.get("mandate_active", False)), mandate_revoked_at=str(banking.get("mandate_revoked_at", "")), status=str(membership.get("status", "application")), accepted_at=str(membership.get("accepted_at", "")), membership_started_at=str(membership.get("started_at", "")), honorary=bool(membership.get("honorary", False)), contribution_rule_id=str(contribution.get("rule_id", "standard-2022")), payment_frequency=str(contribution.get("payment_frequency", "annual")), notes=str(data.get("notes", "")), created_at=str(data.get("created_at", _iso_now())), updated_at=str(data.get("updated_at", _iso_now())), ) @dataclass(slots=True) class Asset: asset_id: str label: str category: str = "" inventory_number: str = "" serial_number: str = "" status: str = "available" current_holder_member_id: str = "" deposit_amount_default: str = "0.00" notes: str = "" created_at: str = field(default_factory=_iso_now) updated_at: str = field(default_factory=_iso_now) schema_version: int = 1 def to_dict(self) -> dict[str, Any]: return { "schema_version": self.schema_version, "asset_id": self.asset_id, "label": self.label, "category": self.category, "inventory_number": self.inventory_number, "serial_number": self.serial_number, "status": self.status, "current_holder_member_id": self.current_holder_member_id, "deposit_amount_default": self.deposit_amount_default, "notes": self.notes, "created_at": self.created_at, "updated_at": self.updated_at, } @classmethod def from_dict(cls, data: dict[str, Any]) -> Asset: return cls( schema_version=int(data.get("schema_version", 1)), asset_id=str(data["asset_id"]), label=str(data.get("label", "")), category=str(data.get("category", "")), inventory_number=str(data.get("inventory_number", "")), serial_number=str(data.get("serial_number", "")), status=str(data.get("status", "available")), current_holder_member_id=str(data.get("current_holder_member_id", "")), deposit_amount_default=str(data.get("deposit_amount_default", "0.00")), notes=str(data.get("notes", "")), created_at=str(data.get("created_at", _iso_now())), updated_at=str(data.get("updated_at", _iso_now())), ) @dataclass(slots=True) class Event: event_id: str timestamp: str event_type: str summary: str actor_type: str = "system" actor_name: str = "CCMA" references: dict[str, str] = field(default_factory=dict) data: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: return { "schema_version": 1, "event_id": self.event_id, "timestamp": self.timestamp, "type": self.event_type, "actor": {"type": self.actor_type, "name": self.actor_name}, "summary": self.summary, "references": self.references, "data": self.data, } @classmethod def from_dict(cls, data: dict[str, Any]) -> Event: actor = data.get("actor") or {} return cls( event_id=str(data["event_id"]), timestamp=str(data["timestamp"]), event_type=str(data.get("type", "unknown")), summary=str(data.get("summary", "")), actor_type=str(actor.get("type", "system")), actor_name=str(actor.get("name", "CCMA")), references=dict(data.get("references") or {}), data=dict(data.get("data") or {}), ) @dataclass(slots=True) class ContributionData: claims: list[dict[str, Any]] = field(default_factory=list) payments: list[dict[str, Any]] = field(default_factory=list) credits: list[dict[str, Any]] = field(default_factory=list) allocations: list[dict[str, Any]] = field(default_factory=list) reminders: list[dict[str, Any]] = field(default_factory=list) schema_version: int = 1 def to_dict(self) -> dict[str, Any]: return { "schema_version": self.schema_version, "claims": self.claims, "payments": self.payments, "credits": self.credits, "allocations": self.allocations, "reminders": self.reminders, } @classmethod def from_dict(cls, data: dict[str, Any]) -> ContributionData: return cls( schema_version=int(data.get("schema_version", 1)), claims=list(data.get("claims") or []), payments=list(data.get("payments") or []), credits=list(data.get("credits") or []), allocations=list(data.get("allocations") or []), reminders=list(data.get("reminders") or []), ) @dataclass(frozen=True, slots=True) class HousekeeperFinding: severity: str code: str title: str detail: str member_id: str = "" asset_id: str = "" target_type: str = "member" due_date: date | None = None key: str = "" def money(value: str | int | float | Decimal) -> Decimal: return Decimal(str(value)).quantize(Decimal("0.01"))