From 52f2286ee925e16518c6ba3065caaff6b524acd7 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 6 Jul 2026 13:29:38 +0000 Subject: [PATCH] =?UTF-8?q?Runner-up=20number=20fills=20the=20contact's=20?= =?UTF-8?q?secondary=20phone=20number=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- infrastructure/abri/abri_client.py | 51 ++++++++++++++++--- .../tenant_data_sync_orchestrator.py | 1 + 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/infrastructure/abri/abri_client.py b/infrastructure/abri/abri_client.py index 8594e2473..bdf246b8e 100644 --- a/infrastructure/abri/abri_client.py +++ b/infrastructure/abri/abri_client.py @@ -1,4 +1,5 @@ import xml.etree.ElementTree as ET +from dataclasses import dataclass from datetime import date from typing import List, Optional, Tuple, Union @@ -38,10 +39,45 @@ def _phone_priority(entry: ET.Element) -> Tuple[int, int]: return (1, 0) -def _select_phone_number(entries: List[ET.Element]) -> Optional[str]: - usable = [entry for entry in entries if (entry.text or "").strip()] - ranked = sorted(usable, key=_phone_priority) # stable: ties keep document order - return (ranked[0].text or "").strip() if ranked else None +def _best_phone_number(entries: List[ET.Element]) -> Optional[ET.Element]: + ranked = sorted(entries, key=_phone_priority) # stable: ties keep document order + return ranked[0] if ranked else None + + +@dataclass(frozen=True) +class _SelectedPhoneNumbers: + mobile: Optional[str] + telephone: Optional[str] + secondary: Optional[str] + + +def _select_phone_numbers(tenant_element: ET.Element) -> _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 tenant_element + if entry.tag in ("Mobile", "Telephone") and (entry.text or "").strip() + ] + + best_mobile = _best_phone_number([e for e in usable if e.tag == "Mobile"]) + best_telephone = _best_phone_number([e for e in usable if e.tag == "Telephone"]) + runner_up = _best_phone_number( + [e for e in usable if e is not best_mobile and e is not best_telephone] + ) + + def _text(entry: Optional[ET.Element]) -> Optional[str]: + return (entry.text or "").strip() if entry is not None else None + + return _SelectedPhoneNumbers( + mobile=_text(best_mobile), + telephone=_text(best_telephone), + secondary=_text(runner_up), + ) class AbriClient: @@ -211,12 +247,13 @@ class AbriClient: def _parse_tenant(element: ET.Element) -> Tenant: # Data minimisation: person_title, main-contact and the per-person # reference (the element text) are deliberately not read. + numbers = _select_phone_numbers(element) return Tenant( forenames=(element.get("forenames") or "").strip(), surname=(element.get("surname") or "").strip(), - mobile=_select_phone_number(element.findall("Mobile")), - telephone=_select_phone_number(element.findall("Telephone")), - secondary_number=None, + mobile=numbers.mobile, + telephone=numbers.telephone, + secondary_number=numbers.secondary, vulnerabilities=(), ) diff --git a/orchestration/tenant_data_sync_orchestrator.py b/orchestration/tenant_data_sync_orchestrator.py index 038489ea7..c5a5ad557 100644 --- a/orchestration/tenant_data_sync_orchestrator.py +++ b/orchestration/tenant_data_sync_orchestrator.py @@ -12,6 +12,7 @@ def _contact_properties(tenant: Tenant) -> Dict[str, str]: "lastname": tenant.surname, "mobilephone": tenant.mobile, "phone": tenant.telephone, + "secondary_phone_number": tenant.secondary_number, } properties = {name: value for name, value in candidates.items() if value} # Always written, so "not vulnerable" is distinguishable from "not synced".