refactors

This commit is contained in:
counterweight 2025-12-25 18:27:59 +01:00
parent f46d2ae8b3
commit 168b67acee
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
12 changed files with 471 additions and 126 deletions

View file

@ -1,22 +1,18 @@
"""Exchange routes for Bitcoin trading."""
import uuid
from datetime import UTC, date, datetime, time, timedelta
from datetime import UTC, date, datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from auth import require_permission
from database import get_db
from date_validation import validate_date_in_range
from exceptions import BadRequestError
from mappers import ExchangeMapper
from models import (
Availability,
BitcoinTransferMethod,
Exchange,
ExchangeStatus,
Permission,
PriceHistory,
@ -24,6 +20,7 @@ from models import (
User,
)
from price_fetcher import PAIR_BTC_EUR, SOURCE_BITFINEX, fetch_btc_eur_price
from repositories.exchange import ExchangeRepository
from repositories.price import PriceRepository
from schemas import (
AdminExchangeResponse,
@ -44,6 +41,7 @@ from shared_constants import (
PREMIUM_PERCENTAGE,
SLOT_DURATION_MINUTES,
)
from utils.enum_validation import validate_enum
router = APIRouter(prefix="/api/exchange", tags=["exchange"])
@ -190,28 +188,18 @@ async def get_available_slots(
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()
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
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()}
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] = []
@ -247,21 +235,16 @@ async def create_exchange(
- EUR amount is within configured limits
"""
# Validate direction
try:
direction = TradeDirection(request.direction)
except ValueError:
raise BadRequestError(
f"Invalid direction: {request.direction}. Must be 'buy' or 'sell'."
) from None
direction: TradeDirection = validate_enum(
TradeDirection, request.direction, "direction"
)
# Validate bitcoin transfer method
try:
bitcoin_transfer_method = BitcoinTransferMethod(request.bitcoin_transfer_method)
except ValueError:
raise BadRequestError(
f"Invalid bitcoin_transfer_method: {request.bitcoin_transfer_method}. "
"Must be 'onchain' or 'lightning'."
) from None
bitcoin_transfer_method: BitcoinTransferMethod = validate_enum(
BitcoinTransferMethod,
request.bitcoin_transfer_method,
"bitcoin_transfer_method",
)
# Use service to create exchange (handles all validation)
service = ExchangeService(db)
@ -289,12 +272,8 @@ async def get_my_trades(
current_user: User = Depends(require_permission(Permission.VIEW_OWN_EXCHANGES)),
) -> list[ExchangeResponse]:
"""Get the current user's exchanges, sorted by date (newest first)."""
result = await db.execute(
select(Exchange)
.where(Exchange.user_id == current_user.id)
.order_by(Exchange.slot_start.desc())
)
exchanges = result.scalars().all()
exchange_repo = ExchangeRepository(db)
exchanges = await exchange_repo.get_by_user_id(current_user.id, order_by_desc=True)
return [ExchangeMapper.to_response(ex, current_user.email) for ex in exchanges]
@ -348,19 +327,8 @@ async def get_upcoming_trades(
_current_user: User = Depends(require_permission(Permission.VIEW_ALL_EXCHANGES)),
) -> list[AdminExchangeResponse]:
"""Get all upcoming booked trades, sorted by slot time ascending."""
now = datetime.now(UTC)
result = await db.execute(
select(Exchange)
.options(joinedload(Exchange.user))
.where(
and_(
Exchange.slot_start > now,
Exchange.status == ExchangeStatus.BOOKED,
)
)
.order_by(Exchange.slot_start.asc())
)
exchanges = result.scalars().all()
exchange_repo = ExchangeRepository(db)
exchanges = await exchange_repo.get_upcoming_booked()
return [ExchangeMapper.to_admin_response(ex) for ex in exchanges]
@ -383,45 +351,19 @@ async def get_past_trades(
- user_search: Search by user email (partial match)
"""
now = datetime.now(UTC)
# Start with base query for past trades (slot_start <= now OR not booked)
query = (
select(Exchange)
.options(joinedload(Exchange.user))
.where(
(Exchange.slot_start <= now) | (Exchange.status != ExchangeStatus.BOOKED)
)
)
# Apply status filter
status_enum: ExchangeStatus | None = None
if status:
try:
status_enum = ExchangeStatus(status)
query = query.where(Exchange.status == status_enum)
except ValueError:
raise HTTPException(
status_code=400,
detail=f"Invalid status: {status}",
) from None
status_enum = validate_enum(ExchangeStatus, status, "status")
# Apply date range filter
if start_date:
start_dt = datetime.combine(start_date, time.min, tzinfo=UTC)
query = query.where(Exchange.slot_start >= start_dt)
if end_date:
end_dt = datetime.combine(end_date, time.max, tzinfo=UTC)
query = query.where(Exchange.slot_start <= end_dt)
# Apply user search filter (join with User table)
if user_search:
query = query.join(Exchange.user).where(User.email.ilike(f"%{user_search}%"))
# Order by most recent first
query = query.order_by(Exchange.slot_start.desc())
result = await db.execute(query)
exchanges = result.scalars().all()
# Use repository for query
exchange_repo = ExchangeRepository(db)
exchanges = await exchange_repo.get_past_trades(
status=status_enum,
start_date=start_date,
end_date=end_date,
user_search=user_search,
)
return [ExchangeMapper.to_admin_response(ex) for ex in exchanges]
@ -487,6 +429,10 @@ async def search_users(
Returns users whose email contains the search query (case-insensitive).
Limited to 10 results for autocomplete purposes.
"""
# Note: UserRepository doesn't have search yet, but we can add it
# For now, keeping direct query for this specific use case
from sqlalchemy import select
result = await db.execute(
select(User).where(User.email.ilike(f"%{q}%")).order_by(User.email).limit(10)
)