from datetime import datetime from typing import Literal from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator RepairStatus = Literal[ "new", "accepted", "diagnosis", "estimate", "waiting_for_customer", "approved", "repair", "final_test", "ready_for_pickup", "shipped", "completed", "cancelled", ] RepairPriority = Literal["low", "normal", "high", "urgent"] RepairSource = Literal["manual", "website", "customer_portal", "email", "phone"] def normalize_text(value: object) -> str: if value is None: return "" return str(value).strip() class RepairBase(BaseModel): customer_id: int | None = None contact_id: int | None = None status: RepairStatus = "new" priority: RepairPriority = "normal" source: RepairSource = "manual" source_reference: str | None = Field(default=None, max_length=120) customer_name: str = Field(default="", max_length=255) customer_email: EmailStr | None = None customer_phone: str = Field(default="", max_length=80) device_manufacturer: str = Field(min_length=1, max_length=160) device_model: str = Field(min_length=1, max_length=160) device_serial_number: str | None = Field(default=None, max_length=160) device_type: str = Field(default="", max_length=120) accessories: str = "" fault_description: str = Field(min_length=1) previous_work: str = "" device_opened: bool = False intake_notes: str = "" diagnosis_notes: str = "" repair_notes: str = "" estimate_notes: str = "" estimated_cost_cents: int | None = Field(default=None, ge=0) @field_validator( "source_reference", "customer_name", "customer_phone", "device_manufacturer", "device_model", "device_serial_number", "device_type", "accessories", "fault_description", "previous_work", "intake_notes", "diagnosis_notes", "repair_notes", "estimate_notes", mode="before", ) @classmethod def normalize_strings(cls, value: object) -> str | None: if value is None: return None return normalize_text(value) @model_validator(mode="after") def validate_customer_identity(self): if self.customer_id is None and not self.customer_name: raise ValueError("Kundenname ist erforderlich, wenn kein Kunde ausgewählt ist") return self class RepairCreate(RepairBase): pass class RepairUpdate(RepairBase): pass class RepairStatusUpdate(BaseModel): status: RepairStatus note: str | None = None @field_validator("note", mode="before") @classmethod def normalize_note(cls, value: object) -> str | None: if value is None: return None text = normalize_text(value) return text or None class RepairResponse(RepairBase): id: int repair_number: str customer_email: str device_serial_number: str | None approved_at: datetime | None completed_at: datetime | None created_at: datetime updated_at: datetime model_config = ConfigDict(from_attributes=True) class RepairListResponse(BaseModel): items: list[RepairResponse] total: int limit: int offset: int class RepairStatusHistoryResponse(BaseModel): id: int repair_id: int old_status: str | None new_status: str note: str | None actor_user_id: int | None actor_username: str = "" actor_display_name: str = "" created_at: datetime model_config = ConfigDict(from_attributes=True) class RepairIntakePayload(BaseModel): name: str = Field(min_length=1, max_length=255) email: EmailStr | None = None phone: str = Field(default="", max_length=80) manufacturer: str = Field(min_length=1, max_length=160) model: str = Field(min_length=1, max_length=160) serial_number: str | None = Field(default=None, max_length=160) device_type: str = Field(default="", max_length=120) accessories: str = "" fault_description: str = Field(min_length=1) previous_work: str = "" device_opened: bool = False source_reference: str | None = Field(default=None, max_length=120) @field_validator("*", mode="before") @classmethod def normalize_payload(cls, value: object) -> object: if isinstance(value, str): return value.strip() return value class RepairIntakeResponse(BaseModel): repair_number: str status: RepairStatus message: str class RepairPublicLinkResponse(BaseModel): is_active: bool token_hint: str | None = None expires_at: datetime | None = None last_used_at: datetime | None = None created_at: datetime | None = None revoked_at: datetime | None = None public_status_path: str | None = None class RepairPublicLinkCreateResponse(RepairPublicLinkResponse): token: str public_status_path: str class RepairPublicStatusHistoryItem(BaseModel): status: RepairStatus status_label: str created_at: datetime class RepairPublicStatusResponse(BaseModel): repair_number: str public_status_label: str device_manufacturer: str device_model: str status_history_public: list[RepairPublicStatusHistoryItem] updated_at: datetime class RepairNotificationTemplateResponse(BaseModel): event_type: str status: RepairStatus | None = None subject: str text: str html: str | None = None class RepairNotificationEventResponse(BaseModel): id: int repair_id: int event_type: str channel: str recipient: str subject: str template: str status: str success: bool error_message: str | None created_at: datetime sent_at: datetime | None model_config = ConfigDict(from_attributes=True) class RepairNotificationOverviewResponse(BaseModel): templates: list[RepairNotificationTemplateResponse] events: list[RepairNotificationEventResponse]