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)