app.routes.bookings

Booking routing module.

Provides authenticated endpoints for submitting bookings, viewing bookings, and administering booking lifecycle transitions.

  1"""
  2Booking routing module.
  3
  4Provides authenticated endpoints for submitting bookings, viewing bookings,
  5and administering booking lifecycle transitions.
  6"""
  7
  8from __future__ import annotations
  9
 10from datetime import date, datetime, time
 11from typing import Literal
 12from uuid import UUID
 13
 14from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
 15from pydantic import BaseModel, ConfigDict
 16from sqlmodel.ext.asyncio.session import AsyncSession
 17
 18from app.database import get_session
 19from app.models import Booking, BookingStatus, RecurrenceFrequency, User
 20from app.services.auth import current_active_user, require_admin
 21from app.services.booking_service import (
 22    BookingConflictError,
 23    BookingNotFoundError,
 24    BookingServiceError,
 25    BookingStateError,
 26    get_all_bookings,
 27    get_user_bookings,
 28    process_booking_action,
 29    submit_booking,
 30)
 31
 32router = APIRouter(prefix="/api/bookings", tags=["bookings"])
 33
 34
 35class TimeSlotRead(BaseModel):
 36    model_config = ConfigDict(from_attributes=True)
 37
 38    id: int
 39    room_id: int
 40    slot_date: date
 41    start_time: time
 42    end_time: time
 43    status: str
 44    booking_id: int | None
 45
 46
 47class BookingRead(BaseModel):
 48    model_config = ConfigDict(from_attributes=True)
 49
 50    id: int
 51    userID: UUID
 52    submittedByRole: str
 53    roomID: int
 54    status: str
 55    recurrenceFrequency: str
 56    recurrenceEndDate: date | None
 57    createdAt: datetime
 58    timeSlots: list[TimeSlotRead]
 59
 60
 61class BookingCreate(BaseModel):
 62    room_id: int
 63    date: date
 64    slot_ids: list[int]
 65    recurrence_freq: Literal["none", "weekly"] = "none"
 66    recurrence_end_date: date | None = None
 67
 68
 69class BookingActionUpdate(BaseModel):
 70    action: Literal["approve", "deny", "cancel"]
 71
 72
 73def _translate_booking_error(exc: Exception) -> HTTPException:
 74    if isinstance(exc, BookingConflictError):
 75        return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
 76    if isinstance(exc, BookingNotFoundError):
 77        return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
 78    if isinstance(exc, (BookingServiceError, BookingStateError, ValueError)):
 79        return HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
 80    raise exc
 81
 82
 83@router.post("", response_model=BookingRead, status_code=status.HTTP_201_CREATED)
 84async def create_booking(
 85    booking_in: BookingCreate,
 86    response: Response,
 87    user: User = Depends(current_active_user),
 88    session: AsyncSession = Depends(get_session),
 89):
 90    try:
 91        booking = await session.run_sync(
 92            lambda sync_session: submit_booking(
 93                user=user,
 94                room_id=booking_in.room_id,
 95                slot_ids=booking_in.slot_ids,
 96                recurrence_freq=booking_in.recurrence_freq,
 97                recurrence_end_date=booking_in.recurrence_end_date,
 98                session=sync_session,
 99            )
100        )
101    except Exception as exc:
102        raise _translate_booking_error(exc) from exc
103
104    response.status_code = status.HTTP_201_CREATED
105    return booking
106
107
108@router.get("", response_model=list[BookingRead])
109async def list_bookings(
110    status_filter: str | None = Query(default=None, alias="status"),
111    user: User = Depends(current_active_user),
112    session: AsyncSession = Depends(get_session),
113):
114    try:
115        if user.role == "admin":
116            return await session.run_sync(
117                lambda sync_session: get_all_bookings(sync_session, status_filter)
118            )
119        return await session.run_sync(
120            lambda sync_session: get_user_bookings(user.id, sync_session)
121        )
122    except Exception as exc:
123        raise _translate_booking_error(exc) from exc
124
125
126@router.patch("/{booking_id}", response_model=BookingRead)
127async def update_booking(
128    booking_id: int,
129    booking_update: BookingActionUpdate,
130    admin_user: User = Depends(require_admin),
131    session: AsyncSession = Depends(get_session),
132):
133    del admin_user
134    try:
135        booking = await session.run_sync(
136            lambda sync_session: process_booking_action(
137                booking_id, booking_update.action, sync_session
138            )
139        )
140    except Exception as exc:
141        raise _translate_booking_error(exc) from exc
142
143    return booking
router = <fastapi.routing.APIRouter object>
class TimeSlotRead(pydantic.main.BaseModel):
36class TimeSlotRead(BaseModel):
37    model_config = ConfigDict(from_attributes=True)
38
39    id: int
40    room_id: int
41    slot_date: date
42    start_time: time
43    end_time: time
44    status: str
45    booking_id: int | None

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes: __class_vars__: The names of the class variables defined on the model. __private_attributes__: Metadata about the private attributes of the model. __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.

__pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
__pydantic_core_schema__: The core schema of the model.
__pydantic_custom_init__: Whether the model has a custom `__init__` function.
__pydantic_decorators__: Metadata containing the decorators defined on the model.
    This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.
__pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to
    __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
__pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__: The name of the post-init method for the model, if defined.
__pydantic_root_model__: Whether the model is a [`RootModel`][pydantic.root_model.RootModel].
__pydantic_serializer__: The `pydantic-core` `SchemaSerializer` used to dump instances of the model.
__pydantic_validator__: The `pydantic-core` `SchemaValidator` used to validate instances of the model.

__pydantic_fields__: A dictionary of field names and their corresponding [`FieldInfo`][pydantic.fields.FieldInfo] objects.
__pydantic_computed_fields__: A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects.

__pydantic_extra__: A dictionary containing extra values, if [`extra`][pydantic.config.ConfigDict.extra]
    is set to `'allow'`.
__pydantic_fields_set__: The names of fields explicitly set during instantiation.
__pydantic_private__: Values of private attributes set on the model instance.
id: int = PydanticUndefined
room_id: int = PydanticUndefined
slot_date: datetime.date = PydanticUndefined
start_time: datetime.time = PydanticUndefined
end_time: datetime.time = PydanticUndefined
status: str = PydanticUndefined
booking_id: int | None = PydanticUndefined
class BookingRead(pydantic.main.BaseModel):
48class BookingRead(BaseModel):
49    model_config = ConfigDict(from_attributes=True)
50
51    id: int
52    userID: UUID
53    submittedByRole: str
54    roomID: int
55    status: str
56    recurrenceFrequency: str
57    recurrenceEndDate: date | None
58    createdAt: datetime
59    timeSlots: list[TimeSlotRead]

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes: __class_vars__: The names of the class variables defined on the model. __private_attributes__: Metadata about the private attributes of the model. __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.

__pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
__pydantic_core_schema__: The core schema of the model.
__pydantic_custom_init__: Whether the model has a custom `__init__` function.
__pydantic_decorators__: Metadata containing the decorators defined on the model.
    This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.
__pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to
    __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
__pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__: The name of the post-init method for the model, if defined.
__pydantic_root_model__: Whether the model is a [`RootModel`][pydantic.root_model.RootModel].
__pydantic_serializer__: The `pydantic-core` `SchemaSerializer` used to dump instances of the model.
__pydantic_validator__: The `pydantic-core` `SchemaValidator` used to validate instances of the model.

__pydantic_fields__: A dictionary of field names and their corresponding [`FieldInfo`][pydantic.fields.FieldInfo] objects.
__pydantic_computed_fields__: A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects.

__pydantic_extra__: A dictionary containing extra values, if [`extra`][pydantic.config.ConfigDict.extra]
    is set to `'allow'`.
__pydantic_fields_set__: The names of fields explicitly set during instantiation.
__pydantic_private__: Values of private attributes set on the model instance.
id: int = PydanticUndefined
userID: uuid.UUID = PydanticUndefined
submittedByRole: str = PydanticUndefined
roomID: int = PydanticUndefined
status: str = PydanticUndefined
recurrenceFrequency: str = PydanticUndefined
recurrenceEndDate: datetime.date | None = PydanticUndefined
createdAt: datetime.datetime = PydanticUndefined
timeSlots: list[TimeSlotRead] = PydanticUndefined
class BookingCreate(pydantic.main.BaseModel):
62class BookingCreate(BaseModel):
63    room_id: int
64    date: date
65    slot_ids: list[int]
66    recurrence_freq: Literal["none", "weekly"] = "none"
67    recurrence_end_date: date | None = None

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes: __class_vars__: The names of the class variables defined on the model. __private_attributes__: Metadata about the private attributes of the model. __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.

__pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
__pydantic_core_schema__: The core schema of the model.
__pydantic_custom_init__: Whether the model has a custom `__init__` function.
__pydantic_decorators__: Metadata containing the decorators defined on the model.
    This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.
__pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to
    __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
__pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__: The name of the post-init method for the model, if defined.
__pydantic_root_model__: Whether the model is a [`RootModel`][pydantic.root_model.RootModel].
__pydantic_serializer__: The `pydantic-core` `SchemaSerializer` used to dump instances of the model.
__pydantic_validator__: The `pydantic-core` `SchemaValidator` used to validate instances of the model.

__pydantic_fields__: A dictionary of field names and their corresponding [`FieldInfo`][pydantic.fields.FieldInfo] objects.
__pydantic_computed_fields__: A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects.

__pydantic_extra__: A dictionary containing extra values, if [`extra`][pydantic.config.ConfigDict.extra]
    is set to `'allow'`.
__pydantic_fields_set__: The names of fields explicitly set during instantiation.
__pydantic_private__: Values of private attributes set on the model instance.
room_id: int = PydanticUndefined
date: datetime.date = PydanticUndefined
slot_ids: list[int] = PydanticUndefined
recurrence_freq: Literal['none', 'weekly'] = 'none'
recurrence_end_date: datetime.date | None = None
class BookingActionUpdate(pydantic.main.BaseModel):
70class BookingActionUpdate(BaseModel):
71    action: Literal["approve", "deny", "cancel"]

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes: __class_vars__: The names of the class variables defined on the model. __private_attributes__: Metadata about the private attributes of the model. __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.

__pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
__pydantic_core_schema__: The core schema of the model.
__pydantic_custom_init__: Whether the model has a custom `__init__` function.
__pydantic_decorators__: Metadata containing the decorators defined on the model.
    This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.
__pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to
    __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
__pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__: The name of the post-init method for the model, if defined.
__pydantic_root_model__: Whether the model is a [`RootModel`][pydantic.root_model.RootModel].
__pydantic_serializer__: The `pydantic-core` `SchemaSerializer` used to dump instances of the model.
__pydantic_validator__: The `pydantic-core` `SchemaValidator` used to validate instances of the model.

__pydantic_fields__: A dictionary of field names and their corresponding [`FieldInfo`][pydantic.fields.FieldInfo] objects.
__pydantic_computed_fields__: A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects.

__pydantic_extra__: A dictionary containing extra values, if [`extra`][pydantic.config.ConfigDict.extra]
    is set to `'allow'`.
__pydantic_fields_set__: The names of fields explicitly set during instantiation.
__pydantic_private__: Values of private attributes set on the model instance.
action: Literal['approve', 'deny', 'cancel'] = PydanticUndefined
@router.post('', response_model=BookingRead, status_code=status.HTTP_201_CREATED)
async def create_booking( booking_in: BookingCreate, response: starlette.responses.Response, user: app.models.User = Depends(dependency=<function Authenticator.current_user.<locals>.current_user_dependency>, use_cache=True, scope=None), session: sqlmodel.ext.asyncio.session.AsyncSession = Depends(dependency=<function get_session>, use_cache=True, scope=None)):
 84@router.post("", response_model=BookingRead, status_code=status.HTTP_201_CREATED)
 85async def create_booking(
 86    booking_in: BookingCreate,
 87    response: Response,
 88    user: User = Depends(current_active_user),
 89    session: AsyncSession = Depends(get_session),
 90):
 91    try:
 92        booking = await session.run_sync(
 93            lambda sync_session: submit_booking(
 94                user=user,
 95                room_id=booking_in.room_id,
 96                slot_ids=booking_in.slot_ids,
 97                recurrence_freq=booking_in.recurrence_freq,
 98                recurrence_end_date=booking_in.recurrence_end_date,
 99                session=sync_session,
100            )
101        )
102    except Exception as exc:
103        raise _translate_booking_error(exc) from exc
104
105    response.status_code = status.HTTP_201_CREATED
106    return booking
@router.get('', response_model=list[BookingRead])
async def list_bookings( status_filter: str | None = Query(None), user: app.models.User = Depends(dependency=<function Authenticator.current_user.<locals>.current_user_dependency>, use_cache=True, scope=None), session: sqlmodel.ext.asyncio.session.AsyncSession = Depends(dependency=<function get_session>, use_cache=True, scope=None)):
109@router.get("", response_model=list[BookingRead])
110async def list_bookings(
111    status_filter: str | None = Query(default=None, alias="status"),
112    user: User = Depends(current_active_user),
113    session: AsyncSession = Depends(get_session),
114):
115    try:
116        if user.role == "admin":
117            return await session.run_sync(
118                lambda sync_session: get_all_bookings(sync_session, status_filter)
119            )
120        return await session.run_sync(
121            lambda sync_session: get_user_bookings(user.id, sync_session)
122        )
123    except Exception as exc:
124        raise _translate_booking_error(exc) from exc
@router.patch('/{booking_id}', response_model=BookingRead)
async def update_booking( booking_id: int, booking_update: BookingActionUpdate, admin_user: app.models.User = Depends(dependency=<function require_admin>, use_cache=True, scope=None), session: sqlmodel.ext.asyncio.session.AsyncSession = Depends(dependency=<function get_session>, use_cache=True, scope=None)):
127@router.patch("/{booking_id}", response_model=BookingRead)
128async def update_booking(
129    booking_id: int,
130    booking_update: BookingActionUpdate,
131    admin_user: User = Depends(require_admin),
132    session: AsyncSession = Depends(get_session),
133):
134    del admin_user
135    try:
136        booking = await session.run_sync(
137            lambda sync_session: process_booking_action(
138                booking_id, booking_update.action, sync_session
139            )
140        )
141    except Exception as exc:
142        raise _translate_booking_error(exc) from exc
143
144    return booking