Fix housekeeper false-positive overdue errors and ended-member birthday/anniversary noise

An overdue claim whose reminder had already been sent, with that reminder's own
payment deadline still running, fell through to the plain "überfällig" error
finding whenever reminder_due's escalation check returned None to signal "not
yet time for the next Mahnstufe" -- that None was indistinguishable from "never
handled at all" to the caller, so it looked neglected even though a reminder was
already out. Give that case its own low-priority "reminder_awaiting_deadline"
finding instead of silently falling back to the overdue-error path.

Birthday and membership-anniversary findings also kept firing for members whose
status is "ended", which doesn't make sense once the membership itself is over.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marcel Peterkau
2026-08-15 03:20:18 +02:00
co-authored by Claude Sonnet 5
parent 4ecdf87d05
commit 56a3f5f037
5 changed files with 95 additions and 1 deletions
+29 -1
View File
@@ -105,8 +105,14 @@ def _reminder_action(context: RuleContext, claim, claim_id: str, levels, policy)
return None
level = int(next_level.get("level", 0))
trigger_date = _trigger_date(claim, sent_levels, level, policy)
if not trigger_date or context.today < trigger_date:
if not trigger_date:
return None
if context.today < trigger_date:
# A reminder for the previous level was already sent and its own payment
# deadline hasn't expired yet -- this is being handled, not neglected, so it
# must not show up as the same "error" severity as a plain unaddressed overdue
# claim (that fallback is what the caller uses when this returns None).
return _awaiting_deadline_action(context, claim, claim_id, level, sent_levels, trigger_date)
draft_exists = any(
int(item.get("level", 0)) == level and str(item.get("status", "draft")) in {"draft", "generated"}
for item in reminders
@@ -134,6 +140,28 @@ def _reminder_action(context: RuleContext, claim, claim_id: str, levels, policy)
)
def _awaiting_deadline_action(
context: RuleContext, claim, claim_id: str, level: int, sent_levels: dict, trigger_date: date
) -> RuleAction:
previous_level = level - 1
previous = sent_levels.get(previous_level) or {}
name = str(previous.get("name") or f"Mahnstufe {previous_level}")
balance = money_text(claim_balance(context.contributions, claim))
return task(
rule_id=RULE_ID,
member=context.member,
key_suffix=f"{claim_id}:level-{previous_level}:awaiting",
severity="info",
code="reminder_awaiting_deadline",
title=f"{context.member.display_name}: {name} versandt, Frist läuft noch",
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 _trigger_date(claim, sent_levels, level: int, policy) -> date | None:
if level == 1:
try: