Fix: Update permissions and add missing /api/exchange/slots endpoint

- Updated auth-context.tsx to use new exchange permissions
  (CREATE_EXCHANGE, VIEW_OWN_EXCHANGES, etc.) instead of old
  appointment permissions (BOOK_APPOINTMENT, etc.)

- Updated exchange/page.tsx, trades/page.tsx, admin/trades/page.tsx
  to use correct permission constants

- Updated profile/page.test.tsx mock permissions

- Updated admin/availability/page.tsx to use constants.exchange
  instead of constants.booking

- Added /api/exchange/slots endpoint to return available slots
  for a date, filtering out already booked slots

- Fixed E2E tests:
  - exchange.spec.ts: Wait for button to be enabled before clicking
  - permissions.spec.ts: Use more specific heading selector
  - price-history.spec.ts: Expect /exchange redirect for regular users
This commit is contained in:
counterweight 2025-12-22 21:42:42 +01:00
parent 65ba4dc42a
commit 1008eea2d9
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
11 changed files with 184 additions and 379 deletions

View file

@ -2,7 +2,7 @@
from datetime import UTC, date, datetime, time, timedelta
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import and_, desc, select
from sqlalchemy.exc import IntegrityError
@ -78,6 +78,20 @@ class ExchangePriceResponse(BaseModel):
error: str | None = None
class BookableSlot(BaseModel):
"""A single bookable time slot."""
start_time: datetime
end_time: datetime
class AvailableSlotsResponse(BaseModel):
"""Response containing available slots for a date."""
date: date
slots: list[BookableSlot]
# =============================================================================
# Helper functions
# =============================================================================
@ -266,6 +280,86 @@ async def get_exchange_price(
)
# =============================================================================
# Available Slots Endpoint
# =============================================================================
def _expand_availability_to_slots(
avail: Availability, slot_date: date, booked_starts: set[datetime]
) -> list[BookableSlot]:
"""
Expand an availability block into individual slots, filtering out booked ones.
"""
slots: list[BookableSlot] = []
# Start from the availability's start time
current_start = datetime.combine(slot_date, avail.start_time, tzinfo=UTC)
avail_end = datetime.combine(slot_date, avail.end_time, tzinfo=UTC)
while current_start + timedelta(minutes=SLOT_DURATION_MINUTES) <= avail_end:
slot_end = current_start + timedelta(minutes=SLOT_DURATION_MINUTES)
# Only include if not already booked
if current_start not in booked_starts:
slots.append(BookableSlot(start_time=current_start, end_time=slot_end))
current_start = slot_end
return slots
@router.get("/slots", response_model=AvailableSlotsResponse)
async def get_available_slots(
date_param: date = Query(..., alias="date"),
db: AsyncSession = Depends(get_db),
_current_user: User = Depends(require_permission(Permission.CREATE_EXCHANGE)),
) -> AvailableSlotsResponse:
"""
Get available booking slots for a specific date.
Returns all slots that:
- Fall within admin-defined availability windows
- Are not already booked by another user
"""
validate_date_in_range(date_param, context="book")
# Get availability for the date
result = await db.execute(
select(Availability).where(Availability.date == date_param)
)
availabilities = result.scalars().all()
if not availabilities:
return AvailableSlotsResponse(date=date_param, slots=[])
# Get already booked slots for the date
date_start = datetime.combine(date_param, time.min, tzinfo=UTC)
date_end = datetime.combine(date_param, time.max, tzinfo=UTC)
result = await db.execute(
select(Exchange.slot_start).where(
and_(
Exchange.slot_start >= date_start,
Exchange.slot_start <= date_end,
Exchange.status == ExchangeStatus.BOOKED,
)
)
)
booked_starts = {row[0] for row in result.all()}
# Expand each availability into slots
all_slots: list[BookableSlot] = []
for avail in availabilities:
slots = _expand_availability_to_slots(avail, date_param, booked_starts)
all_slots.extend(slots)
# Sort by start time
all_slots.sort(key=lambda s: s.start_time)
return AvailableSlotsResponse(date=date_param, slots=all_slots)
# =============================================================================
# Create Exchange Endpoint
# =============================================================================