Files
service-finder/backend/app/models/marketplace/service.py
2026-07-08 08:03:57 +00:00

228 lines
12 KiB
Python

# /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[Optional[Any]] = mapped_column(Geometry(geometry_type='POINT', srid=4326, spatial_index=False), nullable=True, 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'),
Index('idx_expertise_tags_name_i18n', 'name_i18n', postgresql_using='gin'),
{"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))
# --- 🏗️ i18n JSONB Migration (Phase 1) ---
name_i18n: Mapped[dict] = mapped_column(JSONB, nullable=False, server_default=text("'{\"hu\": \"\"}'::jsonb"))
description_i18n: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
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())