165 lines
6.3 KiB
Python
Executable File
165 lines
6.3 KiB
Python
Executable File
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
|
from typing import Optional, List, Any
|
|
from datetime import datetime
|
|
|
|
from app.schemas.address import AddressIn, AddressOut
|
|
|
|
|
|
# =============================================================================
|
|
# 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"
|
|
|
|
# --- P0 REFACTORED: Unified address via AddressIn ---
|
|
address: AddressIn = Field(..., description="Székhely címe (AddressIn struktúrában)")
|
|
|
|
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
|
|
# ── P0 REFACTORED: Unified address via AddressIn ──
|
|
address: Optional[AddressIn] = 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)
|
|
# ── P0 REFACTORED: Unified address via AddressOut ──
|
|
address: Optional[AddressOut] = None
|
|
|
|
model_config = ConfigDict(from_attributes=True) |