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)