á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

@@ -3,46 +3,53 @@
from app.database import Base
# 1. Alapvető identitás és szerepkörök
from .identity import Person, User, Wallet, VerificationToken, SocialAccount, UserRole
from .identity.identity import Person, User, Wallet, VerificationToken, SocialAccount, UserRole
# 2. Földrajzi adatok és címek
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Rating
from .identity.address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Rating
# 3. Jármű definíciók
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
from .vehicle.vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
from .reference_data import ReferenceLookup
from .vehicle import CostCategory, VehicleCost
from .vehicle.vehicle import CostCategory, VehicleCost, GbCatalogDiscovery
from .vehicle.external_reference import ExternalReferenceLibrary
from .vehicle.external_reference_queue import ExternalReferenceQueue
# 4. Szervezeti felépítés
from .organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment, OrgType, OrgUserRole, Branch
from .marketplace.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
from .vehicle.asset import Asset, AssetCatalog, AssetCost, AssetEvent, AssetAssignment, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate, CatalogDiscovery, VehicleOwnership
# 6. Üzleti logika és előfizetések
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
from .payment import PaymentIntent, PaymentIntentStatus
from .finance import Issuer, IssuerType
from .marketplace.payment import PaymentIntent, PaymentIntentStatus
from .marketplace.finance import Issuer, IssuerType
# 7. Szolgáltatások és staging
from .service import ServiceProfile, ExpertiseTag, ServiceExpertise, ServiceStaging, DiscoveryParameter
# JAVÍTVA: ServiceStaging és társai a staged_data-ból jönnek!
from .marketplace.service import ServiceProfile, ExpertiseTag, ServiceExpertise
from .marketplace.staged_data import ServiceStaging, DiscoveryParameter, StagedVehicleData
from .marketplace.service_request import ServiceRequest
# 8. Közösségi és értékelési modellek (Social 3)
from .social import ServiceProvider, Vote, Competition, UserScore, ServiceReview, ModerationStatus, SourceType
from .identity.social import ServiceProvider, Vote, Competition, UserScore, ServiceReview, ModerationStatus, SourceType
# 9. Rendszer, Gamification és egyebek
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger
from .gamification.gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger, UserContribution, Season
# --- 2.2 ÚJDONSÁG: InternalNotification hozzáadása ---
from .system import SystemParameter, InternalNotification
from .system.system import SystemParameter, ParameterScope, InternalNotification, SystemServiceStaging
from .system.document import Document
from .system.translation import Translation
# Direct import from audit module
from .system.audit import SecurityAuditLog, OperationalLog, ProcessLog, FinancialLedger, WalletType, LedgerStatus, LedgerEntryType
from .vehicle.history import AuditLog, LogSeverity
from .identity.security import PendingAction, ActionStatus
from .system.legal import LegalDocument, LegalAcceptance
from .marketplace.logistics import Location, LocationType
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
@@ -53,25 +60,26 @@ ServiceRecord = AssetEvent
__all__ = [
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
"Organization", "OrganizationMember", "OrganizationSalesAssignment", "OrgType", "OrgUserRole",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetAssignment", "AssetFinancials",
"AssetTelemetry", "AssetReview", "ExchangeRate", "CatalogDiscovery",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger", "UserContribution",
# --- 2.2 ÚJDONSÁG KIEGÉSZÍTÉS ---
"SystemParameter", "InternalNotification",
"SystemParameter", "ParameterScope", "InternalNotification",
# Social models (Social 3)
"ServiceProvider", "Vote", "Competition", "UserScore", "ServiceReview", "ModerationStatus", "SourceType",
"Document", "Translation", "PendingAction",
"Document", "Translation", "PendingAction", "ActionStatus",
"SubscriptionTier", "OrganizationSubscription", "CreditTransaction", "ServiceSpecialty",
"PaymentIntent", "PaymentIntentStatus",
"AuditLog", "VehicleOwnership", "LogSeverity",
"SecurityAuditLog", "ProcessLog", "FinancialLedger",
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter",
"SecurityAuditLog", "OperationalLog", "ProcessLog",
"FinancialLedger", "WalletType", "LedgerStatus", "LedgerEntryType",
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter", "ServiceRequest",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition", "ReferenceLookup",
"VehicleType", "FeatureDefinition", "ModelFeatureMap", "LegalDocument", "LegalAcceptance",
"Location", "LocationType", "Issuer", "IssuerType", "CostCategory", "VehicleCost"
]
from app.models.payment import PaymentIntent, WithdrawalRequest
"Location", "LocationType", "Issuer", "IssuerType", "CostCategory", "VehicleCost", "ExternalReferenceLibrary", "ExternalReferenceQueue",
"GbCatalogDiscovery", "Season", "StagedVehicleData"
]

135
backend/app/models/audit.py Executable file → Normal file
View File

@@ -1,115 +1,24 @@
# /opt/docker/dev/service_finder/backend/app/models/audit.py
import enum
import uuid
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 sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
from app.database import Base
# Backward compatibility stub for audit module
# After restructuring, audit models moved to system.audit
# This file re-exports everything to maintain compatibility
class SecurityAuditLog(Base):
""" Kiemelt biztonsági események és a 4-szem elv naplózása. """
__tablename__ = "security_audit_logs"
__table_args__ = {"schema": "audit"}
from .system.audit import (
SecurityAuditLog,
OperationalLog,
ProcessLog,
LedgerEntryType,
WalletType,
LedgerStatus,
FinancialLedger,
)
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"
__table_args__ = {"schema": "audit"}
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"
__table_args__ = {"schema": "audit"}
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 LedgerEntryType(str, enum.Enum):
DEBIT = "DEBIT"
CREDIT = "CREDIT"
class WalletType(str, enum.Enum):
EARNED = "EARNED"
PURCHASED = "PURCHASED"
SERVICE_COINS = "SERVICE_COINS"
VOUCHER = "VOUCHER"
class LedgerStatus(str, enum.Enum):
PENDING = "PENDING"
SUCCESS = "SUCCESS"
FAILED = "FAILED"
REFUNDED = "REFUNDED"
REFUND = "REFUND"
class FinancialLedger(Base):
""" Minden pénz- és kreditmozgás központi naplója. Billing Engine alapja. """
__tablename__ = "financial_ledger"
__table_args__ = {"schema": "audit"}
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())
# Új mezők doubleentry és okos levonáshoz
entry_type: Mapped[LedgerEntryType] = mapped_column(
PG_ENUM(LedgerEntryType, name="ledger_entry_type", schema="audit"),
nullable=False
)
balance_after: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
wallet_type: Mapped[Optional[WalletType]] = mapped_column(
PG_ENUM(WalletType, name="wallet_type", schema="audit")
)
# Economy 1: számlázási mezők
issuer_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("finance.issuers.id"), nullable=True)
invoice_status: Mapped[Optional[str]] = mapped_column(String(50), default="PENDING")
tax_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
gross_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
net_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
transaction_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), default=uuid.uuid4, nullable=False, index=True
)
status: Mapped[LedgerStatus] = mapped_column(
PG_ENUM(LedgerStatus, name="ledger_status", schema="audit"),
default=LedgerStatus.SUCCESS,
nullable=False
)
# Re-export everything
__all__ = [
"SecurityAuditLog",
"OperationalLog",
"ProcessLog",
"LedgerEntryType",
"WalletType",
"LedgerStatus",
"FinancialLedger",
]

View File

@@ -1,86 +0,0 @@
# /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": "system"}
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": "system"}
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": "system"}
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": "system"}
# 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": "system"}
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": "system"}
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("system.badges.id"))
earned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")

View File

@@ -0,0 +1,22 @@
# gamification package exports
from .gamification import (
PointRule,
LevelConfig,
PointsLedger,
UserStats,
Badge,
UserBadge,
UserContribution,
Season,
)
__all__ = [
"PointRule",
"LevelConfig",
"PointsLedger",
"UserStats",
"Badge",
"UserBadge",
"UserContribution",
"Season",
]

View File

@@ -0,0 +1,144 @@
# /opt/docker/dev/service_finder/backend/app/models/gamification/gamification.py
import uuid
from datetime import datetime, date
from typing import Optional, List, TYPE_CHECKING
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean, Text, text, Date
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
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": "gamification", "extend_existing": True}
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": "gamification", "extend_existing": True}
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": "gamification", "extend_existing": True}
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": "gamification", "extend_existing": True}
# 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)
penalty_quota_remaining: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
places_discovered: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
places_validated: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
banned_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
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": "gamification", "extend_existing": True}
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": "gamification", "extend_existing": True}
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("gamification.badges.id"))
earned_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
user: Mapped["User"] = relationship("User")
class UserContribution(Base):
"""
Felhasználói hozzájárulások nyilvántartása (szerviz beküldés, validálás, jelentés).
Ez a tábla tárolja, hogy melyik felhasználó milyen tevékenységet végzett és milyen jutalmat kapott.
"""
__tablename__ = "user_contributions"
__table_args__ = {"schema": "gamification"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=False, index=True)
season_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("gamification.seasons.id"), nullable=True, index=True)
# --- HIÁNYZÓ MEZŐK PÓTOLVA A SPAM VÉDELEMHEZ ---
service_fingerprint: Mapped[Optional[str]] = mapped_column(String(255), index=True)
cooldown_end: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
action_type: Mapped[int] = mapped_column(Integer, nullable=False)
earned_xp: Mapped[int] = mapped_column(Integer, nullable=False)
contribution_type: Mapped[str] = mapped_column(String(50), nullable=False, index=True) # 'service_submission', 'service_validation', 'report_abuse'
entity_type: Mapped[Optional[str]] = mapped_column(String(50), index=True) # 'service', 'review', 'comment'
entity_id: Mapped[Optional[int]] = mapped_column(Integer, index=True) # ID of the contributed entity
points_awarded: Mapped[int] = mapped_column(Integer, default=0)
xp_awarded: Mapped[int] = mapped_column(Integer, default=0)
status: Mapped[str] = mapped_column(String(20), default="pending", index=True) # 'pending', 'approved', 'rejected'
reviewed_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"), nullable=True)
reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
# --- JAVÍTOTT FOGLALT SZÓ ---
provided_fields: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# Relationships
user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
reviewer: Mapped[Optional["User"]] = relationship("User", foreign_keys=[reviewed_by])
season: Mapped[Optional["Season"]] = relationship("Season")
class Season(Base):
""" Szezonális versenyek tárolása. """
__tablename__ = "seasons"
__table_args__ = {"schema": "gamification"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String(100), nullable=False)
start_date: Mapped[date] = mapped_column(Date, nullable=False)
end_date: Mapped[date] = mapped_column(Date, nullable=False)
is_active: 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,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

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/address.py
# /opt/docker/dev/service_finder/backend/app/models/identity/address.py
import uuid
from datetime import datetime
from typing import Any, List, Optional

View File

@@ -1,3 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/identity/identity.py
from __future__ import annotations
import uuid
import enum
@@ -56,25 +57,51 @@ class Person(Base):
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"))
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, 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"))
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, server_default=text("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), server_default=func.now())
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 ---
users: Mapped[List["User"]] = relationship("User", back_populates="person")
memberships: Mapped[List["OrganizationMember"]] = relationship("OrganizationMember", back_populates="person")
owned_business_entities: Mapped[List["Organization"]] = relationship("Organization", back_populates="legal_owner")
# 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"
@@ -97,6 +124,7 @@ class User(Base):
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"))
@@ -115,10 +143,32 @@ class User(Base):
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# --- KAPCSOLATOK ---
person: Mapped[Optional["Person"]] = relationship("Person", back_populates="users")
# 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")
# JAVÍTÁS: Ez a sor KELL az OCR robot és a Trust Engine működéséhez
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")
@@ -126,6 +176,9 @@ class User(Base):
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")

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/security.py
# /opt/docker/dev/service_finder/backend/app/models/identity/security.py
import enum
from datetime import datetime
from typing import Optional, TYPE_CHECKING

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/social.py
# /opt/docker/dev/service_finder/backend/app/models/identity/social.py
import enum
import uuid
from datetime import datetime
@@ -59,7 +59,7 @@ class Vote(Base):
class Competition(Base):
""" Gamifikált versenyek (pl. Januári Feltöltő Verseny). """
__tablename__ = "competitions"
__table_args__ = {"schema": "system"}
__table_args__ = {"schema": "gamification"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String, nullable=False)
@@ -73,12 +73,12 @@ class UserScore(Base):
__tablename__ = "user_scores"
__table_args__ = (
UniqueConstraint('user_id', 'competition_id', name='uq_user_competition_score'),
{"schema": "system"}
{"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("system.competitions.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())

View File

@@ -1,234 +0,0 @@
# /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("system.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"))
trust_profile: Mapped[Optional["UserTrustProfile"]] = relationship("UserTrustProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
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")
# PaymentIntent kapcsolatok
payment_intents_as_payer: Mapped[List["PaymentIntent"]] = relationship(
"PaymentIntent",
foreign_keys="[PaymentIntent.payer_id]",
back_populates="payer"
)
withdrawal_requests: Mapped[List["WithdrawalRequest"]] = relationship("WithdrawalRequest", foreign_keys="[WithdrawalRequest.user_id]", back_populates="user", cascade="all, delete-orphan")
payment_intents_as_beneficiary: Mapped[List["PaymentIntent"]] = relationship(
"PaymentIntent",
foreign_keys="[PaymentIntent.beneficiary_id]",
back_populates="beneficiary"
)
# Service reviews
service_reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", back_populates="user", cascade="all, delete-orphan")
@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")
active_vouchers: Mapped[List["ActiveVoucher"]] = relationship("ActiveVoucher", back_populates="wallet", cascade="all, delete-orphan")
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")
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())
# Kapcsolatok
wallet: Mapped["Wallet"] = relationship("Wallet", back_populates="active_vouchers")
class UserTrustProfile(Base):
"""
Gondos Gazda Index (Trust Score) tárolása felhasználónként.
A pontszámot a trust_engine számolja dinamikusan a SystemParameter-ek alapján.
"""
__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) # 0-100 pont
maintenance_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False) # 0.0-1.0
quality_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False) # 0.0-1.0
preventive_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0.0, nullable=False) # 0.0-1.0
last_calculated: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False
)
# Kapcsolatok
user: Mapped["User"] = relationship("User", back_populates="trust_profile", uselist=False)

View File

@@ -0,0 +1,53 @@
# marketplace package exports
from .organization import (
Organization,
OrganizationMember,
OrganizationFinancials,
OrganizationSalesAssignment,
OrgType,
OrgUserRole,
Branch,
)
from .payment import PaymentIntent, PaymentIntentStatus
from .finance import Issuer, IssuerType
from .service import (
ServiceProfile,
ExpertiseTag,
ServiceExpertise,
)
from .logistics import Location, LocationType
# THOUGHT PROCESS: A StagedVehicleData nevet StagedVehicleData-ra javítottuk,
# és ide csoportosítottuk a staged_data.py-ban lévő többi osztályt is.
from .staged_data import (
StagedVehicleData,
ServiceStaging,
DiscoveryParameter
)
from .service_request import ServiceRequest
__all__ = [
"Organization",
"OrganizationMember",
"OrganizationFinancials",
"OrganizationSalesAssignment",
"OrgType",
"OrgUserRole",
"Branch",
"PaymentIntent",
"PaymentIntentStatus",
"Issuer",
"IssuerType",
"ServiceProfile",
"ExpertiseTag",
"ServiceExpertise",
"ServiceStaging",
"DiscoveryParameter",
"Location",
"LocationType",
"StagedVehicleData",
"ServiceRequest",
]

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/finance.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/finance.py
"""
Finance modellek: Issuer (Kibocsátó) és FinancialLedger (Pénzügyi főkönyv) bővítése.
"""

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/logistics.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/logistics.py
import enum
from typing import Optional
from sqlalchemy import Integer, String, Enum

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/organization.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/organization.py
import enum
import uuid
from datetime import datetime
@@ -8,6 +8,7 @@ from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, J
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
from geoalchemy2 import Geometry
# MB 2.0: A központi aszinkron adatbázis motorból húzzuk be a Base-t
from app.database import Base
@@ -202,6 +203,12 @@ class Branch(Base):
door: Mapped[Optional[str]] = mapped_column(String(20))
hrsz: Mapped[Optional[str]] = mapped_column(String(50))
# PostGIS location field for geographic queries
location: Mapped[Optional[Any]] = mapped_column(
Geometry(geometry_type='POINT', srid=4326),
nullable=True
)
opening_hours: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
branch_rating: Mapped[float] = mapped_column(Float, default=0.0)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/payment.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/payment.py
"""
Payment Intent modell a Stripe integrációhoz és belső fizetésekhez.
Kettős Lakat (Double Lock) biztonságot valósít meg.
@@ -14,7 +14,7 @@ from sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
from sqlalchemy.sql import func
from app.database import Base
from app.models.audit import WalletType
from app.models.system.audit import WalletType
class PaymentIntentStatus(str, enum.Enum):

View File

@@ -1,16 +1,23 @@
# /opt/docker/dev/service_finder/backend/app/models/service.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/service.py
import enum
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 sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB, ENUM as SQLEnum
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 ServiceStatus(str, enum.Enum):
ghost = "ghost" # Nyers, robot által talált, nem validált
active = "active" # Publikus, aktív szerviz
flagged = "flagged" # Gyanús, kézi ellenőrzést igényel
suspended = "suspended" # Felfüggesztett, tiltott szerviz
class ServiceProfile(Base):
""" Szerviz szolgáltató adatai (v1.3.1). """
__tablename__ = "service_profiles"
@@ -26,7 +33,12 @@ class ServiceProfile(Base):
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)
status: Mapped[ServiceStatus] = mapped_column(
SQLEnum(ServiceStatus, name="service_status", schema="marketplace"),
server_default=ServiceStatus.ghost.value,
nullable=False,
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)
@@ -73,55 +85,29 @@ class ExpertiseTag(Base):
__table_args__ = {"schema": "marketplace"}
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": "marketplace"}
@@ -129,13 +115,9 @@ class ServiceExpertise(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id", ondelete="CASCADE"))
expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.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")
@@ -154,6 +136,14 @@ class ServiceStaging(Base):
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"))
# Audit fix: contact_email hossza rögzítve a DB szinkronhoz
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
contact_phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
website: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
external_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
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())

View File

@@ -0,0 +1,175 @@
# /opt/docker/dev/service_finder/backend/app/models/marketplace/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": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), unique=True)
parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("marketplace.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)
# Aggregated verified review ratings (Social 3)
rating_verified_count: Mapped[Optional[int]] = mapped_column(Integer, server_default=text("0"))
rating_price_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_quality_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_time_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_communication_avg: Mapped[Optional[float]] = mapped_column(Float)
rating_overall: Mapped[Optional[float]] = mapped_column(Float)
last_review_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
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")
reviews: Mapped[List["ServiceReview"]] = relationship("ServiceReview", 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": "marketplace"}
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": "marketplace"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
service_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.service_profiles.id", ondelete="CASCADE"))
expertise_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketplace.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": "marketplace"}
)
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"))
# Additional contact and identification fields
contact_phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
website: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
external_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, index=True)
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": "marketplace"}
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))

View File

@@ -0,0 +1,95 @@
# /opt/docker/dev/service_finder/backend/app/models/marketplace/service_request.py
"""
ServiceRequest - Piactér központi tranzakciós modellje.
Epic 7: Marketplace ServiceRequest dedikált modell.
"""
from typing import Optional
from datetime import datetime
from sqlalchemy import String, ForeignKey, Text, DateTime, Numeric, Integer, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from app.database import Base
class ServiceRequest(Base):
"""
Szervizigény (ServiceRequest) tábla.
Egy felhasználó által létrehozott szervizigényt reprezentál, amely lehetővé teszi
a szervizszolgáltatók számára árajánlatok készítését és a tranzakciók lebonyolítását.
"""
__tablename__ = "service_requests"
__table_args__ = (
Index('idx_service_request_status', 'status'),
Index('idx_service_request_user_id', 'user_id'),
Index('idx_service_request_asset_id', 'asset_id'),
Index('idx_service_request_branch_id', 'branch_id'),
{"schema": "marketplace"}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# Idegen kulcsok (Kapcsolódási pontok)
user_id: Mapped[int] = mapped_column(
ForeignKey("identity.users.id", ondelete="CASCADE"),
nullable=False,
index=True,
comment="A szervizigényt létrehozó felhasználó"
)
asset_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("vehicle.assets.id", ondelete="SET NULL"),
nullable=True,
comment="Érintett jármű (opcionális)"
)
branch_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("fleet.branches.id", ondelete="SET NULL"),
nullable=True,
comment="Célzott szerviz (ha van)"
)
# Üzleti logika mezők
status: Mapped[str] = mapped_column(
String(50),
server_default="pending",
index=True,
comment="pending, quoted, accepted, scheduled, completed, cancelled"
)
description: Mapped[Optional[str]] = mapped_column(
Text,
nullable=True,
comment="A szervizigény részletes leírása"
)
price_estimate: Mapped[Optional[float]] = mapped_column(
Numeric(10, 2),
nullable=True,
comment="Becsült ár (opcionális)"
)
requested_date: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True),
nullable=True,
comment="Kért szerviz dátum"
)
# Audit
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
comment="Létrehozás időbélyege"
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
comment="Utolsó módosítás időbélyege"
)
# Relationships (opcionális, de ajánlott a lazy loading miatt)
user = relationship("User", back_populates="service_requests", lazy="selectin")
asset = relationship("Asset", back_populates="service_requests", lazy="selectin")
branch = relationship("Branch", back_populates="service_requests", lazy="selectin")
def __repr__(self) -> str:
return f"<ServiceRequest(id={self.id}, status='{self.status}', user_id={self.user_id})>"

View File

@@ -0,0 +1,94 @@
from datetime import datetime
from typing import Optional, Any
from sqlalchemy import String, Integer, DateTime, text, Boolean, Float, Text, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
from app.database import Base # MB 2.0 Standard: Központi bázis használata
class StagedVehicleData(Base):
""" Robot 2.1 (Researcher) nyers adatgyűjtője. """
__tablename__ = "staged_vehicle_data"
__table_args__ = {"schema": "system", "extend_existing": True}
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 és a Robot 5 (Auditor) naplója.
A séma és a mezők szinkronban az adatbázis audittal.
"""
__tablename__ = "service_staging"
__table_args__ = {"schema": "marketplace", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(255), index=True)
# 1. ⚠️ EXTRA OSZLOP: source
source: Mapped[Optional[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)
# Elérhetőségek
city: Mapped[str] = mapped_column(String(100), index=True)
postal_code: Mapped[Optional[str]] = mapped_column(String(10))
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))
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# 2. ⚠️ EXTRA OSZLOP: description
description: Mapped[Optional[str]] = mapped_column(Text)
# 3. ⚠️ EXTRA OSZLOP: submitted_by
submitted_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
# 4. ⚠️ EXTRA OSZLOP: trust_score
trust_score: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
raw_data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
validation_level: Mapped[int] = mapped_column(Integer, default=40, server_default=text("40"))
# --- Robot 5 (Auditor) technikai mezők ---
# 5. ⚠️ EXTRA OSZLOP: rejection_reason
rejection_reason: Mapped[Optional[str]] = mapped_column(String(500))
# 6. ⚠️ EXTRA OSZLOP: published_at
published_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
# 7. ⚠️ EXTRA OSZLOP: service_profile_id
service_profile_id: Mapped[Optional[int]] = mapped_column(Integer)
# 8. ⚠️ EXTRA OSZLOP: organization_id
organization_id: Mapped[Optional[int]] = mapped_column(Integer)
# 9. ⚠️ EXTRA OSZLOP: audit_trail
audit_trail: Mapped[Optional[dict]] = mapped_column(JSONB)
# Időbélyegek
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
# 10. ⚠️ EXTRA OSZLOP: updated_at
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": "marketplace", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
city: Mapped[str] = mapped_column(String(100), unique=True, index=True)
country_code: Mapped[Optional[str]] = mapped_column(String(2), nullable=True, default="HU")
keyword: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
last_run_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/staged_data.py
# /opt/docker/dev/service_finder/backend/app/models/marketplace/staged_data.py
from datetime import datetime
from typing import Optional, Any
from sqlalchemy import String, Integer, DateTime, text, Boolean, Float
@@ -22,25 +22,42 @@ class StagedVehicleData(Base):
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. """
""" Robot 1.3 (Scout) által talált nyers szerviz adatok és a Robot 5 (Auditor) naplója. """
__tablename__ = "service_staging"
__table_args__ = {"schema": "system"}
__table_args__ = {"schema": "marketplace"}
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))
source: Mapped[Optional[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)
# Elérhetőségek
city: Mapped[str] = mapped_column(String(100), index=True)
postal_code: Mapped[Optional[str]] = mapped_column(String(10))
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))
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# Beküldés és Bizalom
description: Mapped[Optional[str]] = mapped_column(Text)
submitted_by: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("identity.users.id"))
trust_score: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"))
# Nyers adatok és Státusz
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)
# --- Robot 5 (Auditor) technikai mezők ---
# Ezek kellenek a munka naplózásához
rejection_reason: Mapped[Optional[str]] = mapped_column(String(500))
published_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
service_profile_id: Mapped[Optional[int]] = mapped_column(Integer)
organization_id: Mapped[Optional[int]] = mapped_column(Integer)
audit_trail: Mapped[Optional[dict]] = mapped_column(JSONB)
# 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())

View File

@@ -1,4 +1,4 @@
# /app/app/models/reference_data.py
# /opt/docker/dev/service_finder/backend/app/models/reference_data.py
from sqlalchemy import Column, Integer, String, DateTime, func
from sqlalchemy.dialects.postgresql import JSONB
from app.database import Base

View File

@@ -0,0 +1,12 @@
# system package barrel
from .system import SystemParameter, ParameterScope, InternalNotification, SystemServiceStaging
from .audit import SecurityAuditLog, OperationalLog, ProcessLog, FinancialLedger, WalletType, LedgerStatus, LedgerEntryType
from .document import Document
from .translation import Translation
from .legal import LegalDocument, LegalAcceptance
__all__ = [
"SystemParameter", "InternalNotification", "SystemServiceStaging",
"SecurityAuditLog", "ProcessLog", "FinancialLedger", "WalletType", "LedgerStatus", "LedgerEntryType",
"Document", "Translation", "LegalDocument", "LegalAcceptance"
]

View File

@@ -0,0 +1,115 @@
# /opt/docker/dev/service_finder/backend/app/models/system/audit.py
import enum
import uuid
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 sqlalchemy.dialects.postgresql import UUID as PG_UUID, ENUM as PG_ENUM
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"
__table_args__ = {"schema": "audit"}
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"
__table_args__ = {"schema": "audit"}
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"
__table_args__ = {"schema": "audit"}
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 LedgerEntryType(str, enum.Enum):
DEBIT = "DEBIT"
CREDIT = "CREDIT"
class WalletType(str, enum.Enum):
EARNED = "EARNED"
PURCHASED = "PURCHASED"
SERVICE_COINS = "SERVICE_COINS"
VOUCHER = "VOUCHER"
class LedgerStatus(str, enum.Enum):
PENDING = "PENDING"
SUCCESS = "SUCCESS"
FAILED = "FAILED"
REFUNDED = "REFUNDED"
REFUND = "REFUND"
class FinancialLedger(Base):
""" Minden pénz- és kreditmozgás központi naplója. Billing Engine alapja. """
__tablename__ = "financial_ledger"
__table_args__ = {"schema": "audit"}
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())
# Új mezők doubleentry és okos levonáshoz
entry_type: Mapped[LedgerEntryType] = mapped_column(
PG_ENUM(LedgerEntryType, name="ledger_entry_type", schema="audit"),
nullable=False
)
balance_after: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
wallet_type: Mapped[Optional[WalletType]] = mapped_column(
PG_ENUM(WalletType, name="wallet_type", schema="audit")
)
# Economy 1: számlázási mezők
issuer_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("finance.issuers.id"), nullable=True)
invoice_status: Mapped[Optional[str]] = mapped_column(String(50), default="PENDING")
tax_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
gross_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
net_amount: Mapped[Optional[float]] = mapped_column(Numeric(18, 4))
transaction_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), default=uuid.uuid4, nullable=False, index=True
)
status: Mapped[LedgerStatus] = mapped_column(
PG_ENUM(LedgerStatus, name="ledger_status", schema="audit"),
default=LedgerStatus.SUCCESS,
nullable=False
)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/document.py
# /opt/docker/dev/service_finder/backend/app/models/system/document.py
import uuid
from datetime import datetime
from typing import Optional
@@ -6,7 +6,7 @@ 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
from app.database import Base # MB 2.0: Egységesített Base a szinkronitáshoz
class Document(Base):
""" NAS alapú dokumentumtár metaadatai. """
@@ -35,18 +35,6 @@ class Document(Base):
# =========================================================================
# Probléma: Az `ocr_robot.py` (Robot 3) módosítani próbálta a dokumentumok
# állapotát és menteni akarta az AI eredményeket, de a mezők hiányoztak.
#
# Megoldás: Hozzáadtuk a szükséges mezőket a munkafolyamat (Workflow)
# támogatásához.
#
# 1. `status`: A robot a 'pending_ocr' státuszra szűr. Indexeljük,
# mert a WHERE feltételben szerepel, így az adatbázis sokkal gyorsabb lesz.
#
# 2. `ocr_data`: A kinyert adatokat tárolja. Text típust használunk String
# helyett, mert az AI válasza (pl. JSON formátumú adat) hosszú lehet.
#
# 3. `error_log`: Ha az AI hibázik, vagy üres választ ad, itt rögzítjük
# a hiba okát a könnyebb debuggolás érdekében.
# =========================================================================
status: Mapped[str] = mapped_column(String(50), default="uploaded", index=True)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/legal.py
# /opt/docker/dev/service_finder/backend/app/models/system/legal.py
from datetime import datetime
from typing import Optional
from sqlalchemy import Integer, String, Text, DateTime, ForeignKey, Boolean

View File

@@ -1,9 +1,9 @@
# /opt/docker/dev/service_finder/backend/app/models/system.py
# /opt/docker/dev/service_finder/backend/app/models/system/system.py
import uuid
from datetime import datetime
from datetime import datetime, date
from enum import Enum
from typing import Optional
from sqlalchemy import String, Integer, Boolean, DateTime, text, UniqueConstraint, ForeignKey, Text, Enum as SQLEnum
from sqlalchemy import String, Integer, Boolean, DateTime, text, UniqueConstraint, ForeignKey, Text, Enum as SQLEnum, Date
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.sql import func
@@ -28,7 +28,7 @@ class SystemParameter(Base):
category: Mapped[str] = mapped_column(String, server_default="general", index=True)
value: Mapped[dict] = mapped_column(JSONB, nullable=False)
scope_level: Mapped[ParameterScope] = mapped_column(SQLEnum(ParameterScope, name="parameter_scope"), server_default=ParameterScope.GLOBAL.value, index=True)
scope_level: Mapped[ParameterScope] = mapped_column(SQLEnum(ParameterScope, name="parameter_scope", schema="system"), server_default=ParameterScope.GLOBAL.value, index=True)
scope_id: Mapped[Optional[str]] = mapped_column(String(50))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
@@ -49,12 +49,43 @@ class InternalNotification(Base):
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
category: Mapped[str] = mapped_column(String(50), server_default="info")
priority: Mapped[str] = mapped_column(String(20), server_default="medium")
read_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
data: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
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)
class SystemServiceStaging(Base):
""" Robot 1.3 (Scout) által talált nyers szerviz adatok. """
__tablename__ = "service_staging"
__table_args__ = {"schema": "system"}
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)
# Metaadatok a gyors eléréshez (melyik autó, melyik VIN)
data: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
postal_code: Mapped[Optional[str]] = mapped_column(String(20), 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))
contact_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
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())
# JAVÍTÁS: Ezeket az oszlopokat vissza kell tenni, mert az audit szerint
# az adatbázisban léteznek a system.service_staging táblában.
read_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
data: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/translation.py
# /opt/docker/dev/service_finder/backend/app/models/system/translation.py
from sqlalchemy import String, Integer, Text, Boolean, text
from sqlalchemy.orm import Mapped, mapped_column

View File

@@ -0,0 +1,63 @@
# vehicle package exports
from .vehicle_definitions import (
VehicleModelDefinition,
VehicleType,
FeatureDefinition,
ModelFeatureMap,
)
from .vehicle import (
CostCategory,
VehicleCost,
VehicleOdometerState,
VehicleUserRating,
GbCatalogDiscovery,
)
from .external_reference import ExternalReferenceLibrary
from .external_reference_queue import ExternalReferenceQueue
from .asset import (
Asset,
AssetCatalog,
AssetCost,
AssetEvent,
AssetFinancials,
AssetTelemetry,
AssetReview,
ExchangeRate,
CatalogDiscovery,
VehicleOwnership,
)
from .history import AuditLog, LogSeverity
# --- ÚJ MOTOROS SPECIFIKÁCIÓ MODELL BEEMELÉSE ---
from .motorcycle_specs import MotorcycleSpecs
__all__ = [
"VehicleModelDefinition",
"VehicleType",
"FeatureDefinition",
"ModelFeatureMap",
"CostCategory",
"VehicleCost",
"VehicleOdometerState",
"VehicleUserRating",
"GbCatalogDiscovery",
"ExternalReferenceLibrary",
"ExternalReferenceQueue",
"Asset",
"AssetCatalog",
"AssetCost",
"AssetEvent",
"AssetFinancials",
"AssetTelemetry",
"AssetReview",
"ExchangeRate",
"CatalogDiscovery",
"VehicleOwnership",
"AuditLog",
"LogSeverity",
# --- EXPORT LISTA KIEGÉSZÍTÉSE ---
"MotorcycleSpecs",
]

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/asset.py
# /opt/docker/dev/service_finder/backend/app/models/vehicle/asset.py
from __future__ import annotations
import uuid
from datetime import datetime
@@ -80,6 +80,12 @@ class Asset(Base):
assignments: Mapped[List["AssetAssignment"]] = relationship("AssetAssignment", back_populates="asset")
ownership_history: Mapped[List["VehicleOwnership"]] = relationship("VehicleOwnership", back_populates="asset")
# --- COMPUTED PROPERTIES (for Pydantic schema compatibility) ---
@property
def is_verified(self) -> bool:
"""Always False for now, as verification is not yet implemented."""
return False
class AssetFinancials(Base):
""" I. Beszerzés és IV. Értékcsökkenés (Amortizáció). """
__tablename__ = "asset_financials"

View File

@@ -0,0 +1,36 @@
# /opt/docker/dev/service_finder/backend/app/models/vehicle/external_reference.py
from sqlalchemy import Column, Integer, String, JSON, DateTime, UniqueConstraint, ForeignKey
from sqlalchemy.sql import func
from app.database import Base
class ExternalReferenceLibrary(Base):
__tablename__ = "external_reference_library"
__table_args__ = (
UniqueConstraint('source_url', name='_source_url_uc'),
{"schema": "vehicle"}
)
id = Column(Integer, primary_key=True, index=True)
source_name = Column(String(50), default="auto-data.net") # Később jöhet más forrás is (motorokhoz/kamionokhoz)
make = Column(String(100), index=True)
model = Column(String(100), index=True)
generation = Column(String(255))
modification = Column(String(255))
year_from = Column(Integer)
year_to = Column(Integer, nullable=True)
power_kw = Column(Integer, index=True)
engine_cc = Column(Integer, index=True)
category = Column(String(20), default='car', index=True) # ÚJ
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Minden egyéb technikai adat (olaj, gumi, fogyasztás stb.) ide megy
specifications = Column(JSON, default={})
source_url = Column(String(500), unique=True)
last_scraped_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
pipeline_status = Column(String(30), default='pending_enrich', index=True)
matched_vmd_id = Column(Integer, ForeignKey('vehicle.vehicle_model_definitions.id'), nullable=True, index=True)
# Biztosítjuk, hogy ne legyen duplikáció azonos linkről

View File

@@ -0,0 +1,33 @@
# /opt/docker/dev/service_finder/backend/app/models/vehicle/external_reference_queue.py
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, text
from sqlalchemy.sql import func
from app.database import Base
class ExternalReferenceQueue(Base):
__tablename__ = "auto_data_crawler_queue"
__table_args__ = {"schema": "vehicle"}
id = Column(Integer, primary_key=True, index=True)
url = Column(String(500), unique=True, nullable=False)
# Szintek: 'brand', 'model', 'generation', 'engine'
level = Column(String(20), nullable=False, index=True)
# Kategóriák
category = Column(String(20), default='car', index=True)
# Szülő azonosító (pl. a modell tudja, melyik márkához tartozik)
parent_id = Column(Integer, nullable=True)
# Megjelenítési név (pl. "Audi", "A3 Sportback")
name = Column(String(255))
# Állapot: 'pending', 'processing', 'completed', 'error'
status = Column(String(20), default='pending', index=True)
# Hibakezeléshez
error_msg = Column(String(1000), nullable=True)
retry_count = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/history.py
# /opt/docker/dev/service_finder/backend/app/models/vehicle/history.py
import uuid
import enum
from datetime import datetime, date

View File

@@ -0,0 +1,35 @@
from sqlalchemy import Column, Integer, Text, ForeignKey, DateTime
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.sql import func
from app.database import Base
class MotorcycleSpecs(Base):
"""
Gondolatmenet: Ez a modell reprezentálja a motorok végleges technikai adatait.
A JSONB mező lehetővé teszi, hogy az AutoEvolution-ról lekerülő összes változatos
adatot (hengerűrtartalom, nyomaték, hűtés, stb.) sémakötöttség nélkül tároljuk.
"""
__tablename__ = "motorcycle_specs"
__table_args__ = {"schema": "vehicle"}
id = Column(Integer, primary_key=True, index=True)
# Kapcsolat a crawler várólistájával
crawler_id = Column(
Integer,
ForeignKey("vehicle.auto_data_crawler_queue.id", ondelete="CASCADE"),
unique=True,
nullable=False
)
full_name = Column(Text, nullable=False)
url = Column(Text)
# A lényeg: ide kerül minden technikai adat kulcs-érték párban
raw_data = Column(JSONB, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
def __repr__(self):
return f"<MotorcycleSpecs(name='{self.full_name}', crawler_id={self.crawler_id})>"

View File

@@ -1,4 +1,4 @@
# /opt/docker/dev/service_finder/backend/app/models/vehicle.py
# /opt/docker/dev/service_finder/backend/app/models/vehicle/vehicle.py
"""
TCO (Total Cost of Ownership) alapmodelljei a 'vehicle' sémában.
- CostCategory: Standardizált költségkategóriák hierarchiája
@@ -189,4 +189,15 @@ class VehicleUserRating(Base):
def average_score(self) -> float:
"""Számított átlagpontszám a 4 dimenzióból."""
scores = [self.driving_experience, self.reliability, self.comfort, self.consumption_satisfaction]
return sum(scores) / 4.0
return sum(scores) / 4.0
class GbCatalogDiscovery(Base):
__tablename__ = "gb_catalog_discovery"
__table_args__ = {"schema": "vehicle"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
vrm: Mapped[str] = mapped_column(String(20), unique=True)
make: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
model: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
status: Mapped[str] = mapped_column(String(20), default='pending')
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

View File

@@ -1,15 +1,19 @@
# /opt/docker/dev/service_finder/backend/app/models/vehicle_definitions.py
# /opt/docker/dev/service_finder/backend/app/models/vehicle/vehicle_definitions.py
from __future__ import annotations
from datetime import datetime
from typing import Optional, List
from typing import Optional, List, TYPE_CHECKING
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
# Típus ellenőrzés a körkörös importok elkerülésére
if TYPE_CHECKING:
from .asset import AssetCatalog
from .vehicle import VehicleCost, VehicleOdometerState, VehicleUserRating
class VehicleType(Base):
""" Jármű kategóriák (pl. Személyautó, Motorkerékpár, Teherautó, Hajó) """
__tablename__ = "vehicle_types"
@@ -42,109 +46,100 @@ class FeatureDefinition(Base):
class VehicleModelDefinition(Base):
market: Mapped[str] = mapped_column(String(20), server_default=text("'GLOBAL'"), index=True)
"""
Robot v1.1.0 Multi-Tier MDM Master Adattábla.
Az ökoszisztéma technikai igazságforrása.
"""
__tablename__ = "vehicle_model_definitions"
__table_args__ = {"schema": "vehicle"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
market: Mapped[str] = mapped_column(String(20), server_default=text("'EU'"), index=True) # GLOBÁLIS helyett EU az alap
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)
marketing_name: Mapped[str] = mapped_column(String(255), index=True)
official_marketing_name: Mapped[Optional[str]] = mapped_column(String(255))
# --- ROBOT LOGIKAI MEZŐK (JAVÍTVA 2.0 STÍLUSBAN) ---
# --- ROBOT LOGIKAI MEZŐK ---
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
normalized_name: Mapped[str] = mapped_column(String(255), index=True) # EZT KÖTELEZŐVÉ TETTÜK
marketing_name_aliases: Mapped[dict] = mapped_column(JSONB, server_default=text("'[]'::jsonb"))
engine_code: Mapped[Optional[str]] = mapped_column(String(50), index=True)
# --- 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)
technical_code: Mapped[str] = mapped_column(String(100), index=True, server_default=text("'UNKNOWN'"))
variant_code: Mapped[str] = mapped_column(String(100), index=True, server_default=text("'UNKNOWN'"))
version_code: Mapped[str] = mapped_column(String(100), index=True, server_default=text("'UNKNOWN'"))
# --- Ú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)
# --- MŰSZAKI MEZŐK ---
type_approval_number: Mapped[Optional[str]] = mapped_column(String(100), index=True)
seats: Mapped[int] = mapped_column(Integer, server_default=text("0"))
width: Mapped[int] = mapped_column(Integer, server_default=text("0"))
wheelbase: Mapped[int] = mapped_column(Integer, server_default=text("0"))
list_price: Mapped[int] = mapped_column(Integer, server_default=text("0"))
max_speed: Mapped[int] = mapped_column(Integer, server_default=text("0"))
towing_weight_unbraked: Mapped[int] = mapped_column(Integer, server_default=text("0"))
towing_weight_braked: Mapped[int] = mapped_column(Integer, server_default=text("0"))
fuel_consumption_combined: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), server_default=text("0.0"))
co2_emissions_combined: Mapped[int] = mapped_column(Integer, server_default=text("0"))
# --- SPECIFIKÁCIÓK ---
vehicle_type_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("vehicle.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)
fuel_type: Mapped[str] = mapped_column(String(50), index=True, server_default=text("'Unknown'"))
trim_level: Mapped[str] = mapped_column(String(100), server_default=text("''"))
engine_capacity: Mapped[int] = mapped_column(Integer, server_default=text("0"), index=True)
power_kw: Mapped[int] = mapped_column(Integer, server_default=text("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)
curb_weight: Mapped[int] = mapped_column(Integer, server_default=text("0"))
max_weight: Mapped[int] = mapped_column(Integer, server_default=text("0"))
euro_classification: Mapped[Optional[str]] = mapped_column(String(20))
doors: Mapped[Optional[int]] = mapped_column(Integer)
doors: Mapped[int] = mapped_column(Integer, server_default=text("0"))
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)
# --- ÉLETCIKLUS ---
year_from: Mapped[int] = mapped_column(Integer, index=True, server_default=text("0")) # EZT IS BELETETTÜK A KULCSBA
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
production_status: Mapped[Optional[str]] = mapped_column(String(50))
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))
is_manual: Mapped[bool] = mapped_column(Boolean, server_default=text("false"))
source: Mapped[str] = mapped_column(String(100), server_default=text("'ROBOT'"))
# --- ADAT-KONTÉNEREK ---
raw_search_context: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
# --- ADATOK ---
raw_search_context: Mapped[str] = mapped_column(Text, server_default=text("''")) # JSONB helyett TEXT a keresési adatoknak!
raw_api_data: 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
specifications: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
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__ = (
# A LEGONTOSABB SOR: Ez határozza meg, mi számít duplikációnak!
UniqueConstraint('make', 'normalized_name', 'variant_code', 'version_code', 'fuel_type', 'market', 'year_from', name='uix_vmd_precision_v2'),
Index('idx_vmd_lookup_fast', 'make', 'normalized_name'),
Index('idx_vmd_engine_bridge', 'make', 'engine_code'),
{"schema": "vehicle"}
)
# KAPCSOLATOK
# --- KAPCSOLATOK (Relationships) ---
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 neve
variants: Mapped[List["AssetCatalog"]] = relationship("AssetCatalog", back_populates="master_definition")
# TCO költségnapló kapcsolata
costs: Mapped[List["VehicleCost"]] = relationship("VehicleCost", back_populates="vehicle")
# Kilométeróra állapot kapcsolata
odometer_state: Mapped["VehicleOdometerState"] = relationship("VehicleOdometerState", back_populates="vehicle")
# JAVÍTÁS: Ez a sor hiányzott az API indításához!
ratings: Mapped[List["VehicleUserRating"]] = relationship("VehicleUserRating", back_populates="vehicle", cascade="all, delete-orphan")
costs: Mapped[List["VehicleCost"]] = relationship("VehicleCost", back_populates="vehicle", cascade="all, delete-orphan")
odometer_state: Mapped[Optional["VehicleOdometerState"]] = relationship("VehicleOdometerState", back_populates="vehicle", uselist=False, cascade="all, delete-orphan")
class ModelFeatureMap(Base):