# /opt/docker/dev/service_finder/backend/app/models/vehicle/vehicle.py """ TCO (Total Cost of Ownership) alapmodelljei a 'vehicle' sémában. - CostCategory: Standardizált költségkategóriák hierarchiája - VehicleCost: Járműhöz kapcsolódó tényleges költségnapló """ from __future__ import annotations from datetime import datetime from typing import Optional import uuid from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, Text, Numeric, UniqueConstraint, Float, CheckConstraint from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from app.database import Base class CostCategory(Base): """ Standardizált költségkategóriák hierarchikus fája. Rendszerkategóriák (is_system=True) nem törölhetők, csak felhasználói kategóriák. """ __tablename__ = "cost_categories" __table_args__ = {"schema": "vehicle"} id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) parent_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("vehicle.cost_categories.id", ondelete="SET NULL"), nullable=True, index=True ) code: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False) name: Mapped[str] = mapped_column(String(100), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) is_system: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now()) # Hierarchikus kapcsolatok parent: Mapped[Optional["CostCategory"]] = relationship( "CostCategory", remote_side=[id], back_populates="children", foreign_keys=[parent_id] ) children: Mapped[list["CostCategory"]] = relationship( "CostCategory", back_populates="parent", foreign_keys=[parent_id] ) # Kapcsolódó költségek costs: Mapped[list["VehicleCost"]] = relationship("VehicleCost", back_populates="category") def __repr__(self) -> str: return f"CostCategory(id={self.id}, code='{self.code}', name='{self.name}')" class VehicleCost(Base): """ Járműhöz kapcsolódó tényleges költségnapló. Minden költséghez kötelező az odometer állás (km) és a dátum. Az organization_id az Univerzális Flotta hivatkozás (fleet.organizations). """ __tablename__ = "costs" __table_args__ = ( UniqueConstraint("vehicle_id", "category_id", "date", "odometer", name="uq_cost_unique_entry"), {"schema": "vehicle"} ) id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) vehicle_id: Mapped[int] = mapped_column( Integer, ForeignKey("vehicle.vehicle_model_definitions.id", ondelete="CASCADE"), nullable=False, index=True ) organization_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("fleet.organizations.id", ondelete="SET NULL"), nullable=True, index=True ) category_id: Mapped[int] = mapped_column( Integer, ForeignKey("vehicle.cost_categories.id", ondelete="RESTRICT"), nullable=False, index=True ) amount: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) # Összeg currency: Mapped[str] = mapped_column(String(3), default="HUF", server_default="'HUF'") # ISO valutakód odometer: Mapped[int] = mapped_column(Integer, nullable=False) # Kilométeróra állás (km) date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now()) # Kapcsolatok vehicle: Mapped["VehicleModelDefinition"] = relationship("VehicleModelDefinition", back_populates="costs") organization: Mapped[Optional["Organization"]] = relationship("Organization", back_populates="vehicle_costs") category: Mapped["CostCategory"] = relationship("CostCategory", back_populates="costs") def __repr__(self) -> str: return f"VehicleCost(id={self.id}, vehicle_id={self.vehicle_id}, amount={self.amount} {self.currency})" class VehicleOdometerState(Base): """ Jármű kilométeróra állapotának és becslésének tárolása. Adminisztrátor által paraméterezhető algoritmusokkal működik. """ __tablename__ = "vehicle_odometer_states" __table_args__ = {"schema": "vehicle"} vehicle_id: Mapped[int] = mapped_column( Integer, ForeignKey("vehicle.vehicle_model_definitions.id", ondelete="CASCADE"), primary_key=True, nullable=False ) last_recorded_odometer: Mapped[int] = mapped_column(Integer, nullable=False) last_recorded_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) daily_avg_distance: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False) estimated_current_odometer: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) confidence_score: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) manual_override_avg: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now()) # Kapcsolat a jármű definícióval vehicle: Mapped["VehicleModelDefinition"] = relationship("VehicleModelDefinition", back_populates="odometer_state") def __repr__(self) -> str: return f"VehicleOdometerState(vehicle_id={self.vehicle_id}, estimated={self.estimated_current_odometer}, confidence={self.confidence_score})" class VehicleUserRating(Base): """ Jármű értékelési rendszer - User -> Vehicle kapcsolat. Egy felhasználó csak egyszer értékelhet egy adott járművet. Értékelés 4 dimenzióban 1-10 skálán. """ __tablename__ = "vehicle_user_ratings" __table_args__ = ( UniqueConstraint("vehicle_id", "user_id", name="uq_vehicle_user_rating_unique"), CheckConstraint("driving_experience BETWEEN 1 AND 10", name="ck_driving_experience_range"), CheckConstraint("reliability BETWEEN 1 AND 10", name="ck_reliability_range"), CheckConstraint("comfort BETWEEN 1 AND 10", name="ck_comfort_range"), CheckConstraint("consumption_satisfaction BETWEEN 1 AND 10", name="ck_consumption_satisfaction_range"), {"schema": "vehicle"} ) id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, server_default=func.gen_random_uuid() ) vehicle_id: Mapped[int] = mapped_column( Integer, ForeignKey("vehicle.vehicle_model_definitions.id", ondelete="CASCADE"), nullable=False, index=True ) user_id: Mapped[int] = mapped_column( Integer, ForeignKey("identity.users.id", ondelete="CASCADE"), nullable=False, index=True ) driving_experience: Mapped[int] = mapped_column(Integer, nullable=False) reliability: Mapped[int] = mapped_column(Integer, nullable=False) comfort: Mapped[int] = mapped_column(Integer, nullable=False) consumption_satisfaction: Mapped[int] = mapped_column(Integer, nullable=False) comment: Mapped[Optional[str]] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now()) # Kapcsolatok vehicle: Mapped["VehicleModelDefinition"] = relationship("VehicleModelDefinition", back_populates="ratings") user: Mapped["User"] = relationship("User", back_populates="vehicle_ratings") def __repr__(self) -> str: return f"VehicleUserRating(id={self.id}, vehicle_id={self.vehicle_id}, user_id={self.user_id})" @property def average_score(self) -> float: """Számított átlagpontszám a 4 dimenzióból.""" scores = [self.driving_experience, self.reliability, self.comfort, self.consumption_satisfaction] return sum(scores) / 4.0 class GbCatalogDiscovery(Base): __tablename__ = "gb_catalog_discovery" __table_args__ = {"schema": "vehicle"} id: Mapped[int] = mapped_column(Integer, primary_key=True) vrm: Mapped[str] = mapped_column(String(20), unique=True) make: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) model: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) status: Mapped[str] = mapped_column(String(20), default='pending') created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())