átlagos kiegészítséek jó sok
This commit is contained in:
55
backend/app/models/identity/__init__.py
Normal file
55
backend/app/models/identity/__init__.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# identity package exports
|
||||
from .identity import (
|
||||
Person,
|
||||
User,
|
||||
Wallet,
|
||||
VerificationToken,
|
||||
SocialAccount,
|
||||
ActiveVoucher,
|
||||
UserTrustProfile,
|
||||
UserRole,
|
||||
)
|
||||
|
||||
from .address import (
|
||||
Address,
|
||||
GeoPostalCode,
|
||||
GeoStreet,
|
||||
GeoStreetType,
|
||||
Rating,
|
||||
)
|
||||
|
||||
from .security import PendingAction, ActionStatus
|
||||
from .social import (
|
||||
ServiceProvider,
|
||||
Vote,
|
||||
Competition,
|
||||
UserScore,
|
||||
ServiceReview,
|
||||
ModerationStatus,
|
||||
SourceType,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Person",
|
||||
"User",
|
||||
"Wallet",
|
||||
"VerificationToken",
|
||||
"SocialAccount",
|
||||
"ActiveVoucher",
|
||||
"UserTrustProfile",
|
||||
"UserRole",
|
||||
"Address",
|
||||
"GeoPostalCode",
|
||||
"GeoStreet",
|
||||
"GeoStreetType",
|
||||
"Rating",
|
||||
"PendingAction",
|
||||
"ActionStatus",
|
||||
"ServiceProvider",
|
||||
"Vote",
|
||||
"Competition",
|
||||
"UserScore",
|
||||
"ServiceReview",
|
||||
"ModerationStatus",
|
||||
"SourceType",
|
||||
]
|
||||
89
backend/app/models/identity/address.py
Executable file
89
backend/app/models/identity/address.py
Executable file
@@ -0,0 +1,89 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/identity/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": "system"}
|
||||
|
||||
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": "system"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
postal_code_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("system.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": "system"}
|
||||
|
||||
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": "system"}
|
||||
|
||||
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("system.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": "marketplace"}
|
||||
)
|
||||
|
||||
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("fleet.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("fleet.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())
|
||||
268
backend/app/models/identity/identity.py
Normal file
268
backend/app/models/identity/identity.py
Normal file
@@ -0,0 +1,268 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/identity/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
|
||||
from .payment import PaymentIntent, WithdrawalRequest
|
||||
from .social import ServiceReview, SocialAccount
|
||||
|
||||
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 'system' sémában van
|
||||
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("system.addresses.id"))
|
||||
|
||||
# Kritikus azonosító: Név + Anyja neve + Szül.idő hash-elve.
|
||||
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, nullable=False, default=lambda: {}, server_default=text("'{}'::jsonb"))
|
||||
ice_contact: Mapped[Any] = mapped_column(JSON, nullable=False, default=lambda: {}, server_default=text("'{}'::jsonb"))
|
||||
|
||||
lifetime_xp: Mapped[int] = mapped_column(BigInteger, default=-1, nullable=False)
|
||||
penalty_points: Mapped[int] = mapped_column(Integer, default=-1, nullable=False)
|
||||
social_reputation: Mapped[float] = mapped_column(Numeric(3, 2), default=0.0, nullable=False)
|
||||
|
||||
is_sales_agent: Mapped[bool] = mapped_column(Boolean, default=True, nullable=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), default=func.now(), nullable=False)
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# --- KAPCSOLATOK ---
|
||||
|
||||
# JAVÍTÁS 1: Explicit 'foreign_keys' megadás az AmbiguousForeignKeysError ellen
|
||||
users: Mapped[List["User"]] = relationship(
|
||||
"User",
|
||||
foreign_keys="[User.person_id]",
|
||||
back_populates="person",
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
# JAVÍTÁS 2: 'post_update' és 'use_alter' a körbe-függőség (circular cycle) feloldásához
|
||||
active_user_account: Mapped[Optional["User"]] = relationship(
|
||||
"User",
|
||||
foreign_keys="[Person.user_id]",
|
||||
post_update=True
|
||||
)
|
||||
user_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("identity.users.id", use_alter=True, name="fk_person_active_user"),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
memberships: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="person")
|
||||
|
||||
# Kapcsolat a tulajdonolt szervezetekhez (Organization táblában legal_owner_id)
|
||||
owned_business_entities: Mapped[List["Organization"]] = relationship(
|
||||
"Organization",
|
||||
foreign_keys="[Organization.legal_owner_id]",
|
||||
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)
|
||||
|
||||
# JAVÍTÁS 3: Az ajánló és értékesítő mezőknek is kell a tiszta kapcsolat nevesítés
|
||||
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 ---
|
||||
|
||||
# JAVÍTÁS 4: Itt is explicit megadjuk, hogy melyik kulcs köti az emberhez
|
||||
person: Mapped[Optional["Person"]] = relationship(
|
||||
"Person",
|
||||
foreign_keys=[person_id],
|
||||
back_populates="users"
|
||||
)
|
||||
|
||||
# JAVÍTÁS 5: Ajánlói (Referrer) önhivatkozó kapcsolat feloldása
|
||||
referrer: Mapped[Optional["User"]] = relationship(
|
||||
"User",
|
||||
remote_side=[id],
|
||||
foreign_keys=[referred_by_id]
|
||||
)
|
||||
|
||||
# JAVÍTÁS 6: Értékesítő (Sales Agent) önhivatkozó kapcsolat feloldása
|
||||
sales_agent: Mapped[Optional["User"]] = relationship(
|
||||
"User",
|
||||
remote_side=[id],
|
||||
foreign_keys=[current_sales_agent_id]
|
||||
)
|
||||
|
||||
wallet: Mapped[Optional["Wallet"]] = relationship("Wallet", back_populates="user", uselist=False)
|
||||
payment_intents_as_payer = relationship("PaymentIntent", foreign_keys="[PaymentIntent.payer_id]", back_populates="payer")
|
||||
payment_intents_as_beneficiary = relationship("PaymentIntent", foreign_keys="[PaymentIntent.beneficiary_id]", back_populates="beneficiary")
|
||||
|
||||
trust_profile: Mapped[Optional["UserTrustProfile"]] = relationship("UserTrustProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
|
||||
|
||||
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")
|
||||
|
||||
# MB 2.1: Vehicle ratings kapcsolat (hiányzott a listából, visszatéve)
|
||||
vehicle_ratings: Mapped[List["VehicleUserRating"]] = relationship("VehicleUserRating", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
# Pénzügyi és egyéb kapcsolatok
|
||||
withdrawal_requests: Mapped[List["WithdrawalRequest"]] = relationship("WithdrawalRequest", foreign_keys="[WithdrawalRequest.user_id]", back_populates="user", cascade="all, delete-orphan")
|
||||
service_reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
class Wallet(Base):
|
||||
""" Felhasználói pénztárca. """
|
||||
__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")
|
||||
active_vouchers: Mapped[List["ActiveVoucher"]] = relationship("ActiveVoucher", back_populates="wallet", cascade="all, delete-orphan")
|
||||
|
||||
class VerificationToken(Base):
|
||||
""" E-mail és egyéb verifikációs tokenek. """
|
||||
__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):
|
||||
""" Közösségi bejelentkezési adatok (Google, Facebook, stb). """
|
||||
__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")
|
||||
|
||||
class ActiveVoucher(Base):
|
||||
""" Aktív, le nem járt voucher-ek tárolása FIFO elv szerint. """
|
||||
__tablename__ = "active_vouchers"
|
||||
__table_args__ = {"schema": "identity"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
wallet_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.wallets.id", ondelete="CASCADE"), nullable=False)
|
||||
amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
|
||||
original_amount: Mapped[float] = mapped_column(Numeric(18, 4), nullable=False)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
wallet: Mapped["Wallet"] = relationship("Wallet", back_populates="active_vouchers")
|
||||
|
||||
class UserTrustProfile(Base):
|
||||
""" Gondos Gazda Index (Trust Score) tárolása. """
|
||||
__tablename__ = "user_trust_profiles"
|
||||
__table_args__ = {"schema": "identity"}
|
||||
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("identity.users.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
index=True
|
||||
)
|
||||
trust_score: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
maintenance_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False)
|
||||
quality_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False)
|
||||
preventive_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False)
|
||||
last_calculated: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
user: Mapped["User"] = relationship("User", back_populates="trust_profile", uselist=False)
|
||||
124
backend/app/models/identity/registry.py
Normal file
124
backend/app/models/identity/registry.py
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Central Model Registry for Service Finder
|
||||
|
||||
Automatically discovers and imports all SQLAlchemy models from the models directory,
|
||||
ensuring Base.metadata is fully populated with tables, constraints, and indexes.
|
||||
|
||||
Usage:
|
||||
from app.models.registry import Base, get_all_models, ensure_models_loaded
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Type
|
||||
|
||||
from sqlalchemy.ext.declarative import DeclarativeMeta
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
# Import the Base from database (circular dependency will be resolved later)
|
||||
# We'll define our own Base if needed, but better to reuse existing one.
|
||||
# We'll import after path setup.
|
||||
|
||||
# Add backend to path if not already
|
||||
backend_dir = Path(__file__).parent.parent.parent
|
||||
if str(backend_dir) not in sys.path:
|
||||
sys.path.insert(0, str(backend_dir))
|
||||
|
||||
# Import Base from database (this will be the same Base used everywhere)
|
||||
from app.database import Base
|
||||
|
||||
def discover_model_files() -> List[Path]:
|
||||
"""
|
||||
Walk through models directory and collect all .py files except __init__.py and registry.py.
|
||||
"""
|
||||
models_dir = Path(__file__).parent
|
||||
model_files = []
|
||||
for root, _, files in os.walk(models_dir):
|
||||
for file in files:
|
||||
if file.endswith('.py') and file not in ('__init__.py', 'registry.py'):
|
||||
full_path = Path(root) / file
|
||||
model_files.append(full_path)
|
||||
return model_files
|
||||
|
||||
def import_module_from_file(file_path: Path) -> str:
|
||||
"""
|
||||
Import a Python module from its file path.
|
||||
Returns the module name.
|
||||
"""
|
||||
# Compute module name relative to backend/app
|
||||
rel_path = file_path.relative_to(backend_dir)
|
||||
module_name = str(rel_path).replace(os.sep, '.').replace('.py', '')
|
||||
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
if spec is None:
|
||||
raise ImportError(f"Could not load spec for {module_name}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module_name
|
||||
except Exception as e:
|
||||
# Silently skip import errors (maybe due to missing dependencies)
|
||||
# but log for debugging
|
||||
print(f"⚠️ Could not import {module_name}: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
def load_all_models() -> List[str]:
|
||||
"""
|
||||
Dynamically import all model files to populate Base.metadata.
|
||||
Returns list of successfully imported module names.
|
||||
"""
|
||||
model_files = discover_model_files()
|
||||
imported = []
|
||||
for file in model_files:
|
||||
module_name = import_module_from_file(file)
|
||||
if module_name:
|
||||
imported.append(module_name)
|
||||
# Also ensure the __init__.py is loaded (it imports many models manually)
|
||||
try:
|
||||
import app.models
|
||||
imported.append('app.models')
|
||||
except ImportError:
|
||||
pass
|
||||
print(f"✅ Registry loaded {len(imported)} model modules. Total tables in metadata: {len(Base.metadata.tables)}")
|
||||
return imported
|
||||
|
||||
def get_all_models() -> Dict[str, Type[DeclarativeMeta]]:
|
||||
"""
|
||||
Return a mapping of class name to model class for all registered SQLAlchemy models.
|
||||
This works only after models have been imported.
|
||||
"""
|
||||
# This is a heuristic: find all subclasses of Base in loaded modules
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
models = {}
|
||||
for cls in Base.__subclasses__():
|
||||
models[cls.__name__] = cls
|
||||
# Also check deeper inheritance (if models inherit from other models that inherit from Base)
|
||||
for module_name, module in sys.modules.items():
|
||||
if module_name.startswith('app.models.'):
|
||||
for attr_name in dir(module):
|
||||
attr = getattr(module, attr_name)
|
||||
if isinstance(attr, type) and issubclass(attr, Base) and attr is not Base:
|
||||
models[attr.__name__] = attr
|
||||
return models
|
||||
|
||||
def ensure_models_loaded():
|
||||
"""
|
||||
Ensure that all models are loaded into Base.metadata.
|
||||
This is idempotent and can be called multiple times.
|
||||
"""
|
||||
if len(Base.metadata.tables) == 0:
|
||||
load_all_models()
|
||||
else:
|
||||
# Already loaded
|
||||
pass
|
||||
|
||||
# Auto-load models when this module is imported (optional, but useful)
|
||||
# We'll make it explicit via a function call to avoid side effects.
|
||||
# Instead, we'll provide a function to trigger loading.
|
||||
|
||||
# Export
|
||||
__all__ = ['Base', 'discover_model_files', 'load_all_models', 'get_all_models', 'ensure_models_loaded']
|
||||
51
backend/app/models/identity/security.py
Executable file
51
backend/app/models/identity/security.py
Executable file
@@ -0,0 +1,51 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/identity/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])
|
||||
116
backend/app/models/identity/social.py
Executable file
116
backend/app/models/identity/social.py
Executable file
@@ -0,0 +1,116 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/identity/social.py
|
||||
import enum
|
||||
import uuid
|
||||
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, UUID as PG_UUID
|
||||
from sqlalchemy.sql import func
|
||||
from app.database 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"
|
||||
__table_args__ = {"schema": "marketplace"}
|
||||
|
||||
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'),
|
||||
{"schema": "marketplace"}
|
||||
)
|
||||
|
||||
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("marketplace.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"
|
||||
__table_args__ = {"schema": "gamification"}
|
||||
|
||||
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'),
|
||||
{"schema": "gamification"}
|
||||
)
|
||||
|
||||
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("gamification.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())
|
||||
|
||||
|
||||
class ServiceReview(Base):
|
||||
"""
|
||||
Verifikált szerviz értékelések (Social 3).
|
||||
Csak igazolt pénzügyi tranzakció után lehet értékelni.
|
||||
"""
|
||||
__tablename__ = "service_reviews"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('transaction_id', name='uq_service_review_transaction'),
|
||||
{"schema": "marketplace"}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id", ondelete="CASCADE"), nullable=False)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id", ondelete="SET NULL"), nullable=False)
|
||||
transaction_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
# Rating dimensions (1-10)
|
||||
price_rating: Mapped[int] = mapped_column(Integer, nullable=False) # 1-10
|
||||
quality_rating: Mapped[int] = mapped_column(Integer, nullable=False) # 1-10
|
||||
time_rating: Mapped[int] = mapped_column(Integer, nullable=False) # 1-10
|
||||
communication_rating: Mapped[int] = mapped_column(Integer, nullable=False) # 1-10
|
||||
|
||||
comment: Mapped[Optional[str]] = mapped_column(Text)
|
||||
is_verified: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
service: Mapped["ServiceProfile"] = relationship("ServiceProfile", back_populates="reviews")
|
||||
user: Mapped["User"] = relationship("User", back_populates="service_reviews")
|
||||
Reference in New Issue
Block a user