Compare commits

...

2 Commits

11 changed files with 370 additions and 83 deletions

View File

@@ -1,21 +1,36 @@
from app.db.base import Base 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 .organization import Organization, OrgType
from .vehicle import ( from .vehicle import (
Vehicle, VehicleCatalog,
VehicleBrand, Asset,
EngineSpec, AssetEvent,
ServiceProvider, AssetRating,
ServiceRecord, ServiceProvider,
OrganizationMember 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 UserVehicle = Vehicle
VehicleOwnership = Asset
__all__ = [ __all__ = [
"Base", "User", "Person", "Wallet", "UserRole", "Base",
"Vehicle", "UserVehicle", "VehicleBrand", "EngineSpec", "User",
"ServiceProvider", "ServiceRecord", "Organization", "Person",
"OrgType", "OrganizationMember" "Wallet",
"UserRole",
"VerificationToken",
"Organization",
"OrgType",
"VehicleCatalog",
"Asset",
"AssetEvent",
"AssetRating",
"ServiceProvider",
"Vehicle",
"UserVehicle",
"ServiceRecord",
"VehicleOwnership"
] ]

View File

@@ -16,14 +16,14 @@ class Organization(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False) name = Column(String, nullable=False)
org_type = Column(Enum(OrgType), default=OrgType.INDIVIDUAL) # A stabilitás miatt VARCHAR-ként kezeljük a DB-ben
org_type = Column(String(50), default="individual")
# A flotta technikai tulajdonosa (User) # A flotta technikai tulajdonosa (User)
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True) owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
# Üzleti szabályok # Üzleti szabályok
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
# Privát flotta (INDIVIDUAL) esetén False, cégeknél True
is_transferable = Column(Boolean, default=True) is_transferable = Column(Boolean, default=True)
# Verifikáció # Verifikáció
@@ -33,7 +33,20 @@ class Organization(Base):
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Kapcsolatok # Kapcsolatok (Asset-re hivatkozunk a Vehicle helyett)
vehicles = relationship("Vehicle", back_populates="current_org") assets = relationship("Asset", back_populates="organization", cascade="all, delete-orphan")
members = relationship("OrganizationMember", back_populates="organization") members = relationship("OrganizationMember", back_populates="organization")
owner = relationship("User", back_populates="owned_organizations") owner = relationship("User", back_populates="owned_organizations")
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")
# --- EZT A SORT TEDD KÍVÜLRE, A MARGÓRA ---
Organization.vehicles = Organization.assets

View File

@@ -5,15 +5,102 @@ from sqlalchemy.sql import func
import uuid import uuid
from app.db.base import Base from app.db.base import Base
class VehicleBrand(Base): # 1. GLOBÁLIS KATALÓGUS (A rendszer agya)
__tablename__ = "vehicle_brands" class VehicleCatalog(Base):
__tablename__ = "vehicle_catalog"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False, unique=True) brand = Column(String(100), nullable=False)
slug = Column(String(100), unique=True) model = Column(String(100), nullable=False)
country_of_origin = Column(String(50)) generation = Column(String(100))
is_active = Column(Boolean, default=True) 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): class ServiceProvider(Base):
__tablename__ = "service_providers" __tablename__ = "service_providers"
__table_args__ = {"schema": "data"} __table_args__ = {"schema": "data"}
@@ -21,67 +108,9 @@ class ServiceProvider(Base):
name = Column(String(255), nullable=False) name = Column(String(255), nullable=False)
official_brand_partner = Column(Boolean, default=False) official_brand_partner = Column(Boolean, default=False)
technical_rating_pct = Column(Integer, default=80) technical_rating_pct = Column(Integer, default=80)
social_rating_pct = Column(Integer, default=80)
location_city = Column(String(100)) 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) 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 --- # --- KOMPATIBILITÁSI RÉTEG ---
UserVehicle = Vehicle Vehicle = Asset
VehicleOwnership = Vehicle 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())

View File

@@ -93,3 +93,44 @@ A fejlesztések rendben tartásához javaslom a **`17_DEVELOPER_NOTES_AND_PITFAL
**Holnap reggel frissíted a GEM beállításokat is?** Ha igen, a következő lépésben elkészíthetem neked a Step 2 (KYC) végleges Pydantic sémáját és a `complete-kyc` végpont vázlatát! **Holnap reggel frissíted a GEM beállításokat is?** Ha igen, a következő lépésben elkészíthetem neked a Step 2 (KYC) végleges Pydantic sémáját és a `complete-kyc` végpont vázlatát!
## [0.2.0] - 2026-02-07
### ✨ Hozzáadva (Added)
- **Step 2 (KYC) Folyamat:** Teljes körű identitás-kezelés (telefonszám, születési adatok, okmányok, ICE kontakt).
- **Automata Privát Flotta:** Minden felhasználóhoz automatikusan létrejön egy `individual` típusú szervezet (Privát Széf).
- **Automata Wallet:** Minden validált felhasználó kap egy üres pénztárcát (Coin és XP egyenleggel).
- **Trust Tiers:** Bevezetésre került a fokozatos bizalmi szint (Tier 1: Email, Tier 2: KYC/Active).
### 🛠️ Javítva (Fixed)
- **SQLAlchemy Async Fix:** `joinedload` alkalmazása a User-Person kapcsolathoz (MissingGreenlet hiba elhárítva).
- **JSON Serialization:** Pydantic `model_dump(mode='json')` használata a JSONB mezőkhöz (dátum-konverziós hiba javítva).
- **Postgres Schema:** `data.organizations` tábla bővítve hiányzó oszlopokkal (`is_verified`, `updated_at`, stb.).
- **Auth Endpoint:** `/complete-kyc` végpont hozzáadva és JWT védelemmel ellátva.
### ⚙️ Adatbázis Változások (Database)
- Új Enum típus: `data.orgtype` ('individual', 'company').
- `data.persons` bővítve: `phone`, `birth_place`, `birth_date`, `mothers_name`, `identity_docs`, `ice_contact`.
- `data.organizations` bővítve: `is_verified`, `is_transferable`, `verification_expires_at`, `updated_at`.
## [0.2.0] - 2026-02-07
### ✅ Step 2 KYC & Activation Complete
- **Funkció:** Teljes körű személyazonosság-kezelés és fiókaktiválás.
- **Automatizálás:** Regisztrációkor automatikusan létrejön a "Privát Flotta" (Organization) és a digitális pénztárca (Wallet).
- **Adatvédelem:** Elkészült a "Digitális Széf" logika az okmányok és vészhelyzeti adatok biztonságos tárolására.
- **Technikai fix:** SQLAlchemy `joinedload` integráció az aszinkron adatkezeléshez és JSON-safe dátumkezelés.
## [0.3.0] - 2026-02-07
### ✨ Hozzáadva (Added)
- **Asset DNS Modell:** Új, univerzális eszközkezelő rendszer (Assets, VehicleCatalog, AssetEvents, AssetRatings).
- **Harvester Robot:** Automata adatgyűjtő rendszer, amely külső forrásokból tölti fel a globális járműkatalógust.
- **UUID Implementáció:** Az eszközök (Assets) és események (Events) mostantól biztonságos UUID azonosítókat használnak.
### ⚙️ Adatbázis Változások (Database)
- `data.vehicle_catalog` tábla létrehozva a globális specifikációknak.
- `data.assets` tábla létrehozva a konkrét példányok (VIN/HIN alapú) tárolására.
- `data.asset_events` és `data.asset_ratings` táblák az életút és közösségi visszajelzések kezelésére.
### 🛠️ Refaktor (Refactor)
- **Modell Konszolidáció:** A korábbi `Vehicle` és `VehicleBrand` modellek beolvasztva az új `Asset` és `VehicleCatalog` struktúrába.
- **Kapcsolati Térkép:** Az `Organization` és `User` modellek frissítve az új Asset logikához.

View File

@@ -0,0 +1,85 @@
# 🏎️ Asset és Flotta Specifikáció: A Járművek DNS-e
Ez a dokumentum írja le a rendszer magját képező "széf" logikát, ahol minden közlekedési eszköz (Asset) egyedi életutat és digitális lenyomatot kap.
## 1. Az Alapelv: "Mindenki Flottatulajdonos"
A rendszerben nincs különbség egy magánszemély és egy cég között a technikai rétegben.
- **Privát Flotta:** A regisztráció (Step 2) során automatikusan létrejövő szervezet (Organization).
- **Széf (Safe Deposit):** A flotta része, ahol az eszközök (járművek) és azok bizalmas okmányai laknak.
## 2. Eszköz Típusok és Speciális Azonosítók
Minden eszköz rendelkezik egy **Univerzális Állandó Azonosítóval (UAI)**, ami az életútja során soha nem változik.
| Típus | Elsődleges Azonosító (UAI) | Speciális Adatpontok |
| :--- | :--- | :--- |
| **Közúti** | VIN (Alvázszám) | Rendszám, Motorkód, Sebességváltó kód |
| **Vízi** | HIN (Hull ID / Testszám) | MMSI kód, IMO szám, Név |
| **Légi** | Serial Number (Gyári szám) | Lajstromjel (Registration), Típusjelzés |
| **Egyéb** | Egyedi sorozatszám | Gyártó, Teljesítmény |
### Kiegészítő mérőszámok:
- **Futásteljesítmény (Odometer):** Közúti járműveknél (km/mérföld).
- **Üzemóra (Operating Hours):** Hajók, repülők, munkagépek és versenytechnika esetén kritikus.
## 3. A Jármű DNS (Deep Data Structure)
Az adatbázisnak ismernie kell a járművet "gyári" állapotában és annak minden módosítását.
### A) Gyári Konfiguráció (Factory Specs)
- **Trim Level:** Felszereltségi csomag (pl. S-Line, AMG Pack, Comfortline).
- **Technikai paraméterek:** Motorválaszték, kW/LE, nyomaték, gyári felni- és gumiméret (ET számmal), folyadékmennyiségek.
- **Szervizintervallumok:** Gyártó által előírt periodikus karbantartások (idő vagy távolság alapú).
### B) Aktuális Állapot és Módosítások (Modifications)
- **Gyári extrák:** Mi az, ami benne maradt? (pl. bőrbelső, napfénytető).
- **Utólagos (Aftermarket):** Mi került bele? (pl. vonóhorog, gázszett).
- **Hiányzó:** Mi került ki belőle? (pl. kiszerelt gyári hifi).
## 4. Digitális Szervizkönyv (Digital Service Book)
Nem csak egy lista, hanem egy **Eseményalapú Idővonal (Timeline)**. Minden bejegyzés megváltoztathatatlan (immutable-szerű) logként rögzül.
- **Típusok:** Karbantartás, Javítás, Műszaki Vizsga, Baleset, Tulajdonosváltás.
- **Csatolmányok:** Fotók az alkatrészekről, számlák PDF-ben, munkalapok.
## 5. Jármű Minősítés és Értékelés
A jármű két különálló, de egymást kiegészítő minősítést kap:
### A) Technikai Minősítés (AI Health Score)
- **Algoritmus alapú:** A szerviztörténet, az üzemóra/futás aránya és a gyári specifikációk betartása alapján kalkulált pontszám.
### B) Emocionális és Közösségi Értékelés (Driver Rating)
A járművet használó sofőrök értékelhetik az eszközt szubjektív szempontok alapján:
- **Komfort:** Mennyire kényelmes hosszú távon?
- **Vezetési élmény:** "Lelke van", vagy csak egy gép?
- **Praktikum:** Mennyire használható a mindennapokban?
- **Megbízhatóság érzet:** Mennyire érzi magát benne biztonságban a sofőr?
Ez a kettős mérőszám adja meg a jármű valós "piaci és használati értékét".
## 6. Az Adat-Gondnok (Harvester Robot)
A rendszer integritásáért és az adatok pontosságáért egy automata Robot felel.
### Funkciók:
1. **Initial Load:** A legnépszerűbb 1000 európai járműtípus alapértelmezett feltöltése.
2. **On-Demand Fetch:** Ha egy felhasználó ismeretlen típust keres, a Robot prioritással kutatja fel és rögzíti azt.
3. **Deep Data Scrape:** A Robot nemcsak a típust, hanem a gyári specifikációkat (olajmennyiség, guminyomás, szervizintervallum) is gyűjti.
4. **Maintenance:** Negyedévente frissíti a meglévő adatokat (új modellévek, módosított gyári előírások).
### Adatforrások hierarchiája:
1. Hivatalos gyártói API-k (ahol elérhető).
2. Nyilvános műszaki adatbázisok (Auto-Data, UltimateSpecs).
3. VIN/HIN dekóder algoritmusok.
## 7. Kivételkezelés: Ismeretlen és Egyedi Járművek
Ha egy jármű nem található a globális katalógusban, a rendszer kétlépcsős mentőövet nyújt:
### A) On-Demand Harvester (Robot hívása)
1. A felhasználó jelzi, hogy hiányzik a típus.
2. A Robot utasítást kap egy mélyebb keresésre (Deep Web Search).
3. Ha találat van, a Robot rögzíti a katalógusba, és a felhasználó folytathatja a rögzítést.
### B) Custom Asset (Egyedi/Sport jármű rögzítése)
Ha a jármű sehol nem szerepel (pl. épített versenyautó, egyedi yacht):
1. **Manuális nyilatkozat:** A felhasználó rögzíti az adatokat.
2. **Dokumentum alapú validáció:** A forgalmi engedély vagy sportigazolvány fotóját kötelező feltölteni.
3. **AI Verifikáció:** A rendszer OCR-rel (szövegfelismerés) kiolvassa az adatokat a fotóról, és összeveti a manuális bevitelével.
4. **"Unverified Model" jelzés:** A katalógusban egyedi azonosítót kap, amíg egy admin vagy a Robot más forrásból meg nem erősíti.