Move slot expansion logic to ExchangeService

- Add get_available_slots() and _expand_availability_to_slots() to ExchangeService
- Update routes/exchange.py to use ExchangeService.get_available_slots()
- Remove all business logic from get_available_slots endpoint
- Add AvailabilityRepository to ExchangeService dependencies
- Add Availability and BookableSlot imports to ExchangeService
- Fix import path for validate_date_in_range (use date_validation module)
- Remove unused user_repo variable and import from routes/invites.py
- Fix mypy error in ValidationError by adding proper type annotation
This commit is contained in:
counterweight 2025-12-25 18:42:46 +01:00
parent c3a501e3b2
commit 280c1e5687
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
12 changed files with 571 additions and 303 deletions

View file

@ -1,17 +1,15 @@
"""Exchange routes for Bitcoin trading."""
import uuid
from datetime import UTC, date, datetime, timedelta
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from auth import require_permission
from database import get_db
from date_validation import validate_date_in_range
from mappers import ExchangeMapper
from models import (
Availability,
BitcoinTransferMethod,
ExchangeStatus,
Permission,
@ -25,7 +23,6 @@ from repositories.price import PriceRepository
from schemas import (
AdminExchangeResponse,
AvailableSlotsResponse,
BookableSlot,
ExchangeConfigResponse,
ExchangePriceResponse,
ExchangeRequest,
@ -39,7 +36,6 @@ from shared_constants import (
EUR_TRADE_MAX,
EUR_TRADE_MIN,
PREMIUM_PERCENTAGE,
SLOT_DURATION_MINUTES,
)
from utils.enum_validation import validate_enum
@ -148,30 +144,6 @@ async def get_exchange_price(
# =============================================================================
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"),
@ -185,32 +157,8 @@ async def get_available_slots(
- 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
from repositories.availability import AvailabilityRepository
from repositories.exchange import ExchangeRepository
availability_repo = AvailabilityRepository(db)
availabilities = await availability_repo.get_by_date(date_param)
if not availabilities:
return AvailableSlotsResponse(date=date_param, slots=[])
# Get already booked slots for the date
exchange_repo = ExchangeRepository(db)
booked_starts = await exchange_repo.get_booked_slots_for_date(date_param)
# 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)
service = ExchangeService(db)
return await service.get_available_slots(date_param)
# =============================================================================