feat: Asset Catalog system, PostGIS integration and RobotScout V1

This commit is contained in:
2026-02-11 22:47:38 +00:00
parent a63e6c8fac
commit 09a0430384
53 changed files with 2756 additions and 426 deletions

View File

@@ -1,34 +1,54 @@
# /opt/docker/dev/service_finder/backend/app/models/__init__.py
from app.db.base_class import Base
from .identity import User, Person, Wallet, UserRole, VerificationToken
# Identitás és Jogosultság
from .identity import User, Person, Wallet, UserRole, VerificationToken, SocialAccount
# Szervezeti struktúra
from .organization import Organization, OrganizationMember
# Járművek és Eszközök (Digital Twin)
from .asset import (
Asset, AssetCatalog, AssetCost, AssetEvent,
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
)
# Szerviz és Szakértelem (ÚJ)
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise
# Földrajzi adatok és Címek
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType
# Gamification és Economy
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger
# Rendszerkonfiguráció és Alapok
from .system_config import SystemParameter
from .document import Document
from .translation import Translation # <--- HOZZÁADVA
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
from .history import AuditLog, VehicleOwnership
from .security import PendingAction # <--- HOZZÁADVA
from .translation import Translation
# Aliasok
# Üzleti logika és Előfizetés
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
# Naplózás és Biztonság
from .history import AuditLog, VehicleOwnership
from .security import PendingAction
# Aliasok a kényelmesebb fejlesztéshez
Vehicle = Asset
UserVehicle = Asset
VehicleCatalog = AssetCatalog
ServiceRecord = AssetEvent
__all__ = [
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken",
"Organization", "OrganizationMember", "Asset", "AssetCatalog", "AssetCost",
"AssetEvent", "AssetFinancials", "AssetTelemetry", "AssetReview", "ExchangeRate",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "PointRule",
"LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"SystemParameter", "Document", "Translation", "PendingAction", # <--- BŐVÍTVE
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
"Organization", "OrganizationMember",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
"AssetTelemetry", "AssetReview", "ExchangeRate",
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", # <--- HOZZÁADVA
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"SystemParameter", "Document", "Translation", "PendingAction",
"SubscriptionTier", "OrganizationSubscription",
"CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord"

View File

@@ -6,42 +6,57 @@ from sqlalchemy.sql import func
from app.db.base_class import Base
class AssetCatalog(Base):
"""Globális járműkatalógus (Márka -> Típus -> Generáció -> Motor)."""
__tablename__ = "vehicle_catalog"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
make = Column(String, index=True, nullable=False)
model = Column(String, index=True, nullable=False)
generation = Column(String)
make = Column(String, index=True, nullable=False) # 1. Szint: Audi
model = Column(String, index=True, nullable=False) # 2. Szint: A4
generation = Column(String, index=True) # 3. Szint: B8 (2008-2015)
engine_variant = Column(String) # 4. Szint: 2.0 TDI (150 LE)
year_from = Column(Integer)
year_to = Column(Integer)
vehicle_class = Column(String)
fuel_type = Column(String)
engine_code = Column(String)
factory_data = Column(JSON, server_default=text("'{}'::jsonb"))
factory_data = Column(JSON, server_default=text("'{}'::jsonb")) # Technikai specifikációk
assets = relationship("Asset", back_populates="catalog")
class Asset(Base):
"""Egyedi jármű (Asset) példány - Az ökoszisztéma magja."""
__tablename__ = "assets"
__table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
vin = Column(String(17), unique=True, index=True, nullable=False)
license_plate = Column(String(20), index=True)
name = Column(String)
year_of_manufacture = Column(Integer)
# --- BIZTONSÁGI ÉS JOGOSULTSÁGI IZOLÁCIÓ ---
# A current_organization_id biztosítja a gyors, adatbázis-szintű Scoped RBAC védelmet.
current_organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=True)
catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id"))
is_verified = Column(Boolean, default=False)
verification_method = Column(String(20)) # 'robot', 'ocr', 'manual'
status = Column(String(20), default="active")
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Kapcsolatok (Digital Twin Modules)
catalog = relationship("AssetCatalog", back_populates="assets")
current_org = relationship("Organization")
financials = relationship("AssetFinancials", back_populates="asset", uselist=False)
telemetry = relationship("AssetTelemetry", back_populates="asset", uselist=False)
assignments = relationship("AssetAssignment", back_populates="asset")
events = relationship("AssetEvent", back_populates="asset")
costs = relationship("AssetCost", back_populates="asset")
reviews = relationship("AssetReview", back_populates="asset")
ownership_history = relationship("VehicleOwnership", back_populates="vehicle")
class AssetFinancials(Base):
__tablename__ = "asset_financials"
@@ -77,9 +92,10 @@ class AssetReview(Base):
created_at = Column(DateTime(timezone=True), server_default=func.now())
asset = relationship("Asset", back_populates="reviews")
user = relationship("User") # <--- JAVÍTÁS: Hozzáadva
user = relationship("User")
class AssetAssignment(Base):
"""Jármű flotta-történetének nyilvántartása."""
__tablename__ = "asset_assignments"
__table_args__ = {"schema": "data"}
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
@@ -90,7 +106,7 @@ class AssetAssignment(Base):
status = Column(String(30), default="active")
asset = relationship("Asset", back_populates="assignments")
organization = relationship("Organization") # <--- KRITIKUS JAVÍTÁS: Ez okozta a login hibát
organization = relationship("Organization")
class AssetEvent(Base):
__tablename__ = "asset_events"
@@ -113,16 +129,13 @@ class AssetCost(Base):
amount_local = Column(Numeric(18, 2), nullable=False)
currency_local = Column(String(3), nullable=False)
amount_eur = Column(Numeric(18, 2), nullable=True)
net_amount_local = Column(Numeric(18, 2))
vat_rate = Column(Numeric(5, 2))
exchange_rate_used = Column(Numeric(18, 6))
date = Column(DateTime(timezone=True), server_default=func.now())
mileage_at_cost = Column(Integer)
data = Column(JSON, server_default=text("'{}'::jsonb"))
asset = relationship("Asset", back_populates="costs")
organization = relationship("Organization") # <--- JAVÍTÁS: Hozzáadva
driver = relationship("User") # <--- JAVÍTÁS: Hozzáadva
organization = relationship("Organization")
driver = relationship("User")
class ExchangeRate(Base):
__tablename__ = "exchange_rates"
@@ -131,5 +144,4 @@ class ExchangeRate(Base):
base_currency = Column(String(3), default="EUR")
target_currency = Column(String(3), unique=True)
rate = Column(Numeric(18, 6), nullable=False)
rate_date = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())

View File

@@ -0,0 +1,16 @@
from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, text
from sqlalchemy.sql import func
from app.db.base_class import Base
class AuditLog(Base):
__tablename__ = "audit_logs"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="SET NULL"), nullable=True)
action = Column(String(100), nullable=False) # pl. "LOGIN", "REGISTER", "DELETE_ASSET"
resource_type = Column(String(50)) # pl. "User", "Asset", "Organization"
resource_id = Column(String(100))
details = Column(JSON, server_default=text("'{}'::jsonb"))
ip_address = Column(String(45))
created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -1,85 +1,68 @@
import uuid
import enum
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger, UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.sql import func
from app.db.base_class import Base
class UserRole(str, enum.Enum):
superadmin = "superadmin"
admin = "admin"
user = "user"
service = "service"
fleet_manager = "fleet_manager"
driver = "driver"
superadmin = "superadmin"; admin = "admin"; user = "user"
service = "service"; fleet_manager = "fleet_manager"; driver = "driver"
class Person(Base):
__tablename__ = "persons"
__table_args__ = {"schema": "data"}
__tablename__ = "persons"; __table_args__ = {"schema": "data"}
id = Column(BigInteger, primary_key=True, index=True)
id_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
last_name = Column(String, nullable=False)
first_name = Column(String, nullable=False)
phone = Column(String, nullable=True)
last_name = Column(String, nullable=False); first_name = Column(String, nullable=False); phone = Column(String, nullable=True)
mothers_last_name = Column(String); mothers_first_name = Column(String); birth_place = Column(String); birth_date = Column(DateTime)
identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
is_active = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
users = relationship("User", back_populates="person")
class User(Base):
__tablename__ = "users"
__table_args__ = {"schema": "data"}
__tablename__ = "users"; __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=True)
role = Column(Enum(UserRole), default=UserRole.user)
is_active = Column(Boolean, default=False)
is_deleted = Column(Boolean, default=False)
is_active = Column(Boolean, default=False); is_deleted = Column(Boolean, default=False)
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
# ÚJ MEZŐK HOZZÁADVA:
preferred_language = Column(String(5), server_default="hu")
region_code = Column(String(5), server_default="HU")
# RBAC & SCOPE
scope_level = Column(String(30), server_default="individual")
scope_id = Column(String(50))
custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
folder_slug = Column(String(12), unique=True, index=True)
refresh_token_hash = Column(String(255), nullable=True)
two_factor_secret = Column(String(100), nullable=True)
two_factor_enabled = Column(Boolean, default=False)
preferred_language = Column(String(5), server_default="hu"); region_code = Column(String(5), server_default="HU"); preferred_currency = Column(String(3), server_default="HUF")
scope_level = Column(String(30), server_default="individual"); scope_id = Column(String(50)); custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
created_at = Column(DateTime(timezone=True), server_default=func.now())
person = relationship("Person", back_populates="users"); wallet = relationship("Wallet", back_populates="user", uselist=False)
stats = relationship("UserStats", back_populates="user", uselist=False); ownership_history = relationship("VehicleOwnership", back_populates="user")
owned_organizations = relationship("Organization", back_populates="owner"); social_accounts = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan")
person = relationship("Person", back_populates="users")
wallet = relationship("Wallet", back_populates="user", uselist=False)
stats = relationship("UserStats", back_populates="user", uselist=False)
ownership_history = relationship("VehicleOwnership", back_populates="user")
owned_organizations = relationship("Organization", back_populates="owner")
# A Wallet és VerificationToken osztályok maradnak változatlanok...
class Wallet(Base):
__tablename__ = "wallets"
__table_args__ = {"schema": "data"}
__tablename__ = "wallets"; __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id"), unique=True)
coin_balance = Column(Numeric(18, 2), default=0.00)
credit_balance = Column(Numeric(18, 2), default=0.00)
currency = Column(String(3), default="HUF")
coin_balance = Column(Numeric(18, 2), default=0.00); credit_balance = Column(Numeric(18, 2), default=0.00); currency = Column(String(3), default="HUF")
user = relationship("User", back_populates="wallet")
class VerificationToken(Base):
__tablename__ = "verification_tokens"
__table_args__ = {"schema": "data"}
__tablename__ = "verification_tokens"; __table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
token = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False)
token_type = Column(String(20), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
expires_at = Column(DateTime(timezone=True), nullable=False)
is_used = Column(Boolean, default=False)
token_type = Column(String(20), nullable=False); created_at = Column(DateTime(timezone=True), server_default=func.now())
expires_at = Column(DateTime(timezone=True), nullable=False); is_used = Column(Boolean, default=False)
class SocialAccount(Base):
__tablename__ = "social_accounts"
__table_args__ = (UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'), {"schema": "data"})
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False)
provider = Column(String(50), nullable=False); social_id = Column(String(255), nullable=False, index=True); email = Column(String(255), nullable=False)
extra_data = Column(JSON, server_default=text("'{}'::jsonb")); created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="social_accounts")

View File

@@ -1,5 +1,4 @@
import enum
import uuid
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
from sqlalchemy.orm import relationship
@@ -25,6 +24,9 @@ class Organization(Base):
full_name = Column(String, nullable=False)
name = Column(String, nullable=False)
display_name = Column(String(50))
# --- BIZTONSÁGI BŐVÍTÉS (Mappa elszigetelés) ---
folder_slug = Column(String(12), unique=True, index=True)
default_currency = Column(String(3), default="HUF")
country_code = Column(String(2), default="HU")
@@ -63,7 +65,7 @@ class Organization(Base):
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# String alapú hivatkozás a körkörös import ellen
# Kapcsolatok
assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan")
owner = relationship("User", back_populates="owned_organizations")
@@ -75,8 +77,7 @@ class OrganizationMember(Base):
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")
permissions = Column(JSON, server_default=text("'{}'::jsonb"))
organization = relationship("Organization", back_populates="members")
user = relationship("User") # Egyszerűsített string hivatkozás
user = relationship("User")

View File

@@ -0,0 +1,59 @@
import uuid
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Text
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from geoalchemy2 import Geometry # PostGIS támogatás
from sqlalchemy.sql import func
from app.db.base_class import Base
class ServiceProfile(Base):
"""
Szerviz szolgáltató kiterjesztett adatai.
Egy Organization-höz (org_type='service') kapcsolódik.
"""
__tablename__ = "service_profiles"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
organization_id = Column(Integer, ForeignKey("data.organizations.id"), unique=True)
# PostGIS GPS pont (SRID 4326 = WGS84 koordináták)
location = Column(Geometry(geometry_type='POINT', srid=4326), index=True)
# Trust Engine (Bot Discovery=30, User Entry=50, Admin/Partner=100)
trust_score = Column(Integer, default=30)
is_verified = Column(Boolean, default=False)
verification_log = Column(JSON, server_default=text("'{}'::jsonb"))
opening_hours = Column(JSON, server_default=text("'{}'::jsonb"))
contact_phone = Column(String)
website = Column(String)
bio = Column(Text)
# Kapcsolatok
organization = relationship("Organization")
expertises = relationship("ServiceExpertise", back_populates="service")
class ExpertiseTag(Base):
"""Szakmai szempontok taxonómiája."""
__tablename__ = "expertise_tags"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
key = Column(String(50), unique=True, index=True) # pl. 'bmw_gs_specialist'
name_hu = Column(String(100))
category = Column(String(30)) # 'repair', 'fuel', 'food', 'emergency'
class ServiceExpertise(Base):
"""Kapcsolótábla a szerviz és a szakterület között."""
__tablename__ = "service_expertises"
__table_args__ = {"schema": "data"}
service_id = Column(Integer, ForeignKey("data.service_profiles.id"), primary_key=True)
expertise_id = Column(Integer, ForeignKey("data.expertise_tags.id"), primary_key=True)
# Validációs szint (0-100% - Mennyire hiteles ez a szakértelem)
validation_level = Column(Integer, default=0)
service = relationship("ServiceProfile", back_populates="expertises")
expertise = relationship("ExpertiseTag")

View File

@@ -1,16 +1,18 @@
from sqlalchemy import Column, String, JSON, Integer, Boolean, DateTime, func
from sqlalchemy import Column, String, JSON, Boolean, DateTime, Integer, text
from sqlalchemy.sql import func
from app.db.base_class import Base
class SystemParameter(Base):
"""
Globális rendszerbeállítások (A meglévő data.system_parametersbla alapján).
Rendszerszintű dinamikus paraméterekrolása.
Szinkronban az admin.py és config.py elvárásaival.
"""
__tablename__ = "system_parameters"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True, index=True)
key = Column(String(50), unique=True, index=True, nullable=False)
value = Column(JSON, nullable=False)
# Az admin.py 'key' mezőt vár, nem 'key_name'-et!
key = Column(String(50), primary_key=True, index=True)
value = Column(JSON, server_default=text("'{}'::jsonb"), nullable=False)
description = Column(String(255), nullable=True)
is_active = Column(Boolean, default=True)
description = Column(String, nullable=True)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())