aboutsummaryrefslogtreecommitdiff
path: root/tests/managers/thl/test_contest
diff options
context:
space:
mode:
Diffstat (limited to 'tests/managers/thl/test_contest')
-rw-r--r--tests/managers/thl/test_contest/__init__.py0
-rw-r--r--tests/managers/thl/test_contest/test_leaderboard.py138
-rw-r--r--tests/managers/thl/test_contest/test_milestone.py296
-rw-r--r--tests/managers/thl/test_contest/test_raffle.py474
4 files changed, 908 insertions, 0 deletions
diff --git a/tests/managers/thl/test_contest/__init__.py b/tests/managers/thl/test_contest/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/managers/thl/test_contest/__init__.py
diff --git a/tests/managers/thl/test_contest/test_leaderboard.py b/tests/managers/thl/test_contest/test_leaderboard.py
new file mode 100644
index 0000000..80a88a5
--- /dev/null
+++ b/tests/managers/thl/test_contest/test_leaderboard.py
@@ -0,0 +1,138 @@
+from datetime import datetime, timezone, timedelta
+from zoneinfo import ZoneInfo
+
+from generalresearch.currency import USDCent
+from generalresearch.models.thl.contest.definitions import (
+ ContestStatus,
+ ContestEndReason,
+)
+from generalresearch.models.thl.contest.leaderboard import (
+ LeaderboardContest,
+ LeaderboardContestCreate,
+)
+from generalresearch.models.thl.product import Product
+from generalresearch.models.thl.user import User
+from test_utils.managers.contest.conftest import (
+ leaderboard_contest_in_db as contest_in_db,
+ leaderboard_contest_create as contest_create,
+)
+
+
+class TestLeaderboardContestCRUD:
+
+ def test_create(
+ self,
+ contest_create: LeaderboardContestCreate,
+ product_user_wallet_yes: Product,
+ thl_lm,
+ contest_manager,
+ ):
+ c = contest_manager.create(
+ product_id=product_user_wallet_yes.uuid, contest_create=contest_create
+ )
+ c_out = contest_manager.get(c.uuid)
+ assert c == c_out
+
+ assert isinstance(c, LeaderboardContest)
+ assert c.prize_count == 2
+ assert c.status == ContestStatus.ACTIVE
+ # We have it set in the fixture as the daily contest for 2025-01-01
+ assert c.end_condition.ends_at == datetime(
+ 2025, 1, 1, 23, 59, 59, 999999, tzinfo=ZoneInfo("America/New_York")
+ ).astimezone(tz=timezone.utc) + timedelta(minutes=90)
+
+ def test_enter(
+ self,
+ user_with_wallet: User,
+ contest_in_db: LeaderboardContest,
+ thl_lm,
+ contest_manager,
+ user_manager,
+ thl_redis,
+ ):
+ contest = contest_in_db
+ user = user_with_wallet
+
+ c: LeaderboardContest = contest_manager.get(contest_uuid=contest.uuid)
+
+ c = contest_manager.get_leaderboard_user_view(
+ contest_uuid=contest.uuid,
+ user=user,
+ redis_client=thl_redis,
+ user_manager=user_manager,
+ )
+ assert c.user_rank is None
+
+ lbm = c.get_leaderboard_manager()
+ lbm.hit_complete_count(user.product_user_id)
+
+ c = contest_manager.get_leaderboard_user_view(
+ contest_uuid=contest.uuid,
+ user=user,
+ redis_client=thl_redis,
+ user_manager=user_manager,
+ )
+ assert c.user_rank == 1
+
+ def test_contest_ends(
+ self,
+ user_with_wallet: User,
+ contest_in_db: LeaderboardContest,
+ thl_lm,
+ contest_manager,
+ user_manager,
+ thl_redis,
+ ):
+ # The contest should be over. We need to trigger it.
+ contest = contest_in_db
+ contest._redis_client = thl_redis
+ contest._user_manager = user_manager
+ user = user_with_wallet
+
+ lbm = contest.get_leaderboard_manager()
+ lbm.hit_complete_count(user.product_user_id)
+
+ c = contest_manager.get_leaderboard_user_view(
+ contest_uuid=contest.uuid,
+ user=user,
+ redis_client=thl_redis,
+ user_manager=user_manager,
+ )
+ assert c.user_rank == 1
+
+ bp_wallet = thl_lm.get_account_or_create_bp_wallet_by_uuid(user.product_id)
+ bp_wallet_balance = thl_lm.get_account_balance(account=bp_wallet)
+ assert bp_wallet_balance == 0
+ user_wallet = thl_lm.get_account_or_create_user_wallet(user=user)
+ user_balance = thl_lm.get_account_balance(user_wallet)
+ assert user_balance == 0
+
+ decision, reason = contest.should_end()
+ assert decision
+ assert reason == ContestEndReason.ENDS_AT
+
+ contest_manager.end_contest_if_over(contest=contest, ledger_manager=thl_lm)
+
+ c: LeaderboardContest = contest_manager.get(contest_uuid=contest.uuid)
+ assert c.status == ContestStatus.COMPLETED
+ print(c)
+
+ user_contest = contest_manager.get_leaderboard_user_view(
+ contest_uuid=contest.uuid,
+ user=user,
+ redis_client=thl_redis,
+ user_manager=user_manager,
+ )
+ assert len(user_contest.user_winnings) == 1
+ w = user_contest.user_winnings[0]
+ assert w.product_user_id == user.product_user_id
+ assert w.prize.cash_amount == USDCent(15_00)
+
+ # The prize is $15.00, so the user should get $15, paid by the bp
+ assert thl_lm.get_account_balance(account=user_wallet) == 15_00
+ # contest wallet is 0, and the BP gets 20c
+ contest_wallet = thl_lm.get_account_or_create_contest_wallet_by_uuid(
+ contest_uuid=c.uuid
+ )
+ assert thl_lm.get_account_balance(account=contest_wallet) == 0
+ assert thl_lm.get_account_balance(account=bp_wallet) == -15_00
diff --git a/tests/managers/thl/test_contest/test_milestone.py b/tests/managers/thl/test_contest/test_milestone.py
new file mode 100644
index 0000000..7312a64
--- /dev/null
+++ b/tests/managers/thl/test_contest/test_milestone.py
@@ -0,0 +1,296 @@
+from datetime import datetime, timezone
+
+from generalresearch.models.thl.contest.definitions import (
+ ContestStatus,
+ ContestEndReason,
+)
+from generalresearch.models.thl.contest.milestone import (
+ MilestoneContest,
+ MilestoneContestCreate,
+ MilestoneUserView,
+ ContestEntryTrigger,
+)
+from generalresearch.models.thl.product import Product
+from generalresearch.models.thl.user import User
+from test_utils.managers.contest.conftest import (
+ milestone_contest as contest,
+ milestone_contest_in_db as contest_in_db,
+ milestone_contest_create as contest_create,
+ milestone_contest_factory as contest_factory,
+)
+
+
+class TestMilestoneContest:
+
+ def test_should_end(self, contest: MilestoneContest, thl_lm, contest_manager):
+ # contest is active and has no entries
+ should, msg = contest.should_end()
+ assert not should, msg
+
+ # Change so that the contest ends now
+ contest.end_condition.ends_at = datetime.now(tz=timezone.utc)
+ should, msg = contest.should_end()
+ assert should
+ assert msg == ContestEndReason.ENDS_AT
+
+ # Change the win amount it thinks it past over the target
+ contest.end_condition.ends_at = None
+ contest.end_condition.max_winners = 10
+ contest.win_count = 10
+ should, msg = contest.should_end()
+ assert should
+ assert msg == ContestEndReason.MAX_WINNERS
+
+
+class TestMilestoneContestCRUD:
+
+ def test_create(
+ self,
+ contest_create: MilestoneContestCreate,
+ product_user_wallet_yes: Product,
+ thl_lm,
+ contest_manager,
+ ):
+ c = contest_manager.create(
+ product_id=product_user_wallet_yes.uuid, contest_create=contest_create
+ )
+ c_out = contest_manager.get(c.uuid)
+ assert c == c_out
+
+ assert isinstance(c, MilestoneContest)
+ assert c.prize_count == 2
+ assert c.status == ContestStatus.ACTIVE
+ assert c.end_condition.max_winners == 5
+ assert c.entry_trigger == ContestEntryTrigger.TASK_COMPLETE
+ assert c.target_amount == 3
+ assert c.win_count == 0
+
+ def test_enter(
+ self,
+ user_with_wallet: User,
+ contest_in_db: MilestoneContest,
+ thl_lm,
+ contest_manager,
+ ):
+ # Users CANNOT directly enter a milestone contest through the api,
+ # but we'll call this manager method when a trigger is hit.
+ contest = contest_in_db
+ user = user_with_wallet
+
+ contest_manager.enter_milestone_contest(
+ contest_uuid=contest.uuid,
+ user=user,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ incr=1,
+ )
+
+ c: MilestoneContest = contest_manager.get(contest_uuid=contest.uuid)
+ assert c.status == ContestStatus.ACTIVE
+ assert not hasattr(c, "current_amount")
+ assert not hasattr(c, "current_participants")
+
+ c: MilestoneUserView = contest_manager.get_milestone_user_view(
+ contest_uuid=contest.uuid, user=user_with_wallet
+ )
+ assert c.user_amount == 1
+
+ # Contest wallet should have 0 bc there is no ledger
+ contest_wallet = thl_lm.get_account_or_create_contest_wallet_by_uuid(
+ contest_uuid=contest.uuid
+ )
+ assert thl_lm.get_account_balance(contest_wallet) == 0
+
+ # Enter again!
+ contest_manager.enter_milestone_contest(
+ contest_uuid=contest.uuid,
+ user=user,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ incr=1,
+ )
+ c: MilestoneUserView = contest_manager.get_milestone_user_view(
+ contest_uuid=contest.uuid, user=user_with_wallet
+ )
+ assert c.user_amount == 2
+
+ # We should have ONE entry with a value of 2
+ e = contest_manager.get_entries_by_contest_id(c.id)
+ assert len(e) == 1
+ assert e[0].amount == 2
+
+ def test_enter_win(
+ self,
+ user_with_wallet: User,
+ contest_in_db: MilestoneContest,
+ thl_lm,
+ contest_manager,
+ ):
+ # User enters contest, which brings the USER'S total amount above the limit,
+ # and the user reaches the milestone
+ contest = contest_in_db
+ user = user_with_wallet
+
+ user_wallet = thl_lm.get_account_or_create_user_wallet(user=user)
+ user_balance = thl_lm.get_account_balance(account=user_wallet)
+ bp_wallet = thl_lm.get_account_or_create_bp_wallet_by_uuid(
+ product_uuid=user.product_id
+ )
+ bp_wallet_balance = thl_lm.get_account_balance(account=bp_wallet)
+
+ c: MilestoneUserView = contest_manager.get_milestone_user_view(
+ contest_uuid=contest.uuid, user=user_with_wallet
+ )
+ assert c.user_amount == 0
+ res, msg = c.is_user_eligible(country_iso="us")
+ assert res, msg
+
+ # User reaches the milestone after 3 completes/whatevers.
+ for _ in range(3):
+ contest_manager.enter_milestone_contest(
+ contest_uuid=contest.uuid,
+ user=user,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ incr=1,
+ )
+
+ # to be clear, the contest itself doesn't end!
+ c: MilestoneContest = contest_manager.get(contest_uuid=contest.uuid)
+ assert c.status == ContestStatus.ACTIVE
+
+ c: MilestoneUserView = contest_manager.get_milestone_user_view(
+ contest_uuid=contest.uuid, user=user_with_wallet
+ )
+ assert c.user_amount == 3
+ res, msg = c.is_user_eligible(country_iso="us")
+ assert not res
+ assert msg == "User should have won already"
+
+ assert len(c.user_winnings) == 2
+ assert c.win_count == 1
+
+ # The prize was awarded! User should have won $1.00
+ assert thl_lm.get_account_balance(user_wallet) - user_balance == 100
+ # Which was paid from the BP's balance
+ assert thl_lm.get_account_balance(bp_wallet) - bp_wallet_balance == -100
+
+ # winnings = cm.get_winnings_by_user(user=user)
+ # assert len(winnings) == 1
+ # win = winnings[0]
+ # assert win.product_user_id == user.product_user_id
+
+ def test_enter_ends(
+ self,
+ user_factory,
+ product_user_wallet_yes: Product,
+ contest_in_db: MilestoneContest,
+ thl_lm,
+ contest_manager,
+ ):
+ # Multiple users reach the milestone. Contest ends after 5 wins.
+ users = [user_factory(product=product_user_wallet_yes) for _ in range(5)]
+ contest = contest_in_db
+
+ for u in users:
+ contest_manager.enter_milestone_contest(
+ contest_uuid=contest.uuid,
+ user=u,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ incr=3,
+ )
+
+ c: MilestoneContest = contest_manager.get(contest_uuid=contest.uuid)
+ assert c.status == ContestStatus.COMPLETED
+ assert c.end_reason == ContestEndReason.MAX_WINNERS
+
+ def test_trigger(
+ self,
+ user_with_wallet: User,
+ contest_in_db: MilestoneContest,
+ thl_lm,
+ contest_manager,
+ ):
+ # Pretend user just got a complete
+ cnt = contest_manager.hit_milestone_triggers(
+ country_iso="us",
+ user=user_with_wallet,
+ event=ContestEntryTrigger.TASK_COMPLETE,
+ ledger_manager=thl_lm,
+ )
+ assert cnt == 1
+
+ # Assert this contest got entered
+ c: MilestoneUserView = contest_manager.get_milestone_user_view(
+ contest_uuid=contest_in_db.uuid, user=user_with_wallet
+ )
+ assert c.user_amount == 1
+
+
+class TestMilestoneContestUserViews:
+ def test_list_user_eligible_country(
+ self, user_with_wallet: User, contest_factory, thl_lm, contest_manager
+ ):
+ # No contests exists
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_wallet, country_iso="us"
+ )
+ assert len(cs) == 0
+
+ # Create a contest. It'll be in the US/CA
+ contest_factory(country_isos={"us", "ca"})
+
+ # Not eligible in mexico
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_wallet, country_iso="mx"
+ )
+ assert len(cs) == 0
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_wallet, country_iso="us"
+ )
+ assert len(cs) == 1
+
+ # Create another, any country
+ contest_factory(country_isos=None)
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_wallet, country_iso="mx"
+ )
+ assert len(cs) == 1
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_wallet, country_iso="us"
+ )
+ assert len(cs) == 2
+
+ def test_list_user_eligible(
+ self, user_with_money: User, contest_factory, thl_lm, contest_manager
+ ):
+ # User reaches milestone after 1 complete
+ c = contest_factory(target_amount=1)
+ user = user_with_money
+
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_money, country_iso="us"
+ )
+ assert len(cs) == 1
+
+ contest_manager.enter_milestone_contest(
+ contest_uuid=c.uuid, user=user, country_iso="us", ledger_manager=thl_lm
+ )
+
+ # User isn't eligible anymore
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_money, country_iso="us"
+ )
+ assert len(cs) == 0
+
+ # But it comes back in the list entered
+ cs = contest_manager.get_many_by_user_entered(user=user_with_money)
+ assert len(cs) == 1
+ c = cs[0]
+ assert c.user_amount == 1
+ assert isinstance(c, MilestoneUserView)
+ assert not hasattr(c, "current_win_probability")
+
+ # They won one contest with 2 prizes
+ assert len(contest_manager.get_winnings_by_user(user_with_money)) == 2
diff --git a/tests/managers/thl/test_contest/test_raffle.py b/tests/managers/thl/test_contest/test_raffle.py
new file mode 100644
index 0000000..060055a
--- /dev/null
+++ b/tests/managers/thl/test_contest/test_raffle.py
@@ -0,0 +1,474 @@
+from datetime import datetime, timezone
+
+import pytest
+from pydantic import ValidationError
+from pytest import approx
+
+from generalresearch.currency import USDCent
+from generalresearch.managers.thl.ledger_manager.exceptions import (
+ LedgerTransactionConditionFailedError,
+)
+from generalresearch.models.thl.contest import (
+ ContestPrize,
+ ContestEntryRule,
+ ContestEndCondition,
+)
+from generalresearch.models.thl.contest.definitions import (
+ ContestStatus,
+ ContestPrizeKind,
+ ContestEndReason,
+)
+from generalresearch.models.thl.contest.exceptions import ContestError
+from generalresearch.models.thl.contest.raffle import (
+ ContestEntry,
+ ContestEntryType,
+)
+from generalresearch.models.thl.contest.raffle import (
+ RaffleContest,
+ RaffleContestCreate,
+ RaffleUserView,
+)
+from generalresearch.models.thl.product import Product
+from generalresearch.models.thl.user import User
+from test_utils.managers.contest.conftest import (
+ raffle_contest as contest,
+ raffle_contest_in_db as contest_in_db,
+ raffle_contest_create as contest_create,
+ raffle_contest_factory as contest_factory,
+)
+
+
+class TestRaffleContest:
+
+ def test_should_end(self, contest: RaffleContest, thl_lm, contest_manager):
+ # contest is active and has no entries
+ should, msg = contest.should_end()
+ assert not should, msg
+
+ # Change so that the contest ends now
+ contest.end_condition.ends_at = datetime.now(tz=timezone.utc)
+ should, msg = contest.should_end()
+ assert should
+ assert msg == ContestEndReason.ENDS_AT
+
+ # Change the entry amount it thinks it has to over the target
+ contest.end_condition.ends_at = None
+ contest.current_amount = USDCent(100)
+ should, msg = contest.should_end()
+ assert should
+ assert msg == ContestEndReason.TARGET_ENTRY_AMOUNT
+
+
+class TestRaffleContestCRUD:
+
+ def test_create(
+ self,
+ contest_create: RaffleContestCreate,
+ product_user_wallet_yes: Product,
+ thl_lm,
+ contest_manager,
+ ):
+ c = contest_manager.create(
+ product_id=product_user_wallet_yes.uuid, contest_create=contest_create
+ )
+ c_out = contest_manager.get(c.uuid)
+ assert c == c_out
+
+ assert isinstance(c, RaffleContest)
+ assert c.prize_count == 1
+ assert c.status == ContestStatus.ACTIVE
+ assert c.end_condition.target_entry_amount == USDCent(100)
+ assert c.current_amount == 0
+ assert c.current_participants == 0
+
+ @pytest.mark.parametrize("user_with_money", [{"min_balance": 60}], indirect=True)
+ def test_enter(
+ self,
+ user_with_money: User,
+ contest_in_db: RaffleContest,
+ thl_lm,
+ contest_manager,
+ ):
+ # Raffle ends at $1.00. User enters for $0.60
+ print(user_with_money.product_id)
+ print(contest_in_db.product_id)
+ print(contest_in_db.uuid)
+ contest = contest_in_db
+
+ user_wallet = thl_lm.get_account_or_create_user_wallet(user=user_with_money)
+ user_balance = thl_lm.get_account_balance(account=user_wallet)
+
+ entry = ContestEntry(
+ entry_type=ContestEntryType.CASH, user=user_with_money, amount=USDCent(60)
+ )
+ entry = contest_manager.enter_contest(
+ contest_uuid=contest.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )
+ c: RaffleContest = contest_manager.get(contest_uuid=contest.uuid)
+ assert c.current_amount == USDCent(60)
+ assert c.current_participants == 1
+ assert c.status == ContestStatus.ACTIVE
+
+ c: RaffleUserView = contest_manager.get_raffle_user_view(
+ contest_uuid=contest.uuid, user=user_with_money
+ )
+ assert c.user_amount == USDCent(60)
+ assert c.user_amount_today == USDCent(60)
+ assert c.projected_win_probability == approx(60 / 100, rel=0.01)
+
+ # Contest wallet should have $0.60
+ contest_wallet = thl_lm.get_account_or_create_contest_wallet_by_uuid(
+ contest_uuid=contest.uuid
+ )
+ assert thl_lm.get_account_balance(account=contest_wallet) == 60
+ # User spent 60c
+ assert user_balance - thl_lm.get_account_balance(account=user_wallet) == 60
+
+ @pytest.mark.parametrize("user_with_money", [{"min_balance": 120}], indirect=True)
+ def test_enter_ends(
+ self,
+ user_with_money: User,
+ contest_in_db: RaffleContest,
+ thl_lm,
+ contest_manager,
+ ):
+ # User enters contest, which brings the total amount above the limit,
+ # and the contest should end, with a winner selected
+ contest = contest_in_db
+
+ bp_wallet = thl_lm.get_account_or_create_bp_wallet_by_uuid(
+ user_with_money.product_id
+ )
+ # I bribed the user, so the balance is not 0
+ bp_wallet_balance = thl_lm.get_account_balance(account=bp_wallet)
+
+ for _ in range(2):
+ entry = ContestEntry(
+ entry_type=ContestEntryType.CASH,
+ user=user_with_money,
+ amount=USDCent(60),
+ )
+ contest_manager.enter_contest(
+ contest_uuid=contest.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )
+ c: RaffleContest = contest_manager.get(contest_uuid=contest.uuid)
+ assert c.status == ContestStatus.COMPLETED
+ print(c)
+
+ user_contest = contest_manager.get_raffle_user_view(
+ contest_uuid=contest.uuid, user=user_with_money
+ )
+ assert user_contest.current_win_probability == 1
+ assert user_contest.projected_win_probability == 1
+ assert len(user_contest.user_winnings) == 1
+
+ # todo: make a all winning method
+ winnings = contest_manager.get_winnings_by_user(user=user_with_money)
+ assert len(winnings) == 1
+ win = winnings[0]
+ assert win.product_user_id == user_with_money.product_user_id
+
+ # Contest wallet should have gotten zeroed out
+ contest_wallet = thl_lm.get_account_or_create_contest_wallet_by_uuid(
+ contest_uuid=contest.uuid
+ )
+ assert thl_lm.get_account_balance(contest_wallet) == 0
+ # Expense wallet gets the $1.00 expense
+ expense_wallet = thl_lm.get_account_or_create_bp_expense_by_uuid(
+ product_uuid=user_with_money.product_id, expense_name="Prize"
+ )
+ assert thl_lm.get_account_balance(expense_wallet) == -100
+ # And the BP gets 20c
+ assert thl_lm.get_account_balance(bp_wallet) - bp_wallet_balance == 20
+
+ @pytest.mark.parametrize("user_with_money", [{"min_balance": 120}], indirect=True)
+ def test_enter_ends_cash_prize(
+ self, user_with_money: User, contest_factory, thl_lm, contest_manager
+ ):
+ # Same as test_enter_ends, but the prize is cash. Just
+ # testing the ledger methods
+ c = contest_factory(
+ prizes=[
+ ContestPrize(
+ name="$1.00 bonus",
+ kind=ContestPrizeKind.CASH,
+ estimated_cash_value=USDCent(100),
+ cash_amount=USDCent(100),
+ )
+ ]
+ )
+ assert c.prizes[0].kind == ContestPrizeKind.CASH
+
+ user_wallet = thl_lm.get_account_or_create_user_wallet(user=user_with_money)
+ user_balance = thl_lm.get_account_balance(user_wallet)
+ bp_wallet = thl_lm.get_account_or_create_bp_wallet_by_uuid(
+ user_with_money.product_id
+ )
+ bp_wallet_balance = thl_lm.get_account_balance(bp_wallet)
+
+ ## Enter Contest
+ entry = ContestEntry(
+ entry_type=ContestEntryType.CASH, user=user_with_money, amount=USDCent(120)
+ )
+ entry = contest_manager.enter_contest(
+ contest_uuid=c.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )
+
+ # The prize is $1.00, so the user spent $1.20 entering, won, then got $1.00 back
+ assert (
+ thl_lm.get_account_balance(account=user_wallet) == user_balance + 100 - 120
+ )
+ # contest wallet is 0, and the BP gets 20c
+ contest_wallet = thl_lm.get_account_or_create_contest_wallet_by_uuid(
+ contest_uuid=c.uuid
+ )
+ assert thl_lm.get_account_balance(account=contest_wallet) == 0
+ assert thl_lm.get_account_balance(account=bp_wallet) - bp_wallet_balance == 20
+
+ def test_enter_failure(
+ self,
+ user_with_wallet: User,
+ contest_in_db: RaffleContest,
+ thl_lm,
+ contest_manager,
+ ):
+ c = contest_in_db
+ user = user_with_wallet
+
+ # Tries to enter $0
+ with pytest.raises(ValidationError) as e:
+ entry = ContestEntry(
+ entry_type=ContestEntryType.CASH, user=user, amount=USDCent(0)
+ )
+ assert "Input should be greater than 0" in str(e.value)
+
+ # User has no money
+ entry = ContestEntry(
+ entry_type=ContestEntryType.CASH, user=user, amount=USDCent(20)
+ )
+ with pytest.raises(LedgerTransactionConditionFailedError) as e:
+ entry = contest_manager.enter_contest(
+ contest_uuid=c.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )
+ assert e.value.args[0] == "insufficient balance"
+
+ # Tries to enter with the wrong entry type (count, on a cash contest)
+ entry = ContestEntry(entry_type=ContestEntryType.COUNT, user=user, amount=1)
+ with pytest.raises(AssertionError) as e:
+ entry = contest_manager.enter_contest(
+ contest_uuid=c.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )
+ assert "incompatible entry type" in str(e.value)
+
+ @pytest.mark.parametrize("user_with_money", [{"min_balance": 100}], indirect=True)
+ def test_enter_not_eligible(
+ self, user_with_money: User, contest_factory, thl_lm, contest_manager
+ ):
+ # Max entry amount per user $0.10. Contest still ends at $1.00
+ c = contest_factory(
+ entry_rule=ContestEntryRule(
+ max_entry_amount_per_user=USDCent(10),
+ max_daily_entries_per_user=USDCent(8),
+ )
+ )
+ c: RaffleContest = contest_manager.get(c.uuid)
+ assert c.entry_rule.max_entry_amount_per_user == USDCent(10)
+ assert c.entry_rule.max_daily_entries_per_user == USDCent(8)
+
+ # User tries to enter $0.20
+ entry = ContestEntry(
+ entry_type=ContestEntryType.CASH, user=user_with_money, amount=USDCent(20)
+ )
+ with pytest.raises(ContestError) as e:
+ entry = contest_manager.enter_contest(
+ contest_uuid=c.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )
+ assert "Entry would exceed max amount per user." in str(e.value)
+
+ # User tries to enter $0.10
+ entry = ContestEntry(
+ entry_type=ContestEntryType.CASH, user=user_with_money, amount=USDCent(10)
+ )
+ with pytest.raises(ContestError) as e:
+ entry = contest_manager.enter_contest(
+ contest_uuid=c.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )
+ assert "Entry would exceed max amount per user per day." in str(e.value)
+
+ # User enters $0.08 successfully
+ entry = ContestEntry(
+ entry_type=ContestEntryType.CASH, user=user_with_money, amount=USDCent(8)
+ )
+ entry = contest_manager.enter_contest(
+ contest_uuid=c.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )
+
+ # Then can't anymore
+ entry = ContestEntry(
+ entry_type=ContestEntryType.CASH, user=user_with_money, amount=USDCent(1)
+ )
+ with pytest.raises(ContestError) as e:
+ entry = contest_manager.enter_contest(
+ contest_uuid=c.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )
+ assert "Entry would exceed max amount per user per day." in str(e.value)
+
+
+class TestRaffleContestUserViews:
+ def test_list_user_eligible_country(
+ self, user_with_wallet: User, contest_factory, thl_lm, contest_manager
+ ):
+ # No contests exists
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_wallet, country_iso="us"
+ )
+ assert len(cs) == 0
+
+ # Create a contest. It'll be in the US/CA
+ contest_factory(country_isos={"us", "ca"})
+
+ # Not eligible in mexico
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_wallet, country_iso="mx"
+ )
+ assert len(cs) == 0
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_wallet, country_iso="us"
+ )
+ assert len(cs) == 1
+
+ # Create another, any country
+ contest_factory(country_isos=None)
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_wallet, country_iso="mx"
+ )
+ assert len(cs) == 1
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_wallet, country_iso="us"
+ )
+ assert len(cs) == 2
+
+ def test_list_user_eligible(
+ self, user_with_money: User, contest_factory, thl_lm, contest_manager
+ ):
+ c = contest_factory(
+ end_condition=ContestEndCondition(target_entry_amount=USDCent(10)),
+ entry_rule=ContestEntryRule(
+ max_entry_amount_per_user=USDCent(1),
+ ),
+ )
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_money, country_iso="us"
+ )
+ assert len(cs) == 1
+
+ entry = ContestEntry(
+ entry_type=ContestEntryType.CASH,
+ user=user_with_money,
+ amount=USDCent(1),
+ )
+ contest_manager.enter_contest(
+ contest_uuid=c.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )
+
+ # User isn't eligible anymore
+ cs = contest_manager.get_many_by_user_eligible(
+ user=user_with_money, country_iso="us"
+ )
+ assert len(cs) == 0
+
+ # But it comes back in the list entered
+ cs = contest_manager.get_many_by_user_entered(user=user_with_money)
+ assert len(cs) == 1
+ c = cs[0]
+ assert c.user_amount == USDCent(1)
+ assert c.user_amount_today == USDCent(1)
+ assert c.current_win_probability == 1
+ assert c.projected_win_probability == approx(1 / 10, rel=0.01)
+
+ # And nothing won yet #todo
+ # cs = cm.get_many_by_user_won(user=user_with_money)
+
+ assert len(contest_manager.get_winnings_by_user(user_with_money)) == 0
+
+ def test_list_user_winnings(
+ self, user_with_money: User, contest_factory, thl_lm, contest_manager
+ ):
+ c = contest_factory(
+ end_condition=ContestEndCondition(target_entry_amount=USDCent(100)),
+ )
+ entry = ContestEntry(
+ entry_type=ContestEntryType.CASH,
+ user=user_with_money,
+ amount=USDCent(100),
+ )
+ contest_manager.enter_contest(
+ contest_uuid=c.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )
+ # Contest ends after 100 entry, user enters 100 entry, user wins!
+ ws = contest_manager.get_winnings_by_user(user_with_money)
+ assert len(ws) == 1
+ w = ws[0]
+ assert w.user.user_id == user_with_money.user_id
+ assert w.prize == c.prizes[0]
+ assert w.awarded_cash_amount is None
+
+ cs = contest_manager.get_many_by_user_won(user_with_money)
+ assert len(cs) == 1
+ c = cs[0]
+ w = c.user_winnings[0]
+ assert w.prize == c.prizes[0]
+ assert w.user.user_id == user_with_money.user_id
+
+
+class TestRaffleContestCRUDCount:
+ # This is a COUNT contest. No cash moves. Not really fleshed out what we'd do with this.
+ @pytest.mark.skip
+ def test_enter(
+ self, user_with_wallet: User, contest_factory, thl_lm, contest_manager
+ ):
+ c = contest_factory(entry_type=ContestEntryType.COUNT)
+ entry = ContestEntry(
+ entry_type=ContestEntryType.COUNT,
+ user=user_with_wallet,
+ amount=1,
+ )
+ contest_manager.enter_contest(
+ contest_uuid=c.uuid,
+ entry=entry,
+ country_iso="us",
+ ledger_manager=thl_lm,
+ )