# services/tenant/app/schemas/tenants.py """ Tenant schemas - FIXED VERSION """ from pydantic import BaseModel, Field, field_validator, ValidationInfo from typing import Optional, List, Dict, Any from datetime import datetime from uuid import UUID import re class BakeryRegistration(BaseModel): """Bakery registration schema""" name: str = Field(..., min_length=2, max_length=200) address: str = Field(..., min_length=10, max_length=500) city: str = Field(default="Madrid", max_length=100) postal_code: str = Field(..., pattern=r"^\d{5}$") phone: str = Field(..., min_length=9, max_length=20) business_type: str = Field(default="bakery") business_model: Optional[str] = Field(default="individual_bakery") coupon_code: Optional[str] = Field(None, max_length=50, description="Promotional coupon code") @field_validator('phone') @classmethod def validate_spanish_phone(cls, v): """Validate Spanish phone number""" # Remove spaces and common separators phone = re.sub(r'[\s\-\(\)]', '', v) # Spanish mobile: +34 6/7/8/9 + 8 digits # Spanish landline: +34 9 + 8 digits patterns = [ r'^(\+34|0034|34)?[6789]\d{8}$', # Mobile r'^(\+34|0034|34)?9\d{8}$', # Landline ] if not any(re.match(pattern, phone) for pattern in patterns): raise ValueError('Invalid Spanish phone number') return v @field_validator('business_type') @classmethod def validate_business_type(cls, v): valid_types = ['bakery', 'coffee_shop', 'pastry_shop', 'restaurant'] if v not in valid_types: raise ValueError(f'Business type must be one of: {valid_types}') return v @field_validator('business_model') @classmethod def validate_business_model(cls, v): if v is None: return v valid_models = ['individual_bakery', 'central_baker_satellite', 'retail_bakery', 'hybrid_bakery'] if v not in valid_models: raise ValueError(f'Business model must be one of: {valid_models}') return v class TenantResponse(BaseModel): """Tenant response schema - Updated to use subscription relationship""" id: str # ✅ Keep as str for Pydantic validation name: str subdomain: Optional[str] business_type: str business_model: Optional[str] tenant_type: Optional[str] = "standalone" # standalone, parent, or child parent_tenant_id: Optional[str] = None # For child tenants address: str city: str postal_code: str phone: Optional[str] is_active: bool subscription_plan: Optional[str] = None # Populated from subscription relationship or service ml_model_trained: bool last_training_date: Optional[datetime] owner_id: str # ✅ Keep as str for Pydantic validation created_at: datetime # ✅ FIX: Add custom validator to convert UUID to string @field_validator('id', 'owner_id', 'parent_tenant_id', mode='before') @classmethod def convert_uuid_to_string(cls, v): """Convert UUID objects to strings for JSON serialization""" if isinstance(v, UUID): return str(v) return v class Config: from_attributes = True class TenantAccessResponse(BaseModel): """Tenant access verification response""" has_access: bool role: str permissions: List[str] class TenantMemberResponse(BaseModel): """Tenant member response - FIXED VERSION with enriched user data""" id: str user_id: str role: str is_active: bool joined_at: Optional[datetime] # Enriched user fields (populated via service layer) user_email: Optional[str] = None user_full_name: Optional[str] = None user: Optional[Dict[str, Any]] = None # Full user object for compatibility # ✅ FIX: Add custom validator to convert UUID to string @field_validator('id', 'user_id', mode='before') @classmethod def convert_uuid_to_string(cls, v): """Convert UUID objects to strings for JSON serialization""" if isinstance(v, UUID): return str(v) return v class Config: from_attributes = True class TenantUpdate(BaseModel): """Tenant update schema""" name: Optional[str] = Field(None, min_length=2, max_length=200) address: Optional[str] = Field(None, min_length=10, max_length=500) phone: Optional[str] = None business_type: Optional[str] = None business_model: Optional[str] = None class TenantListResponse(BaseModel): """Response schema for listing tenants""" tenants: List[TenantResponse] total: int page: int per_page: int has_next: bool has_prev: bool class TenantMemberInvitation(BaseModel): """Schema for inviting a member to a tenant""" email: str = Field(..., pattern=r'^[^@]+@[^@]+\.[^@]+$') role: str = Field(..., pattern=r'^(admin|member|viewer)$') message: Optional[str] = Field(None, max_length=500) class TenantMemberUpdate(BaseModel): """Schema for updating tenant member""" role: Optional[str] = Field(None, pattern=r'^(owner|admin|member|viewer)$') is_active: Optional[bool] = None class AddMemberWithUserCreate(BaseModel): """Schema for adding member with optional user creation (pilot phase)""" # For existing users user_id: Optional[str] = Field(None, description="ID of existing user to add") # For new user creation create_user: bool = Field(False, description="Whether to create a new user") email: Optional[str] = Field(None, description="Email for new user (if create_user=True)") full_name: Optional[str] = Field(None, min_length=2, max_length=100, description="Full name for new user") password: Optional[str] = Field(None, min_length=8, max_length=128, description="Password for new user") phone: Optional[str] = Field(None, description="Phone number for new user") language: Optional[str] = Field("es", pattern="^(es|en|eu)$", description="Preferred language") timezone: Optional[str] = Field("Europe/Madrid", description="User timezone") # Common fields role: str = Field(..., pattern=r'^(admin|member|viewer)$', description="Role in the tenant") @field_validator('email', 'full_name', 'password') @classmethod def validate_user_creation_fields(cls, v, info: ValidationInfo): """Validate that required fields are present when creating a user""" if info.data.get('create_user') and info.field_name in ['email', 'full_name', 'password']: if not v: raise ValueError(f"{info.field_name} is required when create_user is True") return v @field_validator('user_id') @classmethod def validate_user_id_or_create(cls, v, info: ValidationInfo): """Ensure either user_id or create_user is provided""" if not v and not info.data.get('create_user'): raise ValueError("Either user_id or create_user must be provided") if v and info.data.get('create_user'): raise ValueError("Cannot specify both user_id and create_user") return v class TenantSubscriptionUpdate(BaseModel): """Schema for updating tenant subscription""" plan: str = Field(..., pattern=r'^(basic|professional|enterprise)$') billing_cycle: str = Field(default="monthly", pattern=r'^(monthly|yearly)$') class TenantStatsResponse(BaseModel): """Tenant statistics response""" tenant_id: str total_members: int active_members: int total_predictions: int models_trained: int last_training_date: Optional[datetime] subscription_plan: str subscription_status: str @field_validator('tenant_id', mode='before') @classmethod def convert_uuid_to_string(cls, v): """Convert UUID objects to strings for JSON serialization""" if isinstance(v, UUID): return str(v) return v class TenantSearchRequest(BaseModel): """Tenant search request schema""" query: Optional[str] = None business_type: Optional[str] = None city: Optional[str] = None status: Optional[str] = None limit: int = Field(default=50, ge=1, le=100) offset: int = Field(default=0, ge=0)