átlagos kiegészítséek jó sok

This commit is contained in:
Roo
2026-03-22 11:02:05 +00:00
parent f53e0b53df
commit 5d44339f21
249 changed files with 20922 additions and 2253 deletions

View 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",
]

View 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())

View 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)

View 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']

View 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])

View 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")