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

276 lines
14 KiB
Python

"""
Provider (szolgáltató) Pydantic sémák a crowdsourced partnerkeresőhöz.
Tartalmazza a keresési, gyors felvételi és szerkesztési sémákat.
P1 CRITICAL ALIGN (2026-06-17): Atomizált címmezők bevezetése.
- street -> address_street_name, address_street_type, address_house_number
- A ProviderSearchResult is tartalmazza az atomizált címmezőket.
P3 UNIFIED ADDRESS REFACTOR (2026-07-01): Egységes AddressIn/AddressOut.
- ProviderSearchResult: address_detail: Optional[AddressOut] a meglévő flat mezők mellett
- ProviderQuickAddIn: address_detail: Optional[AddressIn] a meglévő flat mezők mellett
- ProviderUpdateIn: address_detail: Optional[AddressIn] a meglévő flat mezők mellett
- A flat mezők (address_zip, address_street_name, stb.) MEGMARADNAK a backward compatibility miatt
4-Level Category Architecture (2026-06-17):
===========================================
- CategoryTreeNode: Hierarchikus fa struktúra a frontend checkboxaihoz
- CategoryAutocompleteItem: Szöveges kereső találatok Szint 2 és Szint 3 címkékhez
- ProviderUpdateIn: kibővítve category_ids listával a hibrid mentéshez
"""
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, List
from app.schemas.address import AddressIn, AddressOut
# ──────────────────────────────────────────────
# 4-LEVEL CATEGORY SCHEMAS (2026-06-17)
# ──────────────────────────────────────────────
class CategoryTreeNode(BaseModel):
"""Egy csomópont a kategória hierarchikus fában.
Tartalmazza a gyermek csomópontokat rekurzívan.
"""
id: int
key: str
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
name_i18n: dict = Field(default_factory=dict, description="Lokalizált név (JSONB: {\"hu\": \"...\", \"en\": \"...\"})")
description_i18n: Optional[dict] = Field(None, description="Lokalizált leírás (JSONB)")
# --- [DEPRECATED] Old columns (Phase 3) ---
name_hu: Optional[str] = Field(None, description="[DEPRECATED] Use name_i18n instead")
name_en: Optional[str] = Field(None, description="[DEPRECATED] Use name_i18n instead")
level: int = 0
path: Optional[str] = None
is_official: bool = True
children: List["CategoryTreeNode"] = Field(default_factory=list)
model_config = ConfigDict(from_attributes=True)
class CategoryAutocompleteItem(BaseModel):
"""Egy találat a kategória autocomplete keresőben.
Csak Szint 2 (Szakma) és Szint 3 (Specifikus feladat) címkéket
listáz, ahol is_official=True.
"""
id: int
key: str
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
name_i18n: dict = Field(default_factory=dict, description="Lokalizált név (JSONB: {\"hu\": \"...\", \"en\": \"...\"})")
# --- [DEPRECATED] Old columns (Phase 3) ---
name_hu: Optional[str] = Field(None, description="[DEPRECATED] Use name_i18n instead")
name_en: Optional[str] = Field(None, description="[DEPRECATED] Use name_i18n instead")
level: int = 0
path: Optional[str] = None
parent_id: Optional[int] = None
model_config = ConfigDict(from_attributes=True)
# ──────────────────────────────────────────────
# EXISTING SCHEMAS (extended)
# ──────────────────────────────────────────────
class ExpertiseCategoryOut(BaseModel):
"""Expertise kategória kimeneti sémája a frontend dropdown számára.
Kibővítve a 4-level hierarchy mezőkkel.
"""
id: int
key: str
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
name_i18n: dict = Field(default_factory=dict, description="Lokalizált név (JSONB: {\"hu\": \"...\", \"en\": \"...\"})")
# --- [DEPRECATED] Old columns (Phase 3) ---
name_hu: Optional[str] = Field(None, description="[DEPRECATED] Use name_i18n instead")
name_en: Optional[str] = Field(None, description="[DEPRECATED] Use name_i18n instead")
category: Optional[str] = None
level: int = 0
parent_id: Optional[int] = None
path: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class CategoryInfo(BaseModel):
"""Egy kategória rövid adatai a keresési találatokhoz.
Tartalmazza a kategória ID-ját, nevét és szintjét,
hogy a frontend meg tudja jeleníteni a kapcsolódó
szolgáltatásokat a kártyákon.
"""
id: int
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
name_i18n: dict = Field(default_factory=dict, description="Lokalizált név (JSONB: {\"hu\": \"...\", \"en\": \"...\"})")
# --- [DEPRECATED] Old columns (Phase 3) ---
name_hu: Optional[str] = Field(None, description="[DEPRECATED] Use name_i18n instead")
name_en: Optional[str] = Field(None, description="[DEPRECATED] Use name_i18n instead")
level: int = 0
key: str
class ProviderSearchResult(BaseModel):
"""Egy szolgáltató keresési találatának adatai.
P1 CRITICAL ALIGN (2026-06-17): Atomizált címmezők.
- address_street_name, address_street_type, address_house_number
- A frontend ezeket intelligensen fűzi össze megjelenítéskor.
P3 UNIFIED ADDRESS REFACTOR (2026-07-01): address_detail mező.
- address_detail: Optional[AddressOut] — egységes cím objektum.
- A flat mezők (address_zip, address_street_name, stb.) MEGMARADNAK
a backward compatibility miatt.
P0 PHASE 2 ADDRESS UNIFICATION (2026-07-07):
- address_detail: Optional[AddressOut] — first-class citizen.
- A flat mezők [DEPRECATED] — Phase 3-ban törlendő.
FEATURE (2026-06-17): categories mező.
- A szolgáltatóhoz tartozó kategóriák (ExpertiseTag) listája.
- Tartalmazza az ID-t, nevet és szintet a frontend chip megjelenítéshez.
"""
id: int
name: str
category: Optional[str] = None
specialization: List[str] = Field(default_factory=list)
categories: List[CategoryInfo] = Field(default_factory=list)
city: Optional[str] = Field(None, description="[DEPRECATED] Use address_detail.city instead")
address: Optional[str] = Field(None, description="[DEPRECATED] Use address_detail.full_address_text instead")
address_zip: Optional[str] = Field(None, description="[DEPRECATED] Use address_detail.zip instead")
address_street_name: Optional[str] = Field(None, description="[DEPRECATED] Use address_detail.street_name instead")
address_street_type: Optional[str] = Field(None, description="[DEPRECATED] Use address_detail.street_type instead")
address_house_number: Optional[str] = Field(None, description="[DEPRECATED] Use address_detail.house_number instead")
plus_code: Optional[str] = None
contact_phone: Optional[str] = None
contact_email: Optional[str] = None
website: Optional[str] = None
tags: List[str] = Field(default_factory=list)
source: str = Field(..., description="Forrás: verified_org | staged_data | crowd_added")
is_verified: bool = False
rating: Optional[float] = None
supported_vehicle_classes: Optional[List[str]] = None
specializations: Optional[dict] = None
# P0 PHASE 2: Egységes cím objektum (first-class citizen)
address_detail: Optional[AddressOut] = None
model_config = ConfigDict(from_attributes=True)
class ProviderSearchResponse(BaseModel):
"""Keresési eredmények lapozással."""
results: List[ProviderSearchResult] = Field(default_factory=list)
total: int = 0
page: int = 1
per_page: int = 20
class ProviderQuickAddIn(BaseModel):
"""Gyors szolgáltató felvétel bemeneti adatai.
P1 CRITICAL ALIGN (2026-06-17): Atomizált címmezők.
- street ELTÁVOLÍTVA, helyette address_street_name, address_street_type, address_house_number
P3 UNIFIED ADDRESS REFACTOR (2026-07-01): address_detail mező.
- address_detail: Optional[AddressIn] — egységes cím objektum.
- A flat mezők (address_zip, address_street_name, stb.) MEGMARADNAK
a backward compatibility miatt.
P0 PHASE 2 ADDRESS UNIFICATION (2026-07-07):
- address_detail: Optional[AddressIn] — first-class citizen.
- A flat mezők [DEPRECATED] — Phase 3-ban törlendő.
P0 CRITICAL BUGFIX (2026-06-17): category_id most már opcionális.
- A 4-level kategória rendszer bevezetésével a frontend a category_ids
tömböt használja a kategóriák kiválasztására, nem a régi category_id-t.
- category_id megtartva opcionálisként a backward compatibility miatt.
- category_ids: A kiválasztott kategória ID-k listája (Szint 0-3 checkboxokból)
- new_tags: A user által gépelt új (még nem létező) címkék listája
"""
name: str = Field(..., min_length=2, max_length=200, description="Szolgáltató neve")
category_id: Optional[int] = Field(None, ge=1, description="Elsődleges kategória ID (opcionális, backward compatibility)")
category_ids: Optional[List[int]] = Field(None, description="Kiválasztott kategória ID-k (Szint 0-3 checkboxokból)")
new_tags: Optional[List[str]] = Field(None, description="User által gépelt új címkenevek (is_official=False, level=3)")
city: Optional[str] = Field(None, max_length=100, description="[DEPRECATED] Use address_detail.city instead")
address_zip: Optional[str] = Field(None, max_length=20, description="[DEPRECATED] Use address_detail.zip instead")
address_street_name: Optional[str] = Field(None, max_length=150, description="[DEPRECATED] Use address_detail.street_name instead")
address_street_type: Optional[str] = Field(None, max_length=50, description="[DEPRECATED] Use address_detail.street_type instead")
address_house_number: Optional[str] = Field(None, max_length=20, description="[DEPRECATED] Use address_detail.house_number instead")
plus_code: Optional[str] = Field(None, max_length=20, description="Google Plus Code (pl. 8FVC9X8V+XV)")
contact_phone: Optional[str] = Field(None, max_length=50, description="Telefonszám")
contact_email: Optional[str] = Field(None, max_length=255, description="Email cím")
website: Optional[str] = Field(None, max_length=255, description="Weboldal URL")
tags: Optional[List[str]] = Field(None, description="Szolgáltatás címkék (specialization_tags)")
supported_vehicle_classes: Optional[List[str]] = Field(
None, description="Támogatott járműosztályok (pl. ['car', 'hgv', 'van'])"
)
specializations: Optional[dict] = Field(
None, description="Specializációk (pl. {'brands': ['Volvo'], 'propulsion': ['EV']})"
)
# P0 PHASE 2: Egységes cím objektum (first-class citizen)
address_detail: Optional[AddressIn] = None
class ProviderQuickAddResponse(BaseModel):
"""Gyors szolgáltató felvétel válasza."""
id: int
name: str
status: str = "pending_verification"
message: str = "Szolgáltató sikeresen rögzítve. Ellenőrzés alatt."
earned_points: int = 0
class ProviderUpdateIn(BaseModel):
"""Szolgáltató adatainak szerkesztése (PUT /providers/{id}).
P1 CRITICAL ALIGN (2026-06-17): Atomizált címmezők.
- street ELTÁVOLÍTVA, helyette address_street_name, address_street_type, address_house_number
- Tartalmazza a contact_phone, contact_email, website és tags mezőket is.
P3 UNIFIED ADDRESS REFACTOR (2026-07-01): address_detail mező.
- address_detail: Optional[AddressIn] — egységes cím objektum.
- A flat mezők (address_zip, address_street_name, stb.) MEGMARADNAK
a backward compatibility miatt.
P0 PHASE 2 ADDRESS UNIFICATION (2026-07-07):
- address_detail: Optional[AddressIn] — first-class citizen.
- A flat mezők [DEPRECATED] — Phase 3-ban törlendő.
4-Level Category (2026-06-17):
- category_ids: A kiválasztott kategória ID-k listája (Szint 0-3)
- new_tags: A user által gépelt, új (még nem létező) címkék listája
"""
name: str = Field(..., min_length=2, max_length=200, description="Szolgáltató neve")
city: Optional[str] = Field(None, max_length=100, description="[DEPRECATED] Use address_detail.city instead")
address_zip: Optional[str] = Field(None, max_length=20, description="[DEPRECATED] Use address_detail.zip instead")
address_street_name: Optional[str] = Field(None, max_length=150, description="[DEPRECATED] Use address_detail.street_name instead")
address_street_type: Optional[str] = Field(None, max_length=50, description="[DEPRECATED] Use address_detail.street_type instead")
address_house_number: Optional[str] = Field(None, max_length=20, description="[DEPRECATED] Use address_detail.house_number instead")
plus_code: Optional[str] = Field(None, max_length=20, description="Google Plus Code (pl. 8FVC9X8V+XV)")
contact_phone: Optional[str] = Field(None, max_length=50, description="Telefonszám")
contact_email: Optional[str] = Field(None, max_length=255, description="Email cím")
website: Optional[str] = Field(None, max_length=255, description="Weboldal URL")
tags: Optional[List[str]] = Field(None, description="Szolgáltatás címkék (specialization_tags)")
category_ids: Optional[List[int]] = Field(
None, description="Kiválasztott kategória ID-k (Szint 0-3 checkboxokból)"
)
new_tags: Optional[List[str]] = Field(
None, description="User által gépelt új címkenevek (is_official=False, level=3)"
)
supported_vehicle_classes: Optional[List[str]] = Field(
None, description="Támogatott járműosztályok (pl. ['car', 'hgv', 'van'])"
)
specializations: Optional[dict] = Field(
None, description="Specializációk (pl. {'brands': ['Volvo'], 'propulsion': ['EV']})"
)
# P0 PHASE 2: Egységes cím objektum (first-class citizen)
address_detail: Optional[AddressIn] = None
class ProviderUpdateResponse(BaseModel):
"""Szolgáltató szerkesztés válasza."""
id: int
name: str
message: str = "Szolgáltató adatai sikeresen frissítve."
earned_points: int = 0