From 1f8fb521b211d7c5d535c9d2e9d1274bfa79abe3 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 21 Feb 2026 08:53:04 +0000 Subject: [PATCH] Add ticket +/- quantity support to shared contracts and services - Add ticket_type_id field to TicketDTO for grouping - Add adjust_ticket_quantity to CalendarService protocol + SQL impl - Add stub for adjust_ticket_quantity Co-Authored-By: Claude Opus 4.6 --- contracts/dtos.py | 1 + contracts/protocols.py | 6 +++++ services/calendar_impl.py | 57 +++++++++++++++++++++++++++++++++++++++ services/stubs.py | 5 ++++ 4 files changed, 69 insertions(+) diff --git a/contracts/dtos.py b/contracts/dtos.py index af1c8eb..e78cd95 100644 --- a/contracts/dtos.py +++ b/contracts/dtos.py @@ -56,6 +56,7 @@ class TicketDTO: created_at: datetime | None = None checked_in_at: datetime | None = None entry_id: int | None = None + ticket_type_id: int | None = None price: Decimal | None = None order_id: int | None = None calendar_container_id: int | None = None diff --git a/contracts/protocols.py b/contracts/protocols.py index 4304922..63ea90c 100644 --- a/contracts/protocols.py +++ b/contracts/protocols.py @@ -112,6 +112,12 @@ class CalendarService(Protocol): self, session: AsyncSession, user_id: int, session_id: str, ) -> None: ... + async def adjust_ticket_quantity( + self, session: AsyncSession, entry_id: int, count: int, *, + user_id: int | None, session_id: str | None, + ticket_type_id: int | None = None, + ) -> int: ... + async def entry_ids_for_content( self, session: AsyncSession, content_type: str, content_id: int, ) -> set[int]: ... diff --git a/services/calendar_impl.py b/services/calendar_impl.py index f1cd5af..6afae80 100644 --- a/services/calendar_impl.py +++ b/services/calendar_impl.py @@ -72,6 +72,7 @@ def _ticket_to_dto(ticket: Ticket) -> TicketDTO: created_at=ticket.created_at, checked_in_at=ticket.checked_in_at, entry_id=entry.id if entry else None, + ticket_type_id=ticket.ticket_type_id, price=price, order_id=ticket.order_id, calendar_container_id=cal.container_id if cal else None, @@ -563,3 +564,59 @@ class SqlCalendarService: ) for ticket in result.scalars().all(): ticket.user_id = user_id + + async def adjust_ticket_quantity( + self, session: AsyncSession, entry_id: int, count: int, *, + user_id: int | None, session_id: str | None, + ticket_type_id: int | None = None, + ) -> int: + """Adjust reserved ticket count to target. Returns new count.""" + import uuid + + count = max(count, 0) + + # Current reserved count + filters = [ + Ticket.entry_id == entry_id, + Ticket.state == "reserved", + ] + if user_id is not None: + filters.append(Ticket.user_id == user_id) + elif session_id is not None: + filters.append(Ticket.session_id == session_id) + else: + return 0 + if ticket_type_id is not None: + filters.append(Ticket.ticket_type_id == ticket_type_id) + + current = await session.scalar( + select(func.count(Ticket.id)).where(*filters) + ) or 0 + + if count > current: + # Create tickets + for _ in range(count - current): + ticket = Ticket( + entry_id=entry_id, + ticket_type_id=ticket_type_id, + user_id=user_id, + session_id=session_id, + code=uuid.uuid4().hex, + state="reserved", + ) + session.add(ticket) + await session.flush() + elif count < current: + # Cancel newest tickets + to_cancel = current - count + result = await session.execute( + select(Ticket) + .where(*filters) + .order_by(Ticket.created_at.desc()) + .limit(to_cancel) + ) + for ticket in result.scalars().all(): + ticket.state = "cancelled" + await session.flush() + + return count diff --git a/services/stubs.py b/services/stubs.py index e368ec9..c37ebb7 100644 --- a/services/stubs.py +++ b/services/stubs.py @@ -132,6 +132,11 @@ class StubCalendarService: ) -> None: pass + async def adjust_ticket_quantity( + self, session, entry_id, count, *, user_id, session_id, ticket_type_id=None, + ) -> int: + return 0 + async def entry_ids_for_content(self, session, content_type, content_id): return set()