Stop flagging SEPA members with a running Rücklastschrift-Mahnung as overdue

sepa_debit_overdue fired unconditionally for every overdue SEPA claim past the
grace period, regardless of whether a reminder had already been sent for it --
so a Rücklastschrift-Mahnung created via the reminder mechanism made no
difference to the housekeeper output. Check for an already-sent reminder whose
own payment deadline hasn't expired yet first; if there is one, show a
low-priority "Rücklastschriftklärung läuft" note instead, mirroring the
non-SEPA "reminder_awaiting_deadline" fix. Once that deadline passes without
resolution, the plain sepa_debit_overdue notice returns.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marcel Peterkau
2026-08-15 03:45:27 +02:00
co-authored by Claude Sonnet 5
parent 0641285681
commit 25f5d51d77
2 changed files with 90 additions and 1 deletions
+49 -1
View File
@@ -49,7 +49,7 @@ def evaluate(context: RuleContext) -> list[RuleAction]:
escalation = None
if days_overdue >= grace_days and not _hold_is_active(claim, context.today):
if context.member.mandate_active:
escalation = _sepa_action(context, claim, claim_id, days_overdue)
escalation = _sepa_escalation(context, claim, claim_id, days_overdue)
else:
escalation = _reminder_action(context, claim, claim_id, levels, policy)
actions.append(escalation or _overdue_action(context, claim, claim_id, due, days_overdue))
@@ -72,6 +72,13 @@ def _overdue_action(context: RuleContext, claim, claim_id: str, due: date, days_
)
def _sepa_escalation(context: RuleContext, claim, claim_id: str, days_overdue: int) -> RuleAction:
pending = _latest_pending_reminder(context.contributions.reminders, claim_id, context.today)
if pending:
return _sepa_awaiting_action(context, claim, claim_id, *pending)
return _sepa_action(context, claim, claim_id, days_overdue)
def _sepa_action(context: RuleContext, claim, claim_id: str, days_overdue: int) -> RuleAction:
balance = money_text(claim_balance(context.contributions, claim))
return task(
@@ -90,6 +97,47 @@ def _sepa_action(context: RuleContext, claim, claim_id: str, days_overdue: int)
)
def _sepa_awaiting_action(
context: RuleContext, claim, claim_id: str, reminder: dict, trigger_date: date
) -> RuleAction:
"""A Rücklastschrift-Mahnung (or any reminder) was already sent for this SEPA
claim and its own deadline hasn't expired yet -- staff already engaged, so this
must not keep nagging "check the direct debit" as if nothing had happened."""
name = str(reminder.get("name") or "Rücklastschrift")
balance = money_text(claim_balance(context.contributions, claim))
return task(
rule_id=RULE_ID,
member=context.member,
key_suffix=f"{claim_id}:sepa-awaiting",
severity="info",
code="sepa_debit_awaiting_deadline",
title=f"{context.member.display_name}: {name} versandt, Rücklastschriftklärung läuft",
detail=(
f"Forderung: {claim.get('title', claim_id)}. Offener Betrag: {balance} EUR. "
f"Zahlungsfrist bis {trigger_date.isoformat()} noch nicht abgelaufen."
),
due_date=trigger_date,
)
def _latest_pending_reminder(
reminders: list[dict], claim_id: str, today: date
) -> tuple[dict, date] | None:
best: tuple[dict, date] | None = None
for item in reminders:
if str(item.get("claim_id", "")) != claim_id or str(item.get("status", "")) != "sent":
continue
try:
deadline = date.fromisoformat(str(item.get("payment_deadline", "")))
except ValueError:
continue
if today > deadline:
continue
if best is None or deadline > best[1]:
best = (item, deadline)
return best
def _reminder_action(context: RuleContext, claim, claim_id: str, levels, policy) -> RuleAction | None:
reminders = [
item for item in context.contributions.reminders if str(item.get("claim_id", "")) == claim_id