"""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