add vehicle service profile settings

This commit is contained in:
VPN SaaS Dev
2026-05-12 04:52:42 +09:00
parent b5012ec6e7
commit e75697f83e
10 changed files with 496 additions and 5 deletions

View File

@@ -0,0 +1,73 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_session
from app.models.car import Car, CarServiceLink, ServiceCenter, ServiceInboxMessage
from app.schemas.service_center import (
CarServiceLinkCreate,
CarServiceLinkRead,
ServiceCenterCreate,
ServiceCenterRead,
ServiceInboxCreate,
ServiceInboxRead,
)
router = APIRouter(prefix="/service-centers", tags=["service-centers"])
@router.post("", response_model=ServiceCenterRead, status_code=status.HTTP_201_CREATED)
async def create_service_center(
payload: ServiceCenterCreate, session: AsyncSession = Depends(get_session)
) -> ServiceCenter:
center = ServiceCenter(**payload.model_dump())
session.add(center)
await session.commit()
await session.refresh(center)
return center
@router.get("", response_model=list[ServiceCenterRead])
async def list_service_centers(session: AsyncSession = Depends(get_session)) -> list[ServiceCenter]:
result = await session.execute(select(ServiceCenter).order_by(ServiceCenter.name))
return list(result.scalars())
@router.post("/links", response_model=CarServiceLinkRead, status_code=status.HTTP_201_CREATED)
async def link_car_to_service(
payload: CarServiceLinkCreate, session: AsyncSession = Depends(get_session)
) -> CarServiceLink:
if await session.get(Car, payload.car_id) is None:
raise HTTPException(status_code=404, detail="Car not found")
if await session.get(ServiceCenter, payload.service_center_id) is None:
raise HTTPException(status_code=404, detail="Service center not found")
link = CarServiceLink(**payload.model_dump())
session.add(link)
await session.commit()
await session.refresh(link)
return link
@router.post("/inbox", response_model=ServiceInboxRead, status_code=status.HTTP_201_CREATED)
async def receive_service_message(
payload: ServiceInboxCreate, session: AsyncSession = Depends(get_session)
) -> ServiceInboxMessage:
service_center_id = payload.service_center_id
if not service_center_id and payload.source_chat_id:
result = await session.execute(
select(ServiceCenter).where(ServiceCenter.telegram_chat_id == payload.source_chat_id)
)
center = result.scalar_one_or_none()
service_center_id = center.id if center else None
message = ServiceInboxMessage(
source_chat_id=payload.source_chat_id,
raw_text=payload.raw_text,
car_id=payload.car_id,
service_center_id=service_center_id,
parsed_status="pending",
)
session.add(message)
await session.commit()
await session.refresh(message)
return message

View File

@@ -121,6 +121,16 @@ async def seed_mock_usage(session: AsyncSession, owner: User) -> None:
year=year,
plate_number=f"{MOCK_PLATE_PREFIX}-{index:02d}",
fuel_type=fuel_type,
target_consumption_l_per_100km=None if fuel_type == "electric" else Decimal("7.80"),
fuel_tank_volume_l=None if fuel_type == "electric" else Decimal("58.00"),
engine_oil_type=None if fuel_type == "electric" else "5W-30 API SP",
engine_oil_volume_l=None if fuel_type == "electric" else Decimal("4.50"),
transmission_fluid_type="EV reduction gear oil" if fuel_type == "electric" else "ATF / CVTF по допуску",
transmission_fluid_volume_l=Decimal("1.20") if fuel_type == "electric" else Decimal("7.00"),
coolant_type="LLC",
brake_fluid_type="DOT 4",
tire_pressure_front_bar=Decimal("2.30"),
tire_pressure_rear_bar=Decimal("2.20"),
purchase_date=date(year, min(index, 12), 10),
purchase_price=price,
current_odometer=start_odo,

View File

@@ -2,7 +2,7 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from app.api import cars, catalog, entries, ocr, users
from app.api import cars, catalog, entries, ocr, service_centers, users
app = FastAPI(title="Drivers Bot API", version="0.1.0")
@@ -19,6 +19,7 @@ app.include_router(catalog.router, prefix="/api")
app.include_router(cars.router, prefix="/api")
app.include_router(entries.router, prefix="/api")
app.include_router(ocr.router, prefix="/api")
app.include_router(service_centers.router, prefix="/api")
@app.get("/health")

View File

@@ -1,7 +1,7 @@
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy import Date, DateTime, ForeignKey, Integer, Numeric, String, UniqueConstraint, func
from sqlalchemy import Date, DateTime, ForeignKey, Integer, Numeric, String, Text, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
@@ -20,6 +20,16 @@ class Car(Base):
plate_number: Mapped[str | None] = mapped_column(String(32))
vin: Mapped[str | None] = mapped_column(String(32))
fuel_type: Mapped[str | None] = mapped_column(String(32))
target_consumption_l_per_100km: Mapped[Decimal | None] = mapped_column(Numeric(6, 2))
fuel_tank_volume_l: Mapped[Decimal | None] = mapped_column(Numeric(6, 2))
engine_oil_type: Mapped[str | None] = mapped_column(String(80))
engine_oil_volume_l: Mapped[Decimal | None] = mapped_column(Numeric(5, 2))
transmission_fluid_type: Mapped[str | None] = mapped_column(String(80))
transmission_fluid_volume_l: Mapped[Decimal | None] = mapped_column(Numeric(5, 2))
coolant_type: Mapped[str | None] = mapped_column(String(80))
brake_fluid_type: Mapped[str | None] = mapped_column(String(80))
tire_pressure_front_bar: Mapped[Decimal | None] = mapped_column(Numeric(4, 2))
tire_pressure_rear_bar: Mapped[Decimal | None] = mapped_column(Numeric(4, 2))
purchase_date: Mapped[date | None] = mapped_column(Date)
purchase_price: Mapped[Decimal | None] = mapped_column(Numeric(12, 2))
current_odometer: Mapped[int | None]
@@ -31,6 +41,7 @@ class Car(Base):
owner = relationship("User", back_populates="cars")
fuel_entries = relationship("FuelEntry", back_populates="car", cascade="all, delete-orphan")
service_entries = relationship("ServiceEntry", back_populates="car", cascade="all, delete-orphan")
service_links = relationship("CarServiceLink", back_populates="car", cascade="all, delete-orphan")
class CarMake(Base):
@@ -74,3 +85,48 @@ class CarTrim(Base):
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
model = relationship("CarModel", back_populates="trims")
class ServiceCenter(Base):
__tablename__ = "service_centers"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(160), unique=True, index=True)
telegram_chat_id: Mapped[str | None] = mapped_column(String(80), unique=True, index=True)
contact_phone: Mapped[str | None] = mapped_column(String(40))
address: Mapped[str | None] = mapped_column(String(240))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
car_links = relationship("CarServiceLink", back_populates="service_center", cascade="all, delete-orphan")
inbox_messages = relationship("ServiceInboxMessage", back_populates="service_center")
class CarServiceLink(Base):
__tablename__ = "car_service_links"
__table_args__ = (UniqueConstraint("car_id", "service_center_id", name="uq_car_service_link"),)
id: Mapped[int] = mapped_column(primary_key=True)
car_id: Mapped[int] = mapped_column(ForeignKey("cars.id", ondelete="CASCADE"), index=True)
service_center_id: Mapped[int] = mapped_column(ForeignKey("service_centers.id", ondelete="CASCADE"), index=True)
external_vehicle_ref: Mapped[str | None] = mapped_column(String(120), index=True)
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
car = relationship("Car", back_populates="service_links")
service_center = relationship("ServiceCenter", back_populates="car_links")
class ServiceInboxMessage(Base):
__tablename__ = "service_inbox_messages"
id: Mapped[int] = mapped_column(primary_key=True)
service_center_id: Mapped[int | None] = mapped_column(ForeignKey("service_centers.id", ondelete="SET NULL"), index=True)
car_id: Mapped[int | None] = mapped_column(ForeignKey("cars.id", ondelete="SET NULL"), index=True)
source_chat_id: Mapped[str | None] = mapped_column(String(80), index=True)
raw_text: Mapped[str] = mapped_column(Text)
parsed_status: Mapped[str] = mapped_column(String(32), default="pending", index=True)
parsed_payload: Mapped[str | None] = mapped_column(Text)
error: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
service_center = relationship("ServiceCenter", back_populates="inbox_messages")

View File

@@ -13,6 +13,16 @@ class CarBase(BaseModel):
plate_number: str | None = None
vin: str | None = None
fuel_type: str | None = None
target_consumption_l_per_100km: Decimal | None = None
fuel_tank_volume_l: 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
purchase_date: date | None = None
purchase_price: Decimal | None = None
current_odometer: int | None = None
@@ -31,6 +41,16 @@ class CarUpdate(BaseModel):
plate_number: str | None = None
vin: str | None = None
fuel_type: str | None = None
target_consumption_l_per_100km: Decimal | None = None
fuel_tank_volume_l: 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
purchase_date: date | None = None
purchase_price: Decimal | None = None
current_odometer: int | None = None

View File

@@ -0,0 +1,48 @@
from datetime import datetime
from pydantic import BaseModel, ConfigDict
class ServiceCenterCreate(BaseModel):
name: str
telegram_chat_id: str | None = None
contact_phone: str | None = None
address: str | None = None
class ServiceCenterRead(ServiceCenterCreate):
id: int
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class CarServiceLinkCreate(BaseModel):
car_id: int
service_center_id: int
external_vehicle_ref: str | None = None
is_active: bool = True
class CarServiceLinkRead(CarServiceLinkCreate):
id: int
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
model_config = ConfigDict(from_attributes=True)