224 lines
5.9 KiB
Python
224 lines
5.9 KiB
Python
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]
|