# -*- coding: utf-8 -*- # hub/ticket/domain/task.py """Доменная сущность задачи Ticket и сериализация её состояния.""" from __future__ import annotations from dataclasses import dataclass from datetime import datetime from typing import Any, Mapping from .ticket_types import TicketTaskSnapshot def _normalize_datetime(value: Any) -> datetime | None: """Преобразовать строку или datetime в datetime.""" if value is None or value == "": return None if isinstance(value, datetime): return value if not isinstance(value, str): return None try: if "T" in value: return datetime.fromisoformat(value.replace("Z", "+00:00")) return datetime.strptime(value, "%Y-%m-%d %H:%M:%S") except ValueError: return None def _normalize_color(value: Any) -> str: """Нормализовать представление цвета до hex-строки.""" if isinstance(value, str) and value.startswith("#") and len(value) >= 4: return value name_getter = getattr(value, "name", None) if callable(name_getter): try: normalized = name_getter() if isinstance(normalized, str) and normalized.startswith("#"): return normalized except Exception as _exc: return "#FFFFFF" return "#FFFFFF" def _normalize_int(value: Any, default: int | None = None) -> int | None: """Нормализовать число, сохранив валидное значение 0.""" if value is None or value == "": return default try: return int(value) except (TypeError, ValueError): return default @dataclass(slots=True) class TicketTask: """Каноническая доменная задача Ticket.""" task_id: int location: str state_code: int state_name: str action_text: str = "" color_hex: str = "#FFFFFF" created_at: datetime | None = None completed_at: datetime | None = None refused_from_state: int | None = None refusal_reason: str = "" assigned_specialist: str = "" specialist_photo: str = "" diagnostic_report_signed: bool = False repair_report_signed: bool = False acceptance_report_signed: bool = False sequence_number: int = 0 @classmethod def from_record(cls, record: Mapping[str, Any]) -> TicketTask | None: """Создать задачу из записи файлового хранилища.""" raw_task_id = record.get("button_id") if raw_task_id is None: return None try: task_id = int(raw_task_id) except (TypeError, ValueError): return None return cls( task_id=task_id, location=str(record.get("location", "")), state_code=_normalize_int(record.get("state"), 1) or 0, state_name=str(record.get("state_name", "")), action_text=str(record.get("action", "")), color_hex=_normalize_color(record.get("color", "#FFFFFF")), created_at=_normalize_datetime(record.get("created_time")) or datetime.now(), completed_at=_normalize_datetime(record.get("completed_time")), refused_from_state=_normalize_int(record.get("refused_from_state")), refusal_reason=str(record.get("refusal_reason", "")), assigned_specialist=str(record.get("assigned_specialist", "")), specialist_photo=str(record.get("specialist_photo", "")), diagnostic_report_signed=bool(record.get("diagnostic_report_signed", False)), repair_report_signed=bool(record.get("repair_report_signed", False)), acceptance_report_signed=bool(record.get("acceptance_report_signed", False)), sequence_number=_normalize_int(record.get("sequence_number"), 0) or 0, ) @classmethod def from_snapshot(cls, snapshot: TicketTaskSnapshot) -> TicketTask: """Создать доменную сущность из snapshot.""" return cls( task_id=snapshot.task_id, location=snapshot.location, state_code=snapshot.state_code, state_name=snapshot.state_name, action_text=snapshot.action_text, color_hex=snapshot.color_hex, created_at=snapshot.created_at, completed_at=snapshot.completed_at, refused_from_state=snapshot.refused_from_state, refusal_reason=snapshot.refusal_reason, assigned_specialist=snapshot.assigned_specialist, specialist_photo=snapshot.specialist_photo, diagnostic_report_signed=snapshot.diagnostic_report_signed, repair_report_signed=snapshot.repair_report_signed, acceptance_report_signed=snapshot.acceptance_report_signed, sequence_number=snapshot.sequence_number, ) def to_record(self) -> dict[str, Any]: """Преобразовать доменную сущность в запись для JSON.""" return { "button_id": self.task_id, "location": self.location, "state": self.state_code, "action": self.action_text, "state_name": self.state_name, "color": self.color_hex, "created_time": self.created_at, "completed_time": self.completed_at, "refused_from_state": self.refused_from_state, "refusal_reason": self.refusal_reason, "assigned_specialist": self.assigned_specialist, "specialist_photo": self.specialist_photo, "diagnostic_report_signed": self.diagnostic_report_signed, "repair_report_signed": self.repair_report_signed, "acceptance_report_signed": self.acceptance_report_signed, "sequence_number": self.sequence_number, } def to_snapshot(self) -> TicketTaskSnapshot: """Вернуть неизменяемый снимок задачи для внешних слоёв.""" return TicketTaskSnapshot( task_id=self.task_id, location=self.location, state_code=self.state_code, state_name=self.state_name, action_text=self.action_text, color_hex=self.color_hex, created_at=self.created_at, completed_at=self.completed_at, refused_from_state=self.refused_from_state, refusal_reason=self.refusal_reason, assigned_specialist=self.assigned_specialist, specialist_photo=self.specialist_photo, diagnostic_report_signed=self.diagnostic_report_signed, repair_report_signed=self.repair_report_signed, acceptance_report_signed=self.acceptance_report_signed, sequence_number=self.sequence_number, )