mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 19:26:53 +02:00
feat: add staged reminder workflow
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
from datetime import date, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from ccma.domain.models import ContributionData
|
||||
from ccma.services.housekeeper import Housekeeper
|
||||
from ccma.storage.repository import MemberRepository, RepositoryError
|
||||
|
||||
|
||||
def _overdue_claim_repository(tmp_path):
|
||||
repository = MemberRepository(tmp_path)
|
||||
repository.initialize()
|
||||
member = repository.create_member(first_name="Reminder", last_name="Test", birth_date="1990-01-01")
|
||||
repository.save_contributions(
|
||||
member.member_id,
|
||||
ContributionData(
|
||||
claims=[
|
||||
{
|
||||
"claim_id": "claim-1",
|
||||
"claim_key": "overdue-test",
|
||||
"title": "Offene Forderung",
|
||||
"amount": "100.00",
|
||||
"due_date": "2026-01-31",
|
||||
"status": "open",
|
||||
}
|
||||
]
|
||||
),
|
||||
)
|
||||
return repository, member
|
||||
|
||||
|
||||
def test_reminder_rule_progresses_only_after_sent_deadline(tmp_path) -> None:
|
||||
repository, member = _overdue_claim_repository(tmp_path)
|
||||
housekeeper = Housekeeper(repository)
|
||||
findings = housekeeper.run(today=date(2026, 2, 10))
|
||||
reminder_task = next(item for item in findings if item.code == "reminder_due")
|
||||
assert "Zahlungserinnerung" in reminder_task.title
|
||||
|
||||
draft = repository.create_reminder_draft(
|
||||
member.member_id,
|
||||
"claim-1",
|
||||
level=1,
|
||||
name="Zahlungserinnerung",
|
||||
payment_deadline_days=14,
|
||||
fee="0.00",
|
||||
)
|
||||
findings = housekeeper.run(today=date(2026, 2, 10))
|
||||
reminder_task = next(item for item in findings if item.code == "reminder_due")
|
||||
assert "wartet auf Versand" in reminder_task.title
|
||||
|
||||
sent = repository.mark_reminder_sent(member.member_id, "claim-1", draft["reminder_id"])
|
||||
deadline = date.fromisoformat(sent["payment_deadline"])
|
||||
assert not any(
|
||||
item.code == "reminder_due" for item in housekeeper.run(today=deadline - timedelta(days=1))
|
||||
)
|
||||
findings = housekeeper.run(today=deadline)
|
||||
next_task = next(item for item in findings if item.code == "reminder_due")
|
||||
assert "Erste Mahnung" in next_task.title
|
||||
|
||||
|
||||
def test_dunning_hold_suppresses_and_then_restores_task(tmp_path) -> None:
|
||||
repository, member = _overdue_claim_repository(tmp_path)
|
||||
housekeeper = Housekeeper(repository)
|
||||
repository.set_dunning_hold(
|
||||
member.member_id,
|
||||
"claim-1",
|
||||
active=True,
|
||||
reason="Betrag wird geklärt",
|
||||
)
|
||||
|
||||
assert not any(item.code == "reminder_due" for item in housekeeper.run(today=date(2026, 2, 10)))
|
||||
with pytest.raises(RepositoryError, match="Mahnsperre aktiv"):
|
||||
repository.create_reminder_draft(
|
||||
member.member_id,
|
||||
"claim-1",
|
||||
level=1,
|
||||
name="Zahlungserinnerung",
|
||||
payment_deadline_days=14,
|
||||
)
|
||||
repository.set_dunning_hold(member.member_id, "claim-1", active=False)
|
||||
assert any(item.code == "reminder_due" for item in housekeeper.run(today=date(2026, 2, 10)))
|
||||
|
||||
|
||||
def test_draft_can_be_cancelled_but_sent_reminder_cannot(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,
|
||||
)
|
||||
repository.cancel_reminder(member.member_id, "claim-1", draft["reminder_id"])
|
||||
data = repository.get_contributions(member.member_id)
|
||||
assert data.reminders[0]["status"] == "cancelled"
|
||||
|
||||
second = repository.create_reminder_draft(
|
||||
member.member_id,
|
||||
"claim-1",
|
||||
level=1,
|
||||
name="Zahlungserinnerung",
|
||||
payment_deadline_days=14,
|
||||
)
|
||||
repository.mark_reminder_sent(member.member_id, "claim-1", second["reminder_id"])
|
||||
with pytest.raises(RepositoryError, match="bereits versandte"):
|
||||
repository.cancel_reminder(member.member_id, "claim-1", second["reminder_id"])
|
||||
|
||||
|
||||
def test_reminder_levels_cannot_be_skipped(tmp_path) -> None:
|
||||
repository, member = _overdue_claim_repository(tmp_path)
|
||||
|
||||
with pytest.raises(RepositoryError, match="Mahnstufe 1 wurde noch nicht versandt"):
|
||||
repository.create_reminder_draft(
|
||||
member.member_id,
|
||||
"claim-1",
|
||||
level=2,
|
||||
name="Erste Mahnung",
|
||||
payment_deadline_days=14,
|
||||
fee="5.00",
|
||||
)
|
||||
|
||||
|
||||
def test_payment_resolves_open_reminder_task(tmp_path) -> None:
|
||||
repository, member = _overdue_claim_repository(tmp_path)
|
||||
housekeeper = Housekeeper(repository)
|
||||
assert any(item.code == "reminder_due" for item in housekeeper.run(today=date(2026, 2, 10)))
|
||||
repository.record_payment(
|
||||
member.member_id,
|
||||
"claim-1",
|
||||
payment_date="2026-02-10",
|
||||
amount="100.00",
|
||||
allocation_amount="100.00",
|
||||
)
|
||||
|
||||
findings = housekeeper.run(today=date(2026, 2, 10))
|
||||
assert not any(item.code == "reminder_due" for item in findings)
|
||||
Reference in New Issue
Block a user