from datetime import date, datetime, time from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator APPOINTMENT_STATUSES = { "draft", "requested", "confirmed", "proposed_new_time", "rejected", "cancelled_by_customer", "cancelled_by_sto", "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]