430 lines
12 KiB
Python
430 lines
12 KiB
Python
"""Typed contracts for the Repairs domain."""
|
|
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from typing import Any, Literal
|
|
from uuid import UUID
|
|
|
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator
|
|
from app.components.models import ComponentType
|
|
|
|
|
|
RepairReason = Literal[
|
|
"HARDWARE_FAILURE",
|
|
"LOW_HASHRATE",
|
|
"CONTROL_BOARD",
|
|
"HASHBOARD",
|
|
"PSU",
|
|
"FAN",
|
|
"OVERHEATING",
|
|
"NETWORKING",
|
|
"PREVENTIVE_MAINTENANCE",
|
|
"INSPECTION",
|
|
"OTHER",
|
|
]
|
|
RepairStatus = Literal[
|
|
"OPEN",
|
|
"IN_PROGRESS",
|
|
"WAITING_FOR_PARTS",
|
|
"READY_FOR_TEST",
|
|
"COMPLETED",
|
|
"CANCELLED",
|
|
]
|
|
RepairEventType = Literal[
|
|
"REPAIR_STARTED",
|
|
"INITIAL_NOTE",
|
|
"REPAIR_COMPLETED",
|
|
"CONTROL_BOARD_REPLACED",
|
|
"REPAIR_PART_RECORDED",
|
|
"REPAIR_PART_VOIDED",
|
|
]
|
|
RepairEligibleLifecycle = Literal["active", "spare"]
|
|
RepairPostLifecycle = Literal["active", "spare"]
|
|
RepairMinerLifecycle = Literal["active", "spare", "retired", "in_repair"]
|
|
|
|
|
|
class BeginRepairRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
miner_id: UUID
|
|
reason: RepairReason
|
|
notes: str | None = Field(default=None, max_length=4000)
|
|
|
|
|
|
class RepairCompleteRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
target_lifecycle: RepairPostLifecycle
|
|
completion_summary: str | None = Field(default=None, max_length=4000)
|
|
|
|
|
|
ControlBoardReplacementReason = Literal[
|
|
"failed",
|
|
"upgrade",
|
|
"diagnostic_swap",
|
|
"preventive_maintenance",
|
|
"temporary_donor",
|
|
"unknown",
|
|
]
|
|
RemovedBoardDisposition = Literal[
|
|
"repair_inventory",
|
|
"sent_for_repair",
|
|
"scrapped",
|
|
"retained_for_diagnosis",
|
|
"returned_to_vendor",
|
|
"unknown",
|
|
]
|
|
|
|
|
|
class ControlBoardReplacementRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
new_mac_address: str = Field(min_length=1, max_length=64)
|
|
new_control_board_serial: str | None = Field(..., max_length=255)
|
|
board_model: str | None = Field(default=None, max_length=255)
|
|
board_revision: str | None = Field(default=None, max_length=255)
|
|
firmware_version: str | None = Field(default=None, max_length=255)
|
|
removed_board_disposition: RemovedBoardDisposition
|
|
replacement_reason: ControlBoardReplacementReason
|
|
notes: str | None = Field(default=None, max_length=4000)
|
|
|
|
|
|
class RepairStartedPayload(BaseModel):
|
|
reason: RepairReason
|
|
from_lifecycle: RepairEligibleLifecycle
|
|
to_lifecycle: Literal["in_repair"] = "in_repair"
|
|
|
|
|
|
class InitialRepairNotePayload(BaseModel):
|
|
note: str = Field(min_length=1, max_length=4000)
|
|
|
|
|
|
class RepairCompletedPayload(BaseModel):
|
|
previous_miner_lifecycle: Literal["in_repair"] = "in_repair"
|
|
resulting_miner_lifecycle: RepairPostLifecycle
|
|
pre_repair_lifecycle: RepairEligibleLifecycle | None = None
|
|
completion_summary: str | None = Field(default=None, max_length=4000)
|
|
|
|
|
|
class ControlBoardReplacedPayload(BaseModel):
|
|
old_mac_address: str | None
|
|
new_mac_address: str
|
|
old_control_board_serial: str | None
|
|
new_control_board_serial: str | None
|
|
old_board_model: str | None = None
|
|
new_board_model: str | None = None
|
|
old_board_revision: str | None = None
|
|
new_board_revision: str | None = None
|
|
new_firmware_version: str | None = None
|
|
removed_board_disposition: RemovedBoardDisposition
|
|
replacement_reason: ControlBoardReplacementReason
|
|
notes: str | None = Field(default=None, max_length=4000)
|
|
|
|
|
|
class ControlBoardReplacementResult(BaseModel):
|
|
miner_id: UUID
|
|
asset_id: str | None = None
|
|
repair_id: UUID
|
|
installation_id: UUID
|
|
event_id: UUID
|
|
event_sequence: int = Field(ge=1)
|
|
current_mac: str
|
|
control_board_serial: str | None
|
|
replaced_at: AwareDatetime
|
|
replaced_by: UUID
|
|
|
|
|
|
class ControlBoardReplacementResponse(BaseModel):
|
|
data: ControlBoardReplacementResult
|
|
meta: dict[str, object] = Field(default_factory=dict)
|
|
error: None = None
|
|
|
|
|
|
class BeginRepairResult(BaseModel):
|
|
repair_id: UUID
|
|
miner_id: UUID
|
|
status: Literal["OPEN"]
|
|
reason: RepairReason
|
|
opened_at: datetime
|
|
opened_by: UUID
|
|
|
|
|
|
class BeginRepairResponse(BaseModel):
|
|
data: BeginRepairResult
|
|
meta: dict[str, object] = Field(default_factory=dict)
|
|
error: None = None
|
|
|
|
|
|
class RepairActor(BaseModel):
|
|
user_id: UUID
|
|
display_name: str
|
|
|
|
|
|
class RepairDetail(BaseModel):
|
|
repair_id: UUID
|
|
miner_id: UUID
|
|
miner_asset_id: str | None = None
|
|
status: RepairStatus
|
|
reason: RepairReason
|
|
opened_at: AwareDatetime
|
|
opened_by: RepairActor
|
|
closed_at: AwareDatetime | None = None
|
|
closed_by: RepairActor | None = None
|
|
miner_lifecycle: RepairMinerLifecycle
|
|
latest_event_sequence: int | None = Field(default=None, ge=1)
|
|
|
|
|
|
class RepairTimelineEvent(BaseModel):
|
|
event_id: UUID
|
|
sequence: int = Field(ge=1)
|
|
event_type: RepairEventType
|
|
actor: RepairActor
|
|
occurred_at: AwareDatetime
|
|
payload: dict[str, Any]
|
|
|
|
|
|
class RepairTimeline(BaseModel):
|
|
repair_id: UUID
|
|
events: list[RepairTimelineEvent] = Field(default_factory=list)
|
|
|
|
|
|
class RepairDetailResponse(BaseModel):
|
|
data: RepairDetail
|
|
meta: dict[str, object] = Field(default_factory=dict)
|
|
error: None = None
|
|
|
|
|
|
class RepairTimelineResponse(BaseModel):
|
|
data: RepairTimeline
|
|
meta: dict[str, object] = Field(default_factory=dict)
|
|
error: None = None
|
|
|
|
|
|
ConsumableUnit = Literal["each", "gram", "millilitre", "metre", "centimetre"]
|
|
RepairPartEntryKind = Literal["consumable", "tracked_component"]
|
|
|
|
|
|
class ConsumableInput(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
|
|
description: str = Field(min_length=1, max_length=255)
|
|
quantity: Decimal = Field(gt=0, max_digits=12, decimal_places=3)
|
|
unit: ConsumableUnit
|
|
|
|
@model_validator(mode="after")
|
|
def validate_each_quantity(self) -> "ConsumableInput":
|
|
if self.unit == "each" and self.quantity != self.quantity.to_integral_value():
|
|
raise ValueError("quantity for unit 'each' must be a whole number")
|
|
return self
|
|
|
|
|
|
class RepairPartRecordRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
|
|
consumable: ConsumableInput | None = None
|
|
tracked_component_uuid: UUID | None = None
|
|
notes: str | None = Field(default=None, max_length=4000)
|
|
|
|
@model_validator(mode="after")
|
|
def validate_entry_shape(self) -> "RepairPartRecordRequest":
|
|
if (self.consumable is None) == (self.tracked_component_uuid is None):
|
|
raise ValueError(
|
|
"exactly one of consumable and tracked_component_uuid is required"
|
|
)
|
|
return self
|
|
|
|
|
|
class RepairPartVoidRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
|
|
reason: str = Field(min_length=1, max_length=1000)
|
|
|
|
|
|
class RepairPartConsumable(BaseModel):
|
|
description: str
|
|
quantity: Decimal
|
|
unit: ConsumableUnit
|
|
|
|
|
|
class RepairPartComponentSnapshot(BaseModel):
|
|
component_type: ComponentType
|
|
asset_id: str | None = None
|
|
manufacturer: str | None = None
|
|
model: str | None = None
|
|
manufacturer_serial: str | None = None
|
|
|
|
|
|
class RepairPartDetail(BaseModel):
|
|
repair_part_id: UUID
|
|
repair_id: UUID
|
|
entry_kind: RepairPartEntryKind
|
|
consumable: RepairPartConsumable | None = None
|
|
component_id: UUID | None = None
|
|
component_snapshot: RepairPartComponentSnapshot | None = None
|
|
notes: str | None = None
|
|
recorded_by: UUID
|
|
recorded_at: AwareDatetime
|
|
recorded_event_id: UUID
|
|
voided_by: UUID | None = None
|
|
voided_at: AwareDatetime | None = None
|
|
void_reason: str | None = None
|
|
void_event_id: UUID | None = None
|
|
|
|
|
|
class RepairPartList(BaseModel):
|
|
repair_id: UUID
|
|
entries: list[RepairPartDetail] = Field(default_factory=list)
|
|
|
|
|
|
class RepairPartResponse(BaseModel):
|
|
data: RepairPartDetail
|
|
meta: dict[str, object] = Field(default_factory=dict)
|
|
error: None = None
|
|
|
|
|
|
class RepairPartListResponse(BaseModel):
|
|
data: RepairPartList
|
|
meta: dict[str, object] = Field(default_factory=dict)
|
|
error: None = None
|
|
|
|
|
|
RepairCenterSource = Literal["modular", "legacy"]
|
|
RepairCenterScope = Literal["active", "historical", "all"]
|
|
|
|
|
|
class RepairCenterAccess(BaseModel):
|
|
can_read_repairs: bool = False
|
|
can_view_miners: bool = False
|
|
can_view_locations: bool = False
|
|
can_complete_repairs: bool = False
|
|
can_manage_parts: bool = False
|
|
can_manage_legacy: bool = False
|
|
|
|
|
|
class RepairCenterFilters(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
|
|
query: str | None = Field(default=None, max_length=255)
|
|
scope: RepairCenterScope = "active"
|
|
status: str | None = Field(default=None, max_length=64)
|
|
model: str | None = Field(default=None, max_length=255)
|
|
lifecycle_status: RepairMinerLifecycle | None = None
|
|
site_id: UUID | None = None
|
|
room_id: UUID | None = None
|
|
limit: int = Field(default=50, ge=1, le=250)
|
|
offset: int = Field(default=0, ge=0)
|
|
|
|
|
|
class RepairCenterCapabilities(BaseModel):
|
|
can_open_miner: bool = False
|
|
can_open_map: bool = False
|
|
can_read_modular_timeline: bool = False
|
|
can_read_modular_parts: bool = False
|
|
can_manage_modular_parts: bool = False
|
|
can_complete_modular_repair: bool = False
|
|
can_read_legacy_history: bool = False
|
|
can_manage_legacy: bool = False
|
|
can_mark_legacy_repaired: bool = False
|
|
can_decommission_legacy: bool = False
|
|
|
|
|
|
class RepairCenterItem(BaseModel):
|
|
repair_id: UUID | None = None
|
|
source: RepairCenterSource
|
|
active: bool
|
|
opened_at: AwareDatetime | None = None
|
|
closed_at: AwareDatetime | None = None
|
|
repair_status: str
|
|
reason: str | None = None
|
|
reported_problem: str | None = None
|
|
latest_context: str | None = None
|
|
latest_context_at: AwareDatetime | None = None
|
|
opened_by_display_name: str | None = None
|
|
miner_id: UUID
|
|
asset_id: str | None = None
|
|
model: str | None = None
|
|
miner_sn: str | None = None
|
|
power_sn: str | None = None
|
|
current_mac: str | None = None
|
|
current_ip: str | None = None
|
|
lifecycle_status: RepairMinerLifecycle
|
|
runtime_status: str | None = None
|
|
miner_status: str | None = None
|
|
site_id: UUID | None = None
|
|
site_name: str | None = None
|
|
room_id: UUID | None = None
|
|
room_name: str | None = None
|
|
rack_label: str | None = None
|
|
shelf_label: str | None = None
|
|
position_id: UUID | None = None
|
|
position_label: str | None = None
|
|
position_index: int | None = None
|
|
parts_count: int | None = None
|
|
parts_summary: list[str] = Field(default_factory=list)
|
|
legacy_event_status: str | None = None
|
|
legacy_asset_state: str | None = None
|
|
latest_event_status: str | None = None
|
|
latest_event_title: str | None = None
|
|
latest_event_description: str | None = None
|
|
latest_event_at: AwareDatetime | None = None
|
|
asset_state: str | None = None
|
|
state_notes: str | None = None
|
|
repair_event_count: int = 0
|
|
open_repair_event_count: int = 0
|
|
state_updated_at: AwareDatetime | None = None
|
|
reported_error: str | None = None
|
|
reported_hashrate_ths: str | None = None
|
|
reported_voltage: str | None = None
|
|
location_note: str | None = None
|
|
import_source: str | None = None
|
|
capabilities: RepairCenterCapabilities
|
|
|
|
|
|
class RepairCenterSummary(BaseModel):
|
|
total: int
|
|
active: int
|
|
historical: int
|
|
status_counts: dict[str, int] = Field(default_factory=dict)
|
|
model_counts: dict[str, int] = Field(default_factory=dict)
|
|
error_counts: dict[str, int] = Field(default_factory=dict)
|
|
|
|
|
|
class ActiveRepairSummary(BaseModel):
|
|
active: int
|
|
status_counts: dict[str, int] = Field(default_factory=dict)
|
|
|
|
|
|
class RepairCenterLocationOption(BaseModel):
|
|
id: UUID
|
|
name: str
|
|
|
|
|
|
class RepairCenterRoomOption(RepairCenterLocationOption):
|
|
site_id: UUID
|
|
|
|
|
|
class RepairCenterLocationOptions(BaseModel):
|
|
sites: list[RepairCenterLocationOption] = Field(default_factory=list)
|
|
rooms: list[RepairCenterRoomOption] = Field(default_factory=list)
|
|
|
|
|
|
class RepairCenterList(BaseModel):
|
|
items: list[RepairCenterItem]
|
|
summary: RepairCenterSummary
|
|
|
|
|
|
class RepairCenterMeta(BaseModel):
|
|
limit: int
|
|
offset: int
|
|
returned: int
|
|
total: int
|
|
location_options: RepairCenterLocationOptions = Field(
|
|
default_factory=RepairCenterLocationOptions
|
|
)
|
|
|
|
|
|
class RepairCenterResponse(BaseModel):
|
|
data: RepairCenterList
|
|
meta: RepairCenterMeta
|
|
error: None = None
|