admin frontend elkezdése, járművek tisztázása, frontend fejleszts
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
|
||||
from pydantic import BaseModel, ConfigDict, Field, validator, root_validator, model_validator
|
||||
from typing import Optional, Dict, Any, List
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
@@ -113,8 +113,30 @@ class AssetResponse(BaseModel):
|
||||
class AssetCreate(BaseModel):
|
||||
""" Jármű létrehozásához szükséges adatok - Thick Digital Twin támogatással. """
|
||||
# === CORE IDENTIFICATION (Required for status determination) ===
|
||||
license_plate: str = Field(..., min_length=2, max_length=20, description="Rendszám")
|
||||
license_plate: Optional[str] = Field(None, min_length=2, max_length=20, description="Rendszám")
|
||||
vin: Optional[str] = Field(None, min_length=1, max_length=50, description="VIN szám (opcionális)")
|
||||
|
||||
# ── Pre-validator: convert empty strings to None for optional string fields ──
|
||||
@validator('license_plate', 'vin', 'trim_level',
|
||||
'cylinder_layout', 'transmission_type', 'drive_type',
|
||||
'euro_classification', 'roof_type', 'audio_system_type',
|
||||
'data_status',
|
||||
pre=True, always=True)
|
||||
def empty_str_to_none(cls, v):
|
||||
"""Convert empty strings to None so Pydantic doesn't reject them with min_length."""
|
||||
if v is not None and isinstance(v, str) and v.strip() == '':
|
||||
return None
|
||||
return v
|
||||
|
||||
# ── 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):
|
||||
"""Legalább egy azonosítót meg kell adni: rendszám VAGY VIN szám."""
|
||||
license_plate = values.get('license_plate')
|
||||
vin = values.get('vin')
|
||||
if not license_plate and not vin:
|
||||
raise ValueError('Legalább egy azonosítót meg kell adni: rendszám VAGY VIN szám.')
|
||||
return values
|
||||
|
||||
# === CLASSIFICATION (Optional, but affects status) ===
|
||||
brand: Optional[str] = Field(None, max_length=100, description="Márka (ha nincs catalog_id)")
|
||||
@@ -190,6 +212,35 @@ class AssetUpdate(BaseModel):
|
||||
# === CORE IDENTIFICATION ===
|
||||
license_plate: Optional[str] = Field(None, min_length=2, max_length=20, description="Rendszám")
|
||||
vin: Optional[str] = Field(None, min_length=1, max_length=50, description="VIN szám")
|
||||
|
||||
# ── Pre-validator: convert empty strings to None for optional string fields ──
|
||||
@validator('license_plate', 'vin', 'trim_level',
|
||||
'cylinder_layout', 'transmission_type', 'drive_type',
|
||||
'euro_classification', 'roof_type', 'audio_system_type',
|
||||
pre=True, always=True)
|
||||
def empty_str_to_none(cls, v):
|
||||
"""Convert empty strings to None so Pydantic doesn't reject them with min_length."""
|
||||
if v is not None and isinstance(v, str) and v.strip() == '':
|
||||
return None
|
||||
return v
|
||||
|
||||
# ── Model validator: ensure at least one of vin or license_plate is provided ──
|
||||
@model_validator(mode='after')
|
||||
def validate_vin_or_plate(self):
|
||||
"""Update esetén: ha mindkét mezőt None-ra állítják explicit, az nem engedélyezett.
|
||||
|
||||
Működés:
|
||||
- Ha egyik mező sincs a payload-ban (üres update) → engedélyezve (nincs változás)
|
||||
- Ha mindkettő None explicit → hiba (nem lehet mindkettőt elvenni)
|
||||
- Ha legalább az egyik meg van adva → OK
|
||||
"""
|
||||
# model_fields_set tartalmazza az explicit megadott mezőket
|
||||
if 'license_plate' not in self.model_fields_set and 'vin' not in self.model_fields_set:
|
||||
return self
|
||||
|
||||
if not self.license_plate and not self.vin:
|
||||
raise ValueError('Legalább egy azonosítót meg kell adni: rendszám VAGY VIN szám.')
|
||||
return self
|
||||
|
||||
# === CLASSIFICATION ===
|
||||
brand: Optional[str] = Field(None, max_length=100, description="Márka")
|
||||
|
||||
@@ -6,31 +6,30 @@ from decimal import Decimal
|
||||
from uuid import UUID
|
||||
|
||||
class AssetCostBase(BaseModel):
|
||||
cost_type: str # fuel, service, tax, insurance
|
||||
amount_local: Decimal
|
||||
currency_local: str = "HUF"
|
||||
net_amount_local: Optional[Decimal] = None
|
||||
vat_rate: Optional[Decimal] = Field(default=27.0)
|
||||
|
||||
"""Base schema — matches the AssetCost SQLAlchemy model fields."""
|
||||
amount_net: Decimal
|
||||
currency: str = "HUF"
|
||||
date: datetime = Field(default_factory=datetime.now)
|
||||
mileage_at_cost: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
data: Dict[str, Any] = Field(default_factory=dict) # nyugta adatai, GPS koordináták
|
||||
invoice_number: Optional[str] = None
|
||||
data: Dict[str, Any] = Field(default_factory=dict) # nyugta adatai, GPS koordináták
|
||||
category_id: int # FK a CostCategory táblához
|
||||
mileage_at_cost: Optional[int] = None # Stored inside data JSONB
|
||||
description: Optional[str] = None # Stored inside data JSONB
|
||||
|
||||
class AssetCostCreate(AssetCostBase):
|
||||
asset_id: UUID
|
||||
organization_id: Optional[int] = None # Auto-resolved from asset if not provided
|
||||
category_id: int # FK a CostCategory táblához
|
||||
|
||||
class AssetCostResponse(AssetCostBase):
|
||||
"""Response schema — matches DB columns from vehicle.asset_costs."""
|
||||
id: UUID
|
||||
asset_id: UUID
|
||||
organization_id: int
|
||||
driver_id: Optional[int] = None
|
||||
|
||||
# Pénzügyi dúsítás (Backend számolja)
|
||||
amount_eur: Optional[Decimal] = None
|
||||
exchange_rate_used: Optional[Decimal] = None
|
||||
created_at: datetime
|
||||
# Derived / enriched fields (not in DB model directly)
|
||||
category_name: Optional[str] = None # Resolved from CostCategory relationship
|
||||
category_code: Optional[str] = None # Resolved from CostCategory relationship
|
||||
description: Optional[str] = None # Extracted from data['description']
|
||||
mileage_at_cost: Optional[int] = None # Extracted from data['mileage_at_cost']
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -40,11 +40,26 @@ class CorpOnboardResponse(BaseModel):
|
||||
|
||||
class OrganizationUpdate(BaseModel):
|
||||
"""PATCH séma a szervezet adatainak részleges frissítéséhez.
|
||||
Támogatja a visual_settings JSONB mező biztonságos részleges frissítését."""
|
||||
Támogatja a visual_settings JSONB mező biztonságos részleges frissítését,
|
||||
valamint a cégadatok (név, adószám, cím) szerkesztését."""
|
||||
visual_settings: Optional[dict] = None
|
||||
display_name: Optional[str] = None
|
||||
default_currency: Optional[str] = None
|
||||
language: Optional[str] = None
|
||||
# ── Cégadatok (Company details) ──
|
||||
name: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
tax_number: Optional[str] = None
|
||||
# ── Cím adatok (Address fields) ──
|
||||
address_zip: Optional[str] = None
|
||||
address_city: Optional[str] = None
|
||||
address_street_name: Optional[str] = None
|
||||
address_street_type: Optional[str] = None
|
||||
address_house_number: Optional[str] = None
|
||||
address_stairwell: Optional[str] = None
|
||||
address_floor: Optional[str] = None
|
||||
address_door: Optional[str] = None
|
||||
address_hrsz: Optional[str] = None
|
||||
|
||||
|
||||
class OrganizationResponse(BaseModel):
|
||||
|
||||
33
backend/app/schemas/service_catalog.py
Normal file
33
backend/app/schemas/service_catalog.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/schemas/service_catalog.py
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ServiceCatalogCreate(BaseModel):
|
||||
"""Schema új szolgáltatás katalógus bejegyzés létrehozásához."""
|
||||
service_code: str = Field(..., min_length=3, max_length=50, description="Egyedi szolgáltatás kód (pl. 'SRV_DATA_EXPORT')")
|
||||
name: str = Field(..., min_length=2, max_length=200, description="Szolgáltatás megjelenített neve")
|
||||
description: Optional[str] = Field(None, max_length=1000, description="Részletes leírás")
|
||||
credit_cost: int = Field(0, ge=0, description="Alapértelmezett kredit költség")
|
||||
is_active: bool = Field(True, description="Aktív-e a szolgáltatás")
|
||||
|
||||
|
||||
class ServiceCatalogUpdate(BaseModel):
|
||||
"""Schema meglévő szolgáltatás katalógus bejegyzés módosításához."""
|
||||
name: Optional[str] = Field(None, min_length=2, max_length=200)
|
||||
description: Optional[str] = Field(None, max_length=1000)
|
||||
credit_cost: Optional[int] = Field(None, ge=0)
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class ServiceCatalogResponse(BaseModel):
|
||||
"""Schema szolgáltatás katalógus bejegyzés visszaadásához."""
|
||||
id: int
|
||||
service_code: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
credit_cost: int
|
||||
is_active: bool
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
143
backend/app/schemas/subscription.py
Normal file
143
backend/app/schemas/subscription.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
📦 Subscription Tier Pydantic Schemas
|
||||
|
||||
A subscription_tiers JSONB rules mezőjének erősen tipizált Pydantic modelljei.
|
||||
A rules struktúra a seed_packages.py-ban definiált formátumot követi:
|
||||
|
||||
{
|
||||
"type": "private" | "corporate",
|
||||
"pricing": {"monthly_price": float, "yearly_price": float, "currency": "EUR"},
|
||||
"allowances": {"max_vehicles": int, "max_garages": int, "monthly_free_credits": int},
|
||||
"entitlements": ["SRV_..."],
|
||||
"affiliate": {"commission_rate_percent": int, "referral_bonus_credits": int}
|
||||
}
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
# ── Belső (embedded) modellek a rules JSONB struktúrához ──────────────────────
|
||||
|
||||
|
||||
class PricingModel(BaseModel):
|
||||
"""Árazási információk egy előfizetési csomaghoz."""
|
||||
monthly_price: float = Field(..., ge=0, description="Havi előfizetési díj (EUR)")
|
||||
yearly_price: float = Field(..., ge=0, description="Éves előfizetési díj (EUR)")
|
||||
currency: str = Field(default="EUR", min_length=3, max_length=3, description="Pénznem ISO 4217 kódja")
|
||||
credit_price: Optional[int] = Field(default=None, ge=0, description="Kreditár (havi kredit költség a csomaghoz)")
|
||||
|
||||
|
||||
class AllowancesModel(BaseModel):
|
||||
"""Korlátok és keretek a csomagban."""
|
||||
max_vehicles: int = Field(..., ge=0, description="Maximális járműszám")
|
||||
max_garages: int = Field(..., ge=0, description="Maximális garázsok száma")
|
||||
monthly_free_credits: int = Field(..., ge=0, description="Havi ingyenes kreditek száma")
|
||||
|
||||
|
||||
class LifecycleModel(BaseModel):
|
||||
"""Életciklus állapotok egy előfizetési csomaghoz."""
|
||||
is_public: bool = Field(default=True, description="Publikus-e a csomag (látszik-e a felhasználóknak)")
|
||||
available_until: Optional[datetime] = Field(default=None, description="Meddig érhető el a csomag (None = nincs lejárat)")
|
||||
|
||||
|
||||
class AffiliateModel(BaseModel):
|
||||
"""Partnerprogram (affiliate) beállítások."""
|
||||
commission_rate_percent: int = Field(..., ge=0, le=100, description="Jutalék mértéke százalékban")
|
||||
referral_bonus_credits: int = Field(..., ge=0, description="Ajánlási bónusz kreditek száma")
|
||||
|
||||
|
||||
class SubscriptionRulesModel(BaseModel):
|
||||
"""
|
||||
A subscription_tiers.rules JSONB oszlopának erősen tipizált modellje.
|
||||
|
||||
Fields:
|
||||
type: "private" (magánszemély) vagy "corporate" (vállalati)
|
||||
display_name: Emberi olvasásra szánt megjelenítési név (pl. "Privát Ingyenes")
|
||||
pricing: Árazási adatok (havi/éves díj, pénznem)
|
||||
allowances: Korlátok (max jármű, garázs, kredit)
|
||||
entitlements: Jogosultságok listája (pl. SRV_DATA_EXPORT)
|
||||
affiliate: Partnerprogram beállítások
|
||||
lifecycle: Életciklus állapotok (opcionális, pl. is_public)
|
||||
"""
|
||||
type: str = Field(..., pattern=r"^(private|corporate)$", description="Csomag típusa")
|
||||
display_name: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Emberi olvasásra szánt megjelenítési név (pl. 'Privát Ingyenes')",
|
||||
)
|
||||
pricing: Optional[PricingModel] = Field(
|
||||
default=None,
|
||||
description="Árazási adatok (havi/éves díj, pénznem). Opcionális a hiányos seed adatok miatt."
|
||||
)
|
||||
allowances: Optional[AllowancesModel] = Field(
|
||||
default=None,
|
||||
description="Korlátok (max jármű, garázs, kredit). Opcionális a hiányos seed adatok miatt."
|
||||
)
|
||||
entitlements: List[str] = Field(default_factory=list, description="Jogosultságok (entitlement) listája")
|
||||
affiliate: Optional[AffiliateModel] = Field(
|
||||
default=None,
|
||||
description="Partnerprogram beállítások. Opcionális a hiányos seed adatok miatt."
|
||||
)
|
||||
lifecycle: Optional[LifecycleModel] = Field(
|
||||
default=None,
|
||||
description="Életciklus állapotok (is_public, available_until)"
|
||||
)
|
||||
|
||||
@field_validator("type")
|
||||
@classmethod
|
||||
def validate_type(cls, v: str) -> str:
|
||||
if v.lower() not in ("private", "corporate"):
|
||||
raise ValueError("A 'type' mező csak 'private' vagy 'corporate' lehet.")
|
||||
return v.lower()
|
||||
|
||||
|
||||
# ── API Response / Request modellek ───────────────────────────────────────────
|
||||
|
||||
|
||||
class SubscriptionTierResponse(BaseModel):
|
||||
"""Teljes SubscriptionTier rekord visszaadása (GET /, GET /{id})."""
|
||||
id: int
|
||||
name: str
|
||||
rules: SubscriptionRulesModel
|
||||
is_custom: bool
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class SubscriptionTierCreate(BaseModel):
|
||||
"""Új előfizetési csomag létrehozása (POST /)."""
|
||||
name: str = Field(..., min_length=2, max_length=100, description="Csomag neve (pl. private_pro_v2)")
|
||||
rules: SubscriptionRulesModel
|
||||
is_custom: bool = Field(default=False, description="Egyedi (custom) csomag-e")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("A csomag neve nem lehet üres.")
|
||||
return v.strip().lower()
|
||||
|
||||
|
||||
class SubscriptionTierUpdate(BaseModel):
|
||||
"""Meglévő előfizetési csomag részleges frissítése (PATCH /{tier_id})."""
|
||||
name: Optional[str] = Field(default=None, min_length=2, max_length=100, description="Új csomagnév")
|
||||
rules: Optional[SubscriptionRulesModel] = Field(default=None, description="Frissített rules JSONB")
|
||||
is_custom: Optional[bool] = Field(default=None, description="Egyedi csomag jelölés")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: Optional[str]) -> Optional[str]:
|
||||
if v is not None and not v.strip():
|
||||
raise ValueError("A csomag neve nem lehet üres.")
|
||||
return v.strip().lower() if v else v
|
||||
|
||||
|
||||
class SubscriptionTierListResponse(BaseModel):
|
||||
"""Csomagok listázásának válaszformátuma (GET /)."""
|
||||
total: int
|
||||
tiers: List[SubscriptionTierResponse]
|
||||
@@ -64,6 +64,8 @@ class UserResponse(UserBase):
|
||||
preferred_language: str = "hu"
|
||||
# Beágyazott személyes adatok (Person + Address)
|
||||
person: Optional[PersonResponse] = None
|
||||
# Utolsó admin flag (ha a user az egyetlen aktív admin bármely szervezetében)
|
||||
is_last_admin: bool = False
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user