"""Incoming-hire reservation reconciliation for AUTOMATION paths. The interactive grant paths (assign / claim / transfer / borrow / change_assignee) hard-refuse handing a reserved device to anyone but the reserved hire (`AssetService._ensure_not_reserved_for_other`, 409). Automation paths must NOT hard-fail — offboarding dispositions and inventory writebacks describe reality that already happened (or must happen for the leaver pipeline to complete). For those, the reservation yields: * moved to the reserved hire → the reservation is FULFILLED; * moved to anyone else → the reservation is SUPERSEDED — the earmark is cleared so the device can't show two owners, and the hardware-prep worklist resurfaces the hire as un-prepped on the next view (honest state, no silent dual-ownership). Callers MUST record the returned outcome in their own voucher / audit detail so the supersede is traceable. """ from __future__ import annotations from typing import Any, Literal ReservationOutcome = Literal["fulfilled", "superseded"] def resolve_reservation_on_ownership_move( asset: Any, new_owner_id: Any, ) -> ReservationOutcome | None: """Clear a reservation when automation moves ownership. Returns ``None`` (no reservation), ``"fulfilled"`` (moved to the reserved hire) or ``"superseded"`` (moved elsewhere — caller must surface this in its audit trail). """ if asset.reserved_for_person_id is None: return None outcome: ReservationOutcome = ( "fulfilled" if asset.reserved_for_person_id == new_owner_id else "superseded" ) asset.reserved_for_person_id = None asset.reserved_at = None return outcome