fix error in gitignore

This commit is contained in:
2026-09-24 12:32:59 +03:30
commit 86c117ee2a
165 changed files with 5963 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# reservations app
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ReservationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.reservations"
@@ -0,0 +1,86 @@
# Generated by Django 5.2.17 on 2026-09-15 09:29
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Facility',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=200)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Holiday',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('date', models.DateField(unique=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ProcessingLine',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=200)),
('facility', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='reservations.facility')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Slot',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('date', models.DateField(db_index=True)),
('start_time', models.TimeField()),
('end_time', models.TimeField()),
('capacity', models.PositiveIntegerField(default=1)),
('max_weight_kg', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
('booked_count', models.PositiveIntegerField(default=0)),
('blocked', models.BooleanField(default=False)),
('facility', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slots', to='reservations.facility')),
('line', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='slots', to='reservations.processingline')),
],
),
migrations.CreateModel(
name='Reservation',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('status', models.CharField(choices=[('requested', 'Requested'), ('confirmed', 'Confirmed'), ('arrived', 'Arrived'), ('weighed', 'Weighed'), ('cancelled', 'Cancelled'), ('expired', 'Expired')], default='requested', max_length=16)),
('notes', models.TextField(blank=True)),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='accounts.customer')),
('slot', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reservations', to='reservations.slot')),
],
options={
'ordering': ['-created_at'],
},
),
migrations.AddConstraint(
model_name='slot',
constraint=models.UniqueConstraint(fields=('facility', 'line', 'date', 'start_time'), name='uniq_slot_time'),
),
]
@@ -0,0 +1,56 @@
# Generated by Django 5.2.17 on 2026-09-15 11:56
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('reservations', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='reservation',
name='approved_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='reservation',
name='approved_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approved_reservations', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='reservation',
name='title',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='reservation',
name='status',
field=models.CharField(choices=[('pending', 'Pending approval'), ('confirmed', 'Confirmed'), ('rejected', 'Rejected'), ('arrived', 'Arrived'), ('weighed', 'Weighed'), ('cancelled', 'Cancelled'), ('expired', 'Expired')], default='pending', max_length=16),
),
migrations.CreateModel(
name='FacilitySchedule',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('date', models.DateField(db_index=True)),
('is_open', models.BooleanField(default=True)),
('capacity', models.PositiveIntegerField(default=2)),
('start_time', models.TimeField(blank=True, null=True)),
('end_time', models.TimeField(blank=True, null=True)),
('note', models.CharField(blank=True, max_length=200)),
('facility', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='reservations.facility')),
],
options={
'ordering': ['-date'],
'constraints': [models.UniqueConstraint(fields=('facility', 'date'), name='uniq_facility_schedule_date')],
},
),
]
+102
View File
@@ -0,0 +1,102 @@
from django.db import models
from django.utils import timezone
from apps.core.models import TimeStampedModel, UUIDModel
class Facility(UUIDModel, TimeStampedModel):
name = models.CharField(max_length=200)
def __str__(self) -> str:
return self.name
class ProcessingLine(UUIDModel, TimeStampedModel):
facility = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name="lines")
name = models.CharField(max_length=200)
def __str__(self) -> str:
return f"{self.facility.name} / {self.name}"
class Holiday(UUIDModel):
date = models.DateField(unique=True)
class FacilitySchedule(UUIDModel, TimeStampedModel):
"""Per-facility, per-date availability: capacity for the day and days off.
If a schedule row exists for a date and ``is_open`` is False, no slots are
generated for that day. ``capacity``/``start_time``/``end_time`` override the
global Reservation settings for that date.
"""
facility = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name="schedules")
date = models.DateField(db_index=True)
is_open = models.BooleanField(default=True)
capacity = models.PositiveIntegerField(default=2)
start_time = models.TimeField(null=True, blank=True)
end_time = models.TimeField(null=True, blank=True)
note = models.CharField(max_length=200, blank=True)
class Meta:
ordering = ["-date"]
constraints = [
models.UniqueConstraint(fields=["facility", "date"], name="uniq_facility_schedule_date")
]
def __str__(self) -> str:
return f"{self.facility.name} {self.date}"
class Slot(UUIDModel):
facility = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name="slots")
line = models.ForeignKey(
ProcessingLine, on_delete=models.CASCADE, related_name="slots", null=True, blank=True
)
date = models.DateField(db_index=True)
start_time = models.TimeField()
end_time = models.TimeField()
capacity = models.PositiveIntegerField(default=1)
max_weight_kg = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
booked_count = models.PositiveIntegerField(default=0)
blocked = models.BooleanField(default=False)
class Meta:
constraints = [
models.UniqueConstraint(fields=["facility", "line", "date", "start_time"], name="uniq_slot_time")
]
def is_available(self) -> bool:
return (
not self.blocked
and self.booked_count < self.capacity
and self.date >= timezone.localdate()
)
class Reservation(UUIDModel, TimeStampedModel):
class Status(models.TextChoices):
PENDING = "pending", "Pending approval"
CONFIRMED = "confirmed", "Confirmed"
REJECTED = "rejected", "Rejected"
ARRIVED = "arrived", "Arrived"
WEIGHED = "weighed", "Weighed"
CANCELLED = "cancelled", "Cancelled"
EXPIRED = "expired", "Expired"
title = models.CharField(max_length=200, blank=True)
customer = models.ForeignKey("accounts.Customer", on_delete=models.CASCADE, related_name="reservations")
slot = models.ForeignKey(Slot, on_delete=models.PROTECT, related_name="reservations")
status = models.CharField(max_length=16, choices=Status.choices, default=Status.PENDING)
notes = models.TextField(blank=True)
approved_by = models.ForeignKey(
"accounts.User", null=True, blank=True, on_delete=models.SET_NULL, related_name="approved_reservations"
)
approved_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return self.title or f"Reservation {self.id}"
+53
View File
@@ -0,0 +1,53 @@
from rest_framework import serializers
from apps.reservations.models import Facility, FacilitySchedule, Reservation, Slot
class FacilitySerializer(serializers.ModelSerializer):
class Meta:
model = Facility
fields = ["id", "name"]
class SlotSerializer(serializers.ModelSerializer):
facility_name = serializers.CharField(source="facility.name", read_only=True)
available = serializers.SerializerMethodField()
class Meta:
model = Slot
fields = [
"id", "facility", "facility_name", "line", "date", "start_time",
"end_time", "capacity", "max_weight_kg", "booked_count", "blocked", "available",
]
def get_available(self, obj):
return obj.is_available()
class FacilityScheduleSerializer(serializers.ModelSerializer):
facility_name = serializers.CharField(source="facility.name", read_only=True)
class Meta:
model = FacilitySchedule
fields = [
"id", "facility", "facility_name", "date", "is_open",
"capacity", "start_time", "end_time", "note",
]
class ReservationSerializer(serializers.ModelSerializer):
slot = SlotSerializer(read_only=True)
customer_id = serializers.UUIDField(source="customer.id", read_only=True)
customer_name = serializers.SerializerMethodField()
class Meta:
model = Reservation
fields = [
"id", "title", "customer", "customer_id", "customer_name", "slot",
"status", "notes", "approved_at", "created_at",
]
read_only_fields = ["customer", "status", "approved_at", "created_at"]
def get_customer_name(self, obj):
user = obj.customer.user
return obj.customer.company_name or user.get_full_name() or user.username
+91
View File
@@ -0,0 +1,91 @@
from django.db import transaction
from django.utils import timezone
from apps.core.errors import ApiError
from apps.reservations.models import Reservation, Slot
class ReservationService:
"""Booking with backend-enforced availability, double-booking prevention,
and admin approval (which triggers a customer SMS)."""
@transaction.atomic
def book(self, customer, slot, title: str = "", notes: str = "") -> Reservation:
slot = Slot.objects.select_for_update().get(pk=slot.pk)
if slot.blocked:
raise ApiError("SLOT_BLOCKED", "This slot is blocked.")
if slot.date < timezone.localdate():
raise ApiError("SLOT_PAST", "Cannot book a slot in the past.")
if slot.booked_count >= slot.capacity:
raise ApiError("SLOT_FULL", "This slot is fully booked.")
slot.booked_count += 1
slot.save(update_fields=["booked_count"])
return Reservation.objects.create(
customer=customer,
slot=slot,
title=title,
status=Reservation.Status.PENDING,
notes=notes,
)
@transaction.atomic
def approve(self, reservation, user) -> Reservation:
reservation = Reservation.objects.select_for_update().select_related("slot", "customer").get(pk=reservation.pk)
if reservation.status != Reservation.Status.PENDING:
raise ApiError("INVALID_STATUS", "Only pending reservations can be approved.")
reservation.status = Reservation.Status.CONFIRMED
reservation.approved_by = user
reservation.approved_at = timezone.now()
reservation.save(update_fields=["status", "approved_by", "approved_at"])
from apps.notifications.services import NotificationService
customer = reservation.customer
NotificationService().send_event(
"reservation.approved",
{
"customer_name": customer.company_name or customer.user.get_full_name() or customer.user.username,
"reservation_title": reservation.title,
"slot_date": str(reservation.slot.date),
"slot_time": str(reservation.slot.start_time),
},
recipient=customer.user.phone or customer.user.username,
)
return reservation
@transaction.atomic
def reject(self, reservation, user) -> Reservation:
reservation = Reservation.objects.select_for_update().select_related("slot").get(pk=reservation.pk)
if reservation.status not in (Reservation.Status.PENDING, Reservation.Status.CONFIRMED):
raise ApiError("INVALID_STATUS", "This reservation cannot be rejected.")
reservation.status = Reservation.Status.REJECTED
reservation.approved_by = user
reservation.approved_at = timezone.now()
reservation.save(update_fields=["status", "approved_by", "approved_at"])
slot = reservation.slot
slot.booked_count = max(0, slot.booked_count - 1)
slot.save(update_fields=["booked_count"])
return reservation
@transaction.atomic
def cancel(self, reservation) -> Reservation:
reservation = (
Reservation.objects.select_for_update()
.select_related("slot")
.get(pk=reservation.pk)
)
if reservation.status == Reservation.Status.CANCELLED:
return reservation
if reservation.status == Reservation.Status.WEIGHED:
raise ApiError("RESERVATION_LOCKED", "Cannot cancel a reservation that has been weighed.")
reservation.status = Reservation.Status.CANCELLED
reservation.save(update_fields=["status"])
slot = reservation.slot
slot.booked_count = max(0, slot.booked_count - 1)
slot.save(update_fields=["booked_count"])
return reservation
@transaction.atomic
def reschedule(self, reservation, new_slot) -> Reservation:
self.cancel(reservation)
return self.book(reservation.customer, new_slot, title=reservation.title)
+56
View File
@@ -0,0 +1,56 @@
"""Dynamic slot generation.
Slots are generated from runtime settings (working hours, slot duration,
default capacity) and per-day `FacilitySchedule` overrides (capacity for the
day, days off). Generation is idempotent — re-running updates existing slots
instead of duplicating them.
"""
from datetime import datetime, timedelta
from apps.core.settings_service import SettingsService
from apps.reservations.models import FacilitySchedule, Slot
class SlotService:
def generate_slots(self, facility, day, line=None) -> int:
schedule = FacilitySchedule.objects.filter(facility=facility, date=day).first()
if schedule is not None and not schedule.is_open:
return 0 # day off
service = SettingsService()
if schedule is not None and schedule.start_time:
start_str = schedule.start_time.strftime("%H:%M")
else:
start_str = service.get("reservation.working_hours_start", "08:00")
if schedule is not None and schedule.end_time:
end_str = schedule.end_time.strftime("%H:%M")
else:
end_str = service.get("reservation.working_hours_end", "17:00")
duration = int(service.get("reservation.slot_duration_minutes", 30))
if schedule is not None:
capacity = schedule.capacity
else:
capacity = int(service.get("reservation.default_capacity", 2))
if duration <= 0:
raise ValueError("Slot duration must be positive.")
start_t = datetime.strptime(start_str, "%H:%M").time()
end_t = datetime.strptime(end_str, "%H:%M").time()
current = datetime.combine(day, start_t)
end_dt = datetime.combine(day, end_t)
created = 0
while current + timedelta(minutes=duration) <= end_dt:
slot_end = (current + timedelta(minutes=duration)).time()
_, was_created = Slot.objects.update_or_create(
facility=facility,
line=line,
date=day,
start_time=current.time(),
defaults={"end_time": slot_end, "capacity": capacity},
)
if was_created:
created += 1
current += timedelta(minutes=duration)
return created
+82
View File
@@ -0,0 +1,82 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone
from apps.accounts.models import Customer
from apps.core.errors import ApiError
from apps.reservations.models import Facility, Slot
from apps.reservations.services import ReservationService
User = get_user_model()
class ReservationTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="c1", password="x")
self.customer = Customer.objects.create(user=self.user)
self.facility = Facility.objects.create(name="F1")
self.slot = Slot.objects.create(
facility=self.facility,
date=timezone.localdate(),
start_time="08:00",
end_time="08:30",
capacity=1,
)
self.service = ReservationService()
def test_double_booking_prevented(self):
self.service.book(self.customer, self.slot)
with self.assertRaises(ApiError) as ctx:
self.service.book(self.customer, self.slot)
self.assertEqual(ctx.exception.code, "SLOT_FULL")
def test_cancel_frees_slot(self):
reservation = self.service.book(self.customer, self.slot)
self.service.cancel(reservation)
self.slot.refresh_from_db()
self.assertEqual(self.slot.booked_count, 0)
def test_cannot_book_blocked_slot(self):
self.slot.blocked = True
self.slot.save()
with self.assertRaises(ApiError) as ctx:
self.service.book(self.customer, self.slot)
self.assertEqual(ctx.exception.code, "SLOT_BLOCKED")
def test_generate_slots_from_dynamic_settings(self):
from apps.core.settings_service import SettingsService
from apps.reservations.slot_service import SlotService
facility2 = Facility.objects.create(name="F2")
svc = SettingsService()
svc.set("reservation.working_hours_start", "08:00")
svc.set("reservation.working_hours_end", "09:00")
svc.set("reservation.slot_duration_minutes", 30)
svc.set("reservation.default_capacity", 3)
created = SlotService().generate_slots(facility2, timezone.localdate())
self.assertEqual(created, 2)
slots = Slot.objects.filter(facility=facility2).order_by("start_time")
self.assertEqual(slots.count(), 2)
self.assertEqual(slots.first().capacity, 3)
self.assertEqual(str(slots.first().end_time), "08:30:00")
def test_day_off_generates_no_slots(self):
from apps.reservations.models import FacilitySchedule
from apps.reservations.slot_service import SlotService
facility3 = Facility.objects.create(name="F3")
FacilitySchedule.objects.create(facility=facility3, date=timezone.localdate(), is_open=False)
created = SlotService().generate_slots(facility3, timezone.localdate())
self.assertEqual(created, 0)
def test_approve_sends_sms(self):
from apps.notifications.models import NotificationLog, NotificationTemplate
NotificationTemplate.objects.create(event="reservation.approved", channel="sms", body="Hi {{customer_name}}")
reservation = self.service.book(self.customer, self.slot, title="Batch A")
self.assertEqual(reservation.status, "pending")
self.service.approve(reservation, self.user)
reservation.refresh_from_db()
self.assertEqual(reservation.status, "confirmed")
self.assertEqual(NotificationLog.objects.filter(status="sent").count(), 1)
+16
View File
@@ -0,0 +1,16 @@
from rest_framework.routers import DefaultRouter
from apps.reservations.views import (
FacilityScheduleViewSet,
FacilityViewSet,
ReservationViewSet,
SlotViewSet,
)
router = DefaultRouter()
router.register("facilities", FacilityViewSet, basename="facility")
router.register("schedules", FacilityScheduleViewSet, basename="facility-schedule")
router.register("slots", SlotViewSet, basename="slot")
router.register("", ReservationViewSet, basename="reservation")
urlpatterns = router.urls
+171
View File
@@ -0,0 +1,171 @@
from datetime import date as date_cls
from rest_framework import decorators, mixins, response, status, viewsets
from rest_framework.permissions import IsAuthenticated
from apps.core.errors import ApiError, error_response
from apps.rbac.permissions import ActionPermission
from apps.reservations.models import Facility, FacilitySchedule, ProcessingLine, Reservation, Slot
from apps.reservations.serializers import (
FacilityScheduleSerializer,
FacilitySerializer,
ReservationSerializer,
SlotSerializer,
)
from apps.reservations.services import ReservationService
from apps.reservations.slot_service import SlotService
class FacilityViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Facility.objects.all()
serializer_class = FacilitySerializer
permission_classes = [IsAuthenticated]
class FacilityScheduleViewSet(viewsets.ModelViewSet):
queryset = FacilitySchedule.objects.select_related("facility").all()
serializer_class = FacilityScheduleSerializer
permission_classes = [ActionPermission]
filterset_fields = ["facility", "date"]
code_map = {
"list": "reservation.view",
"retrieve": "reservation.view",
"create": "slot.manage",
"update": "slot.manage",
"partial_update": "slot.manage",
"destroy": "slot.manage",
}
class SlotViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
queryset = Slot.objects.select_related("facility", "line").all()
serializer_class = SlotSerializer
permission_classes = [ActionPermission]
filterset_fields = ["facility", "date", "blocked"]
code_map = {
"list": "reservation.view",
"retrieve": "reservation.view",
"create": "slot.manage",
"update": "slot.manage",
"partial_update": "slot.manage",
"destroy": "slot.manage",
"generate": "slot.manage",
}
@decorators.action(detail=False, methods=["post"])
def generate(self, request):
facility_id = request.data.get("facility")
date_str = request.data.get("date")
line_id = request.data.get("line")
if not facility_id or not date_str:
return response.Response(
error_response("INVALID_INPUT", "facility and date are required."),
status=status.HTTP_400_BAD_REQUEST,
)
try:
facility = Facility.objects.get(pk=facility_id)
except Facility.DoesNotExist:
return response.Response(
error_response("FACILITY_NOT_FOUND", "Facility not found."),
status=status.HTTP_404_NOT_FOUND,
)
line = None
if line_id:
try:
line = ProcessingLine.objects.get(pk=line_id)
except ProcessingLine.DoesNotExist:
return response.Response(
error_response("LINE_NOT_FOUND", "Processing line not found."),
status=status.HTTP_404_NOT_FOUND,
)
try:
day = date_cls.fromisoformat(date_str)
except ValueError:
return response.Response(
error_response("INVALID_DATE", "Invalid date."),
status=status.HTTP_400_BAD_REQUEST,
)
try:
created = SlotService().generate_slots(facility, day, line)
except ValueError as exc:
return response.Response(
error_response("INVALID_SETTINGS", str(exc)),
status=status.HTTP_400_BAD_REQUEST,
)
return response.Response({"created": created})
class ReservationViewSet(viewsets.ModelViewSet):
serializer_class = ReservationSerializer
permission_classes = [ActionPermission]
filterset_fields = ["status"]
code_map = {
"list": "reservation.view",
"retrieve": "reservation.view",
"create": "reservation.create",
"cancel": "reservation.cancel",
"approve": "reservation.approve",
"reject": "reservation.approve",
}
service = ReservationService()
def get_queryset(self):
user = self.request.user
if user.is_superuser or user.user_roles.filter(role__slug__in=["administrator", "facility_manager"]).exists():
return Reservation.objects.select_related("customer__user", "slot").all()
return Reservation.objects.select_related("customer__user", "slot").filter(customer__user=user)
def perform_create(self, serializer):
slot_id = self.request.data.get("slot")
if not slot_id:
raise ApiError("SLOT_REQUIRED", "A slot id is required.")
try:
reservation = self.service.book(
self._customer(),
Slot.objects.get(pk=slot_id),
title=self.request.data.get("title", ""),
notes=self.request.data.get("notes", ""),
)
except Slot.DoesNotExist:
raise ApiError("SLOT_NOT_FOUND", "Slot not found.")
serializer.instance = reservation
def _customer(self):
from apps.accounts.models import Customer
customer, _ = Customer.objects.get_or_create(user=self.request.user)
return customer
@decorators.action(detail=True, methods=["post"])
def cancel(self, request, pk=None):
reservation = self.get_object()
try:
self.service.cancel(reservation)
except ApiError as exc:
return response.Response(error_response(exc.code, exc.message, exc.details), status=exc.status)
return response.Response(ReservationSerializer(reservation).data)
@decorators.action(detail=True, methods=["post"])
def approve(self, request, pk=None):
reservation = self.get_object()
try:
reservation = self.service.approve(reservation, request.user)
except ApiError as exc:
return response.Response(error_response(exc.code, exc.message, exc.details), status=exc.status)
return response.Response(ReservationSerializer(reservation).data)
@decorators.action(detail=True, methods=["post"])
def reject(self, request, pk=None):
reservation = self.get_object()
try:
reservation = self.service.reject(reservation, request.user)
except ApiError as exc:
return response.Response(error_response(exc.code, exc.message, exc.details), status=exc.status)
return response.Response(ReservationSerializer(reservation).data)