# -*- coding: utf-8 -*- # hub/ticket/domain/ticket_state_service.py """Доменный сервис переходов и представления состояния Ticket.""" from __future__ import annotations from dataclasses import dataclass from datetime import datetime from .location_catalog import LocationCatalog from .task import TicketTask from .ticket_constants import ( HARDWARE_SIGNAL_ADVANCE, HARDWARE_SIGNAL_INITIALIZE, STATE_ARCHIVED, STATE_COMPLETED, STATE_CONFIRMATION, STATE_IN_PROGRESS, STATE_REFUSED, STATE_TODO, TICKET_STATE_ACTIONS, TICKET_STATE_COLORS, TICKET_STATE_NAMES, ) from .ticket_transition_policy import TicketTransitionPolicy STATE_NAMES = TICKET_STATE_NAMES STATE_COLORS = TICKET_STATE_COLORS @dataclass(frozen=True, slots=True) class TicketTransitionResult: """Результат обработки доменного перехода Ticket.""" changed: bool task: TicketTask | None message: str blocked_reason: str = "" class TicketStateService: """Каноническая state-machine Ticket без зависимости от GUI и COM-сервиса.""" def __init__( self, location_catalog: LocationCatalog | None = None, transition_policy: TicketTransitionPolicy | None = None, ): self._location_catalog = location_catalog or LocationCatalog() self._transition_policy = transition_policy or TicketTransitionPolicy() def handle_hardware_signal( self, button_id: int, hardware_state: int, current_task: TicketTask | None, ) -> TicketTransitionResult: """Обработать аппаратный сигнал и вычислить доменный результат.""" if hardware_state == HARDWARE_SIGNAL_INITIALIZE: return TicketTransitionResult( changed=False, task=current_task, message=f"Получен запрос инициализации для кнопки {button_id}.", ) if hardware_state not in HARDWARE_SIGNAL_ADVANCE: return TicketTransitionResult( changed=False, task=current_task, message=f"Неизвестное аппаратное состояние: {hardware_state:02X}.", blocked_reason="unsupported_hardware_signal", ) return self.advance_task(button_id, current_task) def advance_task(self, button_id: int, current_task: TicketTask | None) -> TicketTransitionResult: """Перевести задачу в следующее допустимое состояние.""" if current_task is None: task = self._build_task_for_state(button_id, STATE_TODO) return TicketTransitionResult( changed=True, task=task, message=f"Создана новая задача #{button_id} в состоянии '{task.state_name}'.", ) if current_task.state_code == STATE_ARCHIVED: task = self._reset_task_cycle(current_task, STATE_TODO) return TicketTransitionResult( changed=True, task=task, message=f"Архивная задача #{button_id} начала новый цикл.", ) if current_task.state_code == STATE_REFUSED: task = self._reset_task_cycle(current_task, STATE_TODO) return TicketTransitionResult( changed=True, task=task, message=f"Задача #{button_id} сброшена из отказа в новое выполнение.", ) if current_task.state_code == STATE_COMPLETED: task = self._reset_task_cycle(current_task, STATE_TODO) return TicketTransitionResult( changed=True, task=task, message=f"Выполненная задача #{button_id} начала новый цикл.", ) decision = self._transition_policy.can_advance(current_task) if not decision.allowed: return TicketTransitionResult( changed=False, task=current_task, message=f"Переход задачи #{button_id} отклонён.", blocked_reason=decision.reason, ) next_state = self._next_state(current_task.state_code) task = self._clone_with_state(current_task, next_state) return TicketTransitionResult( changed=True, task=task, message=( f"Задача #{button_id} перешла из состояния " f"'{STATE_NAMES[current_task.state_code]}' в '{task.state_name}'." ), ) def mark_task_as_refused( self, button_id: int, current_task: TicketTask | None, refusal_reason: str = "", ) -> TicketTransitionResult: """Перевести задачу в состояние отказа.""" task = current_task or self._build_task_for_state(button_id, STATE_TODO) refused_task = self._clone_with_state(task, STATE_REFUSED) refused_task.refused_from_state = task.state_code refused_task.refusal_reason = str(refusal_reason or "").strip() return TicketTransitionResult( changed=True, task=refused_task, message=f"Задача #{button_id} переведена в отказ.", ) def move_task_to_archive( self, button_id: int, current_task: TicketTask | None, ) -> TicketTransitionResult: """Переместить задачу в архив как отдельное доменное правило.""" task = current_task or self._build_task_for_state(button_id, STATE_TODO) archived_task = self._clone_with_state(task, STATE_ARCHIVED) return TicketTransitionResult( changed=True, task=archived_task, message=f"Задача #{button_id} перемещена в архив.", ) def _next_state(self, current_state: int) -> int: if current_state == STATE_TODO: return STATE_IN_PROGRESS if current_state == STATE_IN_PROGRESS: return STATE_CONFIRMATION if current_state == STATE_CONFIRMATION: return STATE_COMPLETED return current_state def _build_task_for_state(self, button_id: int, state_code: int) -> TicketTask: """Создать новую задачу для button_id в заданном состоянии.""" return TicketTask( task_id=button_id, location=self._location_catalog.get_location(button_id), state_code=state_code, state_name=STATE_NAMES[state_code], action_text=TICKET_STATE_ACTIONS[state_code], color_hex=STATE_COLORS[state_code], created_at=datetime.now(), completed_at=datetime.now() if state_code == STATE_COMPLETED else None, ) def _clone_with_state(self, task: TicketTask, state_code: int) -> TicketTask: """Склонировать задачу с новым доменным состоянием.""" return TicketTask( task_id=task.task_id, location=task.location, state_code=state_code, state_name=STATE_NAMES[state_code], action_text=TICKET_STATE_ACTIONS[state_code], color_hex=STATE_COLORS[state_code], created_at=task.created_at, completed_at=datetime.now() if state_code == STATE_COMPLETED else task.completed_at, refused_from_state=task.refused_from_state, refusal_reason=task.refusal_reason, assigned_specialist=task.assigned_specialist, specialist_photo=task.specialist_photo, diagnostic_report_signed=task.diagnostic_report_signed, repair_report_signed=task.repair_report_signed, acceptance_report_signed=task.acceptance_report_signed, sequence_number=task.sequence_number, ) def _reset_task_cycle(self, task: TicketTask, state_code: int) -> TicketTask: """Начать новый цикл задачи на основе архивной или отказной записи.""" fresh_task = self._build_task_for_state(task.task_id, state_code) fresh_task.location = task.location return fresh_task