Bet matching work done.

This commit is contained in:
2026-01-04 17:13:32 -06:00
parent 93fb46f19b
commit 2e9b2c83de
60 changed files with 7183 additions and 1 deletions

View File

@ -2,7 +2,7 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from app.database import init_db
from app.routers import auth, users, wallet, bets, websocket
from app.routers import auth, users, wallet, bets, websocket, admin, sport_events, spread_bets
@asynccontextmanager
@ -35,6 +35,9 @@ app.include_router(users.router)
app.include_router(wallet.router)
app.include_router(bets.router)
app.include_router(websocket.router)
app.include_router(admin.router)
app.include_router(sport_events.router)
app.include_router(spread_bets.router)
@app.get("/")

View File

@ -2,6 +2,9 @@ from app.models.user import User, UserStatus
from app.models.wallet import Wallet
from app.models.transaction import Transaction, TransactionType, TransactionStatus
from app.models.bet import Bet, BetProposal, BetCategory, BetStatus, BetVisibility, ProposalStatus
from app.models.sport_event import SportEvent, SportType, EventStatus
from app.models.spread_bet import SpreadBet, SpreadBetStatus, TeamSide
from app.models.admin_settings import AdminSettings
__all__ = [
"User",
@ -16,4 +19,11 @@ __all__ = [
"BetStatus",
"BetVisibility",
"ProposalStatus",
"SportEvent",
"SportType",
"EventStatus",
"SpreadBet",
"SpreadBetStatus",
"TeamSide",
"AdminSettings",
]

View File

@ -0,0 +1,20 @@
from sqlalchemy import String, Float, Numeric, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from datetime import datetime
from decimal import Decimal
from app.database import Base
class AdminSettings(Base):
__tablename__ = "admin_settings"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
default_house_commission_percent: Mapped[Decimal] = mapped_column(Numeric(5, 2), default=Decimal("10.00"))
default_min_bet_amount: Mapped[Decimal] = mapped_column(Numeric(12, 2), default=Decimal("10.00"))
default_max_bet_amount: Mapped[Decimal] = mapped_column(Numeric(12, 2), default=Decimal("1000.00"))
default_min_spread: Mapped[float] = mapped_column(Float, default=-10.0)
default_max_spread: Mapped[float] = mapped_column(Float, default=10.0)
spread_increment: Mapped[float] = mapped_column(Float, default=0.5)
platform_name: Mapped[str] = mapped_column(String(100), default="H2H Sports Betting")
maintenance_mode: Mapped[bool] = mapped_column(default=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

View File

@ -0,0 +1,50 @@
from sqlalchemy import String, DateTime, Enum, Float, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
import enum
from app.database import Base
class SportType(enum.Enum):
FOOTBALL = "football"
BASKETBALL = "basketball"
BASEBALL = "baseball"
HOCKEY = "hockey"
SOCCER = "soccer"
class EventStatus(enum.Enum):
UPCOMING = "upcoming"
LIVE = "live"
COMPLETED = "completed"
CANCELLED = "cancelled"
class SportEvent(Base):
__tablename__ = "sport_events"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
sport: Mapped[SportType] = mapped_column(Enum(SportType))
home_team: Mapped[str] = mapped_column(String(100))
away_team: Mapped[str] = mapped_column(String(100))
official_spread: Mapped[float] = mapped_column(Float)
game_time: Mapped[datetime] = mapped_column(DateTime)
venue: Mapped[str | None] = mapped_column(String(200), nullable=True)
league: Mapped[str | None] = mapped_column(String(100), nullable=True)
# Spread betting config
min_spread: Mapped[float] = mapped_column(Float, default=-10.0)
max_spread: Mapped[float] = mapped_column(Float, default=10.0)
min_bet_amount: Mapped[float] = mapped_column(Float, default=10.0)
max_bet_amount: Mapped[float] = mapped_column(Float, default=1000.0)
status: Mapped[EventStatus] = mapped_column(Enum(EventStatus), default=EventStatus.UPCOMING)
final_score_home: Mapped[int | None] = mapped_column(Integer, nullable=True)
final_score_away: Mapped[int | None] = mapped_column(Integer, nullable=True)
created_by: Mapped[int] = mapped_column(ForeignKey("users.id"))
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
spread_bets: Mapped[list["SpreadBet"]] = relationship(back_populates="event")

View File

@ -0,0 +1,47 @@
from sqlalchemy import String, DateTime, Enum, Float, Integer, ForeignKey, Numeric
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
from decimal import Decimal
import enum
from app.database import Base
class TeamSide(enum.Enum):
HOME = "home"
AWAY = "away"
class SpreadBetStatus(enum.Enum):
OPEN = "open"
MATCHED = "matched"
COMPLETED = "completed"
CANCELLED = "cancelled"
DISPUTED = "disputed"
class SpreadBet(Base):
__tablename__ = "spread_bets"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
event_id: Mapped[int] = mapped_column(ForeignKey("sport_events.id"))
spread: Mapped[float] = mapped_column(Float)
team: Mapped[TeamSide] = mapped_column(Enum(TeamSide))
creator_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
taker_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
stake_amount: Mapped[Decimal] = mapped_column(Numeric(12, 2))
house_commission_percent: Mapped[Decimal] = mapped_column(Numeric(5, 2), default=Decimal("10.00"))
status: Mapped[SpreadBetStatus] = mapped_column(Enum(SpreadBetStatus), default=SpreadBetStatus.OPEN)
payout_amount: Mapped[Decimal | None] = mapped_column(Numeric(12, 2), nullable=True)
winner_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
matched_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
# Relationships
event: Mapped["SportEvent"] = relationship(back_populates="spread_bets")
creator: Mapped["User"] = relationship(back_populates="created_spread_bets", foreign_keys=[creator_id])
taker: Mapped["User"] = relationship(back_populates="taken_spread_bets", foreign_keys=[taker_id])

View File

@ -30,6 +30,9 @@ class User(Base):
losses: Mapped[int] = mapped_column(Integer, default=0)
win_rate: Mapped[float] = mapped_column(Float, default=0.0)
# Admin flag
is_admin: Mapped[bool] = mapped_column(default=False)
status: Mapped[UserStatus] = mapped_column(Enum(UserStatus), default=UserStatus.ACTIVE)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@ -39,3 +42,5 @@ class User(Base):
created_bets: Mapped[list["Bet"]] = relationship(back_populates="creator", foreign_keys="Bet.creator_id")
accepted_bets: Mapped[list["Bet"]] = relationship(back_populates="opponent", foreign_keys="Bet.opponent_id")
transactions: Mapped[list["Transaction"]] = relationship(back_populates="user")
created_spread_bets: Mapped[list["SpreadBet"]] = relationship(back_populates="creator", foreign_keys="SpreadBet.creator_id")
taken_spread_bets: Mapped[list["SpreadBet"]] = relationship(back_populates="taker", foreign_keys="SpreadBet.taker_id")

View File

@ -0,0 +1,138 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from typing import List
from datetime import datetime
from decimal import Decimal
from app.database import get_db
from app.models import User, SportEvent, SpreadBet, AdminSettings, EventStatus
from app.schemas.sport_event import SportEventCreate, SportEventUpdate, SportEvent as SportEventSchema
from app.routers.auth import get_current_user
router = APIRouter(prefix="/api/v1/admin", tags=["admin"])
# Dependency to check if user is admin
async def get_admin_user(current_user: User = Depends(get_current_user)) -> User:
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required"
)
return current_user
@router.get("/settings")
async def get_admin_settings(
db: AsyncSession = Depends(get_db),
admin: User = Depends(get_admin_user)
):
result = await db.execute(select(AdminSettings).limit(1))
settings = result.scalar_one_or_none()
if not settings:
settings = AdminSettings()
db.add(settings)
await db.commit()
await db.refresh(settings)
return settings
@router.patch("/settings")
async def update_admin_settings(
updates: dict,
db: AsyncSession = Depends(get_db),
admin: User = Depends(get_admin_user)
):
result = await db.execute(select(AdminSettings).limit(1))
settings = result.scalar_one_or_none()
if not settings:
settings = AdminSettings()
db.add(settings)
for key, value in updates.items():
if hasattr(settings, key):
setattr(settings, key, value)
await db.commit()
await db.refresh(settings)
return settings
@router.post("/events", response_model=SportEventSchema)
async def create_event(
event_data: SportEventCreate,
db: AsyncSession = Depends(get_db),
admin: User = Depends(get_admin_user)
):
event = SportEvent(
**event_data.model_dump(),
created_by=admin.id
)
db.add(event)
await db.commit()
await db.refresh(event)
return event
@router.get("/events", response_model=List[SportEventSchema])
async def list_events(
skip: int = 0,
limit: int = 50,
db: AsyncSession = Depends(get_db),
admin: User = Depends(get_admin_user)
):
result = await db.execute(
select(SportEvent).offset(skip).limit(limit).order_by(SportEvent.game_time.desc())
)
return result.scalars().all()
@router.patch("/events/{event_id}", response_model=SportEventSchema)
async def update_event(
event_id: int,
updates: SportEventUpdate,
db: AsyncSession = Depends(get_db),
admin: User = Depends(get_admin_user)
):
result = await db.execute(select(SportEvent).where(SportEvent.id == event_id))
event = result.scalar_one_or_none()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
update_data = updates.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(event, key, value)
await db.commit()
await db.refresh(event)
return event
@router.delete("/events/{event_id}")
async def delete_event(
event_id: int,
db: AsyncSession = Depends(get_db),
admin: User = Depends(get_admin_user)
):
result = await db.execute(select(SportEvent).where(SportEvent.id == event_id))
event = result.scalar_one_or_none()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check if there are any matched bets
bets_result = await db.execute(
select(SpreadBet).where(
SpreadBet.event_id == event_id,
SpreadBet.status != "open"
)
)
if bets_result.scalar_one_or_none():
raise HTTPException(
status_code=400,
detail="Cannot delete event with matched bets"
)
await db.delete(event)
await db.commit()
return {"message": "Event deleted"}

View File

@ -0,0 +1,123 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from sqlalchemy.orm import selectinload
from typing import List
from datetime import datetime
from app.database import get_db
from app.models import User, SportEvent, SpreadBet, AdminSettings, EventStatus, SpreadBetStatus, TeamSide
from app.schemas.sport_event import SportEvent as SportEventSchema, SportEventWithBets
from app.routers.auth import get_current_user
router = APIRouter(prefix="/api/v1/sport-events", tags=["sport-events"])
def generate_spread_grid(min_spread: float, max_spread: float, increment: float = 0.5) -> List[float]:
"""Generate list of spread values from min to max with given increment"""
spreads = []
current = min_spread
while current <= max_spread:
spreads.append(round(current, 1))
current += increment
return spreads
@router.get("", response_model=List[SportEventSchema])
async def list_upcoming_events(
skip: int = 0,
limit: int = 20,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
result = await db.execute(
select(SportEvent)
.where(SportEvent.status == EventStatus.UPCOMING)
.order_by(SportEvent.game_time.asc())
.offset(skip)
.limit(limit)
)
return result.scalars().all()
@router.get("/{event_id}")
async def get_event_with_grid(
event_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
# Get event
result = await db.execute(
select(SportEvent).where(SportEvent.id == event_id)
)
event = result.scalar_one_or_none()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Get admin settings for spread increment
settings_result = await db.execute(select(AdminSettings).limit(1))
settings = settings_result.scalar_one_or_none()
increment = settings.spread_increment if settings else 0.5
# Generate spread grid
spreads = generate_spread_grid(event.min_spread, event.max_spread, increment)
# Get existing bets for this event
bets_result = await db.execute(
select(SpreadBet)
.options(selectinload(SpreadBet.creator))
.where(
and_(
SpreadBet.event_id == event_id,
SpreadBet.status.in_([SpreadBetStatus.OPEN, SpreadBetStatus.MATCHED])
)
)
)
bets = bets_result.scalars().all()
# Build spread grid with bet info
spread_grid = {}
for spread in spreads:
# Check for HOME team bet at this spread
bet_at_spread = next(
(b for b in bets if b.spread == spread and b.team == TeamSide.HOME),
None
)
if bet_at_spread:
spread_grid[str(spread)] = {
"bet_id": bet_at_spread.id,
"creator_id": bet_at_spread.creator_id,
"creator_username": bet_at_spread.creator.username,
"stake": float(bet_at_spread.stake_amount),
"status": bet_at_spread.status.value,
"team": bet_at_spread.team.value,
"can_take": (
bet_at_spread.status == SpreadBetStatus.OPEN and
bet_at_spread.creator_id != current_user.id
)
}
else:
spread_grid[str(spread)] = None
return {
"id": event.id,
"sport": event.sport.value,
"home_team": event.home_team,
"away_team": event.away_team,
"official_spread": event.official_spread,
"game_time": event.game_time.isoformat(),
"venue": event.venue,
"league": event.league,
"min_spread": event.min_spread,
"max_spread": event.max_spread,
"min_bet_amount": event.min_bet_amount,
"max_bet_amount": event.max_bet_amount,
"status": event.status.value,
"final_score_home": event.final_score_home,
"final_score_away": event.final_score_away,
"created_by": event.created_by,
"created_at": event.created_at.isoformat(),
"updated_at": event.updated_at.isoformat(),
"spread_grid": spread_grid
}

View File

@ -0,0 +1,250 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from sqlalchemy.orm import selectinload
from typing import List
from datetime import datetime
from decimal import Decimal
from app.database import get_db
from app.models import User, SportEvent, SpreadBet, Wallet, Transaction, AdminSettings
from app.models import EventStatus, SpreadBetStatus, TeamSide, TransactionType, TransactionStatus
from app.schemas.spread_bet import SpreadBet as SpreadBetSchema, SpreadBetCreate, SpreadBetDetail
from app.routers.auth import get_current_user
router = APIRouter(prefix="/api/v1/spread-bets", tags=["spread-bets"])
@router.post("", response_model=SpreadBetSchema)
async def create_spread_bet(
bet_data: SpreadBetCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
# Get the event
event_result = await db.execute(
select(SportEvent).where(SportEvent.id == bet_data.event_id)
)
event = event_result.scalar_one_or_none()
if not event:
raise HTTPException(status_code=404, detail="Event not found")
if event.status != EventStatus.UPCOMING:
raise HTTPException(status_code=400, detail="Event is not open for betting")
# Validate stake amount
if bet_data.stake_amount < Decimal(str(event.min_bet_amount)):
raise HTTPException(
status_code=400,
detail=f"Minimum bet amount is ${event.min_bet_amount}"
)
if bet_data.stake_amount > Decimal(str(event.max_bet_amount)):
raise HTTPException(
status_code=400,
detail=f"Maximum bet amount is ${event.max_bet_amount}"
)
# Check if spread is within range
if bet_data.spread < event.min_spread or bet_data.spread > event.max_spread:
raise HTTPException(
status_code=400,
detail=f"Spread must be between {event.min_spread} and {event.max_spread}"
)
# Check for existing bet at this spread
existing_result = await db.execute(
select(SpreadBet).where(
and_(
SpreadBet.event_id == bet_data.event_id,
SpreadBet.spread == bet_data.spread,
SpreadBet.team == bet_data.team,
SpreadBet.status == SpreadBetStatus.OPEN
)
)
)
if existing_result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="A bet already exists at this spread")
# Get admin settings for commission
settings_result = await db.execute(select(AdminSettings).limit(1))
settings = settings_result.scalar_one_or_none()
commission = settings.default_house_commission_percent if settings else Decimal("10.00")
# Create the bet
bet = SpreadBet(
event_id=bet_data.event_id,
spread=bet_data.spread,
team=bet_data.team,
creator_id=current_user.id,
stake_amount=bet_data.stake_amount,
house_commission_percent=commission,
status=SpreadBetStatus.OPEN
)
db.add(bet)
await db.commit()
await db.refresh(bet)
return bet
@router.post("/{bet_id}/take", response_model=SpreadBetSchema)
async def take_spread_bet(
bet_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
# Get the bet
bet_result = await db.execute(
select(SpreadBet)
.options(selectinload(SpreadBet.event))
.where(SpreadBet.id == bet_id)
)
bet = bet_result.scalar_one_or_none()
if not bet:
raise HTTPException(status_code=404, detail="Bet not found")
if bet.status != SpreadBetStatus.OPEN:
raise HTTPException(status_code=400, detail="Bet is not open")
if bet.creator_id == current_user.id:
raise HTTPException(status_code=400, detail="Cannot take your own bet")
if bet.event.status != EventStatus.UPCOMING:
raise HTTPException(status_code=400, detail="Event is no longer open for betting")
# Get wallets
creator_wallet_result = await db.execute(
select(Wallet).where(Wallet.user_id == bet.creator_id)
)
creator_wallet = creator_wallet_result.scalar_one_or_none()
taker_wallet_result = await db.execute(
select(Wallet).where(Wallet.user_id == current_user.id)
)
taker_wallet = taker_wallet_result.scalar_one_or_none()
if not taker_wallet:
raise HTTPException(status_code=400, detail="Wallet not found")
# Check taker has sufficient balance
if taker_wallet.balance < bet.stake_amount:
raise HTTPException(
status_code=400,
detail=f"Insufficient balance. Need ${bet.stake_amount}"
)
# Check creator still has sufficient balance
if creator_wallet.balance < bet.stake_amount:
raise HTTPException(
status_code=400,
detail="Creator no longer has sufficient balance"
)
# Lock funds for both parties
creator_wallet.balance -= bet.stake_amount
creator_wallet.escrow += bet.stake_amount
taker_wallet.balance -= bet.stake_amount
taker_wallet.escrow += bet.stake_amount
# Create transactions
creator_tx = Transaction(
user_id=bet.creator_id,
type=TransactionType.ESCROW_LOCK,
amount=-bet.stake_amount,
balance_after=creator_wallet.balance,
reference_id=bet.id,
description=f"Escrow lock for spread bet #{bet.id}",
status=TransactionStatus.COMPLETED
)
taker_tx = Transaction(
user_id=current_user.id,
type=TransactionType.ESCROW_LOCK,
amount=-bet.stake_amount,
balance_after=taker_wallet.balance,
reference_id=bet.id,
description=f"Escrow lock for spread bet #{bet.id}",
status=TransactionStatus.COMPLETED
)
db.add(creator_tx)
db.add(taker_tx)
# Update bet
bet.taker_id = current_user.id
bet.status = SpreadBetStatus.MATCHED
bet.matched_at = datetime.utcnow()
await db.commit()
await db.refresh(bet)
return bet
@router.get("/my-active", response_model=List[SpreadBetDetail])
async def get_my_active_bets(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
result = await db.execute(
select(SpreadBet)
.options(
selectinload(SpreadBet.event),
selectinload(SpreadBet.creator),
selectinload(SpreadBet.taker)
)
.where(
and_(
SpreadBet.status.in_([SpreadBetStatus.OPEN, SpreadBetStatus.MATCHED]),
(SpreadBet.creator_id == current_user.id) | (SpreadBet.taker_id == current_user.id)
)
)
.order_by(SpreadBet.created_at.desc())
)
bets = result.scalars().all()
return [
SpreadBetDetail(
id=bet.id,
event_id=bet.event_id,
spread=bet.spread,
team=bet.team,
creator_id=bet.creator_id,
taker_id=bet.taker_id,
stake_amount=bet.stake_amount,
house_commission_percent=bet.house_commission_percent,
status=bet.status,
payout_amount=bet.payout_amount,
winner_id=bet.winner_id,
created_at=bet.created_at,
matched_at=bet.matched_at,
completed_at=bet.completed_at,
creator_username=bet.creator.username,
taker_username=bet.taker.username if bet.taker else None,
event_home_team=bet.event.home_team,
event_away_team=bet.event.away_team,
event_official_spread=bet.event.official_spread,
event_game_time=bet.event.game_time
)
for bet in bets
]
@router.delete("/{bet_id}")
async def cancel_spread_bet(
bet_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
):
bet_result = await db.execute(
select(SpreadBet).where(SpreadBet.id == bet_id)
)
bet = bet_result.scalar_one_or_none()
if not bet:
raise HTTPException(status_code=404, detail="Bet not found")
if bet.creator_id != current_user.id:
raise HTTPException(status_code=403, detail="Only the creator can cancel this bet")
if bet.status != SpreadBetStatus.OPEN:
raise HTTPException(status_code=400, detail="Can only cancel open bets")
bet.status = SpreadBetStatus.CANCELLED
await db.commit()
return {"message": "Bet cancelled"}

View File

@ -0,0 +1,59 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional, Dict, Any
from app.models.sport_event import SportType, EventStatus
class SportEventCreate(BaseModel):
sport: SportType
home_team: str
away_team: str
official_spread: float
game_time: datetime
venue: Optional[str] = None
league: Optional[str] = None
min_spread: float = -10.0
max_spread: float = 10.0
min_bet_amount: float = 10.0
max_bet_amount: float = 1000.0
class SportEventUpdate(BaseModel):
sport: Optional[SportType] = None
home_team: Optional[str] = None
away_team: Optional[str] = None
official_spread: Optional[float] = None
game_time: Optional[datetime] = None
venue: Optional[str] = None
league: Optional[str] = None
status: Optional[EventStatus] = None
final_score_home: Optional[int] = None
final_score_away: Optional[int] = None
class SportEvent(BaseModel):
id: int
sport: SportType
home_team: str
away_team: str
official_spread: float
game_time: datetime
venue: Optional[str] = None
league: Optional[str] = None
min_spread: float
max_spread: float
min_bet_amount: float
max_bet_amount: float
status: EventStatus
final_score_home: Optional[int] = None
final_score_away: Optional[int] = None
created_by: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class SportEventWithBets(SportEvent):
spread_grid: Dict[str, Any]

View File

@ -0,0 +1,45 @@
from pydantic import BaseModel
from datetime import datetime
from decimal import Decimal
from typing import Optional
from app.models.spread_bet import TeamSide, SpreadBetStatus
class SpreadBetCreate(BaseModel):
event_id: int
spread: float
team: TeamSide
stake_amount: Decimal
class SpreadBetTake(BaseModel):
pass
class SpreadBet(BaseModel):
id: int
event_id: int
spread: float
team: TeamSide
creator_id: int
taker_id: Optional[int] = None
stake_amount: Decimal
house_commission_percent: Decimal
status: SpreadBetStatus
payout_amount: Optional[Decimal] = None
winner_id: Optional[int] = None
created_at: datetime
matched_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
class Config:
from_attributes = True
class SpreadBetDetail(SpreadBet):
creator_username: str
taker_username: Optional[str] = None
event_home_team: str
event_away_team: str
event_official_spread: float
event_game_time: datetime