Model/domain/abri/phone_selection.py
Daniel Roth 06354a166e Phone-number selection policy lives in the domain layer 🟪
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:47:37 +00:00

64 lines
2.1 KiB
Python

from dataclasses import dataclass
from typing import List, Literal, Optional, Sequence, Tuple
PhoneKind = Literal["mobile", "telephone"]
@dataclass(frozen=True)
class PhoneEntry:
"""One phone number as Abri lists it for a tenant, in document order."""
kind: PhoneKind
number: str
priority: Optional[str] = None
@dataclass(frozen=True)
class SelectedPhoneNumbers:
"""The numbers a contact carries: best of each kind plus the runner-up."""
mobile: Optional[str]
telephone: Optional[str]
secondary: Optional[str]
# TODO(#1474): confirm the priority ordering with Abri — this assumes the
# lowest numeric priority marks the preferred number, and that entries with
# a missing or non-numeric priority rank last (ties keep document order).
def _priority_sort_key(entry: PhoneEntry) -> Tuple[int, int]:
"""Sort key: lowest numeric priority first; unusable priorities last."""
try:
return (0, int((entry.priority or "").strip()))
except ValueError:
return (1, 0)
def _best(entries: List[PhoneEntry]) -> Optional[PhoneEntry]:
ranked = sorted(entries, key=_priority_sort_key) # stable: ties keep document order
return ranked[0] if ranked else None
def _number(entry: Optional[PhoneEntry]) -> Optional[str]:
return entry.number.strip() if entry is not None else None
def select_phone_numbers(entries: Sequence[PhoneEntry]) -> SelectedPhoneNumbers:
"""Best mobile, best landline, then the best of the leftovers of both kinds.
All three picks use the same rule: lowest numeric priority wins, entries
without a usable priority sort last, ties break by document order, blank
numbers are ignored.
"""
usable = [entry for entry in entries if entry.number.strip()]
best_mobile = _best([e for e in usable if e.kind == "mobile"])
best_telephone = _best([e for e in usable if e.kind == "telephone"])
runner_up = _best(
[e for e in usable if e is not best_mobile and e is not best_telephone]
)
return SelectedPhoneNumbers(
mobile=_number(best_mobile),
telephone=_number(best_telephone),
secondary=_number(runner_up),
)