104 lines
3.3 KiB
Python
104 lines
3.3 KiB
Python
"""Business rules and orchestration for repair workflows."""
|
|
|
|
from uuid import UUID
|
|
|
|
from app.repairs.exceptions import (
|
|
ActiveRepairConflict,
|
|
IncompatibleMinerLifecycle,
|
|
InconsistentRepairState,
|
|
RepairActorRequired,
|
|
RepairMinerNotFound,
|
|
)
|
|
from app.repairs.models import (
|
|
BeginRepairRequest,
|
|
BeginRepairResult,
|
|
InitialRepairNotePayload,
|
|
RepairStartedPayload,
|
|
)
|
|
from app.repairs.repository import RepairRepository
|
|
|
|
|
|
ELIGIBLE_LIFECYCLE_STATUSES = frozenset({"active", "spare"})
|
|
REPAIR_LIFECYCLE_STATUS = "in_repair"
|
|
|
|
|
|
class RepairService:
|
|
"""Explicit operational repair workflows independent of FastAPI."""
|
|
|
|
def __init__(self, repository: RepairRepository):
|
|
self.repository = repository
|
|
|
|
@staticmethod
|
|
def normalize_note(note: str | None) -> str | None:
|
|
if note is None:
|
|
return None
|
|
normalized = str(note).strip()
|
|
return normalized or None
|
|
|
|
def begin_repair(
|
|
self,
|
|
request: BeginRepairRequest,
|
|
*,
|
|
actor_user_id: UUID | None,
|
|
) -> BeginRepairResult:
|
|
if actor_user_id is None:
|
|
raise RepairActorRequired()
|
|
|
|
note = self.normalize_note(request.notes)
|
|
|
|
with self.repository.transaction() as repository:
|
|
miner = repository.lock_miner_for_update(request.miner_id)
|
|
if not miner:
|
|
raise RepairMinerNotFound(request.miner_id)
|
|
|
|
lifecycle_status = str(miner["lifecycle_status"])
|
|
active_repair = repository.get_active_repair(request.miner_id)
|
|
if active_repair:
|
|
raise ActiveRepairConflict(request.miner_id)
|
|
if lifecycle_status == REPAIR_LIFECYCLE_STATUS:
|
|
raise InconsistentRepairState(request.miner_id)
|
|
if lifecycle_status not in ELIGIBLE_LIFECYCLE_STATUSES:
|
|
raise IncompatibleMinerLifecycle(
|
|
request.miner_id,
|
|
lifecycle_status,
|
|
)
|
|
|
|
repair = repository.create_repair(
|
|
request.miner_id,
|
|
request.reason,
|
|
actor_user_id,
|
|
)
|
|
started_payload = RepairStartedPayload(
|
|
reason=request.reason,
|
|
from_lifecycle=lifecycle_status,
|
|
)
|
|
repository.create_event(
|
|
repair["id"],
|
|
sequence=1,
|
|
event_type="REPAIR_STARTED",
|
|
created_by=actor_user_id,
|
|
payload=started_payload.model_dump(mode="json"),
|
|
)
|
|
if note is not None:
|
|
note_payload = InitialRepairNotePayload(note=note)
|
|
repository.create_event(
|
|
repair["id"],
|
|
sequence=2,
|
|
event_type="INITIAL_NOTE",
|
|
created_by=actor_user_id,
|
|
payload=note_payload.model_dump(mode="json"),
|
|
)
|
|
repository.set_miner_lifecycle(
|
|
request.miner_id,
|
|
REPAIR_LIFECYCLE_STATUS,
|
|
)
|
|
|
|
return BeginRepairResult(
|
|
repair_id=repair["id"],
|
|
miner_id=repair["miner_id"],
|
|
status=repair["status"],
|
|
reason=repair["reason"],
|
|
opened_at=repair["opened_at"],
|
|
opened_by=repair["opened_by"],
|
|
)
|