Files
service-finder/backend/app/schemas/organization.py

181 lines
6.8 KiB
Python
Executable File

from pydantic import BaseModel, Field, ConfigDict, model_validator
from typing import Optional, List, Any
from datetime import datetime
# =============================================================================
# Organization Member Schemas (P0: Full CRUD for Garage Employees)
# =============================================================================
class OrganizationMemberBase(BaseModel):
"""Base schema for organization member data."""
user_id: int = Field(..., description="The user ID to add as a member")
role: str = Field(default="MEMBER", description="Role within the organization. Valid values: OWNER, ADMIN, MANAGER, MEMBER, AGENT (PG_ENUM orguserrole)")
status: str = Field(default="active", description="Membership status (active, inactive, archived)")
class OrganizationMemberCreate(BaseModel):
"""Schema for adding a new member to a garage.
Supports two modes:
1. By user_id (direct): provide user_id
2. By email (lookup): provide email — the system looks up the user by email
"""
user_id: Optional[int] = Field(default=None, description="The user ID to add as a member")
email: Optional[str] = Field(default=None, description="Email of the user to add (alternative to user_id)")
role: str = Field(default="MEMBER", description="Role within the organization. Valid values: OWNER, ADMIN, MANAGER, MEMBER, AGENT (PG_ENUM orguserrole)")
status: str = Field(default="active", description="Membership status (active, inactive, archived)")
class OrganizationMemberUpdate(BaseModel):
"""Schema for updating an existing member's role/status."""
role: Optional[str] = Field(default=None, description="New role within the organization")
status: Optional[str] = Field(default=None, description="New membership status (active, inactive, archived)")
class PersonBrief(BaseModel):
"""Nested person data for member responses."""
id: Optional[int] = None
first_name: str = ""
last_name: str = ""
email: Optional[str] = None
phone: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class OrganizationMemberResponse(BaseModel):
"""Full member data returned by the API."""
id: int
user_id: Optional[int] = None
organization_id: int
role: str
status: str = "active"
is_verified: bool = False
person: Optional[PersonBrief] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
@model_validator(mode="before")
@classmethod
def coerce_enums_to_strings(cls, data: Any) -> Any:
"""Convert PG_ENUM / Enum values to plain strings before Pydantic validation.
The OrganizationMember.role field uses a PostgreSQL ENUM type (OrgUserRole).
When loaded via SQLAlchemy's from_attributes, the enum instance is not
automatically serialized to a string. This validator ensures any enum value
is converted to its string representation.
"""
if isinstance(data, dict):
for field_name in ("role", "status"):
val = data.get(field_name)
if val is not None and not isinstance(val, str):
# Handle PG_ENUM / Python enum instances
data[field_name] = str(val.value if hasattr(val, "value") else val)
return data
# =============================================================================
# Contact / Onboarding Schemas
# =============================================================================
class ContactCreate(BaseModel):
full_name: str
email: str
phone: Optional[str] = None
contact_type: str = "primary"
class CorpOnboardIn(BaseModel):
""" Teljes onboarding adatcsomag atomizált címekkel. """
full_name: str = Field(..., description="Hivatalos cégnév")
name: str = Field(..., description="Rövid név")
display_name: str
tax_number: str
reg_number: Optional[str] = None
country_code: str = "HU"
language: str = "hu"
default_currency: str = "HUF"
# --- ATOMIZÁLT CÍM (Modell szinkron) ---
address_zip: str
address_city: str
address_street_name: str
address_street_type: str
address_house_number: str
address_stairwell: Optional[str] = None
address_floor: Optional[str] = None
address_door: Optional[str] = None
address_hrsz: Optional[str] = None
contacts: List[ContactCreate] = []
class CorpOnboardResponse(BaseModel):
organization_id: int
status: str
model_config = ConfigDict(from_attributes=True)
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,
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_hrsz: Optional[str] = None
# ── Crowdsourced search fields ──
aliases: Optional[List[str]] = None
tags: Optional[List[str]] = None
# ── Subscription ──
subscription_tier_id: Optional[int] = None
class OrganizationResponse(BaseModel):
"""Szervezet adatainak visszaadása (DTO).
Tartalmazza a visual_settings mezőt a frontend számára."""
id: int
name: str
full_name: str
display_name: Optional[str] = None
tax_number: Optional[str] = None
country_code: str = "HU"
status: str
org_type: Optional[str] = None
is_active: bool = True
is_deleted: bool = False
subscription_plan: str = "FREE"
subscription_expires_at: Optional[datetime] = None
subscription_tier_id: Optional[int] = None
# P0: Real subscription limits from the assigned subscription_tier JSONB rules
max_vehicles: int = 1
max_garages: int = 1
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)
# ── 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_hrsz: Optional[str] = None
model_config = ConfigDict(from_attributes=True)