Files
service-finder/backend/app/models/marketplace/service.py

215 lines
11 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
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)
# 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)
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"))
# 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())
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())