fix(market): per-event bet loss+win

This commit is contained in:
2026-10-07 21:44:33 +02:00
parent 6a5a224d3b
commit 382ba01f3e
8 changed files with 107 additions and 26 deletions
+23 -18
View File
@@ -6,12 +6,19 @@ 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, UserPointChange
from market.models import (
Market,
MarketOption,
UserBet,
UserPointChange,
get_event_points,
)
from market.schemas import (
MarketListSchema,
ResolveMarketSchema,
UserBetCreateSchema,
UserBetSchema,
UserPointsSchema,
)
@@ -45,6 +52,15 @@ def list_user_bets(request, event_path: str):
)
@router.get(f"{PREFIX}/user/points", response=UserPointsSchema)
def get_user_points(request, event_path: str):
"""Get the current user's points balance for this event."""
if not request.user.is_authenticated:
raise HttpError(401, "Authentication required")
return {"points": get_event_points(request.user, request.event)}
@router.post(f"{PREFIX}/{{market_uuid}}/actions/close")
def close_market(request, event_path: str, market_uuid: str):
"""Close a market. Admin only."""
@@ -76,9 +92,7 @@ def resolve_market(
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")
)
all_bets = list(UserBet.objects.filter(option__market=market))
# Calculate total pot
total_pot = sum(bet.amount for bet in all_bets)
@@ -92,7 +106,6 @@ def resolve_market(
total_winning = sum(bet.amount for bet in winning_bets)
point_changes = []
users_to_update = []
with transaction.atomic():
# Award payouts to winners with multiplier
@@ -101,11 +114,9 @@ def resolve_market(
payout = round(
bet.amount / total_winning * total_pot * market.multiplier
)
bet.user.points += payout
users_to_update.append(bet.user)
point_changes.append(
UserPointChange(
user=bet.user,
user_id=bet.user_id,
market=market,
amount=payout,
reason=UserPointChange.Reason.BET_WON,
@@ -116,17 +127,13 @@ def resolve_market(
for bet in losing_bets:
point_changes.append(
UserPointChange(
user=bet.user,
user_id=bet.user_id,
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)
@@ -167,8 +174,8 @@ def create_bet(
# 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:
# Check if user has enough points for this event
if get_event_points(request.user, request.event) < delta:
raise HttpError(400, "Insufficient points for this bet")
user_bet, created = UserBet.objects.update_or_create(
@@ -177,9 +184,7 @@ def create_bet(
defaults={"amount": payload.amount},
)
# Deduct points and record the change
request.user.points -= delta
request.user.save(update_fields=["points"])
# Record the change (the event balance is derived from these)
UserPointChange.objects.create(
user=request.user,
market=market,
+18
View File
@@ -1,10 +1,28 @@
import uuid
from django.db import models
from django.db.models import Sum
from events.models import Event
from events.scoping import EventScopedManager
# Each event's market starts every user with this balance
STARTING_POINTS = 1000
def get_event_points(user, event) -> int:
"""User's market balance for an event: starting points + the event's point changes.
BET_LOST rows are informational (the stake was already deducted by BET_PLACED).
"""
total = (
UserPointChange.objects.filter(user=user, market__event=event)
.exclude(reason=UserPointChange.Reason.BET_LOST)
.aggregate(total=Sum("amount"))["total"]
)
return STARTING_POINTS + (total or 0)
class BaseModel(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
+4
View File
@@ -35,6 +35,10 @@ class ResolveMarketSchema(Schema):
winning_option_uuid: str
class UserPointsSchema(Schema):
points: int
class UserBetCreateSchema(Schema):
option_uuid: str
amount: int