Initial commit: Robot ökoszisztéma v2.0 - Stabilizált jármű és szerviz robotok

This commit is contained in:
Kincses
2026-03-04 02:03:03 +01:00
commit 250f4f4b8f
7942 changed files with 449625 additions and 0 deletions

65
backend/app/models/__init__.py Executable file
View File

@@ -0,0 +1,65 @@
# /opt/docker/dev/service_finder/backend/app/models/__init__.py
# MB 2.0: Kritikus javítás - Mindenki az app.database.Base-t használja!
from app.database import Base
# 1. Alapvető identitás és szerepkörök
from .identity import Person, User, Wallet, VerificationToken, SocialAccount, UserRole
# 2. Földrajzi adatok és címek
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Rating
# 3. Jármű definíciók
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
# 4. Szervezeti felépítés
from .organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment, OrgType, OrgUserRole, Branch
# 5. Eszközök és katalógusok
from .asset import Asset, AssetCatalog, AssetCost, AssetEvent, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate, CatalogDiscovery, VehicleOwnership
# 6. Üzleti logika és előfizetések
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
# 7. Szolgáltatások és staging
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter
# 8. Rendszer, Gamification és egyebek
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger
# --- 2.2 ÚJDONSÁG: InternalNotification hozzáadása ---
from .system import SystemParameter, InternalNotification
from .document import Document
from .translation import Translation
from .audit import SecurityAuditLog, ProcessLog, FinancialLedger
from .history import AuditLog, LogSeverity
from .security import PendingAction
from .legal import LegalDocument, LegalAcceptance
from .logistics import Location, LocationType
# Aliasok a Digital Twin kompatibilitáshoz
Vehicle = Asset
UserVehicle = Asset
VehicleCatalog = AssetCatalog
ServiceRecord = AssetEvent
__all__ = [
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
"Organization", "OrganizationMember", "OrganizationSalesAssignment", "OrgType", "OrgUserRole",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
"AssetTelemetry", "AssetReview", "ExchangeRate", "CatalogDiscovery",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
# --- 2.2 ÚJDONSÁG KIEGÉSZÍTÉS ---
"SystemParameter", "InternalNotification",
"Document", "Translation", "PendingAction",
"SubscriptionTier", "OrganizationSubscription", "CreditTransaction", "ServiceSpecialty",
"AuditLog", "VehicleOwnership", "LogSeverity",
"SecurityAuditLog", "ProcessLog", "FinancialLedger",
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition",
"VehicleType", "FeatureDefinition", "ModelFeatureMap", "LegalDocument", "LegalAcceptance",
"Location", "LocationType"
]

89
backend/app/models/address.py Executable file
View File

@@ -0,0 +1,89 @@
# /opt/docker/dev/service_finder/backend/app/models/address.py
import uuid
from datetime import datetime
from typing import Any, List, Optional
from sqlalchemy import String, Integer, ForeignKey, Text, DateTime, Float, Boolean, text, func, Numeric, Index, and_
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship, foreign
# MB 2.0: Kritikus javítás - a központi metadata-t használjuk az app.database-ből
from app.database import Base
class GeoPostalCode(Base):
"""Irányítószám alapú földrajzi kereső tábla."""
__tablename__ = "geo_postal_codes"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
country_code: Mapped[str] = mapped_column(String(5), default="HU")
zip_code: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
city: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
class GeoStreet(Base):
"""Utcajegyzék tábla."""
__tablename__ = "geo_streets"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.geo_postal_codes.id"))
name: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
class GeoStreetType(Base):
"""Közterület jellege (utca, út, köz stb.)."""
__tablename__ = "geo_street_types"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
class Address(Base):
"""Univerzális cím entitás GPS adatokkal kiegészítve."""
__tablename__ = "addresses"
__table_args__ = {"schema": "data"}
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.geo_postal_codes.id"))
street_name: Mapped[str] = mapped_column(String(200), nullable=False)
street_type: Mapped[str] = mapped_column(String(50), nullable=False)
house_number: Mapped[str] = mapped_column(String(50), nullable=False)
stairwell: Mapped[Optional[str]] = mapped_column(String(20))
floor: Mapped[Optional[str]] = mapped_column(String(20))
door: Mapped[Optional[str]] = mapped_column(String(20))
parcel_id: Mapped[Optional[str]] = mapped_column(String(50))
full_address_text: Mapped[Optional[str]] = mapped_column(Text)
# Robot és térképes funkciók számára
latitude: Mapped[Optional[float]] = mapped_column(Float)
longitude: Mapped[Optional[float]] = mapped_column(Float)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class Rating(Base):
"""Univerzális értékelési rendszer - v1.3.1"""
__tablename__ = "ratings"
__table_args__ = (
Index('idx_rating_org', 'target_organization_id'),
Index('idx_rating_user', 'target_user_id'),
Index('idx_rating_branch', 'target_branch_id'),
{"schema": "data"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# MB 2.0: A felhasználók az identity sémában laknak!
author_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
target_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
target_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
target_branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.branches.id"))
score: Mapped[float] = mapped_column(Numeric(3, 2), nullable=False)
comment: Mapped[Optional[str]] = mapped_column(Text)
images: Mapped[Any] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

221
backend/app/models/asset.py Normal file
View File

@@ -0,0 +1,221 @@
# /opt/docker/dev/service_finder/backend/app/models/asset.py
from __future__ import annotations
import uuid
from datetime import datetime
from typing import List, Optional, TYPE_CHECKING
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint, BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
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"
__table_args__ = (
UniqueConstraint('make', 'model', 'year_from', 'fuel_type', name='uix_vehicle_catalog_full'),
{"schema": "data"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
master_definition_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.vehicle_model_definitions.id"))
make: Mapped[str] = mapped_column(String, index=True, nullable=False)
model: Mapped[str] = mapped_column(String, index=True, nullable=False)
generation: Mapped[Optional[str]] = mapped_column(String, index=True)
year_from: Mapped[Optional[int]] = mapped_column(Integer)
year_to: Mapped[Optional[int]] = mapped_column(Integer)
fuel_type: Mapped[Optional[str]] = mapped_column(String, index=True)
power_kw: Mapped[Optional[int]] = mapped_column(Integer, index=True)
engine_capacity: Mapped[Optional[int]] = mapped_column(Integer, index=True)
factory_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
master_definition: Mapped[Optional["VehicleModelDefinition"]] = relationship("VehicleModelDefinition", back_populates="variants")
assets: Mapped[List["Asset"]] = relationship("Asset", back_populates="catalog")
class Asset(Base):
""" A fizikai eszköz (Digital Twin) - Minden adat itt fut össze. """
__tablename__ = "assets"
__table_args__ = {"schema": "data"}
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
vin: Mapped[str] = mapped_column(String(17), unique=True, index=True, nullable=False)
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
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
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("data.vehicle_catalog.id"))
current_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
# Identity kapcsolatok
owner_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
owner_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.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("data.organizations.id"))
status: Mapped[str] = mapped_column(String(20), default="active")
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)
costs: Mapped[List["AssetCost"]] = relationship("AssetCost", back_populates="asset")
events: Mapped[List["AssetEvent"]] = relationship("AssetEvent", back_populates="asset")
logbook: Mapped[List["VehicleLogbook"]] = relationship("VehicleLogbook", back_populates="asset")
inspections: Mapped[List["AssetInspection"]] = relationship("AssetInspection", back_populates="asset")
reviews: Mapped[List["AssetReview"]] = relationship("AssetReview", back_populates="asset")
telemetry: Mapped[Optional["AssetTelemetry"]] = relationship("AssetTelemetry", back_populates="asset", uselist=False)
assignments: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="asset")
ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="asset")
class AssetFinancials(Base):
""" I. Beszerzés és IV. Értékcsökkenés (Amortizáció). """
__tablename__ = "asset_financials"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
purchase_price_net: Mapped[float] = mapped_column(Numeric(18, 2))
purchase_price_gross: Mapped[float] = mapped_column(Numeric(18, 2))
vat_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=27.00)
activation_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
financing_type: Mapped[str] = mapped_column(String(50))
accounting_details: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
asset: Mapped["Asset"] = relationship("Asset", back_populates="financials")
class AssetCost(Base):
""" II. Üzemeltetés és TCO kimutatás. """
__tablename__ = "asset_costs"
__table_args__ = {"schema": "data"}
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("data.assets.id"), nullable=False)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
cost_category: Mapped[str] = mapped_column(String(50), index=True)
amount_net: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
currency: Mapped[str] = mapped_column(String(3), default="HUF")
date: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
invoice_number: Mapped[Optional[str]] = mapped_column(String(100), index=True)
data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
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"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
driver_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
trip_type: Mapped[str] = mapped_column(String(30), index=True)
is_reimbursable: Mapped[bool] = mapped_column(Boolean, default=False)
start_mileage: Mapped[int] = mapped_column(Integer)
end_mileage: Mapped[Optional[int]] = mapped_column(Integer)
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"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
inspector_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
checklist_results: Mapped[dict] = mapped_column(JSONB, nullable=False)
is_safe: Mapped[bool] = mapped_column(Boolean, default=True)
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"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
overall_rating: Mapped[Optional[int]] = mapped_column(Integer) # 1-5 csillag
comment: Mapped[Optional[str]] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
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"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
acquired_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
disposed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
asset: Mapped["Asset"] = relationship("Asset", back_populates="ownership_history")
# EZ A SOR HIÁNYZIK A KÓDODBÓL ÉS EZ JAVÍTJA A HIBÁT:
user: Mapped["User"] = relationship("User", back_populates="ownership_history")
class AssetTelemetry(Base):
__tablename__ = "asset_telemetry"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
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"
__table_args__ = {"schema": "data"}
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("data.assets.id"), nullable=False)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
status: Mapped[str] = mapped_column(String(30), default="active")
asset: Mapped["Asset"] = relationship("Asset", back_populates="assignments")
organization: Mapped["Organization"] = relationship("Organization", back_populates="assets")
class AssetEvent(Base):
""" Szerviz, baleset és egyéb jelentős események. """
__tablename__ = "asset_events"
__table_args__ = {"schema": "data"}
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("data.assets.id"), nullable=False)
event_type: Mapped[str] = mapped_column(String(50), nullable=False)
asset: Mapped["Asset"] = relationship("Asset", back_populates="events")
class ExchangeRate(Base):
__tablename__ = "exchange_rates"
__table_args__ = {"schema": "data"}
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. """
__tablename__ = "catalog_discovery"
__table_args__ = (UniqueConstraint('make', 'model', name='_make_model_uc'), {"schema": "data"})
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
make: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
model: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)

63
backend/app/models/audit.py Executable file
View File

@@ -0,0 +1,63 @@
# /opt/docker/dev/service_finder/backend/app/models/audit.py
from datetime import datetime
from typing import Any, Optional
from sqlalchemy import String, DateTime, JSON, ForeignKey, text, Numeric, Boolean, BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from app.database import Base
class SecurityAuditLog(Base):
""" Kiemelt biztonsági események és a 4-szem elv naplózása. """
__tablename__ = "security_audit_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
action: Mapped[Optional[str]] = mapped_column(String(50)) # 'ROLE_CHANGE', 'MANUAL_CREDIT_ADJUST'
actor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
target_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
confirmed_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
is_critical: Mapped[bool] = mapped_column(Boolean, default=False)
payload_before: Mapped[Any] = mapped_column(JSON)
payload_after: Mapped[Any] = mapped_column(JSON)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class OperationalLog(Base):
""" Felhasználói szintű napi üzemi események (Audit Trail). """
__tablename__ = "operational_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="SET NULL"))
action: Mapped[str] = mapped_column(String(100), nullable=False) # pl. "ADD_VEHICLE"
resource_type: Mapped[Optional[str]] = mapped_column(String(50))
resource_id: Mapped[Optional[str]] = mapped_column(String(100))
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
ip_address: Mapped[Optional[str]] = mapped_column(String(45))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ProcessLog(Base):
""" Robotok és háttérfolyamatok futási naplója (A reggeli jelentésekhez). """
__tablename__ = "process_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
process_name: Mapped[str] = mapped_column(String(100), index=True) # 'Master-Enricher'
start_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
end_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
items_processed: Mapped[int] = mapped_column(Integer, default=0)
items_failed: Mapped[int] = mapped_column(Integer, default=0)
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class FinancialLedger(Base):
""" Minden pénz- és kreditmozgás központi naplója. Billing Engine alapja. """
__tablename__ = "financial_ledger"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
currency: Mapped[Optional[str]] = mapped_column(String(10))
transaction_type: Mapped[Optional[str]] = mapped_column(String(50))
related_agent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
details: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -0,0 +1,76 @@
# /opt/docker/dev/service_finder/backend/app/models/core_logic.py
from typing import Optional, List, Any
from datetime import datetime # Python saját típusa a típusjelöléshez
from sqlalchemy import String, Integer, ForeignKey, Boolean, DateTime, Numeric, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class SubscriptionTier(Base):
"""
Előfizetési csomagok definíciója (pl. Free, Premium, VIP).
A csomagok határozzák meg a korlátokat (pl. max járműszám).
"""
__tablename__ = "subscription_tiers"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String, unique=True, index=True) # pl. 'premium'
rules: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) # pl. {"max_vehicles": 5}
is_custom: Mapped[bool] = mapped_column(Boolean, default=False)
class OrganizationSubscription(Base):
"""
Szervezetek aktuális előfizetései és azok érvényessége.
"""
__tablename__ = "org_subscriptions"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Kapcsolat a szervezettel (data séma)
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
# Kapcsolat a csomaggal (data séma)
tier_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.subscription_tiers.id"), nullable=False)
valid_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
valid_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class CreditTransaction(Base):
"""
Kreditnapló (Pontok, kreditek vagy virtuális egyenleg követése).
"""
__tablename__ = "credit_logs"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Kapcsolat a szervezettel (data séma)
org_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
amount: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
description: Mapped[Optional[str]] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ServiceSpecialty(Base):
"""
Hierarchikus fa struktúra a szerviz szolgáltatásokhoz (pl. Motor -> Futómű).
"""
__tablename__ = "service_specialties"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Önmagára mutató idegen kulcs a hierarchiához
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.service_specialties.id"))
name: Mapped[str] = mapped_column(String, nullable=False)
slug: Mapped[str] = mapped_column(String, unique=True, index=True)
# Kapcsolat az ős-szolgáltatással (Self-referential relationship)
parent: Mapped[Optional["ServiceSpecialty"]] = relationship("ServiceSpecialty", remote_side=[id], backref="children")

30
backend/app/models/document.py Executable file
View File

@@ -0,0 +1,30 @@
# /opt/docker/dev/service_finder/backend/app/models/document.py
import uuid
from datetime import datetime
from typing import Optional
from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey, text
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from app.db.base_class import Base
class Document(Base):
""" NAS alapú dokumentumtár metaadatai. """
__tablename__ = "documents"
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
parent_type: Mapped[str] = mapped_column(String(20)) # 'organization' vagy 'asset'
parent_id: Mapped[str] = mapped_column(String(50), index=True)
doc_type: Mapped[Optional[str]] = mapped_column(String(50))
original_name: Mapped[str] = mapped_column(String(255))
file_hash: Mapped[str] = mapped_column(String(64))
file_ext: Mapped[str] = mapped_column(String(10), default="webp")
mime_type: Mapped[str] = mapped_column(String(100), default="image/webp")
file_size: Mapped[Optional[int]] = mapped_column(Integer)
has_thumbnail: Mapped[bool] = mapped_column(Boolean, default=False)
thumbnail_path: Mapped[Optional[str]] = mapped_column(String(255))
uploaded_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -0,0 +1,86 @@
# /opt/docker/dev/service_finder/backend/app/models/gamification.py
import uuid
from datetime import datetime
from typing import Optional, List, TYPE_CHECKING
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean, Text, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from app.database import Base # MB 2.0: Központi Base
if TYPE_CHECKING:
from app.models.identity import User
class PointRule(Base):
__tablename__ = "point_rules"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
action_key: Mapped[str] = mapped_column(String, unique=True, index=True)
points: Mapped[int] = mapped_column(Integer, default=0)
description: Mapped[Optional[str]] = mapped_column(String)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class LevelConfig(Base):
__tablename__ = "level_configs"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
level_number: Mapped[int] = mapped_column(Integer, unique=True)
min_points: Mapped[int] = mapped_column(Integer)
rank_name: Mapped[str] = mapped_column(String)
class PointsLedger(Base):
__tablename__ = "points_ledger"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
points: Mapped[int] = mapped_column(Integer, default=0)
penalty_change: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
reason: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")
class UserStats(Base):
__tablename__ = "user_stats"
__table_args__ = {"schema": "data"}
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), primary_key=True)
total_xp: Mapped[int] = mapped_column(Integer, default=0)
social_points: Mapped[int] = mapped_column(Integer, default=0)
current_level: Mapped[int] = mapped_column(Integer, default=1)
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
restriction_level: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
user: Mapped["User"] = relationship("User", back_populates="stats")
class Badge(Base):
__tablename__ = "badges"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String, unique=True)
description: Mapped[str] = mapped_column(String)
icon_url: Mapped[Optional[str]] = mapped_column(String)
class UserBadge(Base):
__tablename__ = "user_badges"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# MB 2.0: User az identity sémában lakik!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id"))
earned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")

47
backend/app/models/history.py Executable file
View File

@@ -0,0 +1,47 @@
# /opt/docker/dev/service_finder/backend/app/models/history.py
import uuid
import enum
from datetime import datetime, date
from typing import Optional, Any
from sqlalchemy import String, DateTime, ForeignKey, JSON, Date, Text, Integer
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class LogSeverity(str, enum.Enum):
info = "info"
warning = "warning"
critical = "critical"
emergency = "emergency"
class AuditLog(Base):
""" Rendszerszintű műveletnapló. """
__tablename__ = "audit_logs"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# MB 2.0 JAVÍTÁS: A felhasználó az identity sémában lakik!
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
severity: Mapped[LogSeverity] = mapped_column(
PG_ENUM(LogSeverity, name="log_severity", schema="data"),
default=LogSeverity.info
)
action: Mapped[str] = mapped_column(String(100), index=True)
target_type: Mapped[Optional[str]] = mapped_column(String(50), index=True)
target_id: Mapped[Optional[str]] = mapped_column(String(50), index=True)
old_data: Mapped[Optional[Any]] = mapped_column(JSON)
new_data: Mapped[Optional[Any]] = mapped_column(JSON)
ip_address: Mapped[Optional[str]] = mapped_column(String(45), index=True)
user_agent: Mapped[Optional[Text]] = mapped_column(Text)
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
user: Mapped[Optional["User"]] = relationship("User")

174
backend/app/models/identity.py Executable file
View File

@@ -0,0 +1,174 @@
# /opt/docker/dev/service_finder/backend/app/models/identity.py
from __future__ import annotations
import uuid
import enum
from datetime import datetime
from typing import Any, List, Optional, TYPE_CHECKING
from sqlalchemy import String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Integer, BigInteger, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
from sqlalchemy.sql import func
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
if TYPE_CHECKING:
from .organization import Organization, OrganizationMember
from .asset import VehicleOwnership
from .gamification import UserStats
class UserRole(str, enum.Enum):
superadmin = "superadmin"
admin = "admin"
region_admin = "region_admin"
country_admin = "country_admin"
moderator = "moderator"
sales_agent = "sales_agent"
user = "user"
service_owner = "service_owner"
fleet_manager = "fleet_manager"
driver = "driver"
class Person(Base):
"""
Természetes személy identitása. A DNS szint.
Minden identitás adat az 'identity' sémába kerül.
"""
__tablename__ = "persons"
__table_args__ = {"schema": "identity"}
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, index=True)
id_uuid: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
# A lakcím a 'data' sémában marad
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
# Kritikus azonosító: Név + Anyja neve + Szül.idő hash-elve.
# Ezzel ismerjük fel a személyt akkor is, ha új User accountot hoz létre.
identity_hash: Mapped[Optional[str]] = mapped_column(String(64), unique=True, index=True)
last_name: Mapped[str] = mapped_column(String, nullable=False)
first_name: Mapped[str] = mapped_column(String, nullable=False)
phone: Mapped[Optional[str]] = mapped_column(String)
mothers_last_name: Mapped[Optional[str]] = mapped_column(String)
mothers_first_name: Mapped[Optional[str]] = mapped_column(String)
birth_place: Mapped[Optional[str]] = mapped_column(String)
birth_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
identity_docs: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
ice_contact: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
lifetime_xp: Mapped[int] = mapped_column(BigInteger, server_default=text("0"))
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"))
social_reputation: Mapped[float] = mapped_column(Numeric(3, 2), server_default=text("1.00"))
is_sales_agent: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
is_ghost: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
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 ---
users: Mapped[List["User"]] = relationship("User", back_populates="person")
memberships: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="person")
# MB 2.0 KIEGÉSZÍTÉS: A személy által birtokolt üzleti entitások (Cégek/Szolgáltatók)
# Ez a lista megmarad akkor is, ha az Organization deaktiválódik.
owned_business_entities: Mapped[List["Organization"]] = relationship("Organization", back_populates="legal_owner")
class User(Base):
""" Login entitás. Bármikor törölhető (GDPR), de Person-höz kötött. """
__tablename__ = "users"
__table_args__ = {"schema": "identity"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
email: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
hashed_password: Mapped[Optional[str]] = mapped_column(String)
role: Mapped[UserRole] = mapped_column(
PG_ENUM(UserRole, name="userrole", schema="identity"),
default=UserRole.user
)
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
subscription_plan: Mapped[str] = mapped_column(String(30), server_default=text("'FREE'"))
subscription_expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
is_vip: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
referral_code: Mapped[Optional[str]] = mapped_column(String(20), unique=True)
referred_by_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
current_sales_agent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
is_active: Mapped[bool] = mapped_column(Boolean, default=False)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
folder_slug: Mapped[Optional[str]] = mapped_column(String(12), unique=True, index=True)
preferred_language: Mapped[str] = mapped_column(String(5), server_default="hu")
region_code: Mapped[str] = mapped_column(String(5), server_default="HU")
preferred_currency: Mapped[str] = mapped_column(String(3), server_default="HUF")
scope_level: Mapped[str] = mapped_column(String(30), server_default="individual")
scope_id: Mapped[Optional[str]] = mapped_column(String(50))
custom_permissions: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Kapcsolatok
person: Mapped[Optional["Person"]] = relationship("Person", back_populates="users")
wallet: Mapped[Optional["Wallet"]] = relationship("Wallet", back_populates="user", uselist=False)
social_accounts: Mapped[List["SocialAccount"]] = relationship("SocialAccount", back_populates="user", cascade="all, delete-orphan")
owned_organizations: Mapped[List["Organization"]] = relationship("Organization", back_populates="owner")
stats: Mapped[Optional["UserStats"]] = relationship("UserStats", back_populates="user", uselist=False, cascade="all, delete-orphan")
ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="user")
@property
def tier_name(self) -> str:
"""Kompatibilitási mező a keresőhöz: a 'FREE' -> 'free' konverzióhoz"""
return (self.subscription_plan or "free").lower()
class Wallet(Base):
__tablename__ = "wallets"
__table_args__ = {"schema": "identity"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), unique=True)
earned_credits: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
purchased_credits: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
service_coins: Mapped[float] = mapped_column(Numeric(18, 4), server_default=text("0"))
currency: Mapped[str] = mapped_column(String(3), default="HUF")
user: Mapped["User"] = relationship("User", back_populates="wallet")
class VerificationToken(Base):
__tablename__ = "verification_tokens"
__table_args__ = {"schema": "identity"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
token: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="CASCADE"), nullable=False)
token_type: Mapped[str] = mapped_column(String(20), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
is_used: Mapped[bool] = mapped_column(Boolean, default=False)
class SocialAccount(Base):
__tablename__ = "social_accounts"
__table_args__ = (
UniqueConstraint('provider', 'social_id', name='uix_social_provider_id'),
{"schema": "identity"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="CASCADE"), nullable=False)
provider: Mapped[str] = mapped_column(String(50), nullable=False)
social_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
email: Mapped[str] = mapped_column(String(255), nullable=False)
extra_data: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User", back_populates="social_accounts")

31
backend/app/models/legal.py Executable file
View File

@@ -0,0 +1,31 @@
# /opt/docker/dev/service_finder/backend/app/models/legal.py
from datetime import datetime
from typing import Optional
from sqlalchemy import Integer, String, Text, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from app.db.base_class import Base
class LegalDocument(Base):
__tablename__ = "legal_documents"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
title: Mapped[Optional[str]] = mapped_column(String(255))
content: Mapped[str] = mapped_column(Text)
version: Mapped[str] = mapped_column(String(20))
region_code: Mapped[str] = mapped_column(String(5), default="HU")
language: Mapped[str] = mapped_column(String(5), default="hu")
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class LegalAcceptance(Base):
__tablename__ = "legal_acceptances"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
document_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.legal_documents.id"))
accepted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
ip_address: Mapped[Optional[str]] = mapped_column(String(45))
user_agent: Mapped[Optional[str]] = mapped_column(Text)

26
backend/app/models/logistics.py Executable file
View File

@@ -0,0 +1,26 @@
# /opt/docker/dev/service_finder/backend/app/models/logistics.py
import enum
from typing import Optional
from sqlalchemy import Integer, String, Enum
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base_class import Base
class LocationType(str, enum.Enum):
stop = "stop"
warehouse = "warehouse"
client = "client"
class Location(Base):
__tablename__ = "locations"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String)
type: Mapped[LocationType] = mapped_column(
PG_ENUM(LocationType, name="location_type", inherit_schema=True),
nullable=False
)
coordinates: Mapped[Optional[str]] = mapped_column(String)
address_full: Mapped[Optional[str]] = mapped_column(String)
capacity: Mapped[Optional[int]] = mapped_column(Integer)

View File

@@ -0,0 +1,217 @@
# /opt/docker/dev/service_finder/backend/app/models/organization.py
import enum
import uuid
from datetime import datetime
from typing import Any, List, Optional
import sqlalchemy as sa
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text, Numeric, BigInteger, Float
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID as PG_UUID, JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship, foreign
from sqlalchemy.sql import func
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class OrgType(str, enum.Enum):
individual = "individual"
service = "service"
service_provider = "service_provider"
fleet_owner = "fleet_owner"
club = "club"
business = "business"
class OrgUserRole(str, enum.Enum):
OWNER = "OWNER"
ADMIN = "ADMIN"
FLEET_MANAGER = "FLEET_MANAGER"
DRIVER = "DRIVER"
MECHANIC = "MECHANIC"
RECEPTIONIST = "RECEPTIONIST"
class Organization(Base):
"""
Szervezet entitás (MB 2.0).
Támogatja a 'Digital Twin' logikát: a cég törölhető, de a statisztika és
a jármű-életút adatok megmaradnak az eredeti Person-höz kötve.
"""
__tablename__ = "organizations"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# --- 🛡️ BIZTONSÁGI ÉS ÉLETÚT KIEGÉSZÍTÉSEK ---
# A Jogi képviselő/Tulajdonos (A Person örök DNS-e az identity sémában)
# Ez segít felismerni, ha ugyanaz az ember új céggel akar 'tiszta lapot' nyitni.
legal_owner_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"), index=True)
# ÉLETÚT DÁTUMOK (A kért logika alapján)
# 1. A legelső regisztráció dátuma (Soha nem változik)
first_registered_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# 2. Az AKTUÁLIS életciklus kezdete. Újraregisztrációkor ez frissül.
# Az API ezt használja szűrőnek: a cég csak az ezutáni adatokat látja a Dashboardon.
current_lifecycle_started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# 3. Az utolsó deaktiválás/törlés időpontja
last_deactivated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
# Hányszor regisztrált újra ez a cég/adószám (Reinkarnációs index)
lifecycle_index: Mapped[int] = mapped_column(Integer, default=1, server_default=text("1"))
# --- 🏢 ALAPADATOK (MEGŐRIZVE) ---
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
is_anonymized: Mapped[bool] = mapped_column(Boolean, default=False, server_default=text("false"))
anonymized_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
full_name: Mapped[str] = mapped_column(String, nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
display_name: Mapped[Optional[str]] = mapped_column(String(50))
folder_slug: Mapped[str] = mapped_column(String(12), unique=True, index=True)
default_currency: Mapped[str] = mapped_column(String(3), default="HUF")
country_code: Mapped[str] = mapped_column(String(2), default="HU")
language: Mapped[str] = mapped_column(String(5), default="hu")
address_zip: Mapped[Optional[str]] = mapped_column(String(10))
address_city: Mapped[Optional[str]] = mapped_column(String(100))
address_street_name: Mapped[Optional[str]] = mapped_column(String(150))
address_street_type: Mapped[Optional[str]] = mapped_column(String(50))
address_house_number: Mapped[Optional[str]] = mapped_column(String(20))
address_hrsz: Mapped[Optional[str]] = mapped_column(String(50))
tax_number: Mapped[Optional[str]] = mapped_column(String(20), unique=True, index=True)
reg_number: Mapped[Optional[str]] = mapped_column(String(50))
org_type: Mapped[OrgType] = mapped_column(
PG_ENUM(OrgType, name="orgtype", schema="data"),
default=OrgType.individual
)
status: Mapped[str] = mapped_column(String(30), default="pending_verification")
# Soft delete: is_active=False és is_deleted=True esetén a cég 'törölt'
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
subscription_plan: Mapped[str] = mapped_column(String(30), server_default=text("'FREE'"), index=True)
base_asset_limit: Mapped[int] = mapped_column(Integer, server_default=text("1"))
purchased_extra_slots: Mapped[int] = mapped_column(Integer, server_default=text("0"))
notification_settings: Mapped[Any] = mapped_column(JSON, server_default=text("'{\"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1]}'::jsonb"))
external_integration_config: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
# A technikai tulajdonos (User fiók - törölhető)
owner_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
# Időbélyegek az aktuális állapothoz
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())
is_ownership_transferable: Mapped[bool] = mapped_column(Boolean, server_default=text("true"))
# --- 🔗 KAPCSOLATOK (RELATIONSHIPS) ---
assets: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
members: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan")
owner: Mapped[Optional["User"]] = relationship("User", back_populates="owned_organizations")
financials: Mapped[List["OrganizationFinancials"]] = relationship("OrganizationFinancials", back_populates="organization", cascade="all, delete-orphan")
# JAVÍTVA: Ha az Organization törlődik, a ServiceProfile megmarad 'Ghost'-ként (ondelete="SET NULL")
service_profile: Mapped[Optional["ServiceProfile"]] = relationship("ServiceProfile", back_populates="organization", uselist=False)
branches: Mapped[List["Branch"]] = relationship("Branch", back_populates="organization", cascade="all, delete-orphan")
# Kapcsolat az örök személy rekordhoz
legal_owner: Mapped[Optional["Person"]] = relationship("Person", back_populates="owned_business_entities")
class OrganizationFinancials(Base):
__tablename__ = "organization_financials"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
year: Mapped[int] = mapped_column(Integer, nullable=False)
turnover: Mapped[Optional[float]] = mapped_column(Numeric(18, 2))
profit: Mapped[Optional[float]] = mapped_column(Numeric(18, 2))
employee_count: Mapped[Optional[int]] = mapped_column(Integer)
source: Mapped[Optional[str]] = mapped_column(String(50))
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
organization: Mapped["Organization"] = relationship("Organization", back_populates="financials")
class OrganizationMember(Base):
__tablename__ = "organization_members"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
role: Mapped[OrgUserRole] = mapped_column(
PG_ENUM(OrgUserRole, name="orguserrole", schema="data"),
default=OrgUserRole.DRIVER
)
permissions: Mapped[Any] = mapped_column(JSON, server_default=text("'{}'::jsonb"))
is_permanent: Mapped[bool] = mapped_column(Boolean, default=False)
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
organization: Mapped["Organization"] = relationship("Organization", back_populates="members")
user: Mapped[Optional["User"]] = relationship("User")
person: Mapped[Optional["Person"]] = relationship("Person", back_populates="memberships")
class OrganizationSalesAssignment(Base):
__tablename__ = "org_sales_assignments"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"))
agent_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
assigned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class Branch(Base):
"""
Telephely entitás. A fizikai helyszín, ahol a szolgáltatás vagy flotta-kezelés zajlik.
"""
__tablename__ = "branches"
__table_args__ = {"schema": "data"}
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.organizations.id"), nullable=False)
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"))
name: Mapped[str] = mapped_column(String(100), nullable=False)
is_main: Mapped[bool] = mapped_column(Boolean, default=False)
# Denormalizált adatok a gyors lekérdezéshez
postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
street_name: Mapped[Optional[str]] = mapped_column(String(150))
street_type: Mapped[Optional[str]] = mapped_column(String(50))
house_number: Mapped[Optional[str]] = mapped_column(String(20))
stairwell: Mapped[Optional[str]] = mapped_column(String(20))
floor: Mapped[Optional[str]] = mapped_column(String(20))
door: Mapped[Optional[str]] = mapped_column(String(20))
hrsz: Mapped[Optional[str]] = mapped_column(String(50))
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
branch_rating: Mapped[float] = mapped_column(Float, default=0.0)
status: Mapped[str] = mapped_column(String(30), default="active")
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Kapcsolatok
organization: Mapped["Organization"] = relationship("Organization", back_populates="branches")
address: Mapped[Optional["Address"]] = relationship("Address")
# Kapcsolatok (Primaryjoin tartva a rating rendszerhez)
reviews: Mapped[List["Rating"]] = relationship(
"Rating",
primaryjoin="and_(Branch.id==foreign(Rating.target_branch_id))"
)

51
backend/app/models/security.py Executable file
View File

@@ -0,0 +1,51 @@
# /opt/docker/dev/service_finder/backend/app/models/security.py
import enum
from datetime import datetime
from typing import Optional, TYPE_CHECKING
from sqlalchemy import String, Integer, ForeignKey, DateTime, text, Enum
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
# MB 2.0: Központi aszinkron adatbázis motorból származó Base
from app.database import Base
if TYPE_CHECKING:
from .identity import User
class ActionStatus(str, enum.Enum):
pending = "pending"
approved = "approved"
rejected = "rejected"
expired = "expired"
class PendingAction(Base):
""" Sentinel: Kritikus műveletek jóváhagyási lánca. """
__tablename__ = "pending_actions"
__table_args__ = {"schema": "system"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# JAVÍTÁS: A User az identity sémában van, nem a data-ban!
requester_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
approver_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
status: Mapped[ActionStatus] = mapped_column(
Enum(ActionStatus, name="actionstatus", schema="system"),
default=ActionStatus.pending
)
action_type: Mapped[str] = mapped_column(String(50)) # pl. "WALLET_ADJUST"
payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
reason: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
expires_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=text("now() + interval '24 hours'")
)
processed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
# Kapcsolatok meghatározása (String hivatkozással a körkörös import ellen)
requester: Mapped["User"] = relationship("User", foreign_keys=[requester_id])
approver: Mapped[Optional["User"]] = relationship("User", foreign_keys=[approver_id])

159
backend/app/models/service.py Executable file
View File

@@ -0,0 +1,159 @@
# /opt/docker/dev/service_finder/backend/app/models/service.py
import uuid
from datetime import datetime
from typing import Any, List, Optional
from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey, text, Text, Float, Index, Numeric, BigInteger
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from geoalchemy2 import Geometry
from sqlalchemy.sql import func
# MB 2.0: Központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class ServiceProfile(Base):
""" Szerviz szolgáltató adatai (v1.3.1). """
__tablename__ = "service_profiles"
__table_args__ = (
Index('idx_service_fingerprint', 'fingerprint', unique=True),
{"schema": "data"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.organizations.id"), unique=True)
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.service_profiles.id"))
fingerprint: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
location: Mapped[Any] = mapped_column(Geometry(geometry_type='POINT', srid=4326, spatial_index=False), index=True)
status: Mapped[str] = mapped_column(String(20), server_default=text("'ghost'"), index=True)
last_audit_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
google_place_id: Mapped[Optional[str]] = mapped_column(String(100), unique=True)
rating: Mapped[Optional[float]] = mapped_column(Float)
user_ratings_total: Mapped[Optional[int]] = mapped_column(Integer)
vibe_analysis: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
social_links: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
specialization_tags: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
trust_score: Mapped[int] = mapped_column(Integer, default=30)
is_verified: Mapped[bool] = mapped_column(Boolean, default=False)
verification_log: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
contact_phone: Mapped[Optional[str]] = mapped_column(String)
contact_email: Mapped[Optional[str]] = mapped_column(String)
website: Mapped[Optional[str]] = mapped_column(String)
bio: Mapped[Optional[str]] = mapped_column(Text)
# Kapcsolatok
organization: Mapped["Organization"] = relationship("Organization", back_populates="service_profile")
expertises: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="service")
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())
class ExpertiseTag(Base):
"""
Szakmai címkék mesterlistája (MB 2.0).
Ez a tábla vezérli a robotok keresését és a Gamification pontozást is.
"""
__tablename__ = "expertise_tags"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Egyedi azonosító kulcs (pl. 'ENGINE_REBUILD')
key: Mapped[str] = mapped_column(String(50), unique=True, index=True)
# Megjelenítendő nevek
name_hu: Mapped[Optional[str]] = mapped_column(String(100))
name_en: Mapped[Optional[str]] = mapped_column(String(100))
# Főcsoport (pl. 'MECHANICS', 'ELECTRICAL', 'EMERGENCY')
category: Mapped[Optional[str]] = mapped_column(String(30), index=True)
# --- 🎮 GAMIFICATION ÉS DISCOVERY ---
# Hivatalos címke (True) vagy júzer/robot által javasolt (False)
is_official: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))
# Ha júzer javasolta, itt tároljuk, ki volt az (XP jóváíráshoz)
suggested_by_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
# ÁLLÍTHATÓ PONTÉRTÉK: Az adatbázisból jön, így bármikor módosítható.
# Ritka szakmáknál magasabb, gyakoriaknál alacsonyabb érték állítható be.
discovery_points: Mapped[int] = mapped_column(Integer, default=10, server_default=text("10"))
# Robot kulcsszavak (JSONB): ["fék", "betét", "tárcsa", "fékfolyadék"]
# A Scout robot ez alapján azonosítja be a szervizt a weboldala alapján.
search_keywords: Mapped[Any] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
# Népszerűségi mutató (hányszor lett felhasználva a rendszerben)
usage_count: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
# UI ikon azonosító (pl. 'wrench', 'tire-flat', 'car-electric')
icon: Mapped[Optional[str]] = mapped_column(String(50))
# Leírás a szakmáról (Adminisztratív célokra)
description: Mapped[Optional[str]] = mapped_column(Text)
# Időbélyegek
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 ---
services: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="tag")
# Visszamutatás a beküldőre (ha van)
suggested_by: Mapped[Optional["Person"]] = relationship("Person")
class ServiceExpertise(Base):
"""
KAPCSOLÓTÁBLA: Ez köti össze a szervizt a szakmáival.
Itt tároljuk, hogy az adott szerviznél mennyire validált egy szakma.
"""
__tablename__ = "service_expertises"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.service_profiles.id", ondelete="CASCADE"))
expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.expertise_tags.id", ondelete="CASCADE"))
# Mennyire biztos ez a tudás? (0: robot találta, 1: júzer mondta, 2: igazolt szakma)
confidence_level: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
# Kapcsolatok visszafelé
service = relationship("ServiceProfile", back_populates="expertises")
tag = relationship("ExpertiseTag", back_populates="services")
class ServiceStaging(Base):
""" Hunter (robot) adatok tárolója. """
__tablename__ = "service_staging"
__table_args__ = (
Index('idx_staging_fingerprint', 'fingerprint', unique=True),
{"schema": "data"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String, index=True, nullable=False)
postal_code: Mapped[Optional[str]] = mapped_column(String(10), index=True)
city: Mapped[Optional[str]] = mapped_column(String(100), index=True)
full_address: Mapped[Optional[str]] = mapped_column(String)
fingerprint: Mapped[str] = mapped_column(String(255), nullable=False)
raw_data: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
status: Mapped[str] = mapped_column(String(20), server_default=text("'pending'"), index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class DiscoveryParameter(Base):
""" Robot vezérlési paraméterek adminból. """
__tablename__ = "discovery_parameters"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
city: Mapped[str] = mapped_column(String(100))
keyword: Mapped[str] = mapped_column(String(100))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))

78
backend/app/models/social.py Executable file
View File

@@ -0,0 +1,78 @@
# /opt/docker/dev/service_finder/backend/app/models/social.py
import enum
from datetime import datetime
from typing import Optional, List
from sqlalchemy import String, Integer, ForeignKey, DateTime, Boolean, Text, UniqueConstraint, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
from sqlalchemy.sql import func
from app.db.base_class import Base
class ModerationStatus(str, enum.Enum):
pending = "pending"
approved = "approved"
rejected = "rejected"
class SourceType(str, enum.Enum):
manual = "manual"
ocr = "ocr"
api_import = "import"
class ServiceProvider(Base):
""" Közösség által beküldött szolgáltatók (v1.3.1). """
__tablename__ = "service_providers"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String, nullable=False)
address: Mapped[str] = mapped_column(String, nullable=False)
category: Mapped[Optional[str]] = mapped_column(String)
status: Mapped[ModerationStatus] = mapped_column(
PG_ENUM(ModerationStatus, name="moderation_status", inherit_schema=True),
default=ModerationStatus.pending
)
source: Mapped[SourceType] = mapped_column(
PG_ENUM(SourceType, name="source_type", inherit_schema=True),
default=SourceType.manual
)
validation_score: Mapped[int] = mapped_column(Integer, default=0)
evidence_image_path: Mapped[Optional[str]] = mapped_column(String)
added_by_user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class Vote(Base):
""" Közösségi validációs szavazatok. """
__tablename__ = "votes"
__table_args__ = (
UniqueConstraint('user_id', 'provider_id', name='uq_user_provider_vote'),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False)
provider_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.service_providers.id"), nullable=False)
vote_value: Mapped[int] = mapped_column(Integer, nullable=False) # +1 vagy -1
class Competition(Base):
""" Gamifikált versenyek (pl. Januári Feltöltő Verseny). """
__tablename__ = "competitions"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String, nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text)
start_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class UserScore(Base):
""" Versenyenkénti ranglista pontszámok. """
__tablename__ = "user_scores"
__table_args__ = (
UniqueConstraint('user_id', 'competition_id', name='uq_user_competition_score'),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"))
competition_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.competitions.id"))
points: Mapped[int] = mapped_column(Integer, default=0)
last_updated: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

View File

@@ -0,0 +1,56 @@
# /opt/docker/dev/service_finder/backend/app/models/staged_data.py
from datetime import datetime
from typing import Optional, Any
from sqlalchemy import String, Integer, DateTime, text, Boolean, Float
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
from app.db.base_class import Base
class StagedVehicleData(Base):
""" Robot 2.1 (Researcher) nyers adatgyűjtője. """
__tablename__ = "staged_vehicle_data"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
source_url: Mapped[Optional[str]] = mapped_column(String)
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
status: Mapped[str] = mapped_column(String(20), default="PENDING", index=True)
error_log: Mapped[Optional[str]] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
class ServiceStaging(Base):
""" Robot 1.3 (Scout) által talált nyers szerviz adatok. """
__tablename__ = "service_staging"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(255), index=True)
source: Mapped[str] = mapped_column(String(50))
external_id: Mapped[Optional[str]] = mapped_column(String(100), index=True)
fingerprint: Mapped[str] = mapped_column(String(64), unique=True, index=True)
city: Mapped[str] = mapped_column(String(100), index=True)
full_address: Mapped[Optional[str]] = mapped_column(String(500))
contact_phone: Mapped[Optional[str]] = mapped_column(String(50))
website: Mapped[Optional[str]] = mapped_column(String(255))
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
trust_score: Mapped[int] = mapped_column(Integer, default=30)
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())
class DiscoveryParameter(Base):
""" Felderítési paraméterek (Városok, ahol a Scout keres). """
__tablename__ = "discovery_parameters"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
city: Mapped[str] = mapped_column(String(100), unique=True, index=True)
country_code: Mapped[str] = mapped_column(String(5), server_default=text("'HU'"))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))

53
backend/app/models/system.py Executable file
View File

@@ -0,0 +1,53 @@
# /opt/docker/dev/service_finder/backend/app/models/system.py
import uuid
from datetime import datetime
from typing import Optional
from sqlalchemy import String, Integer, Boolean, DateTime, text, UniqueConstraint, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.sql import func
from app.db.base_class import Base
class SystemParameter(Base):
""" Dinamikus konfigurációs motor (Global -> Org -> User). """
__tablename__ = "system_parameters"
__table_args__ = (
UniqueConstraint('key', 'scope_level', 'scope_id', name='uix_param_scope'),
{"extend_existing": True}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
key: Mapped[str] = mapped_column(String, index=True)
category: Mapped[str] = mapped_column(String, server_default="general", index=True)
value: Mapped[dict] = mapped_column(JSONB, nullable=False)
scope_level: Mapped[str] = mapped_column(String(30), server_default=text("'global'"), index=True)
scope_id: Mapped[Optional[str]] = mapped_column(String(50))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
description: Mapped[Optional[str]] = mapped_column(String)
last_modified_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
class InternalNotification(Base):
"""
Belső értesítési központ.
Ezek az üzenetek várják a felhasználót belépéskor.
"""
__tablename__ = "internal_notifications"
__table_args__ = ({"schema": "data", "extend_existing": True})
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id: Mapped[int] = mapped_column(ForeignKey("identity.users.id", ondelete="CASCADE"), nullable=False, index=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
message: Mapped[str] = mapped_column(Text, nullable=False)
category: Mapped[str] = mapped_column(String(50), server_default="info") # insurance, mot, service, legal
priority: Mapped[str] = mapped_column(String(20), server_default="medium") # low, medium, high, critical
is_read: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
read_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
# Metaadatok a gyors eléréshez (melyik autó, melyik VIN)
data: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)

View File

@@ -0,0 +1,27 @@
# /opt/docker/dev/service_finder/backend/app/models/translation.py
from sqlalchemy import String, Integer, Text, Boolean, text
from sqlalchemy.orm import Mapped, mapped_column
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
class Translation(Base):
"""
Többnyelvűséget támogató tábla a felületi elemekhez és dinamikus tartalmakhoz.
"""
__tablename__ = "translations"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# A fordítandó kulcs (pl. 'NAV_DASHBOARD' vagy 'ERR_USER_NOT_FOUND')
key: Mapped[str] = mapped_column(String(255), index=True)
# Nyelvi kód (pl: 'hu', 'en', 'de')
lang: Mapped[str] = mapped_column(String(5), index=True)
# A tényleges fordított szöveg
value: Mapped[str] = mapped_column(Text)
# --- JAVÍTÁS: A diagnosztika által hiányolt publikációs állapot ---
is_published: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))

View File

@@ -0,0 +1,155 @@
# /opt/docker/dev/service_finder/backend/app/models/vehicle_definitions.py
from __future__ import annotations
from datetime import datetime
from typing import Optional, List
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, text, JSON, Index, UniqueConstraint, Text, ARRAY, func, Numeric
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
# MB 2.0: Egységesített Base import a központi adatbázis motorból
from app.database import Base
class VehicleType(Base):
""" Jármű kategóriák (pl. Személyautó, Motorkerékpár, Teherautó, Hajó) """
__tablename__ = "vehicle_types"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
code: Mapped[str] = mapped_column(String(30), unique=True, index=True)
name: Mapped[str] = mapped_column(String(50))
icon: Mapped[Optional[str]] = mapped_column(String(50))
units: Mapped[dict] = mapped_column(JSONB, server_default=text("'{\"power\": \"kW\", \"weight\": \"kg\"}'::jsonb"))
# Kapcsolatok
features: Mapped[List["FeatureDefinition"]] = relationship("FeatureDefinition", back_populates="vehicle_type")
definitions: Mapped[List["VehicleModelDefinition"]] = relationship("VehicleModelDefinition", back_populates="v_type_rel")
class FeatureDefinition(Base):
""" Felszereltségi elemek definíciója (pl. ABS, Klíma, LED fényszóró) """
__tablename__ = "feature_definitions"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
vehicle_type_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.vehicle_types.id"))
code: Mapped[str] = mapped_column(String(50), index=True)
name: Mapped[str] = mapped_column(String(100))
category: Mapped[str] = mapped_column(String(50), index=True)
vehicle_type: Mapped["VehicleType"] = relationship("VehicleType", back_populates="features")
model_maps: Mapped[List["ModelFeatureMap"]] = relationship("ModelFeatureMap", back_populates="feature")
class VehicleModelDefinition(Base):
"""
Robot v1.1.0 Multi-Tier MDM Master Adattábla.
Az ökoszisztéma technikai igazságforrása.
"""
__tablename__ = "vehicle_model_definitions"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
make: Mapped[str] = mapped_column(String(100), index=True)
marketing_name: Mapped[str] = mapped_column(String(255), index=True) # Nyers név az RDW-ből
official_marketing_name: Mapped[Optional[str]] = mapped_column(String(255)) # Dúsított, validált név (Robot 2.2)
# --- ROBOT LOGIKAI MEZŐK (JAVÍTVA 2.0 STÍLUSBAN) ---
attempts: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
last_error: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
priority_score: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
# --- PRECISION LOGIC MEZŐK ---
normalized_name: Mapped[Optional[str]] = mapped_column(String(255), index=True, nullable=True)
marketing_name_aliases: Mapped[list] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
engine_code: Mapped[Optional[str]] = mapped_column(String(50), index=True) # A GLOBÁLIS KAPOCS
# --- TECHNIKAI AZONOSÍTÓK ---
technical_code: Mapped[str] = mapped_column(String(100), index=True) # Holland rendszám (kulcs)
variant_code: Mapped[Optional[str]] = mapped_column(String(100), index=True)
version_code: Mapped[Optional[str]] = mapped_column(String(100), index=True)
# --- ÚJ PRÉMIUM MŰSZAKI MEZŐK ---
type_approval_number: Mapped[Optional[str]] = mapped_column(String(100), index=True) # e1*2001/...
seats: Mapped[Optional[int]] = mapped_column(Integer)
width: Mapped[Optional[int]] = mapped_column(Integer) # cm
wheelbase: Mapped[Optional[int]] = mapped_column(Integer) # cm
list_price: Mapped[Optional[int]] = mapped_column(Integer) # EUR (catalogusprijs)
max_speed: Mapped[Optional[int]] = mapped_column(Integer) # km/h
# Vontatási adatok
towing_weight_unbraked: Mapped[Optional[int]] = mapped_column(Integer)
towing_weight_braked: Mapped[Optional[int]] = mapped_column(Integer)
# Környezetvédelmi adatok
fuel_consumption_combined: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), nullable=True)
co2_emissions_combined: Mapped[Optional[int]] = mapped_column(Integer)
# --- SPECIFIKÁCIÓK ---
vehicle_type_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("data.vehicle_types.id"))
vehicle_class: Mapped[Optional[str]] = mapped_column(String(50), index=True)
body_type: Mapped[Optional[str]] = mapped_column(String(100))
fuel_type: Mapped[Optional[str]] = mapped_column(String(50), index=True)
engine_capacity: Mapped[int] = mapped_column(Integer, default=0, index=True)
power_kw: Mapped[int] = mapped_column(Integer, default=0, index=True)
torque_nm: Mapped[Optional[int]] = mapped_column(Integer)
cylinders: Mapped[Optional[int]] = mapped_column(Integer)
cylinder_layout: Mapped[Optional[str]] = mapped_column(String(50))
curb_weight: Mapped[Optional[int]] = mapped_column(Integer)
max_weight: Mapped[Optional[int]] = mapped_column(Integer)
euro_classification: Mapped[Optional[str]] = mapped_column(String(20))
doors: Mapped[Optional[int]] = mapped_column(Integer)
transmission_type: Mapped[Optional[str]] = mapped_column(String(50))
drive_type: Mapped[Optional[str]] = mapped_column(String(50))
# --- ÉLETCIKLUS ÉS STÁTUSZ ---
year_from: Mapped[Optional[int]] = mapped_column(Integer, index=True)
year_to: Mapped[Optional[int]] = mapped_column(Integer, index=True)
production_status: Mapped[Optional[str]] = mapped_column(String(50)) # active / discontinued
# Státusz szintek: unverified, research_in_progress, awaiting_ai_synthesis, gold_enriched
status: Mapped[str] = mapped_column(String(50), server_default=text("'unverified'"), index=True)
is_manual: Mapped[bool] = mapped_column(Boolean, default=False)
source: Mapped[Optional[str]] = mapped_column(String(100))
# --- ADAT-KONTÉNEREK ---
raw_search_context: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
research_metadata: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
specifications: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) # Robot 2.2/2.5 Arany adatai
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
last_research_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
# --- BEÁLLÍTÁSOK ---
__table_args__ = (
UniqueConstraint('make', 'normalized_name', 'variant_code', 'version_code', 'fuel_type', name='uix_vmd_precision'),
Index('idx_vmd_lookup_fast', 'make', 'normalized_name'),
Index('idx_vmd_engine_bridge', 'make', 'engine_code'),
{"schema": "data"}
)
# KAPCSOLATOK
v_type_rel: Mapped["VehicleType"] = relationship("VehicleType", back_populates="definitions")
feature_maps: Mapped[List["ModelFeatureMap"]] = relationship("ModelFeatureMap", back_populates="model_definition")
# Hivatkozás az asset.py-ban lévő osztályra
# Megjegyzés: Ha az AssetCatalog nincs itt importálva, húzzal adjuk meg a nevet
variants: Mapped[List["AssetCatalog"]] = relationship("AssetCatalog", back_populates="master_definition")
class ModelFeatureMap(Base):
""" Kapcsolótábla a modellek és az alapfelszereltség között """
__tablename__ = "model_feature_maps"
__table_args__ = {"schema": "data"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
model_definition_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.vehicle_model_definitions.id"))
feature_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.feature_definitions.id"))
is_standard: Mapped[bool] = mapped_column(Boolean, default=True)
model_definition: Mapped["VehicleModelDefinition"] = relationship("VehicleModelDefinition", back_populates="feature_maps")
feature: Mapped["FeatureDefinition"] = relationship("FeatureDefinition", back_populates="model_maps")