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:
parent
65ba4dc42a
commit
1008eea2d9
11 changed files with 184 additions and 379 deletions
|
|
@ -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
|
||||
# =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue