aboutsummaryrefslogtreecommitdiff
path: root/tests/models/thl/test_contest/test_raffle_contest.py
diff options
context:
space:
mode:
authorMax Nanis2026-03-06 16:49:46 -0500
committerMax Nanis2026-03-06 16:49:46 -0500
commit91d040211a4ed6e4157896256a762d3854777b5e (patch)
treecd95922ea4257dc8d3f4e4cbe8534474709a20dc /tests/models/thl/test_contest/test_raffle_contest.py
downloadgeneralresearch-91d040211a4ed6e4157896256a762d3854777b5e.tar.gz
generalresearch-91d040211a4ed6e4157896256a762d3854777b5e.zip
Initial commitv3.3.4
Diffstat (limited to 'tests/models/thl/test_contest/test_raffle_contest.py')
-rw-r--r--tests/models/thl/test_contest/test_raffle_contest.py300
1 files changed, 300 insertions, 0 deletions
diff --git a/tests/models/thl/test_contest/test_raffle_contest.py b/tests/models/thl/test_contest/test_raffle_contest.py
new file mode 100644
index 0000000..e1c0a15
--- /dev/null
+++ b/tests/models/thl/test_contest/test_raffle_contest.py
@@ -0,0 +1,300 @@
+from collections import Counter
+from uuid import uuid4
+
+import pytest
+from pytest import approx
+
+from generalresearch.currency import USDCent
+from generalresearch.models.thl.contest import (
+ ContestPrize,
+ ContestEndCondition,
+)
+from generalresearch.models.thl.contest.contest_entry import ContestEntry
+from generalresearch.models.thl.contest.definitions import (
+ ContestEntryType,
+ ContestPrizeKind,
+ ContestType,
+ ContestStatus,
+ ContestEndReason,
+)
+from generalresearch.models.thl.contest.raffle import RaffleContest
+
+from tests.models.thl.test_contest.test_contest import TestContest
+
+
+class TestRaffleContest(TestContest):
+
+ @pytest.fixture(scope="function")
+ def raffle_contest(self, product) -> RaffleContest:
+ return RaffleContest(
+ product_id=product.uuid,
+ name=f"Raffle Contest {uuid4().hex}",
+ contest_type=ContestType.RAFFLE,
+ entry_type=ContestEntryType.CASH,
+ prizes=[
+ ContestPrize(
+ name="iPod 64GB White",
+ kind=ContestPrizeKind.PHYSICAL,
+ estimated_cash_value=USDCent(100_00),
+ )
+ ],
+ end_condition=ContestEndCondition(target_entry_amount=100),
+ )
+
+ @pytest.fixture(scope="function")
+ def ended_raffle_contest(self, raffle_contest, utc_now) -> RaffleContest:
+ # Fake ending the contest
+ raffle_contest = raffle_contest.model_copy()
+ raffle_contest.update(
+ status=ContestStatus.COMPLETED,
+ ended_at=utc_now,
+ end_reason=ContestEndReason.ENDS_AT,
+ )
+ return raffle_contest
+
+
+class TestRaffleContestUserView(TestRaffleContest):
+
+ def test_user_view(self, raffle_contest, user):
+ from generalresearch.models.thl.contest.raffle import RaffleUserView
+
+ data = {
+ "current_amount": USDCent(1_00),
+ "product_user_id": user.product_user_id,
+ "user_amount": USDCent(1),
+ "user_amount_today": USDCent(1),
+ }
+ r = RaffleUserView.model_validate(raffle_contest.model_dump() | data)
+ res = r.model_dump(mode="json")
+
+ assert res["product_user_id"] == user.product_user_id
+ assert res["user_amount_today"] == 1
+ assert res["current_win_probability"] == approx(0.01, rel=0.000001)
+ assert res["projected_win_probability"] == approx(0.01, rel=0.000001)
+
+ # Now change the amount
+ r.current_amount = USDCent(1_01)
+ res = r.model_dump(mode="json")
+ assert res["current_win_probability"] == approx(0.0099, rel=0.001)
+ assert res["projected_win_probability"] == approx(0.0099, rel=0.001)
+
+ def test_win_pct(self, raffle_contest, user):
+ from generalresearch.models.thl.contest.raffle import RaffleUserView
+
+ data = {
+ "current_amount": USDCent(10),
+ "product_user_id": user.product_user_id,
+ "user_amount": USDCent(1),
+ "user_amount_today": USDCent(1),
+ }
+ r = RaffleUserView.model_validate(raffle_contest.model_dump() | data)
+ r.prizes = [
+ ContestPrize(
+ name="iPod 64GB White",
+ kind=ContestPrizeKind.PHYSICAL,
+ estimated_cash_value=USDCent(100_00),
+ ),
+ ContestPrize(
+ name="iPod 64GB White",
+ kind=ContestPrizeKind.PHYSICAL,
+ estimated_cash_value=USDCent(100_00),
+ ),
+ ]
+ # Raffle has 10 entries, user has 1 entry.
+ # There are 2 prizes.
+ assert r.current_win_probability == approx(expected=0.2, rel=0.01)
+ # He can only possibly win 1 prize
+ assert r.current_prize_count_probability[1] == approx(expected=0.2, rel=0.01)
+ # He has a 0 prob of winning 2 prizes
+ assert r.current_prize_count_probability[2] == 0
+ # Contest end when there are 100 entries, so 1/100 * 2 prizes
+ assert r.projected_win_probability == approx(expected=0.02, rel=0.01)
+
+ # Change to user having 2 entries (out of 10)
+ # Still with 2 prizes
+ r.user_amount = USDCent(2)
+ assert r.current_win_probability == approx(expected=0.3777, rel=0.01)
+ # 2/10 chance of winning 1st, 8/9 change of not winning 2nd, plus the
+ # same in the other order
+ p = (2 / 10) * (8 / 9) * 2 # 0.355555
+ assert r.current_prize_count_probability[1] == approx(p, rel=0.01)
+ p = (2 / 10) * (1 / 9) # 0.02222
+ assert r.current_prize_count_probability[2] == approx(p, rel=0.01)
+
+
+class TestRaffleContestWinners(TestRaffleContest):
+
+ def test_winners_1_prize(self, ended_raffle_contest, user_1, user_2, user_3):
+ ended_raffle_contest.entries = [
+ ContestEntry(
+ user=user_1,
+ amount=USDCent(1),
+ entry_type=ContestEntryType.CASH,
+ ),
+ ContestEntry(
+ user=user_2,
+ amount=USDCent(2),
+ entry_type=ContestEntryType.CASH,
+ ),
+ ContestEntry(
+ user=user_3,
+ amount=USDCent(3),
+ entry_type=ContestEntryType.CASH,
+ ),
+ ]
+
+ # There is 1 prize. If we select a winner 1000 times, we'd expect user 1
+ # to win ~ 1/6th of the time, user 2 ~2/6th and 3 3/6th.
+ winners = ended_raffle_contest.select_winners()
+ assert len(winners) == 1
+
+ c = Counter(
+ [
+ ended_raffle_contest.select_winners()[0].user.user_id
+ for _ in range(10000)
+ ]
+ )
+ assert c[user_1.user_id] == approx(
+ 10000 * 1 / 6, rel=0.1
+ ) # 10% relative tolerance
+ assert c[user_2.user_id] == approx(10000 * 2 / 6, rel=0.1)
+ assert c[user_3.user_id] == approx(10000 * 3 / 6, rel=0.1)
+
+ def test_winners_2_prizes(self, ended_raffle_contest, user_1, user_2, user_3):
+ ended_raffle_contest.prizes.append(
+ ContestPrize(
+ name="iPod 64GB Black",
+ kind=ContestPrizeKind.PHYSICAL,
+ estimated_cash_value=USDCent(100_00),
+ )
+ )
+ ended_raffle_contest.entries = [
+ ContestEntry(
+ user=user_3,
+ amount=USDCent(1),
+ entry_type=ContestEntryType.CASH,
+ ),
+ ContestEntry(
+ user=user_1,
+ amount=USDCent(9999999),
+ entry_type=ContestEntryType.CASH,
+ ),
+ ContestEntry(
+ user=user_2,
+ amount=USDCent(1),
+ entry_type=ContestEntryType.CASH,
+ ),
+ ]
+ # In this scenario, user 1 should win both prizes
+ winners = ended_raffle_contest.select_winners()
+ assert len(winners) == 2
+ # Two different prizes
+ assert len({w.prize.name for w in winners}) == 2
+ # Same user
+ assert all(w.user.user_id == user_1.user_id for w in winners)
+
+ def test_winners_2_prizes_1_entry(self, ended_raffle_contest, user_3):
+ ended_raffle_contest.prizes = [
+ ContestPrize(
+ name="iPod 64GB White",
+ kind=ContestPrizeKind.PHYSICAL,
+ estimated_cash_value=USDCent(100_00),
+ ),
+ ContestPrize(
+ name="iPod 64GB Black",
+ kind=ContestPrizeKind.PHYSICAL,
+ estimated_cash_value=USDCent(100_00),
+ ),
+ ]
+ ended_raffle_contest.entries = [
+ ContestEntry(
+ user=user_3,
+ amount=USDCent(1),
+ entry_type=ContestEntryType.CASH,
+ ),
+ ]
+
+ # One prize goes unclaimed
+ winners = ended_raffle_contest.select_winners()
+ assert len(winners) == 1
+
+ def test_winners_2_prizes_1_entry_2_pennies(self, ended_raffle_contest, user_3):
+ ended_raffle_contest.prizes = [
+ ContestPrize(
+ name="iPod 64GB White",
+ kind=ContestPrizeKind.PHYSICAL,
+ estimated_cash_value=USDCent(100_00),
+ ),
+ ContestPrize(
+ name="iPod 64GB Black",
+ kind=ContestPrizeKind.PHYSICAL,
+ estimated_cash_value=USDCent(100_00),
+ ),
+ ]
+ ended_raffle_contest.entries = [
+ ContestEntry(
+ user=user_3,
+ amount=USDCent(2),
+ entry_type=ContestEntryType.CASH,
+ ),
+ ]
+ # User wins both prizes
+ winners = ended_raffle_contest.select_winners()
+ assert len(winners) == 2
+
+ def test_winners_3_prizes_3_entries(
+ self, ended_raffle_contest, product, user_1, user_2, user_3
+ ):
+ ended_raffle_contest.prizes = [
+ ContestPrize(
+ name="iPod 64GB White",
+ kind=ContestPrizeKind.PHYSICAL,
+ estimated_cash_value=USDCent(100_00),
+ ),
+ ContestPrize(
+ name="iPod 64GB Black",
+ kind=ContestPrizeKind.PHYSICAL,
+ estimated_cash_value=USDCent(100_00),
+ ),
+ ContestPrize(
+ name="iPod 64GB Red",
+ kind=ContestPrizeKind.PHYSICAL,
+ estimated_cash_value=USDCent(100_00),
+ ),
+ ]
+ ended_raffle_contest.entries = [
+ ContestEntry(
+ user=user_1,
+ amount=USDCent(1),
+ entry_type=ContestEntryType.CASH,
+ ),
+ ContestEntry(
+ user=user_2,
+ amount=USDCent(2),
+ entry_type=ContestEntryType.CASH,
+ ),
+ ContestEntry(
+ user=user_3,
+ amount=USDCent(3),
+ entry_type=ContestEntryType.CASH,
+ ),
+ ]
+
+ winners = ended_raffle_contest.select_winners()
+ assert len(winners) == 3
+
+ winners = [ended_raffle_contest.select_winners() for _ in range(10000)]
+
+ # There's 3 winners, the 1st should follow the same percentages
+ c = Counter([w[0].user.user_id for w in winners])
+
+ assert c[user_1.user_id] == approx(10000 * 1 / 6, rel=0.1)
+ assert c[user_2.user_id] == approx(10000 * 2 / 6, rel=0.1)
+ assert c[user_3.user_id] == approx(10000 * 3 / 6, rel=0.1)
+
+ # Assume the 1st user won
+ ended_raffle_contest.entries.pop(0)
+ winners = [ended_raffle_contest.select_winners() for _ in range(10000)]
+ c = Counter([w[0].user.user_id for w in winners])
+ assert c[user_2.user_id] == approx(10000 * 2 / 5, rel=0.1)
+ assert c[user_3.user_id] == approx(10000 * 3 / 5, rel=0.1)