frontend kínlódás

This commit is contained in:
Roo
2026-03-31 06:20:43 +00:00
parent 2508ae7452
commit c7cbe60976
46 changed files with 6091 additions and 136 deletions

View File

@@ -20,15 +20,16 @@ async def list_makes(
@router.get("/models", response_model=List[str])
async def list_models(
make: str,
vehicle_class: str = None,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_user)
):
"""2. Szint: Típusok listázása egy adott márkához."""
"""2. Szint: Típusok listázása egy adott márkához, opcionálisan vehicle_class szerint szűrve."""
# Handle empty or invalid parameters gracefully
if not make or make.strip() == "":
return []
models = await AssetService.get_models(db, make)
models = await AssetService.get_models(db, make, vehicle_class)
# Return empty list instead of 404 - frontend can handle empty dropdown
return models or []

View File

@@ -5,9 +5,11 @@ from sqlalchemy.exc import SQLAlchemyError
from typing import Dict, Any
from app.api.deps import get_db, get_current_user
from app.schemas.user import UserResponse, UserUpdate, ActiveOrganizationUpdate
from app.schemas.user import UserResponse, UserUpdate, ActiveOrganizationUpdate, UserWithTokenResponse
from app.models.identity import User
from app.services.trust_engine import TrustEngine
from app.core.security import create_tokens, DEFAULT_RANK_MAP
from app.core.config import settings
router = APIRouter()
trust_engine = TrustEngine()
@@ -157,7 +159,7 @@ async def update_user_preferences(
return UserResponse.model_validate(current_user)
@router.patch("/me/active-organization", response_model=UserResponse)
@router.patch("/me/active-organization", response_model=UserWithTokenResponse)
async def update_active_organization(
update_data: ActiveOrganizationUpdate,
db: AsyncSession = Depends(get_db),
@@ -167,6 +169,7 @@ async def update_active_organization(
Update the user's active organization (scope_id).
Accepts an organization_id (UUID/string) or None to revert to personal mode.
Returns a new JWT token with updated scope_id in the payload.
"""
# Extract organization_id from request
org_id = update_data.organization_id
@@ -200,5 +203,22 @@ async def update_active_organization(
await db.rollback()
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
# Return updated user data
return UserResponse.model_validate(current_user)
# Generate new JWT token with updated scope_id
role_key = current_user.role.value.upper()
token_payload = {
"sub": str(current_user.id),
"role": role_key,
"rank": DEFAULT_RANK_MAP.get(role_key, "user"),
"scope_level": "organization" if org_id else "personal",
"scope_id": org_id,
"person_id": str(current_user.person_id) if current_user.person_id else None,
}
access_token, _ = create_tokens(data=token_payload)
# Return user data with new token
return UserWithTokenResponse(
user=UserResponse.model_validate(current_user),
access_token=access_token,
token_type="bearer"
)

View File

@@ -1,6 +1,7 @@
# /opt/docker/dev/service_finder/backend/app/models/vehicle/asset.py
from __future__ import annotations
import uuid
import enum
from datetime import datetime
from typing import List, Optional, TYPE_CHECKING
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint, BigInteger, Integer, Float
@@ -9,6 +10,7 @@ from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from sqlalchemy.sql import func
from app.database import Base
class AssetCatalog(Base):
""" Jármű katalógus mesteradatok (Validált technikai sablonok). """
__tablename__ = "vehicle_catalog"
@@ -33,46 +35,106 @@ class AssetCatalog(Base):
master_definition: Mapped[Optional["VehicleModelDefinition"]] = relationship("VehicleModelDefinition", back_populates="variants")
assets: Mapped[List["Asset"]] = relationship("Asset", back_populates="catalog")
class VehicleClassEnum(str, enum.Enum):
"""Jármű osztályok a 99_Adattarolás.md alapján."""
PERSONAL = "personal" # Személygépjármű
MOTORCYCLE = "motorcycle" # Motorkerékpár
LIGHT_COMMERCIAL = "light_commercial" # Kishaszon gépjármű
COMMERCIAL = "commercial" # Haszonjármű
WORK_MACHINE = "work_machine" # Munkagép
TRAILER = "trailer" # Pótkocsi/utánfutó
BUS = "bus" # Autóbusz
CAMPER = "camper" # Lakókocsi/lakóautó
BOAT = "boat" # Hajó
AIRCRAFT = "aircraft" # Repülőgép
class RoofTypeEnum(str, enum.Enum):
"""Tető típusok a 99_Adattarolás.md alapján."""
METAL = "metal" # Lemeztető
FABRIC = "fabric" # Vászontető
HARDTOP = "hardtop" # Nyitható keménytető
FOLDING = "folding" # Harmonikatető
TARGA = "targa" # Targatető
FIXED_GLASS = "fixed_glass" # Fix üvegtető
PANORAMIC = "panoramic" # Panorámatető
FIXED_SUNROOF = "fixed_sunroof" # Fix napfénytető
OPENABLE_SUNROOF = "openable_sunroof" # Nyitható napfénytető
RETRACTABLE_SUNROOF = "retractable_sunroof" # Elhúzható napfénytető
MOTORIZED_SUNROOF = "motorized_sunroof" # Motoros napfénytető
OPENABLE_PANORAMIC = "openable_panoramic" # Nyitható panorámatető
class Asset(Base):
""" A fizikai eszköz (Digital Twin) - Minden adat itt fut össze. """
__tablename__ = "assets"
__table_args__ = {"schema": "vehicle"}
# === IDENTIFICATION ===
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
vin: Mapped[Optional[str]] = mapped_column(String(17), unique=True, index=True, nullable=True)
license_plate: Mapped[Optional[str]] = mapped_column(String(20), index=True)
name: Mapped[Optional[str]] = mapped_column(String)
# Állapot és életút mérőszámok
catalog_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("vehicle.vehicle_catalog.id"))
# === CLASSIFICATION ===
vehicle_class: Mapped[Optional[str]] = mapped_column(String(50), index=True) # VehicleClassEnum értékek
brand: Mapped[Optional[str]] = mapped_column(String(100), index=True) # Márka (ha nincs catalog)
model: Mapped[Optional[str]] = mapped_column(String(100), index=True) # Modell (ha nincs catalog)
trim_level: Mapped[Optional[str]] = mapped_column(String(100)) # Felszereltségi szint/kivitel
# === TECHNICAL SPECS ===
fuel_type: Mapped[Optional[str]] = mapped_column(String(50), index=True) # benzin, diesel, elektromos, etanol, gáz
engine_capacity: Mapped[Optional[int]] = mapped_column(Integer, index=True) # cm³
power_kw: Mapped[Optional[int]] = mapped_column(Integer, index=True) # kW
torque_nm: Mapped[Optional[int]] = mapped_column(Integer) # Nm
cylinder_layout: Mapped[Optional[str]] = mapped_column(String(50)) # soros, V, boxer, stb.
transmission_type: Mapped[Optional[str]] = mapped_column(String(50), index=True) # kézi, autómata, CVT, DCT
drive_type: Mapped[Optional[str]] = mapped_column(String(50), index=True) # első, hátsó, összkerék
euro_classification: Mapped[Optional[str]] = mapped_column(String(10)) # EURO 1-6
# === PHYSICAL DIMENSIONS ===
curb_weight: Mapped[Optional[int]] = mapped_column(Integer) # saját tömeg (kg)
max_weight: Mapped[Optional[int]] = mapped_column(Integer) # össztömeg (kg)
cargo_volume_x: Mapped[Optional[float]] = mapped_column(Numeric(10, 2)) # csomagtartó hossz (cm)
cargo_volume_y: Mapped[Optional[float]] = mapped_column(Numeric(10, 2)) # csomagtartó szélesség (cm)
door_count: Mapped[Optional[int]] = mapped_column(Integer) # ajtók száma
seat_count: Mapped[Optional[int]] = mapped_column(Integer) # ülések száma
# === EQUIPMENT ===
roof_type: Mapped[Optional[str]] = mapped_column(String(50)) # RoofTypeEnum értékek
audio_system_type: Mapped[Optional[str]] = mapped_column(String(100)) # rádió típusa
individual_equipment: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) # JSONB extra felszerelések
# === STATUS ===
current_mileage: Mapped[int] = mapped_column(Integer, default=0, index=True)
condition_score: Mapped[int] = mapped_column(Integer, default=100) # 0-100
status: Mapped[str] = mapped_column(String(20), default="active")
data_status: Mapped[Optional[str]] = mapped_column(String(20), nullable=True, server_default=text("'draft'"))
# === TIMELINE ===
year_of_manufacture: Mapped[Optional[int]] = mapped_column(Integer, index=True)
first_registration_date: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
current_mileage: Mapped[int] = mapped_column(Integer, default=0, index=True)
condition_score: Mapped[int] = mapped_column(Integer, default=100)
# Értékesítési modul
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# === SALES MODULE ===
is_for_sale: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
price: Mapped[Optional[float]] = mapped_column(Numeric(15, 2))
currency: Mapped[str] = mapped_column(String(3), default="EUR")
catalog_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("vehicle.vehicle_catalog.id"))
# === ORGANIZATION & LOCATION ===
current_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"))
# Garage-centric hierarchy
branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("fleet.branches.id"))
relocation_performed: Mapped[bool] = mapped_column(Boolean, server_default=text('false'), default=False)
# Identity kapcsolatok
# === IDENTITY RELATIONSHIPS ===
owner_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
owner_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"))
operator_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
operator_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"))
status: Mapped[str] = mapped_column(String(20), default="active")
data_status: Mapped[Optional[str]] = mapped_column(String(20), nullable=True, server_default=text("'draft'"))
individual_equipment: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# --- KAPCSOLATOK ---
catalog: Mapped["AssetCatalog"] = relationship("AssetCatalog", back_populates="assets")
financials: Mapped[Optional["AssetFinancials"]] = relationship("AssetFinancials", back_populates="asset", uselist=False)
@@ -119,12 +181,12 @@ class Asset(Base):
if self.license_plate and self.license_plate.strip():
total_score += default_weights['license_plate']
# 2. make (from catalog)
if self.catalog and self.catalog.make:
# 2. make (from catalog or brand field)
if (self.catalog and self.catalog.make) or self.brand:
total_score += default_weights['make']
# 3. model (from catalog)
if self.catalog and self.catalog.model:
# 3. model (from catalog or model field)
if (self.catalog and self.catalog.model) or self.model:
total_score += default_weights['model']
# 4. vin
@@ -137,6 +199,7 @@ class Asset(Base):
return min(total_score, 100)
class AssetFinancials(Base):
""" I. Beszerzés és IV. Értékcsökkenés (Amortizáció). """
__tablename__ = "asset_financials"
@@ -154,6 +217,7 @@ class AssetFinancials(Base):
asset: Mapped["Asset"] = relationship("Asset", back_populates="financials")
class AssetCost(Base):
""" II. Üzemeltetés és TCO kimutatás. """
__tablename__ = "asset_costs"
@@ -172,6 +236,7 @@ class AssetCost(Base):
asset: Mapped["Asset"] = relationship("Asset", back_populates="costs")
organization: Mapped["Organization"] = relationship("Organization")
class VehicleLogbook(Base):
""" Útnyilvántartás (NAV, Kiküldetés, Munkábajárás). """
__tablename__ = "vehicle_logbook"
@@ -193,7 +258,6 @@ class VehicleLogbook(Base):
end_lng: Mapped[Optional[float]] = mapped_column(Numeric(10, 6), nullable=True)
gps_calculated_distance: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), nullable=True)
# OBDII és telemetria
obd_verified: Mapped[bool] = mapped_column(Boolean, default=False)
max_acceleration: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
average_speed: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
@@ -201,6 +265,7 @@ class VehicleLogbook(Base):
asset: Mapped["Asset"] = relationship("Asset", back_populates="logbook")
driver: Mapped["User"] = relationship("User")
class AssetInspection(Base):
""" Napi ellenőrző lista és Biztonsági check. """
__tablename__ = "asset_inspections"
@@ -216,6 +281,7 @@ class AssetInspection(Base):
asset: Mapped["Asset"] = relationship("Asset", back_populates="inspections")
inspector: Mapped["User"] = relationship("User")
class AssetReview(Base):
""" Jármű értékelések és visszajelzések. """
__tablename__ = "asset_reviews"
@@ -231,6 +297,7 @@ class AssetReview(Base):
asset: Mapped["Asset"] = relationship("Asset", back_populates="reviews")
user: Mapped["User"] = relationship("User")
class VehicleOwnership(Base):
""" Tulajdonosváltások története. """
__tablename__ = "vehicle_ownership_history"
@@ -246,6 +313,7 @@ class VehicleOwnership(Base):
# JAVÍTVA: Kapcsolat a User modellhez
user: Mapped["User"] = relationship("User", back_populates="ownership_history")
class AssetTelemetry(Base):
__tablename__ = "asset_telemetry"
__table_args__ = {"schema": "vehicle"}
@@ -254,6 +322,7 @@ class AssetTelemetry(Base):
current_mileage: Mapped[int] = mapped_column(Integer, default=0)
asset: Mapped["Asset"] = relationship("Asset", back_populates="telemetry")
class AssetAssignment(Base):
""" Eszköz-Szervezet összerendelés. """
__tablename__ = "asset_assignments"
@@ -266,14 +335,44 @@ class AssetAssignment(Base):
asset: Mapped["Asset"] = relationship("Asset", back_populates="assignments")
organization: Mapped["Organization"] = relationship("Organization", back_populates="assets")
class AssetEventTypeEnum(str, enum.Enum):
"""Digitális Szervizkönyv eseménytípusok."""
SERVICE = "SERVICE" # Szerviz
REPAIR = "REPAIR" # Javítás
ACCIDENT = "ACCIDENT" # Baleset
INSPECTION = "INSPECTION" # Műszaki vizsga
TIRE_CHANGE = "TIRE_CHANGE" # Gumi csere
MAINTENANCE = "MAINTENANCE" # Karbantartás
UPGRADE = "UPGRADE" # Fejlesztés
RECALL = "RECALL" # Visszahívás
class AssetEvent(Base):
""" Szerviz, baleset és egyéb jelentős események. """
""" Digitális Szervizkönyv - Szerviz, baleset és egyéb jelentős események. """
__tablename__ = "asset_events"
__table_args__ = {"schema": "vehicle"}
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), nullable=False)
event_type: Mapped[str] = mapped_column(String(50), nullable=False)
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), nullable=True)
event_type: Mapped[str] = mapped_column(String(50), nullable=False) # AssetEventTypeEnum értékek
odometer_reading: Mapped[Optional[int]] = mapped_column(Integer) # Km óra állás az eseménykor
description: Mapped[Optional[str]] = mapped_column(Text)
cost_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.asset_costs.id"), nullable=True)
event_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
# Relationships
asset: Mapped["Asset"] = relationship("Asset", back_populates="events")
user: Mapped[Optional["User"]] = relationship("User")
organization: Mapped[Optional["Organization"]] = relationship("Organization")
cost: Mapped[Optional["AssetCost"]] = relationship("AssetCost")
class ExchangeRate(Base):
__tablename__ = "exchange_rates"
@@ -281,6 +380,7 @@ class ExchangeRate(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
rate: Mapped[float] = mapped_column(Numeric(18, 6), nullable=False)
class CatalogDiscovery(Base):
""" Robot munkaterület a felfedezett modelleknek. """
__tablename__ = "catalog_discovery"
@@ -309,6 +409,7 @@ class CatalogDiscovery(Base):
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
class VehicleExpenses(Base):
""" Jármű költségek a jelentésekhez. """
__tablename__ = "vehicle_expenses"
@@ -347,4 +448,4 @@ class VehicleTransferRequest(Base):
asset: Mapped["Asset"] = relationship("Asset")
requester: Mapped["User"] = relationship("User", foreign_keys=[requester_id])
current_owner: Mapped[Optional["Person"]] = relationship("Person", foreign_keys=[current_owner_id])
proof_document: Mapped[Optional["Document"]] = relationship("Document")
proof_document: Mapped[Optional["Document"]] = relationship("Document")

View File

@@ -1,5 +1,5 @@
# /opt/docker/dev/service_finder/backend/app/schemas/asset.py
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, validator
from typing import Optional, Dict, Any, List
from uuid import UUID
from datetime import datetime
@@ -30,39 +30,143 @@ class AssetCatalogResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
class AssetResponse(BaseModel):
""" A konkrét járműpéldány (Asset) teljes válaszmodellje. """
""" A konkrét járműpéldány (Asset) teljes válaszmodellje - Thick Digital Twin. """
# === IDENTIFICATION ===
id: UUID
vin: Optional[str] = Field(None, min_length=1, max_length=50)
license_plate: Optional[str] = None
name: Optional[str] = None
year_of_manufacture: Optional[int] = None
catalog_id: Optional[int] = None
# Státusz és ellenőrzés
# === CLASSIFICATION ===
vehicle_class: Optional[str] = None
brand: Optional[str] = None
model: Optional[str] = None
trim_level: Optional[str] = None
# === TECHNICAL SPECS ===
fuel_type: Optional[str] = None
engine_capacity: Optional[int] = None
power_kw: Optional[int] = None
torque_nm: Optional[int] = None
cylinder_layout: Optional[str] = None
transmission_type: Optional[str] = None
drive_type: Optional[str] = None
euro_classification: Optional[str] = None
# === PHYSICAL DIMENSIONS ===
curb_weight: Optional[int] = None
max_weight: Optional[int] = None
cargo_volume_x: Optional[float] = None
cargo_volume_y: Optional[float] = None
door_count: Optional[int] = None
seat_count: Optional[int] = None
# === EQUIPMENT ===
roof_type: Optional[str] = None
audio_system_type: Optional[str] = None
individual_equipment: Dict[str, Any] = Field(default_factory=dict)
# === STATUS ===
current_mileage: int = Field(default=0)
condition_score: int = Field(default=100)
status: str
data_status: Optional[str] = None
is_verified: bool
verification_method: Optional[str] = None
catalog_match_score: Optional[float] = None
# Kapcsolt adatok
catalog_id: Optional[int] = None
catalog: Optional[AssetCatalogResponse] = None # Itt jön a dúsítás!
owner_organization_id: Optional[int] = None
operator_person_id: Optional[int] = None
# Profile completion percentage (0-100)
profile_completion_percentage: int = Field(default=0, ge=0, le=100)
# === TIMELINE ===
year_of_manufacture: Optional[int] = None
first_registration_date: Optional[datetime] = None
created_at: datetime
updated_at: Optional[datetime] = None
# === SALES MODULE ===
is_for_sale: bool = Field(default=False)
price: Optional[float] = None
currency: str = Field(default="EUR")
# === ORGANIZATION & LOCATION ===
current_organization_id: Optional[int] = None
branch_id: Optional[UUID] = None
relocation_performed: bool = Field(default=False)
# === IDENTITY RELATIONSHIPS ===
owner_organization_id: Optional[int] = None
operator_person_id: Optional[int] = None
owner_person_id: Optional[int] = None
operator_org_id: Optional[int] = None
# === CATALOG RELATIONSHIP ===
catalog: Optional[AssetCatalogResponse] = None
# === PROFILE COMPLETION ===
profile_completion_percentage: int = Field(default=0, ge=0, le=100)
model_config = ConfigDict(from_attributes=True)
class AssetCreate(BaseModel):
""" Jármű létrehozásához szükséges adatok. """
vin: Optional[str] = Field(None, min_length=1, max_length=50, description="VIN szám (1-50 karakter, opcionális draft módban)")
""" Jármű létrehozásához szükséges adatok - Thick Digital Twin támogatással. """
# === CORE IDENTIFICATION (Required for status determination) ===
license_plate: str = Field(..., min_length=2, max_length=20, description="Rendszám")
vin: Optional[str] = Field(None, min_length=1, max_length=50, description="VIN szám (opcionális)")
# === CLASSIFICATION (Optional, but affects status) ===
brand: Optional[str] = Field(None, max_length=100, description="Márka (ha nincs catalog_id)")
model: Optional[str] = Field(None, max_length=100, description="Modell (ha nincs catalog_id)")
vehicle_class: Optional[str] = Field(None, max_length=50, description="Járműosztály")
fuel_type: Optional[str] = Field(None, max_length=50, description="Üzemanyag típus")
# === TECHNICAL SPECS (Optional) ===
catalog_id: Optional[int] = Field(None, description="Opcionális katalógus ID (ha ismert a modell)")
organization_id: Optional[int] = Field(None, description="Szervezet ID, amelyhez a jármű tartozik (opcionális, alapértelmezett a felhasználó szervezete)")
engine_capacity: Optional[int] = Field(None, ge=0, description="Hengerűrtartalom (cm³)")
power_kw: Optional[int] = Field(None, ge=0, description="Teljesítmény (kW)")
torque_nm: Optional[int] = Field(None, ge=0, description="Nyomaték (Nm)")
cylinder_layout: Optional[str] = Field(None, max_length=50, description="Hengerelrendezés")
transmission_type: Optional[str] = Field(None, max_length=50, description="Váltó típus")
drive_type: Optional[str] = Field(None, max_length=50, description="Hajtás")
euro_classification: Optional[str] = Field(None, max_length=10, description="EURO besorolás")
# === PHYSICAL DIMENSIONS (Optional) ===
curb_weight: Optional[int] = Field(None, ge=0, description="Saját tömeg (kg)")
max_weight: Optional[int] = Field(None, ge=0, description="Össztömeg (kg)")
cargo_volume_x: Optional[float] = Field(None, ge=0, description="Csomagtartó hossz (cm)")
cargo_volume_y: Optional[float] = Field(None, ge=0, description="Csomagtartó szélesség (cm)")
door_count: Optional[int] = Field(None, ge=0, description="Ajtók száma")
seat_count: Optional[int] = Field(None, ge=0, description="Ülések száma")
# === EQUIPMENT (Optional) ===
trim_level: Optional[str] = Field(None, max_length=100, description="Felszereltségi szint")
roof_type: Optional[str] = Field(None, max_length=50, description="Tető típus")
audio_system_type: Optional[str] = Field(None, max_length=100, description="Hangrendszer")
individual_equipment: Dict[str, Any] = Field(default_factory=dict, description="Egyedi felszerelések")
# === TIMELINE (Optional) ===
year_of_manufacture: Optional[int] = Field(None, ge=1900, le=2100, description="Gyártási év")
first_registration_date: Optional[datetime] = Field(None, description="Első forgalomba helyezés dátuma")
# === ORGANIZATION (Optional) ===
organization_id: Optional[int] = Field(None, description="Szervezet ID (alapértelmezett a felhasználó szervezete)")
# === STATUS VALIDATION ===
@validator('status', pre=True, always=True)
def determine_status(cls, v, values):
"""Automatikus státusz meghatározás az adatkomplettség alapján."""
if v is not None:
return v
# Ellenőrizzük az 5 alapvető mezőt
required_fields = ['license_plate', 'brand', 'model', 'vehicle_class', 'fuel_type']
has_all_required = all(
values.get(field) is not None and str(values.get(field)).strip() != ''
for field in required_fields
)
return "active" if has_all_required else "draft"
# === COMPUTED FIELD: status ===
status: Optional[str] = Field(None, description="Automatikusan számított státusz (draft/active)")
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,78 @@
# /opt/docker/dev/service_finder/backend/app/schemas/asset_event.py
from pydantic import BaseModel, ConfigDict, Field, validator
from typing import Optional, Dict, Any, List
from uuid import UUID
from datetime import datetime
from enum import Enum
class AssetEventTypeEnum(str, Enum):
"""Digitális Szervizkönyv eseménytípusok."""
SERVICE = "SERVICE" # Szerviz
REPAIR = "REPAIR" # Javítás
ACCIDENT = "ACCIDENT" # Baleset
INSPECTION = "INSPECTION" # Műszaki vizsga
TIRE_CHANGE = "TIRE_CHANGE" # Gumi csere
MAINTENANCE = "MAINTENANCE" # Karbantartás
UPGRADE = "UPGRADE" # Fejlesztés
RECALL = "RECALL" # Visszahívás
OTHER = "OTHER" # Egyéb
class AssetEventCreate(BaseModel):
"""Digitális Szervizkönyv esemény létrehozásához szükséges adatok."""
event_type: AssetEventTypeEnum = Field(..., description="Esemény típusa")
odometer_reading: Optional[int] = Field(None, ge=0, description="Km óra állás az eseménykor")
description: str = Field(..., min_length=1, max_length=1000, description="Esemény leírása")
event_date: Optional[datetime] = Field(None, description="Esemény dátuma (alapértelmezett: most)")
cost_id: Optional[UUID] = Field(None, description="Kapcsolódó költség rekord ID (opcionális)")
# RBAC ellenőrzés: csak a tulajdonos vagy operátor szervezet adhat hozzá eseményt
# Ezt a service rétegben validáljuk, nem a sémában
model_config = ConfigDict(from_attributes=True)
class AssetEventResponse(BaseModel):
"""Digitális Szervizkönyv esemény teljes válaszmodellje."""
id: UUID
asset_id: UUID
user_id: Optional[int] = None
organization_id: Optional[int] = None
event_type: str
odometer_reading: Optional[int] = None
description: Optional[str] = None
cost_id: Optional[UUID] = None
event_date: datetime
created_at: datetime
updated_at: Optional[datetime] = None
# Kapcsolódó entitások (opcionális, csak ha eager loadoltuk)
user_name: Optional[str] = Field(None, description="Felhasználó neve")
organization_name: Optional[str] = Field(None, description="Szervezet neve")
model_config = ConfigDict(from_attributes=True)
class AssetEventUpdate(BaseModel):
"""Digitális Szervizkönyv esemény frissítéséhez szükséges adatok."""
event_type: Optional[AssetEventTypeEnum] = Field(None, description="Esemény típusa")
odometer_reading: Optional[int] = Field(None, ge=0, description="Km óra állás az eseménykor")
description: Optional[str] = Field(None, min_length=1, max_length=1000, description="Esemény leírása")
event_date: Optional[datetime] = Field(None, description="Esemény dátuma")
cost_id: Optional[UUID] = Field(None, description="Kapcsolódó költség rekord ID")
@validator('description')
def validate_description(cls, v):
if v is not None and len(v.strip()) == 0:
raise ValueError('Description cannot be empty or whitespace only')
return v
model_config = ConfigDict(from_attributes=True)
class AssetEventListResponse(BaseModel):
"""Digitális Szervizkönyv események listázásának válasza."""
items: List[AssetEventResponse] = Field(default_factory=list)
total: int = Field(0, description="Összes esemény száma")
page: int = Field(1, description="Aktuális oldal")
page_size: int = Field(20, description="Oldalméret")
model_config = ConfigDict(from_attributes=True)

View File

@@ -28,4 +28,10 @@ class UserUpdate(BaseModel):
ui_mode: Optional[str] = None
class ActiveOrganizationUpdate(BaseModel):
organization_id: Optional[str] = None # UUID/string or None to revert to personal mode
organization_id: Optional[str] = None # UUID/string or None to revert to personal mode
class UserWithTokenResponse(BaseModel):
"""User response with new JWT token for organization switching"""
user: UserResponse
access_token: str
token_type: str = "bearer"

View File

@@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""
Database Surgery Script for Organization and Vehicle Fix
Fixes:
1. Update Private Organization owner_id from None to tester_pro's person_id
2. Update Corporate Organization owner_id from user_id to person_id
3. Create 2 new Corporate Orgs (Alpha, Beta) with Branches
4. Distribute vehicles: 1 to Private, 1 to Alpha, 1 to Beta
"""
import asyncio
import uuid
from datetime import datetime
from sqlalchemy import select, update, insert
from app.database import AsyncSessionLocal
from app.models.identity.identity import User, Person
from app.models.marketplace.organization import Organization, Branch, OrgType
from app.models.vehicle.asset import Asset, AssetAssignment
async def fix_database():
async with AsyncSessionLocal() as session:
print("=== DATABASE SURGERY STARTED ===")
# 1. Find tester_pro user and person
result = await session.execute(
select(User).where(User.email == 'tester_pro@profibot.hu')
)
user = result.scalar_one_or_none()
if not user:
print("ERROR: tester_pro user not found!")
return
print(f"Found user: {user.email} (id={user.id}, person_id={user.person_id})")
# Get person
result = await session.execute(
select(Person).where(Person.id == user.person_id)
)
person = result.scalar_one_or_none()
if not person:
print("ERROR: tester_pro person not found!")
return
print(f"Found person: id={person.id}")
# 2. Fix Private Organization (ID 21) - set owner_id to person.id
result = await session.execute(
select(Organization).where(
Organization.id == 21,
Organization.org_type == OrgType.individual
)
)
private_org = result.scalar_one_or_none()
if private_org:
print(f"\n1. Fixing Private Organization: {private_org.full_name}")
print(f" Current owner_id: {private_org.owner_id} (should be {person.id})")
if private_org.owner_id != person.id:
await session.execute(
update(Organization)
.where(Organization.id == 21)
.values(owner_id=person.id)
)
print(f" ✓ Updated owner_id to {person.id}")
else:
print(f" ✓ Already correct")
else:
print("\n1. Private Organization (ID 21) not found, creating...")
# Create private org if it doesn't exist
private_org = Organization(
full_name=f"Private Organization for {user.email}",
org_type=OrgType.individual,
owner_id=person.id,
status="active",
is_active=True
)
session.add(private_org)
await session.flush()
print(f" ✓ Created Private Organization with id={private_org.id}")
# 3. Fix Corporate Organization (ID 15) - update owner_id from user.id to person.id
result = await session.execute(
select(Organization).where(
Organization.id == 15,
Organization.org_type == OrgType.fleet_owner
)
)
corp_org = result.scalar_one_or_none()
if corp_org:
print(f"\n2. Fixing Corporate Organization: {corp_org.full_name}")
print(f" Current owner_id: {corp_org.owner_id} (user_id, should be person_id={person.id})")
if corp_org.owner_id != person.id:
await session.execute(
update(Organization)
.where(Organization.id == 15)
.values(owner_id=person.id)
)
print(f" ✓ Updated owner_id from {corp_org.owner_id} to {person.id}")
else:
print(f" ✓ Already correct")
# 4. Create 2 new Corporate Orgs: Alpha and Beta
print(f"\n3. Creating 2 new Corporate Organizations...")
# Alpha Organization
import uuid
from datetime import datetime
alpha_slug = f"a{uuid.uuid4().hex[:6]}" # 7 characters total
now = datetime.utcnow()
alpha_org = Organization(
full_name="Test Kft. Alpha",
name="Test Kft. Alpha", # Required field
folder_slug=alpha_slug, # Required unique field, max 12 chars
org_type=OrgType.fleet_owner,
owner_id=person.id,
status="active",
is_active=True,
first_registered_at=now,
current_lifecycle_started_at=now,
subscription_plan="FREE" # Required field with default
)
session.add(alpha_org)
await session.flush()
# Alpha Branch
alpha_branch = Branch(
organization_id=alpha_org.id,
name="Alpha Main Garage",
is_main=True
)
session.add(alpha_branch)
print(f" ✓ Created Alpha Organization: {alpha_org.full_name} (id={alpha_org.id}, slug={alpha_slug})")
print(f" ✓ Created Alpha Branch: {alpha_branch.name}")
# Beta Organization
beta_slug = f"b{uuid.uuid4().hex[:6]}" # 7 characters total
beta_org = Organization(
full_name="Test Kft. Beta",
name="Test Kft. Beta", # Required field
folder_slug=beta_slug, # Required unique field, max 12 chars
org_type=OrgType.fleet_owner,
owner_id=person.id,
status="active",
is_active=True,
first_registered_at=now,
current_lifecycle_started_at=now,
subscription_plan="FREE" # Required field with default
)
session.add(beta_org)
await session.flush()
# Beta Branch
beta_branch = Branch(
organization_id=beta_org.id,
name="Beta Main Garage",
is_main=True
)
session.add(beta_branch)
print(f" ✓ Created Beta Organization: {beta_org.full_name} (id={beta_org.id}, slug={beta_slug})")
print(f" ✓ Created Beta Branch: {beta_branch.name}")
# 5. Get vehicles owned by tester_pro
result = await session.execute(
select(Asset).where(Asset.owner_person_id == person.id)
)
vehicles = result.scalars().all()
print(f"\n4. Distributing {len(vehicles)} vehicles...")
if len(vehicles) >= 3:
# Distribute: vehicle 0 -> Private, vehicle 1 -> Alpha, vehicle 2 -> Beta
private_vehicle = vehicles[0]
alpha_vehicle = vehicles[1]
beta_vehicle = vehicles[2]
# Update private vehicle to Private Organization
await session.execute(
update(Asset)
.where(Asset.id == private_vehicle.id)
.values(current_organization_id=private_org.id)
)
print(f" ✓ Vehicle '{private_vehicle.license_plate}' -> Private Organization")
# Update alpha vehicle to Alpha Organization
await session.execute(
update(Asset)
.where(Asset.id == alpha_vehicle.id)
.values(current_organization_id=alpha_org.id)
)
print(f" ✓ Vehicle '{alpha_vehicle.license_plate}' -> Alpha Organization")
# Update beta vehicle to Beta Organization
await session.execute(
update(Asset)
.where(Asset.id == beta_vehicle.id)
.values(current_organization_id=beta_org.id)
)
print(f" ✓ Vehicle '{beta_vehicle.license_plate}' -> Beta Organization")
# Update asset assignments as well
# First delete existing assignments for these vehicles
from sqlalchemy import delete
await session.execute(
delete(AssetAssignment).where(
AssetAssignment.asset_id.in_([private_vehicle.id, alpha_vehicle.id, beta_vehicle.id])
)
)
# Create new assignments
assignments = [
AssetAssignment(asset_id=private_vehicle.id, organization_id=private_org.id),
AssetAssignment(asset_id=alpha_vehicle.id, organization_id=alpha_org.id),
AssetAssignment(asset_id=beta_vehicle.id, organization_id=beta_org.id)
]
for assignment in assignments:
session.add(assignment)
print(f" ✓ Updated asset assignments")
else:
print(f" ⚠️ Not enough vehicles (need 3, have {len(vehicles)})")
# Commit all changes
await session.commit()
print(f"\n=== DATABASE SURGERY COMPLETED ===")
print(f"Summary:")
print(f" - Fixed Private Organization owner_id")
print(f" - Fixed Corporate Organization owner_id")
print(f" - Created 2 new Corporate Orgs: Alpha and Beta")
print(f" - Created Branches for each org")
print(f" - Distributed 3 vehicles to Private, Alpha, Beta garages")
if __name__ == "__main__":
asyncio.run(fix_database())

View File

@@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""
Complete database surgery for tester_pro organizations and vehicles.
Creates branches for Alpha/Beta organizations, distributes vehicles, and sets up asset assignments.
"""
import asyncio
import asyncpg
import os
import uuid
async def fix_database():
# Use the correct connection string for asyncpg
DATABASE_URL = "postgresql://service_finder_app:AppSafePass_2026@db:5432/service_finder"
print("=== DATABASE SURGERY STARTED ===")
conn = await asyncpg.connect(DATABASE_URL)
try:
# Start transaction
await conn.execute("BEGIN")
print("1. Checking existing organizations...")
# Get tester_pro's person_id (should be 29)
person = await conn.fetchrow("""
SELECT id FROM identity.persons WHERE email = 'tester_pro@profibot.hu'
""")
if not person:
print(" ❌ tester_pro not found!")
return
person_id = person['id']
print(f" ✓ Found tester_pro with person_id={person_id}")
# Check existing organizations
orgs = await conn.fetch("""
SELECT id, full_name, org_type FROM fleet.organizations
WHERE owner_id = $1 ORDER BY id
""", person_id)
print(f" ✓ Found {len(orgs)} organizations for tester_pro")
for org in orgs:
print(f" - ID {org['id']}: {org['full_name']} ({org['org_type']})")
# 1. Ensure Private Organization has correct owner_id (should already be fixed)
print("\n2. Fixing Private Organization owner_id...")
private_org = await conn.fetchrow("""
SELECT id FROM fleet.organizations
WHERE owner_id = $1 AND org_type = 'individual'
""", person_id)
if private_org:
print(f" ✓ Private Organization exists (ID: {private_org['id']})")
else:
print(" ⚠️ No Private Organization found")
# 2. Fix Corporate Organization owner_id (should already be fixed)
print("\n3. Fixing Corporate Organization owner_id...")
corp_org = await conn.fetchrow("""
SELECT id FROM fleet.organizations
WHERE owner_id = $1 AND org_type = 'fleet_owner' AND full_name LIKE '%Profibot Test Fleet%'
""", person_id)
if corp_org:
print(f" ✓ Corporate Organization exists (ID: {corp_org['id']})")
else:
print(" ⚠️ No Corporate Organization found")
# 3. Create branches for Alpha and Beta organizations if they don't exist
print("\n4. Creating branches for Alpha and Beta organizations...")
# Check Alpha organization
alpha_org = await conn.fetchrow("""
SELECT id, full_name FROM fleet.organizations
WHERE owner_id = $1 AND full_name = 'Test Kft. Alpha'
""", person_id)
if alpha_org:
print(f" ✓ Alpha Organization exists (ID: {alpha_org['id']})")
# Check if Alpha has a branch
alpha_branch = await conn.fetchrow("""
SELECT id FROM fleet.branches WHERE organization_id = $1
""", alpha_org['id'])
if not alpha_branch:
print(" Creating Alpha Branch...")
alpha_branch_id = str(uuid.uuid4())
await conn.execute("""
INSERT INTO fleet.branches (
id, name, organization_id, branch_rating, opening_hours,
status, is_deleted, created_at
) VALUES (
$1, $2, $3, 0.0, '{}'::jsonb, 'active', false, NOW()
)
""", alpha_branch_id, f"{alpha_org['full_name']} - Main Branch", alpha_org['id'])
print(f" ✓ Created Alpha Branch (ID: {alpha_branch_id})")
else:
print(f" ✓ Alpha Branch already exists (ID: {alpha_branch['id']})")
# Check Beta organization
beta_org = await conn.fetchrow("""
SELECT id, full_name FROM fleet.organizations
WHERE owner_id = $1 AND full_name = 'Test Kft. Beta'
""", person_id)
if beta_org:
print(f" ✓ Beta Organization exists (ID: {beta_org['id']})")
# Check if Beta has a branch
beta_branch = await conn.fetchrow("""
SELECT id FROM fleet.branches WHERE organization_id = $1
""", beta_org['id'])
if not beta_branch:
print(" Creating Beta Branch...")
beta_branch_id = str(uuid.uuid4())
await conn.execute("""
INSERT INTO fleet.branches (
id, name, organization_id, branch_rating, opening_hours,
status, is_deleted, created_at
) VALUES (
$1, $2, $3, 0.0, '{}'::jsonb, 'active', false, NOW()
)
""", beta_branch_id, f"{beta_org['full_name']} - Main Branch", beta_org['id'])
print(f" ✓ Created Beta Branch (ID: {beta_branch_id})")
else:
print(f" ✓ Beta Branch already exists (ID: {beta_branch['id']})")
# 4. Distribute vehicles
print("\n5. Distributing vehicles...")
# Get 3 vehicles owned by tester_pro
vehicles = await conn.fetch("""
SELECT id, license_plate, current_organization_id
FROM vehicle.assets
WHERE owner_person_id = $1
ORDER BY license_plate
LIMIT 3
""", person_id)
if len(vehicles) >= 3:
# Vehicle 1: Keep in Private Organization (ID 21)
private_org_id = 21 # From earlier check
await conn.execute("""
UPDATE vehicle.assets
SET current_organization_id = $1
WHERE id = $2
""", private_org_id, vehicles[0]['id'])
print(f" ✓ Vehicle '{vehicles[0]['license_plate']}' -> Private Organization")
# Vehicle 2: Move to Alpha Organization
if alpha_org:
await conn.execute("""
UPDATE vehicle.assets
SET current_organization_id = $1
WHERE id = $2
""", alpha_org['id'], vehicles[1]['id'])
print(f" ✓ Vehicle '{vehicles[1]['license_plate']}' -> Alpha Organization")
# Vehicle 3: Move to Beta Organization
if beta_org:
await conn.execute("""
UPDATE vehicle.assets
SET current_organization_id = $1
WHERE id = $2
""", beta_org['id'], vehicles[2]['id'])
print(f" ✓ Vehicle '{vehicles[2]['license_plate']}' -> Beta Organization")
# 5. Update asset assignments with proper UUIDs and status
print("\n6. Updating asset assignments...")
# Delete existing assignments for these vehicles
await conn.execute("""
DELETE FROM fleet.asset_assignments
WHERE asset_id IN ($1, $2, $3)
""", vehicles[0]['id'], vehicles[1]['id'], vehicles[2]['id'])
# Create new assignments with UUIDs and status
assignments = [
(str(uuid.uuid4()), vehicles[0]['id'], private_org_id, 'active'),
(str(uuid.uuid4()), vehicles[1]['id'], alpha_org['id'] if alpha_org else None, 'active'),
(str(uuid.uuid4()), vehicles[2]['id'], beta_org['id'] if beta_org else None, 'active')
]
for assignment_id, asset_id, org_id, status in assignments:
if org_id: # Skip if organization doesn't exist
await conn.execute("""
INSERT INTO fleet.asset_assignments (id, asset_id, organization_id, status)
VALUES ($1, $2, $3, $4)
""", assignment_id, asset_id, org_id, status)
print(f" ✓ Created {len([a for a in assignments if a[2]])} asset assignments")
else:
print(f" ⚠️ Not enough vehicles (need 3, have {len(vehicles)})")
# Commit transaction
await conn.execute("COMMIT")
print("\n=== DATABASE SURGERY COMPLETED SUCCESSFULLY ===")
except Exception as e:
await conn.execute("ROLLBACK")
print(f"\n=== ERROR: {e} ===")
raise
finally:
await conn.close()
if __name__ == "__main__":
asyncio.run(fix_database())

View File

@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
Final database fix script using raw SQL in a single transaction
"""
import asyncio
import asyncpg
import os
async def fix_database():
# Get DATABASE_URL from environment
db_url = os.getenv('DATABASE_URL', 'postgresql+asyncpg://postgres:postgres@sf_postgres:5432/service_finder')
# Convert to sync URL for asyncpg
sync_url = db_url.replace('+asyncpg', '').replace('postgresql://', 'postgres://')
print("=== DATABASE SURGERY STARTED ===")
conn = await asyncpg.connect(sync_url)
try:
# Start transaction
await conn.execute("BEGIN")
# 1. Fix Private Organization (ID 21)
print("1. Fixing Private Organization owner_id...")
result = await conn.execute("""
UPDATE fleet.organizations
SET owner_id = 29, updated_at = NOW()
WHERE id = 21 AND org_type = 'individual'
""")
print(f" ✓ Updated {result.split()[1]} rows")
# 2. Fix Corporate Organization (ID 15)
print("2. Fixing Corporate Organization owner_id...")
result = await conn.execute("""
UPDATE fleet.organizations
SET owner_id = 29, updated_at = NOW()
WHERE id = 15 AND org_type = 'fleet_owner'
""")
print(f" ✓ Updated {result.split()[1]} rows")
# 3. Create Alpha Organization
print("3. Creating Alpha Organization...")
alpha_org = await conn.fetchrow("""
INSERT INTO fleet.organizations (
full_name, name, folder_slug, org_type, owner_id, status, is_active,
first_registered_at, current_lifecycle_started_at, lifecycle_index,
is_anonymized, default_currency, country_code, language,
subscription_plan, base_asset_limit, purchased_extra_slots,
notification_settings, external_integration_config, is_verified,
created_at, updated_at, is_ownership_transferable
) VALUES (
$1, $2, $3, 'fleet_owner', 29, 'active', true,
NOW(), NOW(), 1,
false, 'HUF', 'HU', 'hu',
'FREE', 1, 0,
'{"notify_owner": true, "alert_days_before": [30, 15, 7, 1]}'::jsonb,
'{}'::jsonb, false,
NOW(), NOW(), true
) RETURNING id, folder_slug
""", 'Test Kft. Alpha', 'Test Kft. Alpha', f'alpha{os.urandom(3).hex()}')
print(f" ✓ Created Alpha Organization (id={alpha_org['id']}, slug={alpha_org['folder_slug']})")
# Create Alpha Branch
import uuid
alpha_branch_id = uuid.uuid4()
await conn.execute("""
INSERT INTO fleet.branches (
id, organization_id, name, is_main,
branch_rating, opening_hours, status, is_deleted, created_at
) VALUES ($1, $2, $3, $4, 0.0, '{}'::jsonb, 'active', false, NOW())
""", alpha_branch_id, alpha_org['id'], 'Alpha Main Garage', True)
print(f" ✓ Created Alpha Branch (id={alpha_branch_id})")
# 4. Create Beta Organization
print("4. Creating Beta Organization...")
beta_org = await conn.fetchrow("""
INSERT INTO fleet.organizations (
full_name, name, folder_slug, org_type, owner_id, status, is_active,
first_registered_at, current_lifecycle_started_at, lifecycle_index,
is_anonymized, default_currency, country_code, language,
subscription_plan, base_asset_limit, purchased_extra_slots,
notification_settings, external_integration_config, is_verified,
created_at, updated_at, is_ownership_transferable
) VALUES (
$1, $2, $3, 'fleet_owner', 29, 'active', true,
NOW(), NOW(), 1,
false, 'HUF', 'HU', 'hu',
'FREE', 1, 0,
'{"notify_owner": true, "alert_days_before": [30, 15, 7, 1]}'::jsonb,
'{}'::jsonb, false,
NOW(), NOW(), true
) RETURNING id, folder_slug
""", 'Test Kft. Beta', 'Test Kft. Beta', f'beta{os.urandom(3).hex()}')
print(f" ✓ Created Beta Organization (id={beta_org['id']}, slug={beta_org['folder_slug']})")
# Create Beta Branch
beta_branch_id = uuid.uuid4()
await conn.execute("""
INSERT INTO fleet.branches (
id, organization_id, name, is_main,
branch_rating, opening_hours, status, is_deleted, created_at
) VALUES ($1, $2, $3, $4, 0.0, '{}'::jsonb, 'active', false, NOW())
""", beta_branch_id, beta_org['id'], 'Beta Main Garage', True)
print(f" ✓ Created Beta Branch (id={beta_branch_id})")
# 5. Get first 3 vehicles owned by person_id 29
print("5. Distributing vehicles...")
vehicles = await conn.fetch("""
SELECT id, license_plate
FROM vehicle.assets
WHERE owner_person_id = 29
AND license_plate IS NOT NULL
ORDER BY id
LIMIT 3
""")
if len(vehicles) >= 3:
# Update vehicle 1 to Private Organization (ID 21)
await conn.execute("""
UPDATE vehicle.assets
SET current_organization_id = 21
WHERE id = $1
""", vehicles[0]['id'])
print(f" ✓ Vehicle '{vehicles[0]['license_plate']}' -> Private Organization")
# Update vehicle 2 to Alpha Organization
await conn.execute("""
UPDATE vehicle.assets
SET current_organization_id = $1
WHERE id = $2
""", alpha_org['id'], vehicles[1]['id'])
print(f" ✓ Vehicle '{vehicles[1]['license_plate']}' -> Alpha Organization")
# Update vehicle 3 to Beta Organization
await conn.execute("""
UPDATE vehicle.assets
SET current_organization_id = $1
WHERE id = $2
""", beta_org['id'], vehicles[2]['id'])
print(f" ✓ Vehicle '{vehicles[2]['license_plate']}' -> Beta Organization")
# Update asset assignments
# Delete existing assignments
await conn.execute("""
DELETE FROM fleet.asset_assignments
WHERE asset_id IN ($1, $2, $3)
""", vehicles[0]['id'], vehicles[1]['id'], vehicles[2]['id'])
# Create new assignments
await conn.execute("""
INSERT INTO fleet.asset_assignments (asset_id, organization_id)
VALUES ($1, 21), ($2, $3), ($4, $5)
""", vehicles[0]['id'], vehicles[1]['id'], alpha_org['id'], vehicles[2]['id'], beta_org['id'])
print(f" ✓ Updated asset assignments")
else:
print(f" ⚠️ Not enough vehicles (need 3, have {len(vehicles)})")
# Commit transaction
await conn.execute("COMMIT")
print("\n=== DATABASE SURGERY COMPLETED SUCCESSFULLY ===")
except Exception as e:
await conn.execute("ROLLBACK")
print(f"\n=== ERROR: {e} ===")
raise
finally:
await conn.close()
if __name__ == "__main__":
asyncio.run(fix_database())

View File

@@ -0,0 +1,202 @@
-- Database Surgery Script for Organization and Vehicle Fix
-- Using raw SQL to avoid SQLAlchemy model complexity
-- 1. Fix Private Organization (ID 21) - set owner_id to tester_pro's person_id (29)
UPDATE fleet.organizations
SET owner_id = 29, updated_at = NOW()
WHERE id = 21 AND org_type = 'individual';
-- 2. Fix Corporate Organization (ID 15) - update owner_id from user_id (28) to person_id (29)
UPDATE fleet.organizations
SET owner_id = 29, updated_at = NOW()
WHERE id = 15 AND org_type = 'fleet_owner';
-- 3. Create Alpha Organization
INSERT INTO fleet.organizations (
full_name,
name,
folder_slug,
org_type,
owner_id,
status,
is_active,
first_registered_at,
current_lifecycle_started_at,
lifecycle_index,
is_anonymized,
default_currency,
country_code,
language,
subscription_plan,
base_asset_limit,
purchased_extra_slots,
notification_settings,
external_integration_config,
is_verified,
created_at,
updated_at,
is_ownership_transferable
) VALUES (
'Test Kft. Alpha',
'Test Kft. Alpha',
'alpha' || substr(md5(random()::text), 1, 6),
'fleet_owner',
29,
'active',
true,
NOW(),
NOW(),
1,
false,
'HUF',
'HU',
'hu',
'FREE',
1,
0,
'{"notify_owner": true, "alert_days_before": [30, 15, 7, 1]}'::jsonb,
'{}'::jsonb,
false,
NOW(),
NOW(),
true
) RETURNING id;
-- Save the Alpha org ID for branch creation
WITH alpha_org AS (
SELECT id FROM fleet.organizations
WHERE folder_slug LIKE 'alpha%' AND full_name = 'Test Kft. Alpha'
ORDER BY created_at DESC LIMIT 1
)
INSERT INTO fleet.branches (organization_id, name, is_main)
SELECT id, 'Alpha Main Garage', true FROM alpha_org;
-- 4. Create Beta Organization
INSERT INTO fleet.organizations (
full_name,
name,
folder_slug,
org_type,
owner_id,
status,
is_active,
first_registered_at,
current_lifecycle_started_at,
lifecycle_index,
is_anonymized,
default_currency,
country_code,
language,
subscription_plan,
base_asset_limit,
purchased_extra_slots,
notification_settings,
external_integration_config,
is_verified,
created_at,
updated_at,
is_ownership_transferable
) VALUES (
'Test Kft. Beta',
'Test Kft. Beta',
'beta' || substr(md5(random()::text), 1, 6),
'fleet_owner',
29,
'active',
true,
NOW(),
NOW(),
1,
false,
'HUF',
'HU',
'hu',
'FREE',
1,
0,
'{"notify_owner": true, "alert_days_before": [30, 15, 7, 1]}'::jsonb,
'{}'::jsonb,
false,
NOW(),
NOW(),
true
) RETURNING id;
-- Save the Beta org ID for branch creation
WITH beta_org AS (
SELECT id FROM fleet.organizations
WHERE folder_slug LIKE 'beta%' AND full_name = 'Test Kft. Beta'
ORDER BY created_at DESC LIMIT 1
)
INSERT INTO fleet.branches (organization_id, name, is_main)
SELECT id, 'Beta Main Garage', true FROM beta_org;
-- 5. Get vehicle IDs for distribution
-- First 3 vehicles owned by person_id 29 (tester_pro)
WITH vehicle_ids AS (
SELECT id, license_plate, ROW_NUMBER() OVER (ORDER BY id) as rn
FROM vehicle.assets
WHERE owner_person_id = 29
AND license_plate IS NOT NULL
ORDER BY id
LIMIT 3
),
org_ids AS (
SELECT
(SELECT id FROM fleet.organizations WHERE id = 21) as private_org_id,
(SELECT id FROM fleet.organizations WHERE full_name = 'Test Kft. Alpha') as alpha_org_id,
(SELECT id FROM fleet.organizations WHERE full_name = 'Test Kft. Beta') as beta_org_id
)
-- Update vehicle 1 to Private Organization
UPDATE vehicle.assets a
SET current_organization_id = o.private_org_id
FROM vehicle_ids v, org_ids o
WHERE a.id = v.id AND v.rn = 1;
-- Update vehicle 2 to Alpha Organization
UPDATE vehicle.assets a
SET current_organization_id = o.alpha_org_id
FROM vehicle_ids v, org_ids o
WHERE a.id = v.id AND v.rn = 2;
-- Update vehicle 3 to Beta Organization
UPDATE vehicle.assets a
SET current_organization_id = o.beta_org_id
FROM vehicle_ids v, org_ids o
WHERE a.id = v.id AND v.rn = 3;
-- 6. Update asset assignments
-- Delete existing assignments for these vehicles
DELETE FROM fleet.asset_assignments
WHERE asset_id IN (
SELECT id FROM vehicle.assets
WHERE owner_person_id = 29
ORDER BY id
LIMIT 3
);
-- Create new assignments
WITH vehicle_ids AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY id) as rn
FROM vehicle.assets
WHERE owner_person_id = 29
AND license_plate IS NOT NULL
ORDER BY id
LIMIT 3
),
org_ids AS (
SELECT
(SELECT id FROM fleet.organizations WHERE id = 21) as private_org_id,
(SELECT id FROM fleet.organizations WHERE full_name = 'Test Kft. Alpha') as alpha_org_id,
(SELECT id FROM fleet.organizations WHERE full_name = 'Test Kft. Beta') as beta_org_id
)
INSERT INTO fleet.asset_assignments (asset_id, organization_id)
SELECT
v.id,
CASE
WHEN v.rn = 1 THEN o.private_org_id
WHEN v.rn = 2 THEN o.alpha_org_id
WHEN v.rn = 3 THEN o.beta_org_id
END
FROM vehicle_ids v, org_ids o
WHERE v.rn IN (1, 2, 3);

View File

@@ -0,0 +1,137 @@
-- Complete database surgery for tester_pro organizations and vehicles
-- Run this in the shared-postgres container
-- 1. Create branches for Alpha and Beta organizations if they don't exist
DO $$
DECLARE
person_id integer := 29;
alpha_org_id integer;
beta_org_id integer;
alpha_branch_id uuid;
beta_branch_id uuid;
vehicle1_id uuid;
vehicle2_id uuid;
vehicle3_id uuid;
BEGIN
RAISE NOTICE '=== DATABASE SURGERY STARTED ===';
-- Get Alpha organization ID
SELECT id INTO alpha_org_id
FROM fleet.organizations
WHERE owner_id = person_id AND full_name = 'Test Kft. Alpha';
-- Get Beta organization ID
SELECT id INTO beta_org_id
FROM fleet.organizations
WHERE owner_id = person_id AND full_name = 'Test Kft. Beta';
RAISE NOTICE 'Alpha Organization ID: %', alpha_org_id;
RAISE NOTICE 'Beta Organization ID: %', beta_org_id;
-- Create branch for Alpha if it doesn't exist
IF alpha_org_id IS NOT NULL THEN
IF NOT EXISTS (SELECT 1 FROM fleet.branches WHERE organization_id = alpha_org_id) THEN
alpha_branch_id := gen_random_uuid();
INSERT INTO fleet.branches (
id, name, organization_id, branch_rating, opening_hours,
status, is_deleted, created_at
) VALUES (
alpha_branch_id, 'Test Kft. Alpha - Main Branch', alpha_org_id,
0.0, '{}'::jsonb, 'active', false, NOW()
);
RAISE NOTICE 'Created Alpha Branch: %', alpha_branch_id;
ELSE
RAISE NOTICE 'Alpha Branch already exists';
END IF;
END IF;
-- Create branch for Beta if it doesn't exist
IF beta_org_id IS NOT NULL THEN
IF NOT EXISTS (SELECT 1 FROM fleet.branches WHERE organization_id = beta_org_id) THEN
beta_branch_id := gen_random_uuid();
INSERT INTO fleet.branches (
id, name, organization_id, branch_rating, opening_hours,
status, is_deleted, created_at
) VALUES (
beta_branch_id, 'Test Kft. Beta - Main Branch', beta_org_id,
0.0, '{}'::jsonb, 'active', false, NOW()
);
RAISE NOTICE 'Created Beta Branch: %', beta_branch_id;
ELSE
RAISE NOTICE 'Beta Branch already exists';
END IF;
END IF;
-- 2. Distribute vehicles
RAISE NOTICE 'Distributing vehicles...';
-- Get 3 vehicles owned by tester_pro
SELECT id INTO vehicle1_id FROM vehicle.assets
WHERE owner_person_id = person_id
ORDER BY license_plate
LIMIT 1 OFFSET 0;
SELECT id INTO vehicle2_id FROM vehicle.assets
WHERE owner_person_id = person_id
ORDER BY license_plate
LIMIT 1 OFFSET 1;
SELECT id INTO vehicle3_id FROM vehicle.assets
WHERE owner_person_id = person_id
ORDER BY license_plate
LIMIT 1 OFFSET 2;
RAISE NOTICE 'Vehicle 1 ID: %', vehicle1_id;
RAISE NOTICE 'Vehicle 2 ID: %', vehicle2_id;
RAISE NOTICE 'Vehicle 3 ID: %', vehicle3_id;
-- Vehicle 1: Keep in Private Organization (ID 21)
UPDATE vehicle.assets
SET current_organization_id = 21
WHERE id = vehicle1_id;
RAISE NOTICE 'Vehicle 1 -> Private Organization (ID 21)';
-- Vehicle 2: Move to Alpha Organization
IF alpha_org_id IS NOT NULL THEN
UPDATE vehicle.assets
SET current_organization_id = alpha_org_id
WHERE id = vehicle2_id;
RAISE NOTICE 'Vehicle 2 -> Alpha Organization (ID %)', alpha_org_id;
END IF;
-- Vehicle 3: Move to Beta Organization
IF beta_org_id IS NOT NULL THEN
UPDATE vehicle.assets
SET current_organization_id = beta_org_id
WHERE id = vehicle3_id;
RAISE NOTICE 'Vehicle 3 -> Beta Organization (ID %)', beta_org_id;
END IF;
-- 3. Update asset assignments
RAISE NOTICE 'Updating asset assignments...';
-- Delete existing assignments for these vehicles
DELETE FROM fleet.asset_assignments
WHERE asset_id IN (vehicle1_id, vehicle2_id, vehicle3_id);
-- Create new assignments with UUIDs and status
IF vehicle1_id IS NOT NULL THEN
INSERT INTO fleet.asset_assignments (id, asset_id, organization_id, status)
VALUES (gen_random_uuid(), vehicle1_id, 21, 'active');
RAISE NOTICE 'Created assignment for Vehicle 1';
END IF;
IF vehicle2_id IS NOT NULL AND alpha_org_id IS NOT NULL THEN
INSERT INTO fleet.asset_assignments (id, asset_id, organization_id, status)
VALUES (gen_random_uuid(), vehicle2_id, alpha_org_id, 'active');
RAISE NOTICE 'Created assignment for Vehicle 2';
END IF;
IF vehicle3_id IS NOT NULL AND beta_org_id IS NOT NULL THEN
INSERT INTO fleet.asset_assignments (id, asset_id, organization_id, status)
VALUES (gen_random_uuid(), vehicle3_id, beta_org_id, 'active');
RAISE NOTICE 'Created assignment for Vehicle 3';
END IF;
RAISE NOTICE '=== DATABASE SURGERY COMPLETED SUCCESSFULLY ===';
END $$;

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
Test Asset Logic - Validates AssetCreate schema, dynamic defaults, and draft/active logic.
This script must be run inside the sf_api container.
"""
import asyncio
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from app.db.session import AsyncSessionLocal
from app.schemas.asset import AssetCreate
from app.services.config_service import config
from datetime import datetime
async def test_asset_create_schema():
"""Test AssetCreate instantiation with various field combinations."""
print("=== Testing AssetCreate Schema ===")
# Test 1: Minimal required fields (license_plate only)
print("\n1. Minimal required fields (license_plate only):")
try:
asset = AssetCreate(license_plate="ABC-123")
print(f" Success: license_plate={asset.license_plate}")
print(f" vehicle_class={asset.vehicle_class}")
print(f" status={asset.status}")
print(f" brand={asset.brand}")
except Exception as e:
print(f" ❌ Error: {e}")
# Test 2: With vehicle_class provided
print("\n2. With vehicle_class provided:")
try:
asset = AssetCreate(license_plate="DEF-456", vehicle_class="SUV")
print(f" Success: vehicle_class={asset.vehicle_class}")
print(f" status={asset.status}")
except Exception as e:
print(f" ❌ Error: {e}")
# Test 3: All core fields (should be active)
print("\n3. All core fields (should be active):")
try:
asset = AssetCreate(
license_plate="GHI-789",
brand="Toyota",
model="Corolla",
vehicle_class="car",
fuel_type="petrol"
)
print(f" Success: status={asset.status}")
print(f" brand={asset.brand}, model={asset.model}")
print(f" vehicle_class={asset.vehicle_class}, fuel_type={asset.fuel_type}")
except Exception as e:
print(f" ❌ Error: {e}")
# Test 4: Missing one core field (should be draft)
print("\n4. Missing one core field (should be draft):")
try:
asset = AssetCreate(
license_plate="JKL-012",
brand="Toyota",
model="Corolla",
vehicle_class="car"
# fuel_type missing
)
print(f" Success: status={asset.status}")
print(f" fuel_type={asset.fuel_type}")
except Exception as e:
print(f" ❌ Error: {e}")
# Test 5: With catalog_id
print("\n5. With catalog_id:")
try:
asset = AssetCreate(
license_plate="MNO-345",
catalog_id=123
)
print(f" Success: catalog_id={asset.catalog_id}")
print(f" status={asset.status}")
except Exception as e:
print(f" ❌ Error: {e}")
print("\n=== Schema tests completed ===\n")
async def test_dynamic_default_vehicle_class():
"""Test that vehicle_class default is fetched from config_service."""
print("=== Testing Dynamic Default Vehicle Class ===")
async with AsyncSessionLocal() as db:
# First, check if DEFAULT_VEHICLE_CLASS parameter exists
default_class = await config.get_setting(db, "DEFAULT_VEHICLE_CLASS", default="car")
print(f"1. DEFAULT_VEHICLE_CLASS from config: '{default_class}'")
# Test 2: Create AssetCreate without vehicle_class, should not be hardcoded
print("\n2. Creating AssetCreate without vehicle_class:")
asset = AssetCreate(license_plate="TEST-001")
print(f" vehicle_class from schema: {asset.vehicle_class}")
print(f" Note: The schema doesn't set a default, so it's None.")
print(f" The service layer should use config default when needed.")
# Test 3: Verify config.get_setting works with different scopes
org_default = await config.get_setting(db, "DEFAULT_VEHICLE_CLASS", default="car", org_id=1)
print(f"\n3. DEFAULT_VEHICLE_CLASS with org_id=1: '{org_default}'")
# Test 4: Insert a test parameter and retrieve it
print("\n4. Testing parameter insertion and retrieval...")
from app.models.system.system import SystemParameter, ParameterScope
from sqlalchemy import select
# Check if parameter exists
stmt = select(SystemParameter).where(
SystemParameter.key == "DEFAULT_VEHICLE_CLASS",
SystemParameter.scope_level == ParameterScope.GLOBAL,
SystemParameter.is_active == True
)
result = await db.execute(stmt)
param = result.scalar_one_or_none()
if param:
print(f" Parameter exists: {param.value}")
else:
print(f" Parameter does not exist, using default 'car'")
print("\n=== Dynamic default tests completed ===\n")
async def test_draft_vs_active_logic():
"""Test the draft vs active status determination logic."""
print("=== Testing Draft vs Active Logic ===")
test_cases = [
{
"name": "All core fields",
"fields": {
"license_plate": "CORE-001",
"brand": "Ford",
"model": "Focus",
"vehicle_class": "car",
"fuel_type": "diesel"
},
"expected_status": "active"
},
{
"name": "Missing brand",
"fields": {
"license_plate": "CORE-002",
"model": "Focus",
"vehicle_class": "car",
"fuel_type": "diesel"
},
"expected_status": "draft"
},
{
"name": "Empty string brand",
"fields": {
"license_plate": "CORE-003",
"brand": "",
"model": "Focus",
"vehicle_class": "car",
"fuel_type": "diesel"
},
"expected_status": "draft"
},
{
"name": "Only license_plate",
"fields": {
"license_plate": "CORE-004"
},
"expected_status": "draft"
},
{
"name": "With catalog_id but missing core fields",
"fields": {
"license_plate": "CORE-005",
"catalog_id": 999
},
"expected_status": "draft"
},
]
for tc in test_cases:
print(f"\nTest: {tc['name']}")
try:
asset = AssetCreate(**tc['fields'])
status = asset.status
print(f" Fields: {list(tc['fields'].keys())}")
print(f" Expected status: {tc['expected_status']}")
print(f" Actual status: {status}")
if status == tc['expected_status']:
print(" ✅ PASS")
else:
print(" ❌ FAIL")
except Exception as e:
print(f" ❌ Error: {e}")
print("\n=== Draft/Active tests completed ===\n")
async def main():
"""Run all tests."""
print("🚀 Starting Asset Logic Tests")
print("=" * 50)
try:
await test_asset_create_schema()
await test_dynamic_default_vehicle_class()
await test_draft_vs_active_logic()
print("=" * 50)
print("✅ All tests completed successfully!")
return 0
except Exception as e:
print(f"❌ Critical error during tests: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

View File

@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""
Truth Serum Script - Database Auditor (STRICT READ-ONLY MODE)
Executes raw SQL JOIN queries to show EXACTLY what is in the database
for the user tester_pro@profibot.hu.
This script MUST NOT modify any data - it's read-only.
"""
import asyncio
import asyncpg
import os
from datetime import datetime
async def get_truth_serum():
"""
Fetch complete map for tester_pro@profibot.hu:
- User/Person: email and person_id
- Organizations: Every Organization linked to this person_id (Private and Corporate)
- Branches (Telephely): Every Branch linked to those Organizations
- Assets (Járművek): Every Asset linked to those Branches
"""
# Use the correct connection string for asyncpg
DATABASE_URL = "postgresql://service_finder_app:AppSafePass_2026@db:5432/service_finder"
print("=== TRUTH SERUM DATABASE AUDIT ===")
print(f"Timestamp: {datetime.now().isoformat()}")
print("User: tester_pro@profibot.hu")
print("Mode: STRICT READ-ONLY (no modifications)")
print("=" * 60)
conn = await asyncpg.connect(DATABASE_URL)
try:
# 1. First, get the person_id for the user
print("\n1. Finding user tester_pro@profibot.hu...")
person = await conn.fetchrow("""
SELECT id, email, first_name, last_name
FROM identity.persons
WHERE email = 'tester_pro@profibot.hu'
""")
if not person:
print(" ❌ ERROR: User tester_pro@profibot.hu not found in identity.persons!")
return
person_id = person['id']
print(f" ✓ Found user: ID={person_id}, Email={person['email']}")
print(f" Name: {person['first_name']} {person['last_name']}")
# 2. Get all organizations for this person
print("\n2. Fetching organizations linked to this person...")
organizations = await conn.fetch("""
SELECT
id as org_id,
full_name as org_name,
org_type,
owner_id,
created_at
FROM fleet.organizations
WHERE owner_id = $1
ORDER BY id
""", person_id)
print(f" ✓ Found {len(organizations)} organizations")
if not organizations:
print(" ⚠️ No organizations found for this user")
return
org_ids = [org['org_id'] for org in organizations]
# 3. Get all branches for these organizations
print("\n3. Fetching branches for these organizations...")
branches = await conn.fetch("""
SELECT
b.id as branch_id,
b.name as branch_name,
b.organization_id,
b.created_at
FROM fleet.branches b
WHERE b.organization_id = ANY($1::int[])
ORDER BY b.organization_id, b.id
""", org_ids)
print(f" ✓ Found {len(branches)} branches")
branch_ids = [branch['branch_id'] for branch in branches]
# 4. Get all assets for these branches
print("\n4. Fetching assets (vehicles) for these branches...")
assets = await conn.fetch("""
SELECT
a.id as asset_id,
a.license_plate,
a.branch_id,
a.make,
a.model,
a.status,
a.created_at
FROM data.assets a
WHERE a.branch_id = ANY($1::int[])
ORDER BY a.branch_id, a.id
""", branch_ids)
print(f" ✓ Found {len(assets)} assets")
# 5. Now create the comprehensive JOIN for the final table
print("\n5. Generating comprehensive JOIN map...")
comprehensive_data = await conn.fetch("""
SELECT
p.email as "User Email",
o.full_name as "Organization Name",
b.name as "Branch Name",
a.license_plate as "License Plate",
o.org_type as "Org Type",
o.id as org_id,
b.id as branch_id,
a.id as asset_id
FROM identity.persons p
LEFT JOIN fleet.organizations o ON o.owner_id = p.id
LEFT JOIN fleet.branches b ON b.organization_id = o.id
LEFT JOIN data.assets a ON a.branch_id = b.id
WHERE p.email = 'tester_pro@profibot.hu'
ORDER BY o.id, b.id, a.id
""")
# 6. Print summary statistics
print("\n" + "=" * 60)
print("SUMMARY STATISTICS:")
print(f" • User: {person['email']} (ID: {person_id})")
print(f" • Organizations: {len(organizations)}")
print(f" • Branches: {len(branches)}")
print(f" • Assets (Vehicles): {len(assets)}")
print(f" • Comprehensive JOIN rows: {len(comprehensive_data)}")
print("=" * 60)
# 7. Print the clean Markdown table
print("\n" + "#" * 80)
print("# TRUTH SERUM DATABASE MAP")
print("# User: tester_pro@profibot.hu")
print("# Generated at: " + datetime.now().isoformat())
print("#" * 80 + "\n")
if comprehensive_data:
print("| User Email | Organization Name | Branch Name | License Plate |")
print("|------------|-------------------|-------------|---------------|")
for row in comprehensive_data:
# Handle None values
org_name = row['Organization Name'] or "(No Organization)"
branch_name = row['Branch Name'] or "(No Branch)"
license_plate = row['License Plate'] or "(No License Plate)"
print(f"| {row['User Email']} | {org_name} | {branch_name} | {license_plate} |")
print(f"\nTotal rows: {len(comprehensive_data)}")
else:
print("⚠️ No data found in comprehensive JOIN.")
# 8. Print detailed breakdown for debugging
print("\n" + "=" * 60)
print("DETAILED BREAKDOWN:")
for org in organizations:
print(f"\nOrganization: {org['org_name']} (ID: {org['org_id']}, Type: {org['org_type']})")
org_branches = [b for b in branches if b['organization_id'] == org['org_id']]
if not org_branches:
print(" No branches")
continue
for branch in org_branches:
print(f" └─ Branch: {branch['branch_name']} (ID: {branch['branch_id']})")
branch_assets = [a for a in assets if a['branch_id'] == branch['branch_id']]
if not branch_assets:
print(" No assets")
else:
for asset in branch_assets:
print(f" └─ Asset: {asset['license_plate']} (ID: {asset['asset_id']}, Make: {asset['make']}, Model: {asset['model']})")
print("\n" + "=" * 60)
print("AUDIT COMPLETE - NO DATA MODIFIED")
print("=" * 60)
except Exception as e:
print(f"\n❌ ERROR during audit: {e}")
import traceback
traceback.print_exc()
finally:
await conn.close()
if __name__ == "__main__":
asyncio.run(get_truth_serum())

View File

@@ -7,10 +7,12 @@ from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_, distinct
from sqlalchemy.orm import selectinload
from fastapi import HTTPException
from app.models import Asset, AssetAssignment, AssetTelemetry, AssetFinancials, VehicleModelDefinition
from app.models.identity import User
from app.models.vehicle.history import LogSeverity
from app.schemas.asset import AssetCreate
from app.services.config_service import config
from app.services.gamification_service import GamificationService
from app.services.security_service import security_service
@@ -33,20 +35,23 @@ class AssetService:
db: AsyncSession,
user_id: int,
org_id: int,
vin: Optional[str] = None,
license_plate: Optional[str] = None,
catalog_id: int = None,
asset_data: AssetCreate,
draft: bool = False
):
"""
Intelligens Jármű Rögzítés:
Ha új: létrehozza.
Intelligens Jármű Rögzítés - Thick Digital Twin támogatással:
Ha új: létrehozza a teljes technikai adatokkal.
Ha már létezik: Transzfer folyamatot indít.
Ha draft=True vagy VIN hiányzik: draft státuszban hozza létre.
Automatikus státusz meghatározás az adatkomplettség alapján.
Catalog Snapshot Sync: Ha catalog_id van, betölti a hiányzó technikai adatokat.
"""
try:
vin_clean = vin.strip().upper() if vin else None
license_plate_clean = license_plate.strip().upper() if license_plate else None
# Clean input data
vin_clean = asset_data.vin.strip().upper() if asset_data.vin else None
license_plate_clean = asset_data.license_plate.strip().upper()
# Use organization_id from asset_data if provided, otherwise use the passed org_id
target_org_id = asset_data.organization_id or org_id
# 1. ADMIN LIMIT ELLENŐRZÉS (csak aktív járművek számítanak)
user_stmt = select(User).where(User.id == user_id)
@@ -54,17 +59,35 @@ class AssetService:
# Get vehicle limit using the new function that checks both user AND organization limits
# Returns the HIGHER value of user-specific and organization-specific limits
allowed_limit = await AssetService.get_user_vehicle_limit(db, user_id, org_id)
allowed_limit = await AssetService.get_user_vehicle_limit(db, user_id, target_org_id)
# Csak aktív járművek számítanak a limitbe (draft-ok nem)
count_stmt = select(func.count(Asset.id)).where(
Asset.current_organization_id == org_id,
Asset.current_organization_id == target_org_id,
Asset.status == "active"
)
current_count = (await db.execute(count_stmt)).scalar()
if current_count >= allowed_limit and not draft:
raise ValueError(f"Limit túllépés! A csomagod {allowed_limit} autót engedélyez.")
# Determine status based on data completeness (use Pydantic validator's logic)
# Check the 5 core fields: license_plate, brand, model, vehicle_class, fuel_type
core_fields_complete = all([
asset_data.license_plate and asset_data.license_plate.strip(),
asset_data.brand and asset_data.brand.strip(),
asset_data.model and asset_data.model.strip(),
asset_data.vehicle_class and asset_data.vehicle_class.strip(),
asset_data.fuel_type and asset_data.fuel_type.strip()
])
# Determine final status
if draft:
status = "draft"
elif not core_fields_complete:
status = "draft"
else:
status = "active"
if current_count >= allowed_limit and status == "active":
raise ValueError(f"Limit túllépés! A csomagod {allowed_limit} aktív autót engedélyez.")
# 2. LÉTEZIK-E MÁR A JÁRMŰ? (csak ha van VIN)
existing_asset = None
@@ -74,41 +97,95 @@ class AssetService:
if existing_asset:
# HA MÁR A JELENLEGI SZERVEZETNÉL VAN
if existing_asset.current_organization_id == org_id:
if existing_asset.current_organization_id == target_org_id:
raise ValueError("Ez a jármű már a te garázsodban van.")
# TRANSZFER FOLYAMAT INDÍTÁSA
return await AssetService.initiate_ownership_transfer(
db, existing_asset, user_id, org_id, license_plate_clean or ""
db, existing_asset, user_id, target_org_id, license_plate_clean or ""
)
# 3. ÚJ JÁRMŰ LÉTREHOZÁSA (Standard vagy Draft Flow)
# Dynamic Gatekeeper Logic: If both vin and catalog_id are missing, status = 'draft'
# If core data is provided (either vin OR catalog_id), status = 'active'
# Also respect the draft parameter if explicitly set
if draft:
status = "draft"
elif not vin_clean and not catalog_id:
status = "draft"
else:
status = "active"
# 3. CATALOG SNAPSHOT SYNC - Ha catalog_id van, betöltjük a hiányzó technikai adatokat
catalog_data = {}
if asset_data.catalog_id:
catalog_stmt = select(VehicleModelDefinition).where(
VehicleModelDefinition.id == asset_data.catalog_id
)
catalog = (await db.execute(catalog_stmt)).scalar_one_or_none()
if catalog:
# Map catalog fields to asset fields (only if not already provided by user)
catalog_data = {
'brand': catalog.make if not asset_data.brand else None,
'model': catalog.marketing_name if not asset_data.model else None,
'vehicle_class': catalog.vehicle_class if not asset_data.vehicle_class else None,
'fuel_type': catalog.fuel_type if not asset_data.fuel_type else None,
'power_kw': catalog.power_kw if not asset_data.power_kw else None,
'engine_capacity': catalog.engine_capacity if not asset_data.engine_capacity else None,
'euro_classification': catalog.euro_class if not asset_data.euro_classification else None,
'body_type': catalog.body_type if not asset_data.trim_level else None,
}
# Remove None values
catalog_data = {k: v for k, v in catalog_data.items() if v is not None}
# 4. ÚJ JÁRMŰ LÉTREHOZÁSA - Thick Digital Twin
# Először összeállítjuk az összes adatot (user input + catalog snapshot)
# Get default vehicle class from config if not provided
default_vehicle_class = await config.get_setting(db, "DEFAULT_VEHICLE_CLASS", default="car")
asset_fields = {
'vin': vin_clean,
'license_plate': license_plate_clean,
'catalog_id': asset_data.catalog_id,
'current_organization_id': target_org_id,
'owner_person_id': user.person_id,
'owner_org_id': asset_data.owner_org_id or target_org_id,
'operator_org_id': asset_data.operator_org_id,
'status': status,
'individual_equipment': asset_data.individual_equipment or {},
'created_at': datetime.utcnow(),
new_asset = Asset(
vin=vin_clean,
license_plate=license_plate_clean,
catalog_id=catalog_id,
current_organization_id=org_id,
owner_person_id=user.person_id,
owner_org_id=org_id,
status=status,
individual_equipment={},
created_at=datetime.utcnow()
)
# Classification
'brand': asset_data.brand or catalog_data.get('brand'),
'model': asset_data.model or catalog_data.get('model'),
'vehicle_class': asset_data.vehicle_class or catalog_data.get('vehicle_class') or default_vehicle_class,
'trim_level': asset_data.trim_level,
# Technical Specs
'fuel_type': asset_data.fuel_type or catalog_data.get('fuel_type'),
'engine_capacity': asset_data.engine_capacity or catalog_data.get('engine_capacity'),
'power_kw': asset_data.power_kw or catalog_data.get('power_kw'),
'torque_nm': asset_data.torque_nm,
'cylinder_layout': asset_data.cylinder_layout,
'transmission_type': asset_data.transmission_type,
'drive_type': asset_data.drive_type,
'euro_classification': asset_data.euro_classification or catalog_data.get('euro_classification'),
# Physical Dimensions
'curb_weight': asset_data.curb_weight,
'max_weight': asset_data.max_weight,
'cargo_volume_x': asset_data.cargo_volume_x,
'cargo_volume_y': asset_data.cargo_volume_y,
'door_count': asset_data.door_count,
'seat_count': asset_data.seat_count,
# Equipment
'roof_type': asset_data.roof_type,
'audio_system_type': asset_data.audio_system_type,
# Timeline
'year_of_manufacture': asset_data.year_of_manufacture,
'first_registration_date': asset_data.first_registration_date,
}
# Remove None values from the dictionary
asset_fields = {k: v for k, v in asset_fields.items() if v is not None}
new_asset = Asset(**asset_fields)
db.add(new_asset)
await db.flush()
# Digitális Iker Alapmodulok
db.add(AssetAssignment(asset_id=new_asset.id, organization_id=org_id, status="active"))
db.add(AssetAssignment(asset_id=new_asset.id, organization_id=target_org_id, status="active"))
db.add(AssetTelemetry(asset_id=new_asset.id))
db.add(AssetFinancials(
asset_id=new_asset.id,
@@ -122,7 +199,7 @@ class AssetService:
await GamificationService.award_points(db, user_id, int(reward), "NEW_ASSET_REG")
# Check if this is user's first vehicle and award "First Car" badge
await AssetService._award_first_car_badge(db, user_id, org_id)
await AssetService._award_first_car_badge(db, user_id, target_org_id)
await db.commit()
return new_asset
@@ -207,11 +284,14 @@ class AssetService:
return [make for make in makes if make] # Filter out None/empty
@staticmethod
async def get_models(db: AsyncSession, make: str) -> List[str]:
"""Get all distinct models for a given make."""
async def get_models(db: AsyncSession, make: str, vehicle_class: str = None) -> List[str]:
"""Get all distinct models for a given make, optionally filtered by vehicle_class."""
stmt = select(distinct(VehicleModelDefinition.marketing_name)).where(
VehicleModelDefinition.make == make
).order_by(VehicleModelDefinition.marketing_name)
)
if vehicle_class:
stmt = stmt.where(VehicleModelDefinition.vehicle_class == vehicle_class)
stmt = stmt.order_by(VehicleModelDefinition.marketing_name)
result = await db.execute(stmt)
models = result.scalars().all()
return [model for model in models if model]