app.services
Service layer package for the room-booking system.
This package encapsulates the core business logic and authentication integrations of the application, separating them from the routing and data access layers. It contains modules for user management, authentication strategies, and other business workflows.
1""" 2Service layer package for the room-booking system. 3 4This package encapsulates the core business logic and authentication integrations 5of the application, separating them from the routing and data access layers. 6It contains modules for user management, authentication strategies, and other 7business workflows. 8""" 9 10from .booking_service import ( 11 BookingConflictError, 12 BookingNotFoundError, 13 BookingServiceError, 14 BookingStateError, 15 approve_booking, 16 cancel_booking, 17 deny_booking, 18 get_all_bookings, 19 get_pending_bookings, 20 get_user_bookings, 21 process_booking_action, 22 submit_booking, 23) 24from .notification_service import ( 25 NotificationNotFoundError, 26 NotificationServiceError, 27 get_notification_for_user, 28 get_notifications, 29 get_unread_count, 30 mark_read, 31 send_notification, 32) 33 34from .room_service import ( 35 RoomNotFoundError, 36 RoomServiceError, 37) 38 39__all__ = [ 40 "BookingServiceError", 41 "BookingNotFoundError", 42 "BookingConflictError", 43 "BookingStateError", 44 "submit_booking", 45 "approve_booking", 46 "deny_booking", 47 "cancel_booking", 48 "get_user_bookings", 49 "get_all_bookings", 50 "get_pending_bookings", 51 "process_booking_action", 52 "NotificationServiceError", 53 "NotificationNotFoundError", 54 "RoomServiceError", 55 "RoomNotFoundError", 56 "send_notification", 57 "get_notifications", 58 "get_notification_for_user", 59 "mark_read", 60 "get_unread_count", 61]
class
BookingServiceError(builtins.ValueError):
Base booking workflow error.
31class BookingNotFoundError(BookingServiceError): 32 """Raised when a booking or timeslot cannot be found."""
Raised when a booking or timeslot cannot be found.
35class BookingConflictError(BookingServiceError): 36 """Raised when a slot cannot be booked because it is unavailable."""
Raised when a slot cannot be booked because it is unavailable.
39class BookingStateError(BookingServiceError): 40 """Raised when a booking lifecycle transition is invalid."""
Raised when a booking lifecycle transition is invalid.
def
submit_booking( user: app.models.User, room_id: int, slot_ids: list[int], recurrence_freq: str | app.models.RecurrenceFrequency, recurrence_end_date, session: sqlmodel.orm.session.Session) -> app.models.Booking:
106def submit_booking( 107 user: User, 108 room_id: int, 109 slot_ids: list[int], 110 recurrence_freq: str | RecurrenceFrequency, 111 recurrence_end_date, 112 session: Session, 113) -> Booking: 114 recurrence_frequency = RecurrenceFrequency(recurrence_freq) 115 anchor_slots = _get_slots_by_id(session, slot_ids) 116 117 for slot in anchor_slots: 118 if slot.room_id != room_id: 119 raise BookingConflictError( 120 f"TimeSlot {slot.id} does not belong to room {room_id}." 121 ) 122 123 if recurrence_frequency == RecurrenceFrequency.WEEKLY: 124 if recurrence_end_date is None: 125 raise BookingServiceError( 126 "recurrence_end_date is required when recurrence_freq is weekly." 127 ) 128 earliest_date = min(slot.slot_date for slot in anchor_slots) 129 if recurrence_end_date < earliest_date: 130 raise BookingServiceError( 131 "recurrence_end_date must be on or after the anchor slot date." 132 ) 133 elif recurrence_end_date is not None: 134 raise BookingStateError( 135 "recurrence_end_date can only be provided for weekly recurrence." 136 ) 137 138 target_slots = _resolve_recurrence( 139 session, anchor_slots, recurrence_frequency, recurrence_end_date 140 ) 141 142 unavailable_slots = [ 143 slot.id for slot in target_slots if slot.status != TimeslotStatus.AVAILABLE 144 ] 145 if unavailable_slots: 146 slot_list = ", ".join(str(slot_id) for slot_id in unavailable_slots) 147 raise BookingConflictError(f"TimeSlot(s) unavailable: {slot_list}") 148 149 for slot in target_slots: 150 slot.hold() 151 152 booking = Booking( 153 userID=user.id, 154 submittedByRole=user.role, 155 roomID=room_id, 156 status=BookingStatus.PENDING, 157 recurrenceFrequency=recurrence_frequency, 158 recurrenceEndDate=recurrence_end_date, 159 timeSlots=target_slots, 160 ) 161 session.add(booking) 162 session.commit() 163 session.refresh(booking) 164 return _get_booking(session, booking.id)
167def approve_booking(booking_id: int, session: Session) -> Booking: 168 booking = _get_booking(session, booking_id) 169 if booking.status != BookingStatus.PENDING: 170 raise BookingStateError("Only pending bookings can be approved.") 171 172 invalid_slots = [ 173 slot.id for slot in booking.timeSlots if slot.status != TimeslotStatus.HELD 174 ] 175 if invalid_slots: 176 slot_list = ", ".join(str(slot_id) for slot_id in invalid_slots) 177 raise BookingStateError(f"Booking slots are not held: {slot_list}") 178 179 for slot in booking.timeSlots: 180 slot.book() 181 182 booking.status = BookingStatus.APPROVED 183 session.add(booking) 184 session.commit() 185 session.refresh(booking) 186 return _get_booking(session, booking.id)
189def deny_booking(booking_id: int, session: Session) -> Booking: 190 booking = _get_booking(session, booking_id) 191 if booking.status != BookingStatus.PENDING: 192 raise BookingStateError("Only pending bookings can be denied.") 193 194 invalid_slots = [ 195 slot.id for slot in booking.timeSlots if slot.status != TimeslotStatus.HELD 196 ] 197 if invalid_slots: 198 slot_list = ", ".join(str(slot_id) for slot_id in invalid_slots) 199 raise BookingStateError(f"Booking slots cannot be denied: {slot_list}") 200 201 for slot in booking.timeSlots: 202 slot.release() 203 204 booking.status = BookingStatus.DENIED 205 session.add(booking) 206 session.commit() 207 session.refresh(booking) 208 return _get_booking(session, booking.id)
211def cancel_booking(booking_id: int, session: Session) -> Booking: 212 booking = _get_booking(session, booking_id) 213 if booking.status != BookingStatus.APPROVED: 214 raise BookingStateError("Only approved bookings can be cancelled.") 215 216 invalid_slots = [ 217 slot.id for slot in booking.timeSlots if slot.status != TimeslotStatus.BOOKED 218 ] 219 if invalid_slots: 220 slot_list = ", ".join(str(slot_id) for slot_id in invalid_slots) 221 raise BookingStateError(f"Booking slots cannot be cancelled: {slot_list}") 222 223 for slot in booking.timeSlots: 224 slot.release() 225 226 booking.status = BookingStatus.CANCELLED 227 session.add(booking) 228 session.commit() 229 session.refresh(booking) 230 return _get_booking(session, booking.id)
def
get_all_bookings( session: sqlmodel.orm.session.Session, status: str | app.models.BookingStatus | None = None) -> list[app.models.Booking]:
252def get_all_bookings( 253 session: Session, status: str | BookingStatus | None = None 254) -> list[Booking]: 255 statement = select(Booking).options(selectinload(Booking.timeSlots)) 256 if status is not None: 257 statement = statement.where(Booking.status == BookingStatus(status)) 258 259 statement = statement.order_by(Booking.createdAt.desc(), Booking.id.desc()) 260 return list(session.exec(statement))
def
process_booking_action( booking_id: int, action: str, session: sqlmodel.orm.session.Session) -> app.models.Booking:
269def process_booking_action(booking_id: int, action: str, session: Session) -> Booking: 270 """Execute a booking lifecycle action and send the corresponding notification.""" 271 action_fn, notification_type = _ACTION_MAP[action] 272 booking = action_fn(booking_id, session) 273 send_notification(booking.userID, booking.id, notification_type, session) 274 return booking
Execute a booking lifecycle action and send the corresponding notification.
class
NotificationServiceError(builtins.ValueError):
Base notification service error.
19class NotificationNotFoundError(NotificationServiceError): 20 """Raised when a notification cannot be found."""
Raised when a notification cannot be found.
class
RoomServiceError(builtins.ValueError):
Base room service error.
Raised when a room cannot be found.
def
send_notification( user_id, booking_id: int, notification_type: str | app.models.NotificationType, session: sqlmodel.orm.session.Session) -> app.models.Notification:
27def send_notification( 28 user_id, 29 booking_id: int, 30 notification_type: str | NotificationType, 31 session: Session, 32) -> Notification: 33 normalized_type = NotificationType(notification_type) 34 notification = Notification( 35 userID=user_id, 36 bookingID=booking_id, 37 message=_build_message(normalized_type, booking_id), 38 type=normalized_type, 39 isRead=False, 40 ) 41 session.add(notification) 42 session.commit() 43 session.refresh(notification) 44 return notification
def
get_notifications( user_id, session: sqlmodel.orm.session.Session) -> list[app.models.Notification]:
def
get_notification_for_user( notification_id: int, user_id, session: sqlmodel.orm.session.Session) -> app.models.Notification:
55def get_notification_for_user(notification_id: int, user_id, session: Session) -> Notification: 56 notification = session.get(Notification, notification_id) 57 if notification is None or notification.userID != user_id: 58 raise NotificationNotFoundError( 59 f"Notification {notification_id} was not found." 60 ) 61 return notification
def
mark_read( notification_id: int, session: sqlmodel.orm.session.Session) -> app.models.Notification:
64def mark_read(notification_id: int, session: Session) -> Notification: 65 notification = session.get(Notification, notification_id) 66 if notification is None: 67 raise NotificationNotFoundError( 68 f"Notification {notification_id} was not found." 69 ) 70 71 notification.isRead = True 72 session.add(notification) 73 session.commit() 74 session.refresh(notification) 75 return notification
def
get_unread_count(user_id, session: sqlmodel.orm.session.Session) -> int: