Init.
This commit is contained in:
0
backend/app/services/__init__.py
Normal file
0
backend/app/services/__init__.py
Normal file
50
backend/app/services/auth_service.py
Normal file
50
backend/app/services/auth_service.py
Normal 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,
|
||||
)
|
||||
178
backend/app/services/bet_service.py
Normal file
178
backend/app/services/bet_service.py
Normal 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
|
||||
52
backend/app/services/wallet_service.py
Normal file
52
backend/app/services/wallet_service.py
Normal 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
|
||||
Reference in New Issue
Block a user