This commit is contained in:
2026-01-02 10:43:20 -06:00
commit 14d9af3036
112 changed files with 14274 additions and 0 deletions

View File

View File

@ -0,0 +1,50 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import User
from app.schemas.user import UserCreate, UserLogin, TokenResponse
from app.crud.user import create_user, get_user_by_email, get_user_by_username
from app.utils.security import verify_password, create_access_token, create_refresh_token
from app.utils.exceptions import InvalidCredentialsError, UserAlreadyExistsError
async def register_user(db: AsyncSession, user_data: UserCreate) -> TokenResponse:
# Check if user already exists
existing_user = await get_user_by_email(db, user_data.email)
if existing_user:
raise UserAlreadyExistsError("Email already registered")
existing_username = await get_user_by_username(db, user_data.username)
if existing_username:
raise UserAlreadyExistsError("Username already taken")
# Create user
user = await create_user(db, user_data)
await db.commit()
# Generate tokens
access_token = create_access_token({"sub": str(user.id)})
refresh_token = create_refresh_token({"sub": str(user.id)})
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
)
async def login_user(db: AsyncSession, login_data: UserLogin) -> TokenResponse:
# Get user by email
user = await get_user_by_email(db, login_data.email)
if not user:
raise InvalidCredentialsError()
# Verify password
if not verify_password(login_data.password, user.password_hash):
raise InvalidCredentialsError()
# Generate tokens
access_token = create_access_token({"sub": str(user.id)})
refresh_token = create_refresh_token({"sub": str(user.id)})
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
)

View File

@ -0,0 +1,178 @@
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import datetime
from app.models import Bet, BetStatus, TransactionType
from app.crud.bet import get_bet_by_id
from app.crud.wallet import get_user_wallet, create_transaction
from app.crud.user import update_user_stats
from app.utils.exceptions import (
BetNotFoundError,
BetNotAvailableError,
CannotAcceptOwnBetError,
InsufficientFundsError,
NotBetParticipantError,
)
async def accept_bet(db: AsyncSession, bet_id: int, user_id: int) -> Bet:
from sqlalchemy.orm import selectinload
# Use transaction for atomic operations
async with db.begin_nested():
# Get and lock the bet
bet = await db.get(Bet, bet_id, with_for_update=True)
if not bet or bet.status != BetStatus.OPEN:
raise BetNotAvailableError()
if bet.creator_id == user_id:
raise CannotAcceptOwnBetError()
# Get user's wallet and verify funds
user_wallet = await get_user_wallet(db, user_id)
if not user_wallet or user_wallet.balance < bet.stake_amount:
raise InsufficientFundsError()
# Get creator's wallet and lock their funds too
creator_wallet = await get_user_wallet(db, bet.creator_id)
if not creator_wallet or creator_wallet.balance < bet.stake_amount:
raise BetNotAvailableError()
# Lock funds in escrow for both parties
user_wallet.balance -= bet.stake_amount
user_wallet.escrow += bet.stake_amount
creator_wallet.balance -= bet.stake_amount
creator_wallet.escrow += bet.stake_amount
# Update bet
bet.opponent_id = user_id
bet.status = BetStatus.MATCHED
# Create transaction records
await create_transaction(
db=db,
user_id=user_id,
wallet_id=user_wallet.id,
transaction_type=TransactionType.ESCROW_LOCK,
amount=-bet.stake_amount,
balance_after=user_wallet.balance,
reference_id=bet.id,
description=f"Escrow for bet: {bet.title}",
)
await create_transaction(
db=db,
user_id=bet.creator_id,
wallet_id=creator_wallet.id,
transaction_type=TransactionType.ESCROW_LOCK,
amount=-bet.stake_amount,
balance_after=creator_wallet.balance,
reference_id=bet.id,
description=f"Escrow for bet: {bet.title}",
)
await db.commit()
# Refresh and eagerly load relationships
from sqlalchemy import select
result = await db.execute(
select(Bet)
.where(Bet.id == bet_id)
.options(selectinload(Bet.creator), selectinload(Bet.opponent))
)
bet = result.scalar_one()
return bet
async def settle_bet(
db: AsyncSession,
bet_id: int,
winner_id: int,
settler_id: int
) -> Bet:
async with db.begin_nested():
bet = await get_bet_by_id(db, bet_id)
if not bet:
raise BetNotFoundError()
# Verify settler is a participant
if settler_id not in [bet.creator_id, bet.opponent_id]:
raise NotBetParticipantError()
# Verify winner is a participant
if winner_id not in [bet.creator_id, bet.opponent_id]:
raise ValueError("Invalid winner")
# Determine loser
loser_id = bet.opponent_id if winner_id == bet.creator_id else bet.creator_id
# Get wallets
winner_wallet = await get_user_wallet(db, winner_id)
loser_wallet = await get_user_wallet(db, loser_id)
if not winner_wallet or not loser_wallet:
raise ValueError("Wallet not found")
# Calculate payout (winner gets both stakes)
total_payout = bet.stake_amount * 2
# Release escrow and distribute funds
winner_wallet.escrow -= bet.stake_amount
winner_wallet.balance += total_payout
loser_wallet.escrow -= bet.stake_amount
# Update bet
bet.winner_id = winner_id
bet.status = BetStatus.COMPLETED
bet.settled_at = datetime.utcnow()
bet.settled_by = "participant"
# Create transaction records
await create_transaction(
db=db,
user_id=winner_id,
wallet_id=winner_wallet.id,
transaction_type=TransactionType.BET_WON,
amount=total_payout,
balance_after=winner_wallet.balance,
reference_id=bet.id,
description=f"Won bet: {bet.title}",
)
await create_transaction(
db=db,
user_id=loser_id,
wallet_id=loser_wallet.id,
transaction_type=TransactionType.BET_LOST,
amount=-bet.stake_amount,
balance_after=loser_wallet.balance,
reference_id=bet.id,
description=f"Lost bet: {bet.title}",
)
# Update user stats
await update_user_stats(db, winner_id, won=True)
await update_user_stats(db, loser_id, won=False)
await db.commit()
await db.refresh(bet)
return bet
async def cancel_bet(db: AsyncSession, bet_id: int, user_id: int) -> Bet:
async with db.begin_nested():
bet = await get_bet_by_id(db, bet_id)
if not bet:
raise BetNotFoundError()
if bet.creator_id != user_id:
raise NotBetParticipantError()
if bet.status != BetStatus.OPEN:
raise BetNotAvailableError()
bet.status = BetStatus.CANCELLED
await db.commit()
await db.refresh(bet)
return bet

View File

@ -0,0 +1,52 @@
from sqlalchemy.ext.asyncio import AsyncSession
from decimal import Decimal
from app.models import Wallet, TransactionType
from app.crud.wallet import get_user_wallet, create_transaction
from app.utils.exceptions import InsufficientFundsError
async def deposit_funds(db: AsyncSession, user_id: int, amount: Decimal) -> Wallet:
wallet = await get_user_wallet(db, user_id)
if not wallet:
raise ValueError("Wallet not found")
wallet.balance += amount
await create_transaction(
db=db,
user_id=user_id,
wallet_id=wallet.id,
transaction_type=TransactionType.DEPOSIT,
amount=amount,
balance_after=wallet.balance,
description=f"Deposit of ${amount}",
)
await db.commit()
await db.refresh(wallet)
return wallet
async def withdraw_funds(db: AsyncSession, user_id: int, amount: Decimal) -> Wallet:
wallet = await get_user_wallet(db, user_id)
if not wallet:
raise ValueError("Wallet not found")
if wallet.balance < amount:
raise InsufficientFundsError()
wallet.balance -= amount
await create_transaction(
db=db,
user_id=user_id,
wallet_id=wallet.id,
transaction_type=TransactionType.WITHDRAWAL,
amount=-amount,
balance_after=wallet.balance,
description=f"Withdrawal of ${amount}",
)
await db.commit()
await db.refresh(wallet)
return wallet