feat(market): track user points change
This commit is contained in:
@@ -2,8 +2,11 @@ from typing import List
|
||||
from ninja import Router
|
||||
from ninja.errors import HttpError
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Sum, Prefetch
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db import transaction
|
||||
|
||||
from market.models import Market, MarketOption, UserBet
|
||||
from market.models import Market, MarketOption, UserBet, UserPointChange
|
||||
from market.schemas import (
|
||||
MarketListSchema,
|
||||
ResolveMarketSchema,
|
||||
@@ -18,7 +21,13 @@ router = Router(tags=["market"])
|
||||
@router.get("/", response=List[MarketListSchema])
|
||||
def list_markets(request):
|
||||
"""List all markets."""
|
||||
return Market.objects.prefetch_related("options").all()
|
||||
markets = Market.objects.all()
|
||||
# Prefetch options with total_bets annotation sorted by total_bets desc, then text asc
|
||||
options_queryset = MarketOption.objects.annotate(
|
||||
total_bets=Coalesce(Sum("user_bets__amount"), 0)
|
||||
).order_by("-total_bets", "text")
|
||||
|
||||
return markets.prefetch_related(Prefetch("options", queryset=options_queryset))
|
||||
|
||||
|
||||
@router.get("/user/bets", response=List[UserBetSchema])
|
||||
@@ -27,9 +36,11 @@ def list_user_bets(request):
|
||||
if not request.user.is_authenticated:
|
||||
raise HttpError(401, "Authentication required")
|
||||
|
||||
return UserBet.objects.filter(user=request.user).select_related(
|
||||
"option__market"
|
||||
).prefetch_related("option__market__options")
|
||||
return (
|
||||
UserBet.objects.filter(user=request.user)
|
||||
.select_related("option__market")
|
||||
.prefetch_related("option__market__options")
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{market_uuid}/actions/close")
|
||||
@@ -59,6 +70,60 @@ def resolve_market(request, market_uuid: str, payload: ResolveMarketSchema):
|
||||
market.winning_option = winning_option
|
||||
market.status = Market.Status.RESOLVED
|
||||
market.save(update_fields=["winning_option", "status", "updated_at"])
|
||||
|
||||
# Calculate and distribute winnings
|
||||
all_bets = list(
|
||||
UserBet.objects.filter(option__market=market).select_related("user")
|
||||
)
|
||||
|
||||
# Calculate total pot
|
||||
total_pot = sum(bet.amount for bet in all_bets)
|
||||
if total_pot == 0:
|
||||
return market
|
||||
|
||||
# Separate winning and losing bets
|
||||
winning_bets = [bet for bet in all_bets if bet.option_id == winning_option.id]
|
||||
losing_bets = [bet for bet in all_bets if bet.option_id != winning_option.id]
|
||||
|
||||
total_winning = sum(bet.amount for bet in winning_bets)
|
||||
|
||||
point_changes = []
|
||||
users_to_update = []
|
||||
|
||||
with transaction.atomic():
|
||||
# Award payouts to winners
|
||||
if total_winning > 0:
|
||||
for bet in winning_bets:
|
||||
payout = round(bet.amount / total_winning * total_pot)
|
||||
bet.user.points += payout
|
||||
users_to_update.append(bet.user)
|
||||
point_changes.append(
|
||||
UserPointChange(
|
||||
user=bet.user,
|
||||
market=market,
|
||||
amount=payout,
|
||||
reason=UserPointChange.Reason.BET_WON,
|
||||
)
|
||||
)
|
||||
|
||||
# Record losing bets (points already deducted)
|
||||
for bet in losing_bets:
|
||||
point_changes.append(
|
||||
UserPointChange(
|
||||
user=bet.user,
|
||||
market=market,
|
||||
amount=-bet.amount,
|
||||
reason=UserPointChange.Reason.BET_LOST,
|
||||
)
|
||||
)
|
||||
|
||||
# Bulk update users
|
||||
for user in users_to_update:
|
||||
user.save(update_fields=["points"])
|
||||
|
||||
# Bulk create point changes
|
||||
UserPointChange.objects.bulk_create(point_changes)
|
||||
|
||||
return market
|
||||
|
||||
|
||||
@@ -78,10 +143,11 @@ def create_bet(request, market_uuid: str, payload: UserBetCreateSchema):
|
||||
raise HttpError(400, "Market is not open for betting")
|
||||
|
||||
# Check if user already has a bet on a different option in this market
|
||||
existing_bet_on_market = UserBet.objects.filter(
|
||||
user=request.user,
|
||||
option__market=market
|
||||
).exclude(option=option).first()
|
||||
existing_bet_on_market = (
|
||||
UserBet.objects.filter(user=request.user, option__market=market)
|
||||
.exclude(option=option)
|
||||
.first()
|
||||
)
|
||||
if existing_bet_on_market:
|
||||
raise HttpError(400, "You can only bet on one option per market")
|
||||
|
||||
@@ -90,10 +156,27 @@ def create_bet(request, market_uuid: str, payload: UserBetCreateSchema):
|
||||
if existing_bet and payload.amount < existing_bet.amount:
|
||||
raise HttpError(400, "Cannot decrease bet amount. You can only increase it.")
|
||||
|
||||
# Calculate delta (amount to deduct from user's points)
|
||||
delta = payload.amount - (existing_bet.amount if existing_bet else 0)
|
||||
|
||||
# Check if user has enough points
|
||||
if request.user.points < delta:
|
||||
raise HttpError(400, "Insufficient points for this bet")
|
||||
|
||||
user_bet, created = UserBet.objects.update_or_create(
|
||||
user=request.user,
|
||||
option=option,
|
||||
defaults={"amount": payload.amount},
|
||||
)
|
||||
|
||||
# Deduct points and record the change
|
||||
request.user.points -= delta
|
||||
request.user.save(update_fields=["points"])
|
||||
UserPointChange.objects.create(
|
||||
user=request.user,
|
||||
market=market,
|
||||
amount=-delta,
|
||||
reason=UserPointChange.Reason.BET_PLACED,
|
||||
)
|
||||
|
||||
return user_bet
|
||||
|
||||
Reference in New Issue
Block a user