from django.db import transaction from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from apps.bookings.models import Booking, BookingStatus from apps.bookings.services import validate_booking_request from apps.salons.models import Service, StaffProfile class BookingSerializer(serializers.ModelSerializer): salon_name = serializers.CharField(source="salon.name", read_only=True) service_name = serializers.CharField(source="service.name", read_only=True) staff_name = serializers.SerializerMethodField() class Meta: model = Booking fields = [ "id", "salon", "salon_name", "service", "service_name", "staff", "staff_name", "start_time", "end_time", "status", "price_amount", "currency", "notes", "created_at", ] read_only_fields = ["id", "salon", "price_amount", "currency", "created_at"] def get_staff_name(self, obj): if not obj.staff: return None return obj.staff.user.display_name def validate(self, attrs): if not self.instance or "status" not in attrs: return attrs new_status = attrs["status"] old_status = self.instance.status if new_status == old_status: return attrs user = self.context["request"].user role = getattr(user, "role", None) if new_status == BookingStatus.CONFIRMED and role not in {"manager", "staff", "admin"}: raise serializers.ValidationError({"status": _("Only staff or managers can confirm bookings.")}) if new_status == BookingStatus.COMPLETED and role not in {"manager", "staff", "admin"}: raise serializers.ValidationError({"status": _("Only staff or managers can complete bookings.")}) if new_status == BookingStatus.CANCELLED and role not in {"manager", "staff", "admin", "customer"}: raise serializers.ValidationError({"status": _("You are not allowed to cancel this booking.")}) return attrs class BookingCreateSerializer(serializers.ModelSerializer): class Meta: model = Booking fields = ["service", "staff", "start_time", "end_time", "notes"] extra_kwargs = {"staff": {"required": True}} def validate(self, attrs): service: Service = attrs["service"] staff = attrs.get("staff") validate_booking_request(service, staff, attrs["start_time"], attrs["end_time"]) return attrs def create(self, validated_data): request = self.context["request"] service = validated_data["service"] staff = validated_data.get("staff") start_time = validated_data["start_time"] end_time = validated_data["end_time"] with transaction.atomic(): # Lock the staff row so concurrent booking requests for the same staff # member are serialized. Without this, two requests that both pass the # overlap check in validate() can race and both commit overlapping # bookings. On SQLite (dev/tests) the FOR UPDATE clause is silently # ignored but the transaction still serializes writes; PostgreSQL # (production) gets true row-level locking. StaffProfile.objects.select_for_update().get(pk=staff.pk) # Re-run the overlap check inside the lock so the check and the insert # are atomic with respect to other writers. overlap = Booking.objects.filter( staff=staff, status__in=[BookingStatus.PENDING, BookingStatus.CONFIRMED], start_time__lt=end_time, end_time__gt=start_time, ).exists() if overlap: raise serializers.ValidationError( {"start_time": _("Booking overlaps an existing appointment")} ) return Booking.objects.create( salon=service.salon, customer=request.user, service=service, staff=staff, start_time=start_time, end_time=end_time, notes=validated_data.get("notes", ""), price_amount=service.price_amount, currency=service.currency, )