Files
Salon/backend/apps/bookings/serializers.py
T

113 lines
4.3 KiB
Python

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,
)