# /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, ENUM as SQLEnum, ARRAY 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" __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) # P0 HYBRID VENDOR REFACTOR: Új elsődleges hivatkozás a marketplace.service_providers táblára service_provider_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("marketplace.service_providers.id"), nullable=True, index=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[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) 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) # === P0 3D FILTERING MATRIX: Járműosztályok és specializációk === supported_vehicle_classes: Mapped[Optional[list]] = mapped_column( ARRAY(String), server_default=text("'{}'"), nullable=True ) specializations: Mapped[Optional[dict]] = mapped_column( JSONB, server_default=text("'{}'::jsonb"), nullable=True ) # 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. 4-Level Category Hierarchy (2026-06-17): ========================================= Level 0: Járműtípus (Vehicle Type) — e.g. "Személyautó", "Motorkerékpár" Level 1: Iparág/Főcsoport (Industry) — e.g. "Karbantartás és javítás", "Üzemanyagtöltő" Level 2: Szakma (Profession) — e.g. "Autószerelő", "Gumiszerviz" Level 3: Specifikus feladat/címke (Specific Tag) — e.g. "Vezérműszíj csere", "Klíma töltés" Adjacency List (parent_id) + Materialized Path (path) for efficient tree queries. """ __tablename__ = "expertise_tags" __table_args__ = ( Index('idx_expertise_path', 'path'), {"schema": "marketplace"} ) id: Mapped[int] = mapped_column(Integer, primary_key=True) key: Mapped[str] = mapped_column(String(50), unique=True, index=True) name_hu: Mapped[Optional[str]] = mapped_column(String(100)) name_en: Mapped[Optional[str]] = mapped_column(String(100)) name_translations: Mapped[Any] = mapped_column(JSONB, server_default=text("'{}'::jsonb")) category: Mapped[Optional[str]] = mapped_column(String(30), index=True) # --- 🏗️ 4-LEVEL HIERARCHY (2026-06-17) --- parent_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("marketplace.expertise_tags.id"), nullable=True, index=True ) level: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0"), nullable=False) path: Mapped[Optional[str]] = mapped_column(String(255), nullable=True, index=True) # --- 🎮 GAMIFICATION ÉS DISCOVERY --- is_official: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true")) suggested_by_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id")) discovery_points: Mapped[int] = mapped_column(Integer, default=10, server_default=text("10")) search_keywords: Mapped[Any] = mapped_column(JSONB, server_default=text("'[]'::jsonb")) usage_count: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0")) icon: Mapped[Optional[str]] = mapped_column(String(50)) description: Mapped[Optional[str]] = mapped_column(Text) 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()) # Self-referential relationship for tree traversal # NOTE: cascade="all, delete-orphan" is intentionally NOT set here because # this is a self-referential tree. Deleting a parent should NOT cascade-delete # children (they may be re-parented). The "many" side of a self-referential # relationship cannot have delete-orphan without single_parent=True. children: Mapped[List["ExpertiseTag"]] = relationship( "ExpertiseTag", back_populates="parent", remote_side=[id], ) parent: Mapped[Optional["ExpertiseTag"]] = relationship( "ExpertiseTag", back_populates="children", remote_side=[parent_id], ) services: Mapped[List["ServiceExpertise"]] = relationship("ServiceExpertise", back_populates="tag") suggested_by: Mapped[Optional["Person"]] = relationship("Person") class ServiceExpertise(Base): """ KAPCSOLÓTÁBLA: Ez köti össze a szervizt a szakmáival. """ __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")) 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()")) 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) address_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("system.addresses.id")) 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()) # Kapcsolat a címhez address: Mapped[Optional["Address"]] = relationship("Address", lazy="selectin") 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)) class Cost(Base): """ Költségnapló a trust engine számára. """ __tablename__ = "costs" __table_args__ = {"schema": "marketplace"} id: Mapped[int] = mapped_column(Integer, primary_key=True) vehicle_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) category: Mapped[str] = mapped_column(String(50), nullable=False, index=True) amount: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) currency: Mapped[str] = mapped_column(String(3), default="HUF") odometer_km: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) occurrence_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) is_deleted: Mapped[bool] = mapped_column(Boolean, default=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())