generated from bisco/codex-bootstrap
feat: add check-in service layer
This commit is contained in:
130
backend/checkins/services.py
Normal file
130
backend/checkins/services.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from django.db import IntegrityError, transaction
|
||||
|
||||
from bookings.models import Reservation, ReservationToken
|
||||
|
||||
from .models import CheckIn
|
||||
|
||||
|
||||
class CheckInServiceError(Exception):
|
||||
"""Base class for check-in service domain errors."""
|
||||
|
||||
|
||||
class InvalidToken(CheckInServiceError):
|
||||
pass
|
||||
|
||||
|
||||
class ReservationNotConfirmed(CheckInServiceError):
|
||||
pass
|
||||
|
||||
|
||||
class AlreadyCheckedIn(CheckInServiceError):
|
||||
pass
|
||||
|
||||
|
||||
class MissingStaffUser(CheckInServiceError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CheckInPreview:
|
||||
reservation_id: int
|
||||
performance_id: int
|
||||
show_title: str
|
||||
venue_name: str
|
||||
starts_at: object
|
||||
party_size: int
|
||||
checked_in: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CheckInResult:
|
||||
check_in: CheckIn
|
||||
preview: CheckInPreview
|
||||
|
||||
|
||||
def preview_check_in_token(raw_token, *, staff_user):
|
||||
_validate_staff_user(staff_user)
|
||||
reservation = _get_reservation_for_check_in_token(raw_token)
|
||||
_validate_reservation_for_check_in(reservation)
|
||||
return _build_preview(reservation)
|
||||
|
||||
|
||||
def confirm_check_in_from_token(raw_token, *, staff_user, source=CheckIn.Source.QR_SCAN):
|
||||
_validate_staff_user(staff_user)
|
||||
|
||||
with transaction.atomic():
|
||||
reservation = _get_reservation_for_check_in_token(raw_token, lock_token=True)
|
||||
reservation = (
|
||||
Reservation.objects.select_for_update()
|
||||
.select_related("performance__show", "performance__venue")
|
||||
.get(pk=reservation.pk)
|
||||
)
|
||||
_validate_reservation_for_check_in(reservation)
|
||||
|
||||
try:
|
||||
check_in = CheckIn.objects.create(
|
||||
reservation=reservation,
|
||||
checked_in_by=staff_user,
|
||||
source=source,
|
||||
)
|
||||
except IntegrityError as exc:
|
||||
raise AlreadyCheckedIn("Reservation has already been checked in.") from exc
|
||||
|
||||
return CheckInResult(check_in=check_in, preview=_build_preview(reservation))
|
||||
|
||||
|
||||
def _validate_staff_user(staff_user):
|
||||
if staff_user is None:
|
||||
raise MissingStaffUser("A staff user is required for check-in.")
|
||||
if not getattr(staff_user, "is_authenticated", False):
|
||||
raise MissingStaffUser("An authenticated staff user is required for check-in.")
|
||||
if not getattr(staff_user, "is_staff", False):
|
||||
raise MissingStaffUser("A staff user is required for check-in.")
|
||||
|
||||
|
||||
def _get_reservation_for_check_in_token(raw_token, *, lock_token=False):
|
||||
if not isinstance(raw_token, str) or not raw_token:
|
||||
raise InvalidToken("Check-in token is invalid.")
|
||||
|
||||
queryset = ReservationToken.objects.select_related(
|
||||
"reservation__performance__show",
|
||||
"reservation__performance__venue",
|
||||
)
|
||||
if lock_token:
|
||||
queryset = queryset.select_for_update()
|
||||
|
||||
try:
|
||||
token = queryset.get(
|
||||
token_hash=ReservationToken.hash_token(raw_token),
|
||||
purpose=ReservationToken.Purpose.CHECK_IN,
|
||||
used_at__isnull=True,
|
||||
)
|
||||
except ReservationToken.DoesNotExist as exc:
|
||||
raise InvalidToken("Check-in token is invalid.") from exc
|
||||
|
||||
if token.is_expired:
|
||||
raise InvalidToken("Check-in token is invalid.")
|
||||
|
||||
return token.reservation
|
||||
|
||||
|
||||
def _validate_reservation_for_check_in(reservation):
|
||||
if reservation.status != Reservation.Status.CONFIRMED:
|
||||
raise ReservationNotConfirmed("Reservation must be confirmed before check-in.")
|
||||
if hasattr(reservation, "check_in"):
|
||||
raise AlreadyCheckedIn("Reservation has already been checked in.")
|
||||
|
||||
|
||||
def _build_preview(reservation):
|
||||
performance = reservation.performance
|
||||
return CheckInPreview(
|
||||
reservation_id=reservation.id,
|
||||
performance_id=performance.id,
|
||||
show_title=performance.show.title,
|
||||
venue_name=performance.venue.name,
|
||||
starts_at=performance.starts_at,
|
||||
party_size=reservation.party_size,
|
||||
checked_in=hasattr(reservation, "check_in"),
|
||||
)
|
||||
Reference in New Issue
Block a user