szolgáltatók beálltásai, szerkesztése , létrehozása
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/schemas/asset.py
|
||||
from pydantic import BaseModel, ConfigDict, Field, validator, root_validator, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, validator, root_validator, model_validator, field_validator
|
||||
from typing import Optional, Dict, Any, List
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
@@ -82,6 +82,10 @@ class AssetResponse(BaseModel):
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
# === WARRANTY ===
|
||||
is_under_warranty: bool = Field(default=False, description="Érvényes jótállás")
|
||||
warranty_expiry_date: Optional[datetime] = Field(None, description="Jótállás érvényességi dátuma")
|
||||
|
||||
# === SALES MODULE ===
|
||||
is_for_sale: bool = Field(default=False)
|
||||
price: Optional[float] = None
|
||||
@@ -128,6 +132,47 @@ class AssetCreate(BaseModel):
|
||||
return None
|
||||
return v
|
||||
|
||||
# ── Data normalization validators ──
|
||||
@field_validator('brand')
|
||||
@classmethod
|
||||
def normalize_brand(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""Brand: UPPERCASE, strip whitespace."""
|
||||
if v is None or not isinstance(v, str):
|
||||
return v
|
||||
return v.strip().upper()
|
||||
|
||||
@field_validator('model')
|
||||
@classmethod
|
||||
def normalize_model(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""Model: UPPERCASE, remove ALL spaces."""
|
||||
if v is None or not isinstance(v, str):
|
||||
return v
|
||||
return v.upper().replace(' ', '')
|
||||
|
||||
# ── Model validator: normalize type_designation inside individual_equipment ──
|
||||
@model_validator(mode='after')
|
||||
def normalize_individual_equipment(self):
|
||||
"""Normalize type_designation inside individual_equipment.car_specs.
|
||||
|
||||
SAFE None handling: checks every level before calling .upper().
|
||||
- individual_equipment may be None or empty dict
|
||||
- car_specs may be None or missing key
|
||||
- type_designation may be None, empty string, or a valid string
|
||||
"""
|
||||
if not self.individual_equipment:
|
||||
return self
|
||||
if not isinstance(self.individual_equipment, dict):
|
||||
return self
|
||||
|
||||
car_specs = self.individual_equipment.get('car_specs')
|
||||
if not car_specs or not isinstance(car_specs, dict):
|
||||
return self
|
||||
|
||||
td = car_specs.get('type_designation')
|
||||
if td is not None and isinstance(td, str) and td.strip():
|
||||
car_specs['type_designation'] = td.strip().upper().replace(' ', '')
|
||||
return self
|
||||
|
||||
# ── Root validator: ensure at least one of vin or license_plate is provided ──
|
||||
@root_validator(skip_on_failure=True)
|
||||
def validate_vin_or_plate(cls, values):
|
||||
@@ -166,7 +211,7 @@ class AssetCreate(BaseModel):
|
||||
trim_level: Optional[str] = Field(None, max_length=100, description="Felszereltségi szint")
|
||||
roof_type: Optional[str] = Field(None, max_length=50, description="Tető típus")
|
||||
audio_system_type: Optional[str] = Field(None, max_length=100, description="Hangrendszer")
|
||||
individual_equipment: Dict[str, Any] = Field(default_factory=dict, description="Egyedi felszerelések")
|
||||
individual_equipment: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Egyedi felszerelések")
|
||||
|
||||
# === MILEAGE (Optional) ===
|
||||
current_mileage: Optional[int] = Field(None, ge=0, description="Aktuális km óra állás")
|
||||
@@ -179,6 +224,10 @@ class AssetCreate(BaseModel):
|
||||
organization_id: Optional[int] = Field(None, description="Szervezet ID (alapértelmezett a felhasználó szervezete)")
|
||||
branch_id: Optional[UUID] = Field(None, description="Garázs (Branch) ID, ahova a járművet rendeljük. Ha nincs megadva, a szervezet központi garázsába kerül.")
|
||||
|
||||
# === WARRANTY ===
|
||||
is_under_warranty: bool = Field(default=False, description="Érvényes jótállás")
|
||||
warranty_expiry_date: Optional[datetime] = Field(None, description="Jótállás érvényességi dátuma")
|
||||
|
||||
# === PRIMARY VEHICLE FLAG ===
|
||||
is_primary: bool = Field(default=False, description="Elsődleges jármű (tárolva: individual_equipment JSONB)")
|
||||
|
||||
@@ -224,6 +273,47 @@ class AssetUpdate(BaseModel):
|
||||
return None
|
||||
return v
|
||||
|
||||
# ── Data normalization validators ──
|
||||
@field_validator('brand')
|
||||
@classmethod
|
||||
def normalize_brand(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""Brand: UPPERCASE, strip whitespace."""
|
||||
if v is None or not isinstance(v, str):
|
||||
return v
|
||||
return v.strip().upper()
|
||||
|
||||
@field_validator('model')
|
||||
@classmethod
|
||||
def normalize_model(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""Model: UPPERCASE, remove ALL spaces."""
|
||||
if v is None or not isinstance(v, str):
|
||||
return v
|
||||
return v.upper().replace(' ', '')
|
||||
|
||||
# ── Model validator: normalize type_designation inside individual_equipment ──
|
||||
@model_validator(mode='after')
|
||||
def normalize_individual_equipment(self):
|
||||
"""Normalize type_designation inside individual_equipment.car_specs.
|
||||
|
||||
SAFE None handling: checks every level before calling .upper().
|
||||
- individual_equipment may be None or empty dict
|
||||
- car_specs may be None or missing key
|
||||
- type_designation may be None, empty string, or a valid string
|
||||
"""
|
||||
if not self.individual_equipment:
|
||||
return self
|
||||
if not isinstance(self.individual_equipment, dict):
|
||||
return self
|
||||
|
||||
car_specs = self.individual_equipment.get('car_specs')
|
||||
if not car_specs or not isinstance(car_specs, dict):
|
||||
return self
|
||||
|
||||
td = car_specs.get('type_designation')
|
||||
if td is not None and isinstance(td, str) and td.strip():
|
||||
car_specs['type_designation'] = td.strip().upper().replace(' ', '')
|
||||
return self
|
||||
|
||||
# ── Model validator: ensure at least one of vin or license_plate is provided ──
|
||||
@model_validator(mode='after')
|
||||
def validate_vin_or_plate(self):
|
||||
@@ -276,8 +366,63 @@ class AssetUpdate(BaseModel):
|
||||
year_of_manufacture: Optional[int] = Field(None, ge=1900, le=2100, description="Gyártási év")
|
||||
first_registration_date: Optional[datetime] = Field(None, description="Első forgalomba helyezés dátuma")
|
||||
|
||||
# === WARRANTY ===
|
||||
is_under_warranty: Optional[bool] = Field(None, description="Érvényes jótállás")
|
||||
warranty_expiry_date: Optional[datetime] = Field(None, description="Jótállás érvényességi dátuma")
|
||||
|
||||
# === STATUS ===
|
||||
current_mileage: Optional[int] = Field(None, ge=0, description="Aktuális km óra állás")
|
||||
is_primary: Optional[bool] = Field(None, description="Elsődleges jármű")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class AssetEventCreate(BaseModel):
|
||||
"""Schema for creating a new AssetEvent (Service Book entry)."""
|
||||
event_type: str = Field(..., description="Event type: SERVICE, REPAIR, ACCIDENT, INSPECTION, TIRE_CHANGE, MAINTENANCE, UPGRADE, RECALL")
|
||||
event_date: Optional[datetime] = Field(None, description="Event date (default: now)")
|
||||
odometer_reading: Optional[int] = Field(None, ge=0, description="Odometer reading at event time")
|
||||
description: Optional[str] = Field(None, max_length=2000, description="Event description")
|
||||
cost_id: Optional[UUID] = Field(None, description="Associated cost record ID")
|
||||
cost_amount: Optional[float] = Field(None, gt=0, description="Cost amount in the given currency. If > 0, an AssetCost record is auto-created.")
|
||||
currency: Optional[str] = Field(None, min_length=3, max_length=3, description="Currency code (e.g. HUF, EUR). Defaults to HUF on backend.")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class AssetEventResponse(BaseModel):
|
||||
"""Schema for returning an AssetEvent."""
|
||||
id: UUID
|
||||
asset_id: UUID
|
||||
user_id: Optional[int] = None
|
||||
organization_id: Optional[int] = None
|
||||
event_type: str
|
||||
odometer_reading: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
cost_id: Optional[UUID] = None
|
||||
cost_amount: Optional[float] = Field(None, description="Auto-created cost amount")
|
||||
currency: Optional[str] = Field(None, description="Currency code (e.g. HUF)")
|
||||
event_date: datetime
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def model_validate(cls, obj, **kwargs):
|
||||
"""Override to resolve cost_amount and currency from the related AssetCost."""
|
||||
data = {}
|
||||
if hasattr(obj, '__dict__'):
|
||||
# Copy ORM attributes
|
||||
for field in cls.model_fields:
|
||||
if hasattr(obj, field):
|
||||
data[field] = getattr(obj, field)
|
||||
|
||||
# Resolve cost_amount and currency from the cost relationship
|
||||
cost = getattr(obj, 'cost', None)
|
||||
if cost is not None:
|
||||
if data.get('cost_amount') is None:
|
||||
data['cost_amount'] = float(cost.amount_net) if cost.amount_net is not None else None
|
||||
if data.get('currency') is None:
|
||||
data['currency'] = cost.currency
|
||||
return cls(**data)
|
||||
@@ -60,6 +60,9 @@ class OrganizationUpdate(BaseModel):
|
||||
address_floor: Optional[str] = None
|
||||
address_door: Optional[str] = None
|
||||
address_hrsz: Optional[str] = None
|
||||
# ── Crowdsourced search fields ──
|
||||
aliases: Optional[List[str]] = None
|
||||
tags: Optional[List[str]] = None
|
||||
|
||||
|
||||
class OrganizationResponse(BaseModel):
|
||||
@@ -79,5 +82,7 @@ class OrganizationResponse(BaseModel):
|
||||
visual_settings: Optional[dict] = None
|
||||
notification_settings: Optional[Any] = None
|
||||
external_integration_config: Optional[Any] = None
|
||||
aliases: List[str] = Field(default_factory=list)
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
112
backend/app/schemas/provider.py
Normal file
112
backend/app/schemas/provider.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class ExpertiseCategoryOut(BaseModel):
|
||||
"""Expertise kategória kimeneti sémája a frontend dropdown számára."""
|
||||
id: int
|
||||
key: str
|
||||
name_hu: Optional[str] = None
|
||||
name_en: Optional[str] = None
|
||||
category: str
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
id: int
|
||||
name: str
|
||||
category: Optional[str] = None
|
||||
specialization: List[str] = Field(default_factory=list)
|
||||
city: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
address_zip: Optional[str] = None
|
||||
address_street_name: Optional[str] = None
|
||||
address_street_type: Optional[str] = None
|
||||
address_house_number: 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
|
||||
|
||||
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
|
||||
"""
|
||||
name: str = Field(..., min_length=2, max_length=200, description="Szolgáltató neve")
|
||||
category_id: int = Field(..., ge=1, description="Kategória ID (marketplace.expertise_tags.id)")
|
||||
city: Optional[str] = Field(None, max_length=100, description="Város")
|
||||
address_zip: Optional[str] = Field(None, max_length=20, description="Irányítószám")
|
||||
address_street_name: Optional[str] = Field(None, max_length=150, description="Utca neve (pl. Egressy)")
|
||||
address_street_type: Optional[str] = Field(None, max_length=50, description="Közterület jellege (pl. utca, út, tér)")
|
||||
address_house_number: Optional[str] = Field(None, max_length=20, description="Házszám (pl. 4/b)")
|
||||
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)")
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
name: str = Field(..., min_length=2, max_length=200, description="Szolgáltató neve")
|
||||
city: Optional[str] = Field(None, max_length=100, description="Város")
|
||||
address_zip: Optional[str] = Field(None, max_length=20, description="Irányítószám")
|
||||
address_street_name: Optional[str] = Field(None, max_length=150, description="Utca neve (pl. Egressy)")
|
||||
address_street_type: Optional[str] = Field(None, max_length=50, description="Közterület jellege (pl. utca, út, tér)")
|
||||
address_house_number: Optional[str] = Field(None, max_length=20, description="Házszám (pl. 4/b)")
|
||||
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)")
|
||||
|
||||
|
||||
class ProviderUpdateResponse(BaseModel):
|
||||
"""Szolgáltató szerkesztés válasza."""
|
||||
id: int
|
||||
name: str
|
||||
message: str = "Szolgáltató adatai sikeresen frissítve."
|
||||
Reference in New Issue
Block a user