Files
drivers_bot/app/schemas/service_center.py
VPN SaaS Dev 069b0a66c0
Some checks failed
ci / test (push) Has been cancelled
Sync completed work orders into vehicle records
2026-05-16 12:17:45 +09:00

664 lines
20 KiB
Python

from datetime import date, datetime
from decimal import Decimal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.services.vehicle_identity import normalize_license_plate, validate_vin
class ServiceCenterCreate(BaseModel):
legal_name: str | None = None
display_name: str
country: str | None = None
city: str | None = None
address: str | None = None
phone: str | None = None
business_registration_number: str | None = None
telegram_chat_id: str | None = None
contact_phone: str | None = None
description: str | None = None
specializations: list[str] | None = None
working_hours: str | None = None
facade_photo_url: str | None = None
document_photo_urls: list[str] | None = None
additional_photo_urls: list[str] | None = None
contact_person: str | None = None
class ServiceCenterRead(ServiceCenterCreate):
id: int
name: str
verification_status: str
owner_user_id: int | None = None
employee_role: str | None = None
employee_status: str | None = None
created_at: datetime
verified_at: datetime | None = None
suspended_at: datetime | None = None
rating_avg: Decimal | None = None
reviews_count: int = 0
model_config = ConfigDict(from_attributes=True)
class ServiceCenterPublicRead(BaseModel):
id: int
display_name: str | None = None
name: str
country: str | None = None
city: str | None = None
address: str | None = None
phone: str | None = None
description: str | None = None
specializations: list[str] | None = None
working_hours: str | None = None
facade_photo_url: str | None = None
verification_status: str
rating_avg: Decimal | None = None
reviews_count: int = 0
model_config = ConfigDict(from_attributes=True)
class ServiceCenterVerificationCreate(BaseModel):
submitted_documents: list[dict] | None = None
comment: str | None = None
class ServiceCenterVerificationRead(ServiceCenterVerificationCreate):
id: int
service_center_id: int
status: str
reviewed_by: int | None = None
reviewed_at: datetime | None = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class ServiceEmployeeInvite(BaseModel):
telegram_id: int
role: str = "receptionist"
permissions: dict | None = None
expires_in_hours: int = Field(default=72, ge=0, le=720)
class ServiceEmployeeUpdate(BaseModel):
role: str | None = None
permissions: dict | None = None
status: str | None = None
class ServiceEmployeeRead(BaseModel):
id: int
service_center_id: int
user_id: int
telegram_id: int | None = None
username: str | None = None
first_name: str | None = None
last_name: str | None = None
role: str
permissions: dict | None = None
status: str
invite_token: str | None = None
invite_expires_at: datetime | None = None
invite_revoked_at: datetime | None = None
activated_at: datetime | None = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class VehicleAccessGrant(BaseModel):
service_center_id: int | None = None
user_id: int | None = None
role: str = "viewer"
class VehicleAccessRead(BaseModel):
id: int
vehicle_id: int
user_id: int
role: str
status: str
created_at: datetime
revoked_at: datetime | None = None
model_config = ConfigDict(from_attributes=True)
class VehicleCreate(BaseModel):
name: str
make: str | None = None
model: str | None = None
trim: str | None = None
generation: str | None = None
body_type: str | None = None
year: int | None = None
license_plate: str | None = None
license_plate_country: str | None = None
vin: str | None = None
current_odometer: int | None = None
fuel_type: str | None = None
engine_volume_l: Decimal | None = None
transmission: str | None = None
drive_type: str | None = None
engine_oil_type: str | None = None
engine_oil_volume_l: Decimal | None = None
transmission_fluid_type: str | None = None
transmission_fluid_volume_l: Decimal | None = None
coolant_type: str | None = None
brake_fluid_type: str | None = None
tire_pressure_front_bar: Decimal | None = None
tire_pressure_rear_bar: Decimal | None = None
tire_size: str | None = None
oil_change_interval_km: int | None = None
oil_change_interval_months: int | None = None
fuel_tank_volume_l: Decimal | None = None
target_consumption_l_per_100km: Decimal | None = None
purchase_date: date | None = None
purchase_price: Decimal | None = None
purchase_currency: str | None = None
purchase_type: str = "unknown"
currency: str = "RUB"
include_depreciation: bool = False
expected_ownership_months: int | None = None
expected_residual_value: Decimal | None = None
loan_principal: Decimal | None = None
loan_down_payment: Decimal | None = None
loan_term_months: int | None = None
loan_annual_interest_rate: Decimal | None = None
loan_first_payment_date: date | None = None
loan_payment_day: int | None = None
loan_payment_type: str = "annuity"
loan_currency: str | None = None
loan_comment: str | None = None
notes: str | None = None
@field_validator("vin")
@classmethod
def validate_vin_field(cls, value: str | None) -> str | None:
return validate_vin(value)
class VehicleUpdate(BaseModel):
name: str | None = None
make: str | None = None
model: str | None = None
trim: str | None = None
generation: str | None = None
body_type: str | None = None
year: int | None = None
license_plate: str | None = None
license_plate_country: str | None = None
vin: str | None = None
current_odometer: int | None = None
fuel_type: str | None = None
engine_volume_l: Decimal | None = None
transmission: str | None = None
drive_type: str | None = None
fuel_tank_volume_l: Decimal | None = None
target_consumption_l_per_100km: Decimal | None = None
purchase_date: date | None = None
purchase_price: Decimal | None = None
purchase_currency: str | None = None
purchase_type: str | None = None
currency: str | None = None
include_depreciation: bool | None = None
expected_ownership_months: int | None = None
expected_residual_value: Decimal | None = None
engine_oil_type: str | None = None
engine_oil_volume_l: Decimal | None = None
transmission_fluid_type: str | None = None
transmission_fluid_volume_l: Decimal | None = None
coolant_type: str | None = None
brake_fluid_type: str | None = None
tire_pressure_front_bar: Decimal | None = None
tire_pressure_rear_bar: Decimal | None = None
tire_size: str | None = None
oil_change_interval_km: int | None = None
oil_change_interval_months: int | None = None
loan_principal: Decimal | None = None
loan_down_payment: Decimal | None = None
loan_term_months: int | None = None
loan_annual_interest_rate: Decimal | None = None
loan_first_payment_date: date | None = None
loan_payment_day: int | None = None
loan_payment_type: str | None = None
loan_currency: str | None = None
loan_comment: str | None = None
notes: str | None = None
@field_validator("vin")
@classmethod
def validate_vin_field(cls, value: str | None) -> str | None:
return validate_vin(value)
class VehicleRead(BaseModel):
id: int
owner_id: int
name: str
make: str | None = None
model: str | None = None
trim: str | None = None
generation: str | None = None
body_type: str | None = None
year: int | None = None
license_plate_display: str | None = None
license_plate_country: str | None = None
vin_normalized: str | None = None
current_odometer: int | None = None
fuel_type: str | None = None
engine_volume_l: Decimal | None = None
transmission: str | None = None
drive_type: str | None = None
fuel_tank_volume_l: Decimal | None = None
target_consumption_l_per_100km: Decimal | None = None
purchase_date: date | None = None
purchase_price: Decimal | None = None
purchase_currency: str | None = None
purchase_type: str = "unknown"
currency: str = "RUB"
include_depreciation: bool = False
expected_ownership_months: int | None = None
expected_residual_value: Decimal | None = None
engine_oil_type: str | None = None
engine_oil_volume_l: Decimal | None = None
transmission_fluid_type: str | None = None
transmission_fluid_volume_l: Decimal | None = None
coolant_type: str | None = None
brake_fluid_type: str | None = None
tire_pressure_front_bar: Decimal | None = None
tire_pressure_rear_bar: Decimal | None = None
tire_size: str | None = None
oil_change_interval_km: int | None = None
oil_change_interval_months: int | None = None
loan_principal: Decimal | None = None
loan_down_payment: Decimal | None = None
loan_term_months: int | None = None
loan_annual_interest_rate: Decimal | None = None
loan_first_payment_date: date | None = None
loan_payment_day: int | None = None
loan_payment_type: str = "annuity"
loan_currency: str | None = None
loan_comment: str | None = None
notes: str | None = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class ServiceVisitCreate(BaseModel):
vehicle_id: int
visit_date: date
odometer: int | None = None
notes: str | None = None
total_cost: Decimal | None = None
currency: str = "RUB"
class ServiceVisitRead(ServiceVisitCreate):
id: int
service_center_id: int
work_order_number: str | None = None
owner_id: int | None = None
created_by_employee_id: int | None = None
assigned_employee_id: int | None = None
status: str
customer_complaint: str | None = None
diagnosis: str | None = None
service_comment: str | None = None
owner_comment: str | None = None
recommendations_text: str | None = None
attachment_urls: list[str] | None = None
labor_total: Decimal = Decimal("0")
product_total: Decimal = Decimal("0")
discount_total: Decimal = Decimal("0")
final_total: Decimal = Decimal("0")
approval_required: bool = False
version: int = 1
completed_snapshot: dict | None = None
opened_at: datetime | None = None
approved_at: datetime | None = None
completed_at: datetime | None = None
owner_resolved_at: datetime | None = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class WorkOrderCatalogItemCreate(BaseModel):
service_center_id: int | None = None
item_type: str = Field(pattern="^(work|product)$")
title: str = Field(min_length=2, max_length=180)
category: str | None = None
description: str | None = None
work_type: str | None = None
product_type: str | None = None
brand: str | None = None
sku: str | None = None
unit: str = "pcs"
default_quantity: Decimal = Decimal("1")
default_unit_price: Decimal = Decimal("0")
volume: Decimal | None = None
viscosity: str | None = None
specification: str | None = None
metadata_json: dict | None = None
is_active: bool = True
@model_validator(mode="after")
def validate_catalog_item(self) -> "WorkOrderCatalogItemCreate":
if self.default_quantity <= 0:
raise ValueError("default_quantity must be positive")
if self.default_unit_price < 0:
raise ValueError("default_unit_price must be non-negative")
return self
class WorkOrderCatalogItemRead(WorkOrderCatalogItemCreate):
id: int
created_at: datetime
updated_at: datetime | None = None
model_config = ConfigDict(from_attributes=True)
class WorkOrderCatalogSuggestion(BaseModel):
source: str = "vehicle_profile"
item_type: str = "product"
title: str
category: str | None = None
product_type: str | None = None
unit: str = "pcs"
default_quantity: Decimal = Decimal("1")
default_unit_price: Decimal = Decimal("0")
volume: Decimal | None = None
viscosity: str | None = None
specification: str | None = None
metadata_json: dict | None = None
class WorkOrderCatalogRead(BaseModel):
items: list[WorkOrderCatalogItemRead]
vehicle_suggestions: list[WorkOrderCatalogSuggestion] = []
missing_vehicle_fields: list[str] = []
class VehicleProfileRequest(BaseModel):
missing_fields: list[str] | None = None
comment: str | None = None
class ServiceWorkItemCreate(BaseModel):
work_type: str = "other"
title: str
category: str | None = None
description: str | None = None
quantity: Decimal = Decimal("1")
unit: str = "pcs"
unit_price: Decimal | None = None
discount: Decimal = Decimal("0")
parts: list[dict] | None = None
oil_brand: str | None = None
oil_viscosity: str | None = None
oil_volume: Decimal | None = None
next_due_odometer: int | None = None
next_due_date: date | None = None
price: Decimal | None = None
warranty_days: int | None = None
warranty_odometer_km: int | None = None
@model_validator(mode="after")
def validate_item(self) -> "ServiceWorkItemCreate":
if self.quantity <= 0:
raise ValueError("quantity must be positive")
if self.discount < 0:
raise ValueError("discount must be non-negative")
if self.unit_price is not None and self.unit_price < 0:
raise ValueError("unit_price must be non-negative")
if self.price is not None and self.price < 0:
raise ValueError("price must be non-negative")
return self
class ServiceWorkItemRead(ServiceWorkItemCreate):
id: int
service_visit_id: int
total: Decimal | None = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class ServiceProductItemCreate(BaseModel):
title: str
category: str | None = None
product_type: str = "other"
brand: str | None = None
sku: str | None = None
quantity: Decimal = Decimal("1")
unit: str = "pcs"
unit_price: Decimal = Decimal("0")
discount: Decimal = Decimal("0")
volume: Decimal | None = None
viscosity: str | None = None
specification: str | None = None
used_volume: Decimal | None = None
@model_validator(mode="after")
def validate_product(self) -> "ServiceProductItemCreate":
if self.quantity <= 0:
raise ValueError("quantity must be positive")
if self.unit_price < 0 or self.discount < 0:
raise ValueError("price and discount must be non-negative")
return self
class ServiceProductItemRead(ServiceProductItemCreate):
id: int
service_visit_id: int
total: Decimal
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class WorkOrderDetailRead(BaseModel):
visit: ServiceVisitRead
vehicle: VehicleRead
service_center: ServiceCenterPublicRead
work_items: list[ServiceWorkItemRead] = []
product_items: list[ServiceProductItemRead] = []
catalog: WorkOrderCatalogRead
class WorkOrderUpdate(BaseModel):
odometer: int | None = None
assigned_employee_id: int | None = None
customer_complaint: str | None = None
diagnosis: str | None = None
notes: str | None = None
service_comment: str | None = None
owner_comment: str | None = None
recommendations_text: str | None = None
attachment_urls: list[str] | None = None
discount_total: Decimal | None = None
approval_required: bool | None = None
class WorkOrderDecision(BaseModel):
comment: str | None = None
odometer: int | None = None
confirm_lower_odometer: bool = False
class WorkOrderStatusHistoryRead(BaseModel):
id: int
service_visit_id: int
from_status: str | None = None
to_status: str
changed_by_user_id: int | None = None
comment: str | None = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class WorkOrderCorrectionCreate(BaseModel):
reason: str = Field(min_length=3, max_length=4000)
proposed_changes: dict | None = None
owner_approval_required: bool = True
class WorkOrderCorrectionRead(WorkOrderCorrectionCreate):
id: int
service_visit_id: int
requested_by_user_id: int | None = None
status: str
created_version: int
resolved_at: datetime | None = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class VehicleDataChangeRequestCreate(BaseModel):
vehicle_id: int
field_name: str
new_value: str | None = None
class VehicleDataChangeRequestRead(VehicleDataChangeRequestCreate):
id: int
requested_by_service_center_id: int | None = None
requested_by_employee_id: int | None = None
old_value: str | None = None
status: str
owner_user_id: int
created_at: datetime
resolved_at: datetime | None = None
model_config = ConfigDict(from_attributes=True)
class VehicleSearchRequest(BaseModel):
license_plate: str | None = None
country_code: str | None = None
vin: str | None = None
@field_validator("vin")
@classmethod
def validate_vin_field(cls, value: str | None) -> str | None:
return validate_vin(value)
@field_validator("license_plate")
@classmethod
def normalize_plate_field(cls, value: str | None) -> str | None:
return normalize_license_plate(value)
class VehicleSearchResult(BaseModel):
vehicle_id: int | None = None
make: str | None = None
model: str | None = None
year: int | None = None
masked_license_plate: str | None = None
masked_vin: str | None = None
access_status: str = "none"
class CarServiceLinkCreate(BaseModel):
car_id: int
service_center_id: int
external_vehicle_ref: str | None = None
access_level: str = "basic"
is_active: bool = True
class CarServiceLinkRead(CarServiceLinkCreate):
id: int
status: str = "pending"
requested_by_user_id: int | None = None
approved_by_user_id: int | None = None
approved_at: datetime | None = None
revoked_at: datetime | None = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class ServiceCenterAccessRequest(BaseModel):
car_id: int
access_level: str = "basic"
external_vehicle_ref: str | None = None
@field_validator("access_level")
@classmethod
def validate_access_level(cls, value: str) -> str:
allowed = {"basic", "service_history", "full"}
if value not in allowed:
raise ValueError(f"access_level must be one of {', '.join(sorted(allowed))}")
return value
class ServiceCenterReviewCreate(BaseModel):
rating: int = Field(ge=1, le=5)
text: str | None = None
photo_urls: list[str] | None = None
@model_validator(mode="after")
def validate_review(self) -> "ServiceCenterReviewCreate":
if self.text is not None and len(self.text.strip()) < 3:
raise ValueError("review text is too short")
return self
class ServiceCenterReviewRead(ServiceCenterReviewCreate):
id: int
service_center_id: int
user_id: int
status: str
service_response: str | None = None
service_responded_at: datetime | None = None
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class ServiceCenterReviewCommentCreate(BaseModel):
text: str = Field(min_length=2, max_length=2000)
class ServiceCenterReviewCommentRead(ServiceCenterReviewCommentCreate):
id: int
review_id: int
user_id: int
status: str
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class ServiceInboxCreate(BaseModel):
source_chat_id: str | None = None
raw_text: str
car_id: int | None = None
service_center_id: int | None = None
class ServiceInboxRead(ServiceInboxCreate):
id: int
parsed_status: str
parsed_payload: str | None = None
error: str | None = None
created_at: datetime
class AdminModerationDecision(BaseModel):
comment: str | None = None
reason: str | None = None
model_config = ConfigDict(from_attributes=True)