feat(market): track user points change
This commit is contained in:
parent
42e3571fab
commit
a264336bd8
@ -34,3 +34,6 @@ class CustomUserAdmin(UserAdmin):
|
|||||||
return obj.get_cas_groups_display()
|
return obj.get_cas_groups_display()
|
||||||
|
|
||||||
get_cas_groups_display.short_description = "CAS Groups"
|
get_cas_groups_display.short_description = "CAS Groups"
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-23 18:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("accounts", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="customuser",
|
||||||
|
name="points",
|
||||||
|
field=models.IntegerField(default=1000),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -14,6 +14,9 @@ class CustomUser(AbstractUser):
|
|||||||
# Additional fields that might come from CAS
|
# Additional fields that might come from CAS
|
||||||
cas_attributes = models.JSONField(default=dict, blank=True)
|
cas_attributes = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
# Market points balance
|
||||||
|
points = models.IntegerField(default=1000)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.username} ({self.cas_user_id})"
|
return f"{self.username} ({self.cas_user_id})"
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from market.models import Market, MarketOption, UserBet
|
from market.models import Market, MarketOption, UserBet, UserPointChange
|
||||||
|
|
||||||
|
|
||||||
class MarketOptionInline(admin.TabularInline):
|
class MarketOptionInline(admin.TabularInline):
|
||||||
model = MarketOption
|
model = MarketOption
|
||||||
extra = 1
|
extra = 1
|
||||||
fields = ["text", "position"]
|
fields = ["text"]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Market)
|
@admin.register(Market)
|
||||||
class MarketAdmin(admin.ModelAdmin):
|
class MarketAdmin(admin.ModelAdmin):
|
||||||
list_display = ["title", "type", "status", "end_date", "created_by", "created_at"]
|
list_display = ["title", "status", "end_date", "created_by", "created_at"]
|
||||||
list_filter = ["status", "type", "created_at"]
|
list_filter = ["status", "created_at"]
|
||||||
search_fields = ["uuid", "title"]
|
search_fields = ["uuid", "title"]
|
||||||
readonly_fields = [
|
readonly_fields = [
|
||||||
"uuid",
|
"uuid",
|
||||||
@ -23,7 +23,7 @@ class MarketAdmin(admin.ModelAdmin):
|
|||||||
inlines = [MarketOptionInline]
|
inlines = [MarketOptionInline]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
("Info", {"fields": ["uuid", "title", "description"]}),
|
("Info", {"fields": ["uuid", "title", "description"]}),
|
||||||
("Configuration", {"fields": ["type", "end_date"]}),
|
("Configuration", {"fields": ["end_date"]}),
|
||||||
("Status", {"fields": ["status", "winning_option"]}),
|
("Status", {"fields": ["status", "winning_option"]}),
|
||||||
("Metadata", {"fields": ["created_by", "created_at", "updated_at"]}),
|
("Metadata", {"fields": ["created_by", "created_at", "updated_at"]}),
|
||||||
)
|
)
|
||||||
@ -45,7 +45,7 @@ class MarketAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(MarketOption)
|
@admin.register(MarketOption)
|
||||||
class MarketOptionAdmin(admin.ModelAdmin):
|
class MarketOptionAdmin(admin.ModelAdmin):
|
||||||
list_display = ["text", "market", "position"]
|
list_display = ["text", "market"]
|
||||||
list_filter = ["market"]
|
list_filter = ["market"]
|
||||||
search_fields = ["uuid", "text", "market__title"]
|
search_fields = ["uuid", "text", "market__title"]
|
||||||
readonly_fields = ["uuid"]
|
readonly_fields = ["uuid"]
|
||||||
@ -54,7 +54,7 @@ class MarketOptionAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(UserBet)
|
@admin.register(UserBet)
|
||||||
class UserBetAdmin(admin.ModelAdmin):
|
class UserBetAdmin(admin.ModelAdmin):
|
||||||
list_display = ["user", "option", "amount", "created_at"]
|
list_display = ["user", "option", "amount", "created_at"]
|
||||||
list_filter = ["created_at", "option__market"]
|
list_filter = ["user", "created_at", "option__market"]
|
||||||
search_fields = ["uuid", "user__username", "option__text"]
|
search_fields = ["uuid", "user__username", "option__text"]
|
||||||
readonly_fields = ["uuid", "user", "option", "amount", "created_at", "updated_at"]
|
readonly_fields = ["uuid", "user", "option", "amount", "created_at", "updated_at"]
|
||||||
|
|
||||||
@ -63,3 +63,17 @@ class UserBetAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserPointChange)
|
||||||
|
class UserPointChangeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["user", "market", "amount", "reason", "created_at"]
|
||||||
|
list_filter = ["user", "reason", "created_at", "market"]
|
||||||
|
search_fields = ["uuid", "user__username", "market__title"]
|
||||||
|
readonly_fields = ["uuid", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|||||||
@ -2,8 +2,11 @@ from typing import List
|
|||||||
from ninja import Router
|
from ninja import Router
|
||||||
from ninja.errors import HttpError
|
from ninja.errors import HttpError
|
||||||
from django.shortcuts import get_object_or_404
|
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 (
|
from market.schemas import (
|
||||||
MarketListSchema,
|
MarketListSchema,
|
||||||
ResolveMarketSchema,
|
ResolveMarketSchema,
|
||||||
@ -18,7 +21,13 @@ router = Router(tags=["market"])
|
|||||||
@router.get("/", response=List[MarketListSchema])
|
@router.get("/", response=List[MarketListSchema])
|
||||||
def list_markets(request):
|
def list_markets(request):
|
||||||
"""List all markets."""
|
"""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])
|
@router.get("/user/bets", response=List[UserBetSchema])
|
||||||
@ -27,9 +36,11 @@ def list_user_bets(request):
|
|||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
raise HttpError(401, "Authentication required")
|
raise HttpError(401, "Authentication required")
|
||||||
|
|
||||||
return UserBet.objects.filter(user=request.user).select_related(
|
return (
|
||||||
"option__market"
|
UserBet.objects.filter(user=request.user)
|
||||||
).prefetch_related("option__market__options")
|
.select_related("option__market")
|
||||||
|
.prefetch_related("option__market__options")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{market_uuid}/actions/close")
|
@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.winning_option = winning_option
|
||||||
market.status = Market.Status.RESOLVED
|
market.status = Market.Status.RESOLVED
|
||||||
market.save(update_fields=["winning_option", "status", "updated_at"])
|
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
|
return market
|
||||||
|
|
||||||
|
|
||||||
@ -78,10 +143,11 @@ def create_bet(request, market_uuid: str, payload: UserBetCreateSchema):
|
|||||||
raise HttpError(400, "Market is not open for betting")
|
raise HttpError(400, "Market is not open for betting")
|
||||||
|
|
||||||
# Check if user already has a bet on a different option in this market
|
# Check if user already has a bet on a different option in this market
|
||||||
existing_bet_on_market = UserBet.objects.filter(
|
existing_bet_on_market = (
|
||||||
user=request.user,
|
UserBet.objects.filter(user=request.user, option__market=market)
|
||||||
option__market=market
|
.exclude(option=option)
|
||||||
).exclude(option=option).first()
|
.first()
|
||||||
|
)
|
||||||
if existing_bet_on_market:
|
if existing_bet_on_market:
|
||||||
raise HttpError(400, "You can only bet on one option per 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:
|
if existing_bet and payload.amount < existing_bet.amount:
|
||||||
raise HttpError(400, "Cannot decrease bet amount. You can only increase it.")
|
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_bet, created = UserBet.objects.update_or_create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
option=option,
|
option=option,
|
||||||
defaults={"amount": payload.amount},
|
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
|
return user_bet
|
||||||
|
|||||||
73
polylan_submitter/market/migrations/0002_userpointchange.py
Normal file
73
polylan_submitter/market/migrations/0002_userpointchange.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-23 18:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("market", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserPointChange",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
("amount", models.IntegerField()),
|
||||||
|
(
|
||||||
|
"reason",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("bet_placed", "Bet Placed"),
|
||||||
|
("bet_won", "Bet Won"),
|
||||||
|
("bet_lost", "Bet Lost"),
|
||||||
|
],
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"market",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="point_changes",
|
||||||
|
to="market.market",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="point_changes",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-created_at"],
|
||||||
|
"indexes": [
|
||||||
|
models.Index(
|
||||||
|
fields=["user", "-created_at"],
|
||||||
|
name="market_user_user_id_631ba9_idx",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-23 18:19
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("market", "0002_userpointchange"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="market",
|
||||||
|
name="type",
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-05-23 18:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("market", "0003_remove_market_type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="marketoption",
|
||||||
|
options={"ordering": ["text"]},
|
||||||
|
),
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="marketoption",
|
||||||
|
name="unique_market_option_position",
|
||||||
|
),
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name="marketoption",
|
||||||
|
name="market_mark_market__8679ce_idx",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="marketoption",
|
||||||
|
name="position",
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="marketoption",
|
||||||
|
index=models.Index(
|
||||||
|
fields=["market"], name="market_mark_market__67f63b_idx"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="marketoption",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("market", "text"), name="unique_market_option_text"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -12,10 +12,6 @@ class BaseModel(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Market(BaseModel):
|
class Market(BaseModel):
|
||||||
class Type(models.TextChoices):
|
|
||||||
YES_NO = "yes_no", "Yes/No"
|
|
||||||
MULTIPLE = "multiple", "Multiple Choice"
|
|
||||||
|
|
||||||
class Status(models.TextChoices):
|
class Status(models.TextChoices):
|
||||||
OPEN = "open", "Open"
|
OPEN = "open", "Open"
|
||||||
CLOSED = "closed", "Closed"
|
CLOSED = "closed", "Closed"
|
||||||
@ -23,7 +19,6 @@ class Market(BaseModel):
|
|||||||
|
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
type = models.CharField(max_length=10, choices=Type.choices, default=Type.YES_NO)
|
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=10, choices=Status.choices, default=Status.OPEN
|
max_length=10, choices=Status.choices, default=Status.OPEN
|
||||||
)
|
)
|
||||||
@ -51,18 +46,17 @@ class Market(BaseModel):
|
|||||||
class MarketOption(BaseModel):
|
class MarketOption(BaseModel):
|
||||||
market = models.ForeignKey(Market, on_delete=models.CASCADE, related_name="options")
|
market = models.ForeignKey(Market, on_delete=models.CASCADE, related_name="options")
|
||||||
text = models.CharField(max_length=255)
|
text = models.CharField(max_length=255)
|
||||||
position = models.PositiveIntegerField(default=0)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["position"]
|
ordering = ["text"]
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=["market", "position"],
|
fields=["market", "text"],
|
||||||
name="unique_market_option_position",
|
name="unique_market_option_text",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["market", "position"]),
|
models.Index(fields=["market"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -91,3 +85,28 @@ class UserBet(BaseModel):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.username} bet {self.amount} on {self.option.text}"
|
return f"{self.user.username} bet {self.amount} on {self.option.text}"
|
||||||
|
|
||||||
|
|
||||||
|
class UserPointChange(BaseModel):
|
||||||
|
class Reason(models.TextChoices):
|
||||||
|
BET_PLACED = "bet_placed", "Bet Placed"
|
||||||
|
BET_WON = "bet_won", "Bet Won"
|
||||||
|
BET_LOST = "bet_lost", "Bet Lost"
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
"accounts.CustomUser", on_delete=models.CASCADE, related_name="point_changes"
|
||||||
|
)
|
||||||
|
market = models.ForeignKey(
|
||||||
|
Market, on_delete=models.CASCADE, related_name="point_changes"
|
||||||
|
)
|
||||||
|
amount = models.IntegerField()
|
||||||
|
reason = models.CharField(max_length=20, choices=Reason.choices)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["user", "-created_at"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.username} {self.reason}: {self.amount} pts on {self.market.title}"
|
||||||
|
|||||||
@ -8,9 +8,9 @@ from pydantic import field_serializer, model_validator
|
|||||||
class MarketOptionSchema(Schema):
|
class MarketOptionSchema(Schema):
|
||||||
uuid: UUID
|
uuid: UUID
|
||||||
text: str
|
text: str
|
||||||
position: int
|
total_bets: int = 0
|
||||||
|
|
||||||
@field_serializer('uuid')
|
@field_serializer("uuid")
|
||||||
def serialize_uuid(self, value: UUID) -> str:
|
def serialize_uuid(self, value: UUID) -> str:
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
@ -19,14 +19,13 @@ class MarketListSchema(Schema):
|
|||||||
uuid: UUID
|
uuid: UUID
|
||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
type: str
|
|
||||||
status: str
|
status: str
|
||||||
end_date: datetime
|
end_date: datetime
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
options: List[MarketOptionSchema]
|
options: List[MarketOptionSchema]
|
||||||
winning_option: Optional[MarketOptionSchema] = None
|
winning_option: Optional[MarketOptionSchema] = None
|
||||||
|
|
||||||
@field_serializer('uuid')
|
@field_serializer("uuid")
|
||||||
def serialize_uuid(self, value: UUID) -> str:
|
def serialize_uuid(self, value: UUID) -> str:
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
@ -47,13 +46,13 @@ class UserBetSchema(Schema):
|
|||||||
option: MarketOptionSchema
|
option: MarketOptionSchema
|
||||||
market: Optional[MarketListSchema] = None
|
market: Optional[MarketListSchema] = None
|
||||||
|
|
||||||
@field_serializer('uuid')
|
@field_serializer("uuid")
|
||||||
def serialize_uuid(self, value: UUID) -> str:
|
def serialize_uuid(self, value: UUID) -> str:
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
@model_validator(mode='before')
|
@model_validator(mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def resolve_market_from_option(cls, data: Any) -> Any:
|
def resolve_market_from_option(cls, data: Any) -> Any:
|
||||||
if hasattr(data, 'option') and hasattr(data.option, 'market'):
|
if hasattr(data, "option") and hasattr(data.option, "market"):
|
||||||
data.market = data.option.market
|
data.market = data.option.market
|
||||||
return data
|
return data
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|||||||
@ -978,10 +978,6 @@ export type MarketListSchema = {
|
|||||||
* Description
|
* Description
|
||||||
*/
|
*/
|
||||||
description: string;
|
description: string;
|
||||||
/**
|
|
||||||
* Type
|
|
||||||
*/
|
|
||||||
type: string;
|
|
||||||
/**
|
/**
|
||||||
* Status
|
* Status
|
||||||
*/
|
*/
|
||||||
@ -1014,9 +1010,9 @@ export type MarketOptionSchema = {
|
|||||||
*/
|
*/
|
||||||
text: string;
|
text: string;
|
||||||
/**
|
/**
|
||||||
* Position
|
* Total Bets
|
||||||
*/
|
*/
|
||||||
position: number;
|
total_bets?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { polylanSubmitterApiGetUserInfo, marketApiCreateBet, marketApiListUserBets } from "../api";
|
import { polylanSubmitterApiGetUserInfo, marketApiCreateBet, marketApiListUserBets } from "../api";
|
||||||
import type { Market } from "../types";
|
import type { Market, MarketOption } from "../types";
|
||||||
import type { UserInfoOut, UserBetSchema } from "../api/types.gen";
|
import type { UserInfoOut, UserBetSchema } from "../api/types.gen";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -53,6 +53,21 @@ const canBet = computed(() => {
|
|||||||
return props.market.status === "open" && userInfo.value?.is_authenticated && selectedOption.value && betAmount.value > 0;
|
return props.market.status === "open" && userInfo.value?.is_authenticated && selectedOption.value && betAmount.value > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const totalPot = computed(() => {
|
||||||
|
return props.market.options.reduce((sum, opt) => sum + opt.total_bets, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMultiplier = (option: MarketOption) => {
|
||||||
|
if (option.total_bets === 0) return 0;
|
||||||
|
return totalPot.value / option.total_bets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPotentialGain = (option: MarketOption) => {
|
||||||
|
if (!existingBet.value || existingBet.value.option.uuid !== option.uuid) return 0;
|
||||||
|
const multiplier = getMultiplier(option);
|
||||||
|
return Math.round(existingBet.value.amount * multiplier);
|
||||||
|
};
|
||||||
|
|
||||||
const placeBet = async () => {
|
const placeBet = async () => {
|
||||||
if (!selectedOption.value || !betAmount.value) return;
|
if (!selectedOption.value || !betAmount.value) return;
|
||||||
|
|
||||||
@ -75,7 +90,14 @@ const placeBet = async () => {
|
|||||||
await loadUserBets();
|
await loadUserBets();
|
||||||
betAmount.value = 0;
|
betAmount.value = 0;
|
||||||
} else {
|
} else {
|
||||||
error.value = String(response.error) || "Failed to place bet";
|
const err = response.error as any;
|
||||||
|
if (typeof err === 'object' && err?.detail) {
|
||||||
|
error.value = err.detail;
|
||||||
|
} else if (typeof err === 'string') {
|
||||||
|
error.value = err;
|
||||||
|
} else {
|
||||||
|
error.value = "Failed to place bet";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = "Error placing bet";
|
error.value = "Error placing bet";
|
||||||
@ -145,10 +167,18 @@ onMounted(async () => {
|
|||||||
]">
|
]">
|
||||||
<div class="flex flex-col gap-2 flex-1">
|
<div class="flex flex-col gap-2 flex-1">
|
||||||
<span class="label-text font-medium">{{ option.text }}</span>
|
<span class="label-text font-medium">{{ option.text }}</span>
|
||||||
<span v-if="existingBet && existingBet.option.uuid === option.uuid"
|
<div class="text-xs text-base-content/60">
|
||||||
class="badge badge-primary badge-sm w-fit">
|
<div>Pool: {{ option.total_bets }} pts</div>
|
||||||
|
<div v-if="option.total_bets > 0">Multiplier: {{ getMultiplier(option).toFixed(2) }}x</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="existingBet && existingBet.option.uuid === option.uuid" class="flex flex-col gap-1">
|
||||||
|
<span class="badge badge-primary badge-sm w-fit">
|
||||||
Your bet: {{ existingBet.amount }} pts
|
Your bet: {{ existingBet.amount }} pts
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="option.total_bets > 0" class="badge badge-success badge-sm w-fit">
|
||||||
|
Potential: {{ getPotentialGain(option) }} pts
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="radio" :value="option.uuid" v-model="selectedOption" class="radio radio-primary"
|
<input type="radio" :value="option.uuid" v-model="selectedOption" class="radio radio-primary"
|
||||||
:disabled="market.status !== 'open' || !!(existingBet && existingBet.option.uuid !== option.uuid)" />
|
:disabled="market.status !== 'open' || !!(existingBet && existingBet.option.uuid !== option.uuid)" />
|
||||||
|
|||||||
@ -173,14 +173,13 @@ export interface TournamentPuzzleResults {
|
|||||||
export interface MarketOption {
|
export interface MarketOption {
|
||||||
uuid: string
|
uuid: string
|
||||||
text: string
|
text: string
|
||||||
position: number
|
total_bets: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Market {
|
export interface Market {
|
||||||
uuid: string
|
uuid: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
type: 'yes_no' | 'multiple'
|
|
||||||
status: 'open' | 'closed' | 'resolved'
|
status: 'open' | 'closed' | 'resolved'
|
||||||
end_date: string
|
end_date: string
|
||||||
created_at: string
|
created_at: string
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user