from collections.abc import Mapping from datetime import datetime from typing import Optional from app.dao.binding import BindingDAO class BindingError(ValueError): def __init__(self, message: str, status_code: int = 400) -> None: super().__init__(message) self.status_code = status_code class BindingService: def __init__(self, db): self.dao = BindingDAO(db) def _ensure_bindable_device(self, device_id: str, serial_number: str) -> None: row = self.dao.get_device_auth(device_id) if row is None: raise BindingError("device not found in device_auth", status_code=404) if str(row["serial_number"]) != serial_number: raise BindingError("serial_number does not match device_id", status_code=400) if int(row["is_active"]) != 1: raise BindingError("device is inactive", status_code=400) def start_bind( self, user_id: int, device_id: str, serial_number: str, child_id: int | None = None, ) -> tuple[str, datetime]: self._ensure_bindable_device(device_id, serial_number) bind_token = self.dao.start_bind(user_id, device_id, child_id) return bind_token, datetime.utcnow() def confirm_bind(self, bind_token: str, user_id: int) -> Mapping: session = self.dao.get_session(bind_token, user_id) if not session: raise ValueError("Bind session not found") if datetime.utcnow() > session["expires_at"]: raise ValueError("Bind session expired") if session["status"] != 1: raise ValueError("Bind session already processed") self.dao.confirm_bind(session["id"], session["device_id"], session["target_child_id"], user_id) return {"device_id": session["device_id"], "child_id": session["target_child_id"]} def get_binding(self, device_id: str, user_id: int) -> Optional[Mapping]: return self.dao.get_by_device(device_id, user_id) def get_current_binding(self, user_id: int) -> Optional[Mapping]: return self.dao.get_current_by_user(user_id) def list_bindings(self, user_id: int, limit: int = 20, cursor: int = None) -> tuple[list, bool]: rows = self.dao.list_by_user(user_id, limit, cursor) has_more = len(rows) > limit rows = rows[:limit] return rows, has_more def direct_bind( self, device_id: str, serial_number: str, child_id: int | None, user_id: int, ) -> Mapping: self._ensure_bindable_device(device_id, serial_number) self.dao.direct_bind(device_id, child_id, user_id) return {"device_id": device_id, "child_id": child_id} def set_binding_child(self, device_id: str, child_id: int, user_id: int) -> Mapping: ok = self.dao.set_binding_child(device_id=device_id, child_id=child_id, user_id=user_id) if not ok: raise ValueError("binding not found") return {"device_id": device_id, "child_id": child_id} def unbind(self, device_id: str, user_id: int) -> bool: return self.dao.unbind(device_id, user_id) def list_history(self, device_id: str, limit: int = 20, cursor: datetime = None) -> tuple[list, bool]: rows = self.dao.list_history(device_id, limit, cursor) has_more = len(rows) > limit rows = rows[:limit] return rows, has_more