Allow editing reminders (incl. undoing "sent") and prefill a Rücklastschrift reason

A generated/sent Mahnung email couldn't be corrected before: sent reminders were
locked, so a wording mistake meant living with it. Add edit_reminder_draft() for
draft/generated reminders and revert_reminder_sent() to safely undo a "sent"
reminder (removing the fee items it booked, refusing if a payment already covers
them or a higher Mahnstufe already exists) so it can be edited and resent.

ReminderDialog now supports an edit mode (reminder=... prefills name/detail/
items/deadline/channel, level stays fixed) and claim_tab gained a "Mahnung
bearbeiten" button that reverts-then-edits for sent reminders automatically,
asking for confirmation first since it un-books the fee.

The Rücklastschrift preset now also prefills "Details" with a short explanation
("Leider konnte die Lastschrift ... nicht eingelöst werden.") since that field
goes straight into the reminder email -- the board can append the concrete
reason (mangels Deckung, Konto ungültig, ...) right there instead of the field
starting empty.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marcel Peterkau
2026-08-15 03:13:04 +02:00
co-authored by Claude Sonnet 5
parent f3bdffd347
commit 186af530b0
3 changed files with 322 additions and 23 deletions
+136
View File
@@ -281,6 +281,142 @@ def test_reminder_policy_rejects_invalid_input(tmp_path) -> None:
)
def test_edit_reminder_draft_updates_fields_but_not_level(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=[],
)
edited = repository.edit_reminder_draft(
member.member_id,
"claim-1",
draft["reminder_id"],
name="Erste Mahnung",
payment_deadline_days=10,
detail="Bitte um Ausgleich.",
items=[{"description": "Mahngebühr", "amount": "5.00"}],
)
assert edited["level"] == 1
assert edited["name"] == "Erste Mahnung"
assert edited["payment_deadline_days"] == 10
assert edited["detail"] == "Bitte um Ausgleich."
assert edited["items"] == [{"description": "Mahngebühr", "amount": "5.00"}]
def test_edit_reminder_draft_rejects_already_sent_reminder(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=[],
)
repository.mark_reminder_sent(member.member_id, "claim-1", draft["reminder_id"])
with pytest.raises(RepositoryError, match="Nur ein Entwurf"):
repository.edit_reminder_draft(
member.member_id,
"claim-1",
draft["reminder_id"],
name="Andere Bezeichnung",
payment_deadline_days=14,
)
def test_revert_reminder_sent_removes_booked_fee_and_reopens_draft(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"}],
)
sent = repository.mark_reminder_sent(member.member_id, "claim-1", draft["reminder_id"])
assert len(sent["fee_item_ids"]) == 1
_data, claim = repository.get_claim(member.member_id, "claim-1")
assert claim_total(claim) == Decimal("105.00")
reverted = repository.revert_reminder_sent(member.member_id, "claim-1", draft["reminder_id"])
assert reverted["status"] == "draft"
assert reverted["sent_at"] is None
assert reverted["payment_deadline"] is None
assert reverted["fee_item_ids"] == []
_data, claim = repository.get_claim(member.member_id, "claim-1")
assert claim_total(claim) == Decimal("100.00")
descriptions = {item["description"] for item in claim["items"]}
assert "Rücklastschriftgebühr" not in descriptions
# Now editable and re-sendable, e.g. after fixing the wording.
repository.edit_reminder_draft(
member.member_id,
"claim-1",
draft["reminder_id"],
name="Rücklastschrift",
payment_deadline_days=14,
items=[{"description": "Rücklastschriftgebühr", "amount": "7.50"}],
)
repository.mark_reminder_sent(member.member_id, "claim-1", draft["reminder_id"])
_data, claim = repository.get_claim(member.member_id, "claim-1")
assert claim_total(claim) == Decimal("107.50")
def test_revert_reminder_sent_rejects_when_higher_level_exists(tmp_path) -> None:
repository, member = _overdue_claim_repository(tmp_path)
first = repository.create_reminder_draft(
member.member_id,
"claim-1",
level=1,
name="Zahlungserinnerung",
payment_deadline_days=14,
items=[],
)
repository.mark_reminder_sent(member.member_id, "claim-1", first["reminder_id"])
repository.create_reminder_draft(
member.member_id,
"claim-1",
level=2,
name="Erste Mahnung",
payment_deadline_days=14,
items=[{"description": "Mahngebühr", "amount": "5.00"}],
)
with pytest.raises(RepositoryError, match="höhere Mahnstufe"):
repository.revert_reminder_sent(member.member_id, "claim-1", first["reminder_id"])
def test_revert_reminder_sent_rejects_when_payment_already_covers_fee(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"}],
)
repository.mark_reminder_sent(member.member_id, "claim-1", draft["reminder_id"])
repository.record_payment(
member.member_id,
"claim-1",
payment_date="2026-02-10",
amount="105.00",
allocation_amount="105.00",
)
with pytest.raises(RepositoryError, match="bereits Zahlungen verbucht"):
repository.revert_reminder_sent(member.member_id, "claim-1", draft["reminder_id"])
def test_reminder_policy_backfills_standard_fee_items_for_old_repositories(tmp_path) -> None:
repository = MemberRepository(tmp_path)
repository.initialize()