mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 11:14:52 +02:00
feat: add member address and SEPA data
This commit is contained in:
@@ -32,7 +32,19 @@ DEFAULT_MEMBER_NUMBER_PATTERN = "CCMA-{number:04d}"
|
||||
|
||||
DEFAULT_CONFIGURATION = {
|
||||
"schema_version": 1,
|
||||
"organization": "Chaos Computer Club Mannheim e.V.",
|
||||
"organization": {
|
||||
"name": "Chaos Computer Club Mannheim e.V.",
|
||||
"street": "",
|
||||
"postal_code": "",
|
||||
"city": "Mannheim",
|
||||
"country": "Deutschland",
|
||||
"email": "",
|
||||
"phone": "",
|
||||
"website": "",
|
||||
"iban": "",
|
||||
"bic": "",
|
||||
"creditor_id": "",
|
||||
},
|
||||
"member_number_policy": {
|
||||
"mode": "automatic",
|
||||
"pattern": DEFAULT_MEMBER_NUMBER_PATTERN,
|
||||
@@ -165,7 +177,7 @@ class MemberRepository:
|
||||
raw = read_json(path)
|
||||
if not isinstance(raw, dict):
|
||||
raise TypeError("Wurzelelement muss ein JSON-Objekt sein")
|
||||
for section_name in ("person", "membership", "contribution_profile"):
|
||||
for section_name in ("person", "address", "banking", "membership", "contribution_profile"):
|
||||
if section_name in raw and not isinstance(raw[section_name], dict):
|
||||
raise TypeError(f"{section_name} muss ein JSON-Objekt sein")
|
||||
return Member.from_dict(raw)
|
||||
@@ -193,6 +205,7 @@ class MemberRepository:
|
||||
first_name: str,
|
||||
last_name: str,
|
||||
email: str = "",
|
||||
phone: str = "",
|
||||
birth_date: str = "",
|
||||
member_number: str = "",
|
||||
) -> Member:
|
||||
@@ -221,6 +234,7 @@ class MemberRepository:
|
||||
first_name=first_name.strip(),
|
||||
last_name=last_name.strip(),
|
||||
email=email.strip(),
|
||||
phone=phone.strip(),
|
||||
birth_date=birth_date,
|
||||
)
|
||||
write_json_atomic(directory / "member.json", member.to_dict())
|
||||
@@ -240,6 +254,10 @@ class MemberRepository:
|
||||
member.birth_date = normalize_date_input(member.birth_date, "Geburtsdatum")
|
||||
member.accepted_at = normalize_date_input(member.accepted_at, "Aufnahmebeschluss")
|
||||
member.membership_started_at = normalize_date_input(member.membership_started_at, "Mitglied seit")
|
||||
member.mandate_signed_at = normalize_date_input(member.mandate_signed_at, "Mandat erteilt am")
|
||||
member.mandate_revoked_at = normalize_date_input(
|
||||
member.mandate_revoked_at, "Mandat widerrufen am"
|
||||
)
|
||||
validate_member_dates(
|
||||
birth_date=member.birth_date,
|
||||
accepted_at=member.accepted_at,
|
||||
@@ -247,6 +265,16 @@ class MemberRepository:
|
||||
)
|
||||
except DateValidationError as exc:
|
||||
raise RepositoryError(str(exc)) from exc
|
||||
member.iban = normalize_iban(member.iban)
|
||||
member.bic = member.bic.replace(" ", "").upper()
|
||||
validate_iban(member.iban)
|
||||
validate_bic(member.bic)
|
||||
if member.mandate_active and not (
|
||||
member.iban and member.mandate_reference.strip() and member.mandate_signed_at
|
||||
):
|
||||
raise RepositoryError(
|
||||
"Ein aktives Lastschriftmandat benötigt IBAN, Mandatsreferenz und Erteilungsdatum."
|
||||
)
|
||||
if member.member_number != existing.member_number:
|
||||
self._assert_member_number_available(member.member_number, exclude_member_id=member.member_id)
|
||||
changes = self._summarize_changes(existing, member)
|
||||
@@ -721,6 +749,10 @@ class MemberRepository:
|
||||
member.last_name,
|
||||
member.display_name,
|
||||
member.email,
|
||||
member.phone,
|
||||
member.street,
|
||||
member.postal_code,
|
||||
member.city,
|
||||
member.birth_date,
|
||||
_german_date(member.birth_date),
|
||||
]
|
||||
@@ -767,6 +799,18 @@ class MemberRepository:
|
||||
config.setdefault("member_number_sequences", {})
|
||||
write_json_atomic(self.root / "repository.json", config)
|
||||
|
||||
def save_organization(self, values: dict[str, str]) -> None:
|
||||
organization = {key: str(value).strip() for key, value in values.items()}
|
||||
organization["iban"] = normalize_iban(organization.get("iban", ""))
|
||||
organization["bic"] = organization.get("bic", "").replace(" ", "").upper()
|
||||
validate_iban(organization["iban"])
|
||||
validate_bic(organization["bic"])
|
||||
if not organization.get("name"):
|
||||
raise RepositoryError("Der Vereinsname ist erforderlich.")
|
||||
config = self.get_configuration()
|
||||
config["organization"] = organization
|
||||
write_json_atomic(self.root / "repository.json", config)
|
||||
|
||||
def preview_member_number(self, pattern: str | None = None) -> str:
|
||||
selected_pattern = pattern or self.get_member_number_policy()["pattern"]
|
||||
validate_member_number_pattern(selected_pattern)
|
||||
@@ -832,12 +876,25 @@ class MemberRepository:
|
||||
"first_name": "Vorname",
|
||||
"last_name": "Nachname",
|
||||
"email": "E-Mail-Adresse",
|
||||
"phone": "Telefonnummer",
|
||||
"birth_date": "Geburtsdatum",
|
||||
"status": "Status",
|
||||
"payment_frequency": "Zahlungsweise",
|
||||
"contribution_rule_id": "Beitragsregel",
|
||||
"honorary": "Ehrenmitgliedschaft",
|
||||
"notes": "interne Notiz",
|
||||
"street": "Anschrift",
|
||||
"address_addition": "Adresszusatz",
|
||||
"postal_code": "Postleitzahl",
|
||||
"city": "Ort",
|
||||
"country": "Land",
|
||||
"account_holder": "Kontoinhaber",
|
||||
"iban": "IBAN",
|
||||
"bic": "BIC",
|
||||
"mandate_reference": "SEPA-Mandatsreferenz",
|
||||
"mandate_signed_at": "Mandat erteilt am",
|
||||
"mandate_active": "Lastschriftmandat aktiv",
|
||||
"mandate_revoked_at": "Mandat widerrufen am",
|
||||
}
|
||||
changes: list[str] = []
|
||||
for field, label in labels.items():
|
||||
@@ -854,6 +911,29 @@ class MemberRepository:
|
||||
return changes
|
||||
|
||||
|
||||
def normalize_iban(value: str) -> str:
|
||||
return "".join(value.split()).upper()
|
||||
|
||||
|
||||
def validate_iban(value: str) -> None:
|
||||
if not value:
|
||||
return
|
||||
if not 15 <= len(value) <= 34 or not value[:2].isalpha() or not value[2:].isalnum():
|
||||
raise RepositoryError("Die IBAN hat kein gültiges Format.")
|
||||
rearranged = value[4:] + value[:4]
|
||||
numeric = "".join(
|
||||
str(ord(character) - 55) if character.isalpha() else character
|
||||
for character in rearranged
|
||||
)
|
||||
if int(numeric) % 97 != 1:
|
||||
raise RepositoryError("Die IBAN-Prüfsumme ist ungültig.")
|
||||
|
||||
|
||||
def validate_bic(value: str) -> None:
|
||||
if value and (len(value) not in {8, 11} or not value.isalnum()):
|
||||
raise RepositoryError("Der BIC muss 8 oder 11 Zeichen enthalten.")
|
||||
|
||||
|
||||
def _normalize(value: str) -> str:
|
||||
normalized = unicodedata.normalize("NFKD", value.casefold().strip())
|
||||
return "".join(character for character in normalized if not unicodedata.combining(character))
|
||||
|
||||
Reference in New Issue
Block a user