""" DRF Serializers — validation, representation, and nested relations. """ from django.contrib.auth import get_user_model from django.utils import timezone from rest_framework import serializers from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from api.models import ( Caregiver, CaregiverService, Pet, Service, Booking, BookingStatus, Conversation, Message, Role, ) User = get_user_model() # ───────────────────────────────────────────────────────────────────────────── # JWT Custom Claims # ───────────────────────────────────────────────────────────────────────────── class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): @classmethod def get_token(cls, user): token = super().get_token(user) token['email'] = user.email token['role'] = user.role token['full_name'] = user.full_name return token def validate(self, attrs): data = super().validate(attrs) data['user'] = { 'id': str(self.user.id), 'email': self.user.email, 'role': self.user.role, 'full_name': self.user.full_name, 'avatar_url': self.user.avatar_url, } return data # ───────────────────────────────────────────────────────────────────────────── # Auth Serializers # ───────────────────────────────────────────────────────────────────────────── class RegisterSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True, min_length=10) password_confirm = serializers.CharField(write_only=True) class Meta: model = User fields = ['email', 'first_name', 'last_name', 'password', 'password_confirm'] def validate(self, data): if data['password'] != data['password_confirm']: raise serializers.ValidationError({'password_confirm': 'Passwords do not match.'}) return data def create(self, validated_data): validated_data.pop('password_confirm') return User.objects.create_user( role=Role.USER, **validated_data, ) class UserProfileSerializer(serializers.ModelSerializer): full_name = serializers.ReadOnlyField() class Meta: model = User fields = [ 'id', 'email', 'first_name', 'last_name', 'full_name', 'role', 'phone', 'address', 'avatar_url', 'date_joined', ] read_only_fields = ['id', 'role', 'date_joined'] class UserPublicSerializer(serializers.ModelSerializer): """Minimal public view — no PII.""" full_name = serializers.ReadOnlyField() class Meta: model = User fields = ['id', 'full_name', 'avatar_url'] # ───────────────────────────────────────────────────────────────────────────── # Service # ───────────────────────────────────────────────────────────────────────────── class ServiceSerializer(serializers.ModelSerializer): class Meta: model = Service fields = '__all__' # ───────────────────────────────────────────────────────────────────────────── # Caregiver # ───────────────────────────────────────────────────────────────────────────── class CaregiverServiceSerializer(serializers.ModelSerializer): service_name = serializers.ReadOnlyField(source='service.name') service_icon = serializers.ReadOnlyField(source='service.icon') class Meta: model = CaregiverService fields = ['service', 'service_name', 'service_icon', 'price_per_hour', 'is_active'] class CaregiverListSerializer(serializers.ModelSerializer): """Lightweight serializer for list views / search.""" full_name = serializers.ReadOnlyField(source='user.full_name') avatar_url = serializers.ReadOnlyField(source='user.avatar_url') class Meta: model = Caregiver fields = [ 'id', 'full_name', 'avatar_url', 'profile_image_url', 'city', 'country', 'rating', 'total_reviews', 'total_bookings', 'years_of_experience', 'specializations', 'is_available', 'is_verified', 'is_featured', ] class CaregiverDetailSerializer(serializers.ModelSerializer): """Full detail including services and user profile.""" full_name = serializers.ReadOnlyField(source='user.full_name') email = serializers.ReadOnlyField(source='user.email') avatar_url = serializers.ReadOnlyField(source='user.avatar_url') caregiver_services = CaregiverServiceSerializer( source='caregiverservice_set', many=True, read_only=True ) class Meta: model = Caregiver fields = [ 'id', 'full_name', 'email', 'avatar_url', 'profile_image_url', 'gallery_images', 'bio', 'years_of_experience', 'specializations', 'certifications', 'languages', 'city', 'country', 'latitude', 'longitude', 'rating', 'total_reviews', 'total_bookings', 'is_available', 'is_verified', 'is_featured', 'background_check_passed', 'caregiver_services', 'created_at', ] read_only_fields = ['rating', 'total_reviews', 'total_bookings', 'created_at'] class CaregiverWriteSerializer(serializers.ModelSerializer): """Admin-only — create/update caregiver profiles.""" email = serializers.EmailField(write_only=True) first_name = serializers.CharField(write_only=True, max_length=100) last_name = serializers.CharField(write_only=True, max_length=100) password = serializers.CharField(write_only=True, min_length=10, required=False) class Meta: model = Caregiver fields = [ 'email', 'first_name', 'last_name', 'password', 'bio', 'years_of_experience', 'specializations', 'certifications', 'languages', 'city', 'country', 'latitude', 'longitude', 'is_available', 'is_verified', 'is_featured', 'background_check_passed', 'emergency_contact', ] def create(self, validated_data): email = validated_data.pop('email') first_name = validated_data.pop('first_name') last_name = validated_data.pop('last_name') password = validated_data.pop('password', User.objects.make_random_password(16)) user = User.objects.create_user( email=email, first_name=first_name, last_name=last_name, password=password, role=Role.CAREGIVER, ) return Caregiver.objects.create(user=user, **validated_data) def update(self, instance, validated_data): # Update nested user fields if provided for field in ('email', 'first_name', 'last_name'): val = validated_data.pop(field, None) if val: setattr(instance.user, field, val) password = validated_data.pop('password', None) if password: instance.user.set_password(password) instance.user.save() for attr, val in validated_data.items(): setattr(instance, attr, val) instance.save() return instance # ───────────────────────────────────────────────────────────────────────────── # Pet # ───────────────────────────────────────────────────────────────────────────── class PetSerializer(serializers.ModelSerializer): class Meta: model = Pet fields = '__all__' read_only_fields = ['id', 'owner', 'created_at', 'updated_at'] # ───────────────────────────────────────────────────────────────────────────── # Booking # ───────────────────────────────────────────────────────────────────────────── class BookingCreateSerializer(serializers.ModelSerializer): class Meta: model = Booking fields = [ 'caregiver', 'pet', 'service', 'scheduled_start', 'scheduled_end', 'notes', 'service_address', ] def validate(self, data): if data['scheduled_end'] <= data['scheduled_start']: raise serializers.ValidationError('End time must be after start time.') # Ensure pet belongs to the requesting user request = self.context['request'] if data['pet'].owner != request.user: raise serializers.ValidationError({'pet': 'You can only book for your own pets.'}) return data def create(self, validated_data): user = self.context['request'].user service = validated_data['service'] caregiver = validated_data['caregiver'] # Calculate pricing hours = ( validated_data['scheduled_end'] - validated_data['scheduled_start'] ).total_seconds() / 3600 try: cs = CaregiverService.objects.get( caregiver=caregiver, service=service, is_active=True ) subtotal = cs.price_per_hour * hours except CaregiverService.DoesNotExist: subtotal = service.base_price * hours fees = subtotal * 0.05 # 5% platform fee total = subtotal + fees return Booking.objects.create( user=user, price_subtotal=round(subtotal, 2), price_fees=round(fees, 2), price_total=round(total, 2), **validated_data, ) class BookingSerializer(serializers.ModelSerializer): user = UserPublicSerializer(read_only=True) caregiver = CaregiverListSerializer(read_only=True) service = ServiceSerializer(read_only=True) pet = PetSerializer(read_only=True) class Meta: model = Booking fields = '__all__' read_only_fields = [ 'id', 'user', 'price_subtotal', 'price_fees', 'price_total', 'created_at', 'updated_at', ] class BookingStatusUpdateSerializer(serializers.Serializer): status = serializers.ChoiceField(choices=BookingStatus.choices) cancellation_reason = serializers.CharField(required=False, allow_blank=True) class BookingReviewSerializer(serializers.Serializer): rating = serializers.IntegerField(min_value=1, max_value=5) review = serializers.CharField(max_length=1000, allow_blank=True) def update(self, instance, validated_data): instance.rating = validated_data['rating'] instance.review = validated_data.get('review', '') instance.reviewed_at = timezone.now() instance.save() # Update caregiver aggregate rating caregiver = instance.caregiver bookings_with_reviews = Booking.objects.filter( caregiver=caregiver, rating__isnull=False ) count = bookings_with_reviews.count() if count: avg = sum(b.rating for b in bookings_with_reviews) / count caregiver.rating = round(avg, 2) caregiver.total_reviews = count caregiver.save(update_fields=['rating', 'total_reviews']) return instance # ───────────────────────────────────────────────────────────────────────────── # Messaging # ───────────────────────────────────────────────────────────────────────────── class MessageSerializer(serializers.ModelSerializer): sender = UserPublicSerializer(read_only=True) class Meta: model = Message fields = [ 'id', 'conversation', 'sender', 'message_type', 'content', 'image_url', 'is_read', 'read_at', 'created_at', ] read_only_fields = ['id', 'sender', 'is_read', 'read_at', 'created_at'] class ConversationSerializer(serializers.ModelSerializer): participants = UserPublicSerializer(many=True, read_only=True) last_message = serializers.SerializerMethodField() unread_count = serializers.SerializerMethodField() class Meta: model = Conversation fields = [ 'id', 'participants', 'booking', 'last_message', 'unread_count', 'created_at', 'updated_at', ] def get_last_message(self, obj): msg = obj.messages.last() if msg: return {'content': msg.content[:80], 'created_at': msg.created_at} return None def get_unread_count(self, obj): user = self.context['request'].user return obj.messages.filter(is_read=False).exclude(sender=user).count() # ───────────────────────────────────────────────────────────────────────────── # Image Upload # ───────────────────────────────────────────────────────────────────────────── class ImageUploadSerializer(serializers.Serializer): image = serializers.ImageField() folder = serializers.ChoiceField( choices=['caregivers', 'pets', 'messages'], default='caregivers', )