""" Platform Serializers Serializers for platform-level operations (viewing tenants, users, metrics) """ from rest_framework import serializers from core.models import Tenant, Domain from smoothschedule.users.models import User from .models import TenantInvitation, PlatformSettings, SubscriptionPlan, PlatformEmailAddress class PlatformSettingsSerializer(serializers.Serializer): """Serializer for platform settings (read-only, with masked keys)""" stripe_secret_key_masked = serializers.SerializerMethodField() stripe_publishable_key_masked = serializers.SerializerMethodField() stripe_webhook_secret_masked = serializers.SerializerMethodField() stripe_account_id = serializers.CharField(read_only=True) stripe_account_name = serializers.CharField(read_only=True) stripe_keys_validated_at = serializers.DateTimeField(read_only=True) stripe_validation_error = serializers.CharField(read_only=True) has_stripe_keys = serializers.SerializerMethodField() stripe_keys_from_env = serializers.SerializerMethodField() email_check_interval_minutes = serializers.IntegerField(read_only=True) updated_at = serializers.DateTimeField(read_only=True) def get_stripe_secret_key_masked(self, obj): return obj.mask_key(obj.get_stripe_secret_key()) def get_stripe_publishable_key_masked(self, obj): return obj.mask_key(obj.get_stripe_publishable_key()) def get_stripe_webhook_secret_masked(self, obj): return obj.mask_key(obj.get_stripe_webhook_secret()) def get_has_stripe_keys(self, obj): return obj.has_stripe_keys() def get_stripe_keys_from_env(self, obj): return obj.stripe_keys_from_env() class StripeKeysUpdateSerializer(serializers.Serializer): """Serializer for updating Stripe keys""" stripe_secret_key = serializers.CharField(required=False, allow_blank=True) stripe_publishable_key = serializers.CharField(required=False, allow_blank=True) stripe_webhook_secret = serializers.CharField(required=False, allow_blank=True) class OAuthSettingsSerializer(serializers.Serializer): """Serializer for OAuth settings""" oauth_allow_registration = serializers.BooleanField(required=False, default=True) # Google oauth_google_enabled = serializers.BooleanField(required=False, default=False) oauth_google_client_id = serializers.CharField(required=False, allow_blank=True) oauth_google_client_secret = serializers.CharField(required=False, allow_blank=True) # Apple oauth_apple_enabled = serializers.BooleanField(required=False, default=False) oauth_apple_client_id = serializers.CharField(required=False, allow_blank=True) oauth_apple_client_secret = serializers.CharField(required=False, allow_blank=True) oauth_apple_team_id = serializers.CharField(required=False, allow_blank=True) oauth_apple_key_id = serializers.CharField(required=False, allow_blank=True) # Facebook oauth_facebook_enabled = serializers.BooleanField(required=False, default=False) oauth_facebook_client_id = serializers.CharField(required=False, allow_blank=True) oauth_facebook_client_secret = serializers.CharField(required=False, allow_blank=True) # LinkedIn oauth_linkedin_enabled = serializers.BooleanField(required=False, default=False) oauth_linkedin_client_id = serializers.CharField(required=False, allow_blank=True) oauth_linkedin_client_secret = serializers.CharField(required=False, allow_blank=True) # Microsoft oauth_microsoft_enabled = serializers.BooleanField(required=False, default=False) oauth_microsoft_client_id = serializers.CharField(required=False, allow_blank=True) oauth_microsoft_client_secret = serializers.CharField(required=False, allow_blank=True) oauth_microsoft_tenant_id = serializers.CharField(required=False, allow_blank=True) # Twitter oauth_twitter_enabled = serializers.BooleanField(required=False, default=False) oauth_twitter_client_id = serializers.CharField(required=False, allow_blank=True) oauth_twitter_client_secret = serializers.CharField(required=False, allow_blank=True) # Twitch oauth_twitch_enabled = serializers.BooleanField(required=False, default=False) oauth_twitch_client_id = serializers.CharField(required=False, allow_blank=True) oauth_twitch_client_secret = serializers.CharField(required=False, allow_blank=True) class OAuthSettingsResponseSerializer(serializers.Serializer): """Response serializer for OAuth settings (with masked secrets)""" oauth_allow_registration = serializers.BooleanField() google = serializers.DictField() apple = serializers.DictField() facebook = serializers.DictField() linkedin = serializers.DictField() microsoft = serializers.DictField() twitter = serializers.DictField() twitch = serializers.DictField() class SubscriptionPlanSerializer(serializers.ModelSerializer): """Serializer for subscription plans""" class Meta: model = SubscriptionPlan fields = [ 'id', 'name', 'description', 'plan_type', 'stripe_product_id', 'stripe_price_id', 'price_monthly', 'price_yearly', 'business_tier', 'features', 'limits', 'permissions', 'transaction_fee_percent', 'transaction_fee_fixed', 'is_active', 'is_public', 'is_most_popular', 'show_price', 'created_at', 'updated_at' ] read_only_fields = ['id', 'created_at', 'updated_at'] class SubscriptionPlanCreateSerializer(serializers.ModelSerializer): """Serializer for creating subscription plans with optional Stripe integration""" create_stripe_product = serializers.BooleanField(default=False, write_only=True) class Meta: model = SubscriptionPlan fields = [ 'name', 'description', 'plan_type', 'stripe_product_id', 'stripe_price_id', 'price_monthly', 'price_yearly', 'business_tier', 'features', 'limits', 'permissions', 'transaction_fee_percent', 'transaction_fee_fixed', 'is_active', 'is_public', 'is_most_popular', 'show_price', 'create_stripe_product' ] def create(self, validated_data): create_stripe = validated_data.pop('create_stripe_product', False) if create_stripe and validated_data.get('price_monthly'): # Create Stripe product and price import stripe from django.conf import settings stripe.api_key = settings.STRIPE_SECRET_KEY try: # Create product product = stripe.Product.create( name=validated_data['name'], description=validated_data.get('description', ''), metadata={'plan_type': validated_data.get('plan_type', 'base')} ) validated_data['stripe_product_id'] = product.id # Create price price = stripe.Price.create( product=product.id, unit_amount=int(validated_data['price_monthly'] * 100), # Convert to cents currency='usd', recurring={'interval': 'month'} ) validated_data['stripe_price_id'] = price.id except stripe.error.StripeError as e: raise serializers.ValidationError({'stripe': str(e)}) return super().create(validated_data) class TenantSerializer(serializers.ModelSerializer): """Serializer for Tenant (Business) listing""" subdomain = serializers.SerializerMethodField() tier = serializers.CharField(source='subscription_tier') user_count = serializers.SerializerMethodField() owner = serializers.SerializerMethodField() class Meta: model = Tenant fields = [ 'id', 'name', 'subdomain', 'tier', 'is_active', 'created_on', 'user_count', 'owner', 'max_users', 'max_resources', 'contact_email', 'phone', # Platform permissions 'can_manage_oauth_credentials', ] read_only_fields = fields def get_subdomain(self, obj): """Get primary subdomain for this tenant""" primary_domain = obj.domains.filter(is_primary=True, is_custom_domain=False).first() if primary_domain: # Extract subdomain from domain (e.g., 'business1.lvh.me' -> 'business1') return primary_domain.domain.split('.')[0] return obj.schema_name def get_user_count(self, obj): """Get count of users in this tenant's schema""" # This requires querying the tenant schema # For now, return 0 - we can optimize this with annotations return 0 def get_owner(self, obj): """Get the tenant owner user""" # Query public schema for users with this tenant as their business try: owner = User.objects.filter( role=User.Role.TENANT_OWNER, tenant=obj ).first() if owner: return { 'id': owner.id, 'username': owner.username, 'full_name': owner.full_name, 'email': owner.email, 'role': owner.role.lower(), 'email_verified': owner.email_verified, } except: pass return None class TenantUpdateSerializer(serializers.ModelSerializer): """Serializer for updating Tenant permissions (platform admins only)""" class Meta: model = Tenant fields = [ 'id', 'name', 'is_active', 'subscription_tier', 'max_users', 'max_resources', # Platform permissions 'can_manage_oauth_credentials', 'can_accept_payments', 'can_use_custom_domain', 'can_white_label', 'can_api_access', ] read_only_fields = ['id'] def update(self, instance, validated_data): """Update tenant with validated data""" from django_tenants.utils import schema_context for attr, value in validated_data.items(): setattr(instance, attr, value) # Must save in public schema with schema_context('public'): instance.save() return instance class TenantCreateSerializer(serializers.Serializer): """Serializer for creating a new Tenant with domain""" # Required fields name = serializers.CharField(max_length=100) subdomain = serializers.CharField(max_length=63) # Max subdomain length # Optional fields with defaults subscription_tier = serializers.ChoiceField( choices=['FREE', 'STARTER', 'PROFESSIONAL', 'ENTERPRISE'], default='FREE' ) is_active = serializers.BooleanField(default=True) max_users = serializers.IntegerField(default=5, min_value=1) max_resources = serializers.IntegerField(default=10, min_value=1) contact_email = serializers.EmailField(required=False, allow_blank=True) phone = serializers.CharField(max_length=20, required=False, allow_blank=True) # Platform permissions can_manage_oauth_credentials = serializers.BooleanField(default=False) # Owner details (optional - create owner user if provided) owner_email = serializers.EmailField(required=False) owner_name = serializers.CharField(max_length=150, required=False) owner_password = serializers.CharField(max_length=128, required=False, write_only=True) def validate_subdomain(self, value): """Validate subdomain is unique and valid""" import re # Check format (lowercase alphanumeric and hyphens, must start with letter) if not re.match(r'^[a-z][a-z0-9-]*$', value.lower()): raise serializers.ValidationError( "Subdomain must start with a letter and contain only lowercase letters, numbers, and hyphens" ) # Check if subdomain already exists as schema_name if Tenant.objects.filter(schema_name=value.lower()).exists(): raise serializers.ValidationError("This subdomain is already taken") # Check if domain already exists domain_name = f"{value.lower()}.lvh.me" # TODO: Make base domain configurable if Domain.objects.filter(domain=domain_name).exists(): raise serializers.ValidationError("This subdomain is already taken") # Reserved subdomains reserved = ['www', 'api', 'admin', 'platform', 'app', 'mail', 'smtp', 'ftp', 'public'] if value.lower() in reserved: raise serializers.ValidationError("This subdomain is reserved") return value.lower() def validate(self, attrs): """Cross-field validation""" # If owner email is provided, name and password should also be provided owner_email = attrs.get('owner_email') owner_name = attrs.get('owner_name') owner_password = attrs.get('owner_password') if owner_email: if not owner_name: raise serializers.ValidationError({ 'owner_name': 'Owner name is required when creating an owner' }) if not owner_password: raise serializers.ValidationError({ 'owner_password': 'Owner password is required when creating an owner' }) # Check if email already exists if User.objects.filter(email=owner_email).exists(): raise serializers.ValidationError({ 'owner_email': 'A user with this email already exists' }) return attrs def create(self, validated_data): """Create tenant, domain, and optionally owner user""" from django.db import transaction from django_tenants.utils import schema_context subdomain = validated_data.pop('subdomain') owner_email = validated_data.pop('owner_email', None) owner_name = validated_data.pop('owner_name', None) owner_password = validated_data.pop('owner_password', None) # Must create tenant in public schema with schema_context('public'): with transaction.atomic(): # Create tenant tenant = Tenant.objects.create( schema_name=subdomain, name=validated_data.get('name'), subscription_tier=validated_data.get('subscription_tier', 'FREE'), is_active=validated_data.get('is_active', True), max_users=validated_data.get('max_users', 5), max_resources=validated_data.get('max_resources', 10), contact_email=validated_data.get('contact_email', ''), phone=validated_data.get('phone', ''), can_manage_oauth_credentials=validated_data.get('can_manage_oauth_credentials', False), ) # Create primary domain domain_name = f"{subdomain}.lvh.me" # TODO: Make base domain configurable Domain.objects.create( domain=domain_name, tenant=tenant, is_primary=True, is_custom_domain=False, ) # Create owner user if details provided if owner_email and owner_name and owner_password: # Split name into first/last name_parts = owner_name.split(' ', 1) first_name = name_parts[0] last_name = name_parts[1] if len(name_parts) > 1 else '' owner = User.objects.create_user( username=owner_email, # Use email as username email=owner_email, password=owner_password, first_name=first_name, last_name=last_name, role=User.Role.TENANT_OWNER, tenant=tenant, ) return tenant class PlatformUserSerializer(serializers.ModelSerializer): """Serializer for User listing (platform view)""" business = serializers.SerializerMethodField() business_name = serializers.SerializerMethodField() business_subdomain = serializers.SerializerMethodField() full_name = serializers.SerializerMethodField() role = serializers.SerializerMethodField() class Meta: model = User fields = [ 'id', 'email', 'username', 'first_name', 'last_name', 'full_name', 'role', 'is_active', 'is_staff', 'is_superuser', 'email_verified', 'permissions', 'business', 'business_name', 'business_subdomain', 'date_joined', 'last_login', 'created_at' ] read_only_fields = fields def get_role(self, obj): return obj.role.lower() def get_full_name(self, obj): """Get user's full name""" return obj.full_name def get_business(self, obj): """Get tenant ID if user belongs to a tenant""" if obj.tenant: return obj.tenant.id return None def get_business_name(self, obj): """Get tenant name if user belongs to a tenant""" if obj.tenant: return obj.tenant.name return None def get_business_subdomain(self, obj): """Get tenant subdomain if user belongs to a tenant""" if obj.tenant: primary_domain = obj.tenant.domains.filter(is_primary=True, is_custom_domain=False).first() if primary_domain: return primary_domain.domain.split('.')[0] return obj.tenant.schema_name return None class PlatformMetricsSerializer(serializers.Serializer): """Serializer for platform dashboard metrics""" total_tenants = serializers.IntegerField() active_tenants = serializers.IntegerField() total_users = serializers.IntegerField() mrr = serializers.DecimalField(max_digits=10, decimal_places=2) growth_rate = serializers.FloatField() class TenantInvitationSerializer(serializers.ModelSerializer): """Serializer for TenantInvitation model""" invited_by_email = serializers.ReadOnlyField(source='invited_by.email') created_tenant_name = serializers.ReadOnlyField(source='created_tenant.name') created_user_email = serializers.ReadOnlyField(source='created_user.email') class Meta: model = TenantInvitation fields = [ 'id', 'email', 'token', 'status', 'suggested_business_name', 'subscription_tier', 'custom_max_users', 'custom_max_resources', 'permissions', 'limits', 'personal_message', 'invited_by', 'invited_by_email', 'created_at', 'expires_at', 'accepted_at', 'created_tenant', 'created_tenant_name', 'created_user', 'created_user_email', ] read_only_fields = ['id', 'token', 'status', 'created_at', 'expires_at', 'accepted_at', 'created_tenant', 'created_tenant_name', 'created_user', 'created_user_email', 'invited_by_email'] extra_kwargs = { 'invited_by': {'write_only': True}, # Only send on creation } def validate_permissions(self, value): """Validate that permissions is a dictionary with boolean values""" if not isinstance(value, dict): raise serializers.ValidationError("Permissions must be a dictionary.") for key, val in value.items(): if not isinstance(val, bool): raise serializers.ValidationError(f"Permission '{key}' must be a boolean.") return value def validate_limits(self, value): """Validate that limits is a dictionary with valid values""" if not isinstance(value, dict): raise serializers.ValidationError("Limits must be a dictionary.") # Validate each key's value type boolean_keys = [ 'can_add_video_conferencing', 'can_connect_to_api', 'can_book_repeated_events', 'can_require_2fa', 'can_download_logs', 'can_delete_data', 'can_use_masked_phone_numbers', 'can_use_pos', 'can_use_mobile_app' ] integer_keys = ['max_event_types', 'max_calendars_connected'] for key, val in value.items(): if key in boolean_keys and not isinstance(val, bool): raise serializers.ValidationError(f"Limit '{key}' must be a boolean.") if key in integer_keys and val is not None and not isinstance(val, int): raise serializers.ValidationError(f"Limit '{key}' must be an integer or null.") return value class TenantInvitationCreateSerializer(serializers.ModelSerializer): """Serializer for creating TenantInvitations - sets invited_by automatically""" class Meta: model = TenantInvitation fields = [ 'email', 'suggested_business_name', 'subscription_tier', 'custom_max_users', 'custom_max_resources', 'permissions', 'limits', 'personal_message', ] def validate_limits(self, value): """Validate that limits is a dictionary with valid values""" if not isinstance(value, dict): raise serializers.ValidationError("Limits must be a dictionary.") # Validate each key's value type boolean_keys = [ 'can_add_video_conferencing', 'can_connect_to_api', 'can_book_repeated_events', 'can_require_2fa', 'can_download_logs', 'can_delete_data', 'can_use_masked_phone_numbers', 'can_use_pos', 'can_use_mobile_app' ] integer_keys = ['max_event_types', 'max_calendars_connected'] for key, val in value.items(): if key in boolean_keys and not isinstance(val, bool): raise serializers.ValidationError(f"Limit '{key}' must be a boolean.") if key in integer_keys and val is not None and not isinstance(val, int): raise serializers.ValidationError(f"Limit '{key}' must be an integer or null.") return value def create(self, validated_data): validated_data['invited_by'] = self.context['request'].user return super().create(validated_data) class TenantInvitationAcceptSerializer(serializers.Serializer): """Serializer for accepting a TenantInvitation""" email = serializers.EmailField() password = serializers.CharField(max_length=128, write_only=True) first_name = serializers.CharField(max_length=150) last_name = serializers.CharField(max_length=150) business_name = serializers.CharField(max_length=100) subdomain = serializers.CharField(max_length=63) contact_email = serializers.EmailField(required=False, allow_blank=True) phone = serializers.CharField(max_length=20, required=False, allow_blank=True) def validate_subdomain(self, value): """Validate subdomain is unique and valid""" import re from core.models import Tenant, Domain # Check format (lowercase alphanumeric and hyphens, must start with letter) if not re.match(r'^[a-z][a-z0-9-]*$', value.lower()): raise serializers.ValidationError( "Subdomain must start with a letter and contain only lowercase letters, numbers, and hyphens" ) # Check if subdomain already exists as schema_name if Tenant.objects.filter(schema_name=value.lower()).exists(): raise serializers.ValidationError("This subdomain is already taken") # Check if domain already exists domain_name = f"{value.lower()}.lvh.me" # TODO: Make base domain configurable if Domain.objects.filter(domain=domain_name).exists(): raise serializers.ValidationError("This subdomain is already taken") # Reserved subdomains reserved = ['www', 'api', 'admin', 'platform', 'app', 'mail', 'smtp', 'ftp', 'public'] if value.lower() in reserved: raise serializers.ValidationError("This subdomain is reserved") return value.lower() def validate_email(self, value): """Validate email is unique for owner user""" if User.objects.filter(email=value).exists(): raise serializers.ValidationError("A user with this email already exists.") return value class TenantInvitationDetailSerializer(TenantInvitationSerializer): """Serializer to display invitation details without requiring authentication""" class Meta(TenantInvitationSerializer.Meta): read_only_fields = ['id', 'email', 'token', 'status', 'suggested_business_name', 'subscription_tier', 'custom_max_users', 'custom_max_resources', 'permissions', 'personal_message', 'created_at', 'expires_at', 'accepted_at', 'created_tenant', 'created_user', 'invited_by_email', 'created_tenant_name', 'created_user_email'] extra_kwargs = { 'invited_by': {'read_only': True}, } class AssignedUserSerializer(serializers.Serializer): """Lightweight serializer for assigned user info.""" id = serializers.IntegerField(read_only=True) email = serializers.EmailField(read_only=True) first_name = serializers.CharField(read_only=True) last_name = serializers.CharField(read_only=True) full_name = serializers.SerializerMethodField() def get_full_name(self, obj): return obj.get_full_name() or obj.email class PlatformEmailAddressListSerializer(serializers.ModelSerializer): """Lightweight serializer for listing platform email addresses.""" email_address = serializers.ReadOnlyField() effective_sender_name = serializers.ReadOnlyField() assigned_user = AssignedUserSerializer(read_only=True) class Meta: model = PlatformEmailAddress fields = [ 'id', 'display_name', 'sender_name', 'effective_sender_name', 'local_part', 'domain', 'email_address', 'color', 'assigned_user', 'is_active', 'is_default', 'mail_server_synced', 'last_check_at', 'emails_processed_count', 'created_at', 'updated_at' ] read_only_fields = [ 'email_address', 'effective_sender_name', 'mail_server_synced', 'last_check_at', 'emails_processed_count', 'created_at', 'updated_at' ] class PlatformEmailAddressSerializer(serializers.ModelSerializer): """Full serializer for platform email addresses.""" email_address = serializers.ReadOnlyField() effective_sender_name = serializers.ReadOnlyField() assigned_user = AssignedUserSerializer(read_only=True) assigned_user_id = serializers.IntegerField( write_only=True, required=False, allow_null=True ) imap_settings = serializers.SerializerMethodField() smtp_settings = serializers.SerializerMethodField() class Meta: model = PlatformEmailAddress fields = [ 'id', 'display_name', 'sender_name', 'effective_sender_name', 'local_part', 'domain', 'email_address', 'color', 'assigned_user', 'assigned_user_id', 'password', 'is_active', 'is_default', 'mail_server_synced', 'last_sync_error', 'last_synced_at', 'last_check_at', 'emails_processed_count', 'created_at', 'updated_at', 'imap_settings', 'smtp_settings' ] read_only_fields = [ 'email_address', 'effective_sender_name', 'mail_server_synced', 'last_sync_error', 'last_synced_at', 'last_check_at', 'emails_processed_count', 'created_at', 'updated_at', 'imap_settings', 'smtp_settings' ] extra_kwargs = { 'password': {'write_only': True}, } def validate_assigned_user_id(self, value): """Validate and convert assigned_user_id to User instance.""" if value is None: return None from smoothschedule.users.models import User try: user = User.objects.get( pk=value, role__in=['superuser', 'platform_manager', 'platform_support'], is_active=True ) return user except User.DoesNotExist: raise serializers.ValidationError("User not found or not a platform user.") def create(self, validated_data): assigned_user = validated_data.pop('assigned_user_id', None) instance = super().create(validated_data) if assigned_user is not None: instance.assigned_user = assigned_user instance.save(update_fields=['assigned_user']) return instance def update(self, instance, validated_data): if 'assigned_user_id' in validated_data: instance.assigned_user = validated_data.pop('assigned_user_id') return super().update(instance, validated_data) def get_imap_settings(self, obj): """Return IMAP settings without password.""" settings = obj.get_imap_settings() settings.pop('password', None) return settings def get_smtp_settings(self, obj): """Return SMTP settings without password.""" settings = obj.get_smtp_settings() settings.pop('password', None) return settings def validate_local_part(self, value): """Validate local part of email address.""" import re value = value.lower().strip() # Check format if not re.match(r'^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$', value): raise serializers.ValidationError( "Local part must start and end with a letter or number, " "and can only contain letters, numbers, dots, underscores, and hyphens" ) if len(value) > 64: raise serializers.ValidationError("Local part cannot exceed 64 characters") return value def validate_password(self, value): """Validate password strength.""" if len(value) < 8: raise serializers.ValidationError("Password must be at least 8 characters") return value def validate(self, attrs): """Cross-field validation.""" local_part = attrs.get('local_part', getattr(self.instance, 'local_part', None)) domain = attrs.get('domain', getattr(self.instance, 'domain', None)) if local_part and domain: # Check uniqueness qs = PlatformEmailAddress.objects.filter( local_part=local_part.lower(), domain=domain ) if self.instance: qs = qs.exclude(pk=self.instance.pk) if qs.exists(): raise serializers.ValidationError({ 'local_part': f'Email address {local_part}@{domain} already exists' }) return attrs class PlatformEmailAddressCreateSerializer(PlatformEmailAddressSerializer): """Serializer for creating platform email addresses with mail server sync.""" def create(self, validated_data): """Create the email address and sync to mail server.""" from .mail_server import get_mail_server_service # Normalize local_part validated_data['local_part'] = validated_data['local_part'].lower() # Create the database record first instance = super().create(validated_data) # Sync to mail server service = get_mail_server_service() success, message = service.sync_account(instance) if not success: # Delete the database record if mail server sync failed instance.delete() raise serializers.ValidationError({ 'mail_server': f'Failed to create email account on mail server: {message}' }) return instance class PlatformEmailAddressUpdateSerializer(serializers.ModelSerializer): """Serializer for updating platform email addresses.""" email_address = serializers.ReadOnlyField() assigned_user_id = serializers.IntegerField( write_only=True, required=False, allow_null=True ) class Meta: model = PlatformEmailAddress fields = [ 'id', 'display_name', 'sender_name', 'email_address', 'color', 'assigned_user_id', 'password', 'is_active', 'is_default' ] read_only_fields = ['id', 'email_address'] extra_kwargs = { 'password': {'write_only': True, 'required': False}, 'sender_name': {'required': False}, } def validate_assigned_user_id(self, value): """Validate and convert assigned_user_id to User instance.""" if value is None: return None from smoothschedule.users.models import User try: user = User.objects.get( pk=value, role__in=['superuser', 'platform_manager', 'platform_support'], is_active=True ) return user except User.DoesNotExist: raise serializers.ValidationError("User not found or not a platform user.") def validate_password(self, value): """Validate password strength if provided.""" if value and len(value) < 8: raise serializers.ValidationError("Password must be at least 8 characters") return value def update(self, instance, validated_data): """Update the email address and sync password to mail server if changed.""" from .mail_server import get_mail_server_service password = validated_data.get('password') password_changed = password and password != instance.password # Handle assigned_user_id separately if 'assigned_user_id' in validated_data: instance.assigned_user = validated_data.pop('assigned_user_id') # Update the instance instance = super().update(instance, validated_data) # Sync to mail server if password changed if password_changed: service = get_mail_server_service() success, message = service.sync_account(instance) if not success: raise serializers.ValidationError({ 'mail_server': f'Failed to update password on mail server: {message}' }) return instance