mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-24 22:45:18 +02:00
Handle failed direct debits through the same reminder mechanism
A Rücklastschrift is still the member's own failure to ensure cover or provide valid bank details, so it belongs in the same escalation ladder as a regular reminder -- it now occupies whatever level the claim is next due for, alongside a "Rücklastschrift" preset next to the existing Mahnstufe presets. Mahnung vorbereiten no longer lets the board pick a level (it's computed automatically from what's already been sent, since the sequencing was already server-enforced) and replaces the single fee field with a small items table (Beschreibung + Betrag), so a reminder can carry several charges at once -- e.g. Rücklastschriftgebühr plus Bankgebühr plus Porto -- each landing on the claim as its own line item instead of one lump sum under a generic label. The description field offers the configured standard texts but stays free-editable. Those standard texts and amounts, plus the dunning levels themselves (name/fee/deadline), are now configurable from Optionen -> Mahnungen instead of only being editable by hand-editing repository.json, which was the case for the whole reminder policy until now. Cleaned up reminder_fee/failed_debit_fee on contribution rules while at it -- both were unused leftovers superseded by this. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
46795fc110
commit
a1719cad5e
+107
-2
@@ -1,7 +1,9 @@
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from ccma.domain.contributions import claim_total
|
||||
from ccma.domain.models import ContributionData
|
||||
from ccma.services.housekeeper import Housekeeper
|
||||
from ccma.storage.repository import MemberRepository, RepositoryError
|
||||
@@ -42,7 +44,7 @@ def test_reminder_rule_progresses_only_after_sent_deadline(tmp_path) -> None:
|
||||
level=1,
|
||||
name="Zahlungserinnerung",
|
||||
payment_deadline_days=14,
|
||||
fee="0.00",
|
||||
items=[],
|
||||
)
|
||||
findings = housekeeper.run(today=date(2026, 2, 10))
|
||||
reminder_task = next(item for item in findings if item.code == "reminder_due")
|
||||
@@ -155,7 +157,7 @@ def test_reminder_levels_cannot_be_skipped(tmp_path) -> None:
|
||||
level=2,
|
||||
name="Erste Mahnung",
|
||||
payment_deadline_days=14,
|
||||
fee="5.00",
|
||||
items=[{"description": "Mahngebühr", "amount": "5.00"}],
|
||||
)
|
||||
|
||||
|
||||
@@ -173,3 +175,106 @@ def test_payment_resolves_open_reminder_task(tmp_path) -> None:
|
||||
|
||||
findings = housekeeper.run(today=date(2026, 2, 10))
|
||||
assert not any(item.code == "reminder_due" for item in findings)
|
||||
|
||||
|
||||
def test_reminder_can_carry_multiple_fee_items(tmp_path) -> None:
|
||||
repository, member = _overdue_claim_repository(tmp_path)
|
||||
|
||||
draft = repository.create_reminder_draft(
|
||||
member.member_id,
|
||||
"claim-1",
|
||||
level=1,
|
||||
name="Rücklastschrift",
|
||||
payment_deadline_days=14,
|
||||
items=[
|
||||
{"description": "Rücklastschriftgebühr", "amount": "5.00"},
|
||||
{"description": "Bankgebühr", "amount": "3.50"},
|
||||
],
|
||||
)
|
||||
assert draft["fee_item_ids"] == []
|
||||
|
||||
sent = repository.mark_reminder_sent(member.member_id, "claim-1", draft["reminder_id"])
|
||||
assert len(sent["fee_item_ids"]) == 2
|
||||
|
||||
data, claim = repository.get_claim(member.member_id, "claim-1")
|
||||
descriptions = {item["description"] for item in claim["items"] if item["type"] == "fee"}
|
||||
assert descriptions == {"Rücklastschriftgebühr", "Bankgebühr"}
|
||||
assert claim_total(claim) == Decimal("108.50")
|
||||
|
||||
|
||||
def test_reminder_items_require_a_description(tmp_path) -> None:
|
||||
repository, member = _overdue_claim_repository(tmp_path)
|
||||
|
||||
with pytest.raises(RepositoryError, match="Beschreibung"):
|
||||
repository.create_reminder_draft(
|
||||
member.member_id,
|
||||
"claim-1",
|
||||
level=1,
|
||||
name="Rücklastschrift",
|
||||
payment_deadline_days=14,
|
||||
items=[{"description": " ", "amount": "5.00"}],
|
||||
)
|
||||
|
||||
|
||||
def test_reminder_with_zero_amount_items_creates_no_claim_item(tmp_path) -> None:
|
||||
repository, member = _overdue_claim_repository(tmp_path)
|
||||
|
||||
draft = repository.create_reminder_draft(
|
||||
member.member_id,
|
||||
"claim-1",
|
||||
level=1,
|
||||
name="Zahlungserinnerung",
|
||||
payment_deadline_days=14,
|
||||
items=[{"description": "Mahngebühr", "amount": "0.00"}],
|
||||
)
|
||||
sent = repository.mark_reminder_sent(member.member_id, "claim-1", draft["reminder_id"])
|
||||
assert sent["fee_item_ids"] == []
|
||||
_data, claim = repository.get_claim(member.member_id, "claim-1")
|
||||
assert claim_total(claim) == Decimal("100.00")
|
||||
|
||||
|
||||
def test_reminder_policy_round_trips_through_save_and_get(tmp_path) -> None:
|
||||
repository = MemberRepository(tmp_path)
|
||||
repository.initialize()
|
||||
|
||||
repository.save_reminder_policy(
|
||||
grace_days_after_due=10,
|
||||
levels=[
|
||||
{"name": "Zahlungserinnerung", "fee": "0.00", "payment_deadline_days": 10},
|
||||
{"name": "Mahnung", "fee": "7.50", "payment_deadline_days": 14},
|
||||
],
|
||||
standard_fee_items=[
|
||||
{"description": "Rücklastschriftgebühr", "default_amount": "6.00"},
|
||||
],
|
||||
)
|
||||
|
||||
policy = repository.get_reminder_policy()
|
||||
assert policy["grace_days_after_due"] == 10
|
||||
assert [level["level"] for level in policy["levels"]] == [1, 2]
|
||||
assert policy["levels"][1]["name"] == "Mahnung"
|
||||
assert policy["levels"][1]["fee"] == "7.50"
|
||||
assert policy["standard_fee_items"] == [
|
||||
{"description": "Rücklastschriftgebühr", "default_amount": "6.00"}
|
||||
]
|
||||
|
||||
|
||||
def test_reminder_policy_rejects_invalid_input(tmp_path) -> None:
|
||||
repository = MemberRepository(tmp_path)
|
||||
repository.initialize()
|
||||
|
||||
with pytest.raises(RepositoryError, match="Mindestens eine Mahnstufe"):
|
||||
repository.save_reminder_policy(grace_days_after_due=7, levels=[], standard_fee_items=[])
|
||||
|
||||
with pytest.raises(RepositoryError, match="benötigt einen Namen"):
|
||||
repository.save_reminder_policy(
|
||||
grace_days_after_due=7,
|
||||
levels=[{"name": " ", "fee": "0.00", "payment_deadline_days": 14}],
|
||||
standard_fee_items=[],
|
||||
)
|
||||
|
||||
with pytest.raises(RepositoryError, match="nicht negativ"):
|
||||
repository.save_reminder_policy(
|
||||
grace_days_after_due=7,
|
||||
levels=[{"name": "Mahnung", "fee": "-1.00", "payment_deadline_days": 14}],
|
||||
standard_fee_items=[],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user