"""Level resolver: which follow-up level should fire for this partner? Pure-Python: caller passes the aging report + the configured levels list, and we pick the highest-numbered level whose threshold is met.""" from dataclasses import dataclass @dataclass class FollowupLevelSpec: sequence: int name: str delay_days: int tone: str def __post_init__(self): if self.tone not in ('gentle', 'firm', 'legal'): raise ValueError(f"Invalid tone: {self.tone}") def resolve_level(*, aging_report, levels: list[FollowupLevelSpec]) -> FollowupLevelSpec | None: """Pick the highest-sequence level whose delay_days has been crossed by the most-overdue line in the aging report. Returns None if no overdue lines or no levels configured.""" if not levels or not aging_report: return None max_days_overdue = _max_days_overdue(aging_report) if max_days_overdue <= 0: return None levels_sorted = sorted(levels, key=lambda l: l.sequence, reverse=True) for level in levels_sorted: if level.delay_days <= max_days_overdue: return level return None def _max_days_overdue(aging_report) -> int: """Return the actual max days-overdue tracked on the report, falling back to the highest populated bucket's lower bound when an older aging report (without `max_days_overdue`) is passed in.""" tracked = getattr(aging_report, 'max_days_overdue', 0) or 0 if tracked: return tracked max_days = 0 for b in aging_report.buckets: if b.name == 'current' or b.amount <= 0: continue if b.days_max is None: max_days = max(max_days, b.days_min) else: max_days = max(max_days, b.days_min) return max_days