Add STO booking and maintenance automation

This commit is contained in:
VPN SaaS Dev
2026-05-15 05:17:54 +09:00
parent 2be7ba2099
commit fec9635079
12 changed files with 2178 additions and 5 deletions

187
app/schemas/sto_booking.py Normal file
View File

@@ -0,0 +1,187 @@
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]