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

@ -14,6 +14,7 @@ from exceptions import (
ServiceUnavailableError,
)
from models import (
Availability,
BitcoinTransferMethod,
Exchange,
ExchangeStatus,
@ -21,8 +22,10 @@ from models import (
TradeDirection,
User,
)
from repositories.availability import AvailabilityRepository
from repositories.exchange import ExchangeRepository
from repositories.price import PriceRepository
from schemas import AvailableSlotsResponse, BookableSlot
from shared_constants import (
EUR_TRADE_INCREMENT,
EUR_TRADE_MAX,
@ -44,6 +47,7 @@ class ExchangeService:
self.db = db
self.price_repo = PriceRepository(db)
self.exchange_repo = ExchangeRepository(db)
self.availability_repo = AvailabilityRepository(db)
def apply_premium_for_direction(
self,
@ -379,3 +383,73 @@ class ExchangeService:
await self.db.refresh(exchange)
return exchange
def _expand_availability_to_slots(
self, avail: Availability, slot_date: date, booked_starts: set[datetime]
) -> list[BookableSlot]:
"""
Expand an availability block into individual slots, filtering out booked ones.
Args:
avail: Availability record
slot_date: Date for the slots
booked_starts: Set of already-booked slot start times
Returns:
List of available BookableSlot records
"""
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
async def get_available_slots(self, date_param: date) -> 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
Args:
date_param: Date to get slots for
Returns:
AvailableSlotsResponse with date and list of available slots
Raises:
BadRequestError: If date is out of range
"""
validate_date_in_range(date_param, context="book")
# Get availability for the date
availabilities = await self.availability_repo.get_by_date(date_param)
if not availabilities:
return AvailableSlotsResponse(date=date_param, slots=[])
# Get already booked slots for the date
booked_starts = await self.exchange_repo.get_booked_slots_for_date(date_param)
# Expand each availability into slots
all_slots: list[BookableSlot] = []
for avail in availabilities:
slots = self._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)