192 lines
5.7 KiB
Python
192 lines
5.7 KiB
Python
from datetime import date, datetime, time
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
|
|
APPOINTMENT_STATUSES = {
|
|
"draft",
|
|
"requested",
|
|
"confirmed",
|
|
"confirmed_by_sto",
|
|
"proposed_new_time",
|
|
"rejected",
|
|
"rejected_by_sto",
|
|
"cancelled_by_owner",
|
|
"cancelled_by_customer",
|
|
"cancelled_by_sto",
|
|
"converted_to_work_order",
|
|
"completed",
|
|
"no_show",
|
|
}
|
|
|
|
|
|
class AvailableSlotRead(BaseModel):
|
|
start_at: datetime
|
|
end_at: datetime
|
|
|
|
|
|
class ServiceCatalogItem(BaseModel):
|
|
id: int
|
|
display_name: str | None = None
|
|
name: str
|
|
city: str | None = None
|
|
address: str | None = None
|
|
specializations: list[str] | None = None
|
|
working_hours: str | None = None
|
|
rating_avg: float | None = None
|
|
reviews_count: int = 0
|
|
nearest_slot_at: datetime | None = None
|
|
accepts_online_booking: bool = True
|
|
|
|
|
|
class ServiceCenterBookingSettingsUpsert(BaseModel):
|
|
service_center_id: int
|
|
working_days: list[int] = Field(default_factory=lambda: [0, 1, 2, 3, 4])
|
|
open_time: time = time(9, 0)
|
|
close_time: time = time(18, 0)
|
|
lunch_break_start: time | None = None
|
|
lunch_break_end: time | None = None
|
|
timezone: str = "Asia/Seoul"
|
|
slot_duration_minutes: int = Field(default=30, ge=10, le=240)
|
|
booking_buffer_minutes: int = Field(default=0, ge=0, le=240)
|
|
max_parallel_bookings: int = Field(default=1, ge=1, le=20)
|
|
accepts_online_booking: bool = True
|
|
|
|
@field_validator("working_days")
|
|
@classmethod
|
|
def validate_working_days(cls, value: list[int]) -> list[int]:
|
|
days = sorted(set(value))
|
|
if any(day < 0 or day > 6 for day in days):
|
|
raise ValueError("working_days must contain ISO weekdays 0..6")
|
|
return days
|
|
|
|
@model_validator(mode="after")
|
|
def validate_times(self) -> "ServiceCenterBookingSettingsUpsert":
|
|
if self.open_time >= self.close_time:
|
|
raise ValueError("open_time must be before close_time")
|
|
if bool(self.lunch_break_start) != bool(self.lunch_break_end):
|
|
raise ValueError("both lunch break boundaries are required")
|
|
if self.lunch_break_start and self.lunch_break_end and self.lunch_break_start >= self.lunch_break_end:
|
|
raise ValueError("lunch_break_start must be before lunch_break_end")
|
|
return self
|
|
|
|
|
|
class ServiceCenterBookingSettingsRead(ServiceCenterBookingSettingsUpsert):
|
|
id: int
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class ServiceCenterHolidayCreate(BaseModel):
|
|
service_center_id: int
|
|
holiday_date: date
|
|
reason: str | None = None
|
|
|
|
|
|
class ServiceCenterHolidayRead(ServiceCenterHolidayCreate):
|
|
id: int
|
|
created_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class AppointmentCreate(BaseModel):
|
|
service_center_id: int
|
|
vehicle_id: int
|
|
service_type: str = Field(default="maintenance", max_length=64)
|
|
service_name: str = Field(default="Обслуживание", max_length=180)
|
|
requested_start_at: datetime
|
|
estimated_duration_minutes: int = Field(default=60, ge=10, le=1440)
|
|
customer_comment: str | None = Field(default=None, max_length=4000)
|
|
source_recommendation_id: int | None = None
|
|
|
|
|
|
class AppointmentRead(BaseModel):
|
|
id: int
|
|
service_center_id: int
|
|
vehicle_id: int
|
|
owner_id: int
|
|
created_by: int
|
|
service_type: str
|
|
service_name: str
|
|
requested_start_at: datetime
|
|
requested_end_at: datetime
|
|
confirmed_start_at: datetime | None = None
|
|
confirmed_end_at: datetime | None = None
|
|
proposed_start_at: datetime | None = None
|
|
proposed_end_at: datetime | None = None
|
|
estimated_duration_minutes: int
|
|
status: str
|
|
customer_comment: str | None = None
|
|
service_center_comment: str | None = None
|
|
source_recommendation_id: int | None = None
|
|
linked_work_order_id: int | None = None
|
|
cancellation_reason: str | None = None
|
|
cancelled_at: datetime | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class AppointmentDecision(BaseModel):
|
|
comment: str | None = Field(default=None, max_length=4000)
|
|
|
|
|
|
class AppointmentProposeTime(BaseModel):
|
|
proposed_start_at: datetime
|
|
estimated_duration_minutes: int | None = Field(default=None, ge=10, le=1440)
|
|
comment: str | None = Field(default=None, max_length=4000)
|
|
|
|
|
|
class AppointmentCancel(BaseModel):
|
|
reason: str | None = Field(default=None, max_length=1000)
|
|
|
|
|
|
class AppointmentCreateWorkOrder(BaseModel):
|
|
odometer: int | None = None
|
|
notes: str | None = Field(default=None, max_length=4000)
|
|
|
|
|
|
class MaintenanceRecommendationCreate(BaseModel):
|
|
recommendation_type: str = Field(max_length=64)
|
|
title: str = Field(max_length=180)
|
|
description: str | None = None
|
|
due_odometer_km: int | None = None
|
|
due_date: date | None = None
|
|
priority: str = "medium"
|
|
source: str = "user_rule"
|
|
|
|
|
|
class MaintenanceRecommendationRead(MaintenanceRecommendationCreate):
|
|
id: int
|
|
vehicle_id: int
|
|
status: str
|
|
source_service_center_id: int | None = None
|
|
source_appointment_id: int | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
class MaintenanceRecommendationBook(BaseModel):
|
|
appointment_id: int
|
|
|
|
|
|
class STODashboardRead(BaseModel):
|
|
service_center_id: int
|
|
connected_vehicles: int
|
|
pending_vehicle_links: int
|
|
active_appointments: int
|
|
pending_appointments: int
|
|
confirmed_appointments: int
|
|
active_work_orders: int
|
|
completed_work_orders_month: int
|
|
revenue_month: float
|
|
average_check_month: float
|
|
rating_avg: float | None = None
|
|
reviews_count: int
|
|
warnings: list[str]
|