feat: add per-member payment frequencies

This commit is contained in:
Marcel Peterkau
2026-07-30 19:17:26 +02:00
parent 6d9c50b148
commit c3cdf71506
8 changed files with 168 additions and 6 deletions
+23
View File
@@ -40,9 +40,32 @@ def test_repository_creates_transparent_member_record(tmp_path) -> None:
assert raw["person"]["nickname"] == "Enchantress"
assert raw["schema_version"] == 1
assert raw["membership"]["ended_at"] == ""
assert raw["contribution_profile"]["payment_frequency"] == "semiannual"
assert raw["content_hash"]
def test_existing_member_without_payment_frequency_defaults_to_semiannual(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Legacy", last_name="Member")
member_path = repository.members_root / member.member_id / "member.json"
raw = json.loads(member_path.read_text(encoding="utf-8"))
raw["contribution_profile"].pop("payment_frequency")
member_path.write_text(json.dumps(raw), encoding="utf-8")
assert repository.get_member(member.member_id).payment_frequency == "semiannual"
def test_repository_rejects_unknown_payment_frequency(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(first_name="Invalid", last_name="Frequency")
member.payment_frequency = "weekly"
with pytest.raises(RepositoryError, match="Ungültige Zahlweise"):
repository.save_member(member)
def test_search_matches_name_email_number_and_german_birth_date(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
+42
View File
@@ -1,6 +1,7 @@
import json
import shutil
from datetime import date
from decimal import Decimal
import pytest
@@ -96,6 +97,43 @@ def test_housekeeper_claim_actions_are_idempotent(tmp_path) -> None:
assert state["last_completed_run"] == "2026-04-15:000002"
@pytest.mark.parametrize(
("frequency", "expected_suffixes", "expected_amount"),
[
("monthly", [f"month-{month:02d}" for month in range(1, 13)], "12.50"),
("quarterly", [f"quarter-{quarter}" for quarter in range(1, 5)], "37.50"),
("semiannual", ["first-half", "second-half"], "75.00"),
("annual", ["annual"], "150.00"),
],
)
def test_housekeeper_creates_claims_for_each_payment_frequency(
tmp_path, frequency, expected_suffixes, expected_amount
) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
member = repository.create_member(
first_name=frequency,
last_name="Frequency",
birth_date="1990-01-01",
)
member.status = "active"
member.accepted_at = "2026-01-01"
member.membership_started_at = "2026-01-01"
member.payment_frequency = frequency
repository.save_member(member)
Housekeeper(repository).run(today=date(2026, 12, 31))
membership_claims = [
claim
for claim in repository.get_contributions(member.member_id).claims
if claim.get("type") == "membership_fee"
]
assert [claim["claim_key"].rsplit(":", 1)[-1] for claim in membership_claims] == expected_suffixes
assert {claim["amount"] for claim in membership_claims} == {expected_amount}
assert sum(Decimal(claim["amount"]) for claim in membership_claims) == Decimal("150.00")
def test_housekeeper_creates_membership_claims_retroactively_since_entry(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()
@@ -103,6 +141,7 @@ def test_housekeeper_creates_membership_claims_retroactively_since_entry(tmp_pat
member.status = "active"
member.accepted_at = "2024-04-15"
member.membership_started_at = "2024-04-15"
member.payment_frequency = "annual"
repository.save_member(member)
settings = housekeeper_module.HousekeeperSettings.from_values(
@@ -138,6 +177,7 @@ def test_housekeeper_uses_pre_2022_contribution_amounts_for_legacy_years(tmp_pat
member.status = "active"
member.accepted_at = "2021-04-15"
member.membership_started_at = "2021-04-15"
member.payment_frequency = "annual"
repository.save_member(member)
settings = housekeeper_module.HousekeeperSettings.from_values(
@@ -165,6 +205,7 @@ def test_housekeeper_does_not_create_retroactive_membership_claims_by_default(tm
member.status = "active"
member.accepted_at = "2024-04-15"
member.membership_started_at = "2024-04-15"
member.payment_frequency = "annual"
repository.save_member(member)
Housekeeper(repository).run(today=date(2026, 6, 21))
@@ -185,6 +226,7 @@ def test_housekeeper_does_not_create_claims_due_after_membership_end(tmp_path) -
member.accepted_at = "2024-01-01"
member.membership_started_at = "2024-01-01"
member.membership_ended_at = "2026-12-31"
member.payment_frequency = "annual"
repository.save_member(member)
Housekeeper(repository).run(today=date(2026, 6, 21))
+22
View File
@@ -5,6 +5,7 @@ from email.parser import BytesParser
from email.policy import default
from ccma.domain.models import ContributionData
from ccma.services.housekeeper import Housekeeper
from ccma.services.sepa import PAIN_NAMESPACE, _safe, csv_text, pain008_bytes, pending_direct_debits
from ccma.services.sepa_mail import debit_mail_bytes, generate_debit_mails
from ccma.storage.repository import MemberRepository
@@ -61,6 +62,27 @@ def test_pending_debits_select_due_open_balance(tmp_path):
assert debits[0].claim_ids == ("due",)
def test_pending_debits_follow_members_monthly_payment_frequency(tmp_path):
repository, member = _repository(tmp_path)
member.accepted_at = "2025-01-01"
member.membership_started_at = "2025-01-01"
member.payment_frequency = "monthly"
repository.save_member(member)
repository.save_contributions(member.member_id, ContributionData())
Housekeeper(repository).run(today=date(2026, 3, 31))
debits, warnings = pending_direct_debits(
repository,
due_from=date(2026, 1, 1),
due_until=date(2026, 3, 31),
)
assert warnings == []
assert len(debits) == 1
assert debits[0].amount == Decimal("37.50")
assert len(debits[0].claim_ids) == 3
def test_pending_debits_exclude_claims_before_lower_date(tmp_path):
repository, _member = _repository(tmp_path)