STABLE: KYC and Auth working, before Asset refactor

This commit is contained in:
2026-02-07 01:16:07 +00:00
parent 8020bbd394
commit cd171d3289
9 changed files with 320 additions and 78 deletions

View File

@@ -1,21 +1,36 @@
from app.db.base import Base
from .identity import User, Person, Wallet, UserRole
from .identity import User, Person, Wallet, UserRole, VerificationToken
from .organization import Organization, OrgType
from .vehicle import (
Vehicle,
VehicleBrand,
EngineSpec,
ServiceProvider,
ServiceRecord,
OrganizationMember
VehicleCatalog,
Asset,
AssetEvent,
AssetRating,
ServiceProvider,
Vehicle, # Alias az Asset-re a kompatibilitás miatt
ServiceRecord # Alias az AssetEvent-re a kompatibilitás miatt
)
# Aliasok a kód többi részének
# Aliasok a kód többi részének szinkronizálásához
UserVehicle = Vehicle
VehicleOwnership = Asset
__all__ = [
"Base", "User", "Person", "Wallet", "UserRole",
"Vehicle", "UserVehicle", "VehicleBrand", "EngineSpec",
"ServiceProvider", "ServiceRecord", "Organization",
"OrgType", "OrganizationMember"
"Base",
"User",
"Person",
"Wallet",
"UserRole",
"VerificationToken",
"Organization",
"OrgType",
"VehicleCatalog",
"Asset",
"AssetEvent",
"AssetRating",
"ServiceProvider",
"Vehicle",
"UserVehicle",
"ServiceRecord",
"VehicleOwnership"
]

View File

@@ -5,15 +5,102 @@ from sqlalchemy.sql import func
import uuid
from app.db.base import Base
class VehicleBrand(Base):
__tablename__ = "vehicle_brands"
# 1. GLOBÁLIS KATALÓGUS (A rendszer agya)
class VehicleCatalog(Base):
__tablename__ = "vehicle_catalog"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False, unique=True)
slug = Column(String(100), unique=True)
country_of_origin = Column(String(50))
is_active = Column(Boolean, default=True)
brand = Column(String(100), nullable=False)
model = Column(String(100), nullable=False)
generation = Column(String(100))
year_from = Column(Integer)
year_to = Column(Integer)
category = Column(String(50)) # car, bike, boat, plane, truck
engine_type = Column(String(50)) # petrol, diesel, electric, hybrid
engine_power_kw = Column(Integer)
# DNS ADATOK: Minden technikai részlet (kerékméret, olajmennyiség, stb.)
factory_specs = Column(JSON, default={})
# SZERVIZTERV: Gyári előírások
maintenance_plan = Column(JSON, default={})
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 2. EGYEDI ESZKÖZ (Asset) - A felhasználó tulajdona
class Asset(Base):
__tablename__ = "assets"
__table_args__ = {"schema": "data"}
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
uai = Column(String(100), unique=True, nullable=False) # VIN, HIN vagy Serial Number
organization_id = Column(Integer, ForeignKey("data.organizations.id"))
catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id"), nullable=True)
asset_type = Column(String(20), nullable=False) # car, boat, plane
name = Column(String(255)) # "Kincses E39"
# Dinamikus állapot
current_plate_number = Column(String(20))
current_country_code = Column(String(2))
odometer_value = Column(Numeric(12, 2), default=0)
operating_hours = Column(Numeric(12, 2), default=0)
# Egyedi DNS (Gyári config + utólagos módosítások)
factory_config = Column(JSON, default={})
aftermarket_mods = Column(JSON, default={})
status = Column(String(50), default="active")
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Kapcsolatok
organization = relationship("Organization", back_populates="assets")
catalog_entry = relationship("VehicleCatalog")
events = relationship("AssetEvent", back_populates="asset", cascade="all, delete-orphan")
ratings = relationship("AssetRating", back_populates="asset")
# 3. DIGITÁLIS SZERVIZKÖNYV / ESEMÉNYTÁR
class AssetEvent(Base):
__tablename__ = "asset_events"
__table_args__ = {"schema": "data"}
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id = Column(UUID(as_uuid=True), ForeignKey("data.assets.id"))
event_type = Column(String(50)) # SERVICE, REPAIR, INSPECTION, ACCIDENT, PLATE_CHANGE
odometer_at_event = Column(Numeric(12, 2))
hours_at_event = Column(Numeric(12, 2))
description = Column(String)
cost = Column(Numeric(12, 2))
currency = Column(String(3), default="EUR")
is_verified = Column(Boolean, default=False)
attachments = Column(JSON, default=[]) # Számlák, fotók linkjei
created_at = Column(DateTime(timezone=True), server_default=func.now())
asset = relationship("Asset", back_populates="events")
# 4. EMOCIONÁLIS ÉRTÉKELÉS
class AssetRating(Base):
__tablename__ = "asset_ratings"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
asset_id = Column(UUID(as_uuid=True), ForeignKey("data.assets.id"))
user_id = Column(Integer, ForeignKey("data.users.id"))
comfort_score = Column(Integer) # 1-10
experience_score = Column(Integer) # 1-10
practicality_score = Column(Integer) # 1-10
comment = Column(String)
created_at = Column(DateTime(timezone=True), server_default=func.now())
asset = relationship("Asset", back_populates="ratings")
# 5. SZOLGÁLTATÓK (Szerelők, Partnerek)
class ServiceProvider(Base):
__tablename__ = "service_providers"
__table_args__ = {"schema": "data"}
@@ -21,67 +108,9 @@ class ServiceProvider(Base):
name = Column(String(255), nullable=False)
official_brand_partner = Column(Boolean, default=False)
technical_rating_pct = Column(Integer, default=80)
social_rating_pct = Column(Integer, default=80)
location_city = Column(String(100))
service_type = Column(String(50))
search_tags = Column(String)
latitude = Column(Numeric(10, 8))
longitude = Column(Numeric(11, 8))
is_active = Column(Boolean, default=True)
records = relationship("ServiceRecord", back_populates="provider")
class EngineSpec(Base):
__tablename__ = "engine_specs"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
engine_code = Column(String(50), unique=True)
fuel_type = Column(String(20))
power_kw = Column(Integer)
default_service_interval_km = Column(Integer, default=15000)
vehicles = relationship("Vehicle", back_populates="engine_spec")
class Vehicle(Base):
__tablename__ = "vehicles"
__table_args__ = {"schema": "data"}
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
current_company_id = Column(Integer, ForeignKey("data.organizations.id"))
brand_id = Column(Integer, ForeignKey("data.vehicle_brands.id"))
model_name = Column(String(100))
engine_spec_id = Column(Integer, ForeignKey("data.engine_specs.id"))
identification_number = Column(String(50), unique=True)
license_plate = Column(String(20))
tracking_mode = Column(String(10), default="km")
current_rating_pct = Column(Integer, default=100)
total_real_usage = Column(Numeric(15, 2), default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
engine_spec = relationship("EngineSpec", back_populates="vehicles")
service_records = relationship("ServiceRecord", back_populates="vehicle", cascade="all, delete-orphan")
current_org = relationship("Organization", back_populates="vehicles")
class ServiceRecord(Base):
__tablename__ = "service_records"
__table_args__ = {"schema": "data"}
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
vehicle_id = Column(UUID(as_uuid=True), ForeignKey("data.vehicles.id"))
provider_id = Column(Integer, ForeignKey("data.service_providers.id"))
service_date = Column(Date, nullable=False)
usage_value = Column(Numeric(15, 2))
repair_quality_pct = Column(Integer, default=100)
vehicle = relationship("Vehicle", back_populates="service_records")
provider = relationship("ServiceProvider", back_populates="records")
class OrganizationMember(Base):
__tablename__ = "organization_members"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
role = Column(String, default="driver")
organization = relationship("Organization", back_populates="members")
# --- KOMPATIBILITÁSI RÉTEG ---
UserVehicle = Vehicle
VehicleOwnership = Vehicle
Vehicle = Asset
ServiceRecord = AssetEvent

View File

@@ -0,0 +1,84 @@
import httpx
import asyncio
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.vehicle import VehicleCatalog # Az imént létrehozott modell
class VehicleHarvester:
def __init__(self):
# Az ingyenes CarQueryAPI URL-je (0.3-as verzió)
self.base_url = "https://www.carqueryapi.com/api/0.3/"
self.headers = {"User-Agent": "ServiceFinder-Harvester-Bot/1.0"}
async def get_data(self, params: dict):
"""Segédfüggvény az API hívásokhoz."""
async with httpx.AsyncClient() as client:
try:
response = await client.get(self.base_url, params=params, headers=self.headers, timeout=10.0)
if response.status_code == 200:
# Az API néha JSONP-t ad vissza, ezt itt lekezeljük (levágjuk a felesleget)
text = response.text
if text.startswith("?("): text = text[2:-2]
return response.json()
return None
except Exception as e:
print(f"Robot hiba: {str(e)}")
return None
async def harvest_all(self, db: AsyncSession):
"""A fő folyamat: Minden márka -> Minden modell szinkronizálása."""
print("🤖 Robot: Indul a nagy adatgyűjtés...")
# 1. Márkák lekérése
makes_data = await self.get_data({"cmd": "getMakes", "sold_in_us": 0})
if not makes_data: return
makes = makes_data.get("Makes", [])
for make in makes:
make_id = make['make_id']
make_display = make['make_display']
print(f"--- 🚗 Feldolgozás: {make_display} ---")
# 2. Modellek lekérése ehhez a márkához
models_data = await self.get_data({"cmd": "getModels", "make": make_id})
if not models_data: continue
models = models_data.get("Models", [])
for model in models:
model_name = model['model_name']
# 3. Megnézzük, benne van-e már a katalógusban
stmt = select(VehicleCatalog).where(
VehicleCatalog.brand == make_display,
VehicleCatalog.model == model_name
)
res = await db.execute(stmt)
if res.scalar_one_or_none():
continue # Ha már megvan, ugrunk a következőre
# 4. Új bejegyzés létrehozása alapadatokkal
# Itt a Robot később "mélyebbre" áshat a specifikációkért
new_v = VehicleCatalog(
brand=make_display,
model=model_name,
category="car", # Alapértelmezett, később finomítható
factory_specs={
"api_make_id": make_id,
"harvester_source": "carquery"
}
)
db.add(new_v)
print(f"✅ Robot rögzítve: {make_display} {model_name}")
# Márkánként mentünk, hogy ne vesszen el a munka, ha megszakad
await db.commit()
await asyncio.sleep(1) # Ne terheljük túl az ingyenes API-t (Rate Limit védelem)
print("🏁 Robot: A munka oroszlánrésze kész!")
# Ez a rész csak a teszteléshez kell, ha manuálisan indítod a scriptet
if __name__ == "__main__":
# Itt lehetne egy külön indító logika
pass

20
backend/test_robot.py Normal file
View File

@@ -0,0 +1,20 @@
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.services.harvester_robot import VehicleHarvester
from app.core.config import settings
# Adatbázis kapcsolat felépítése a pontos névvel
engine = create_async_engine(str(settings.DATABASE_URL))
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def run_test():
async with AsyncSessionLocal() as db:
harvester = VehicleHarvester()
print("🚀 Robot indítása...")
# Megpróbáljuk betölteni a katalógust
await harvester.harvest_all(db)
print("✅ Teszt lefutott.")
if __name__ == "__main__":
asyncio.run(run_test())