diff options
| author | stuppie | 2026-09-02 12:36:00 -0600 |
|---|---|---|
| committer | stuppie | 2026-09-02 12:36:00 -0600 |
| commit | ce5a2d02389b0879598dc5c5fbab989ea0913028 (patch) | |
| tree | 5433119a9a8f13c93d1ad2fac1a9591978e5de01 | |
| parent | 4e3fbea26ca1aeee7459d6026b8403c862f85fca (diff) | |
| download | generalresearch-ce5a2d02389b0879598dc5c5fbab989ea0913028.tar.gz generalresearch-ce5a2d02389b0879598dc5c5fbab989ea0913028.zip | |
add is_attempt_credit_eligible, get_user_wallets
| -rw-r--r-- | generalresearch/managers/thl/ledger_manager/thl_ledger.py | 86 | ||||
| -rw-r--r-- | generalresearch/models/thl/session.py | 23 | ||||
| -rw-r--r-- | generalresearch/models/thl/wallet/user_wallet.py | 25 |
3 files changed, 129 insertions, 5 deletions
diff --git a/generalresearch/managers/thl/ledger_manager/thl_ledger.py b/generalresearch/managers/thl/ledger_manager/thl_ledger.py index 27a5ac7..fa09a45 100644 --- a/generalresearch/managers/thl/ledger_manager/thl_ledger.py +++ b/generalresearch/managers/thl/ledger_manager/thl_ledger.py @@ -54,10 +54,15 @@ from generalresearch.models.thl.ledger import ( TransactionMetadataColumns as tmc, ) from generalresearch.models.thl.payout import UserPayoutEvent +from generalresearch.models.thl.payout_format import format_payout_format from generalresearch.models.thl.product import Product from generalresearch.models.thl.session import Session, Status, Wall from generalresearch.models.thl.user import User from generalresearch.models.thl.wallet import PayoutType +from generalresearch.models.thl.wallet.user_wallet import ( + UserLedgerWallet, + UserLedgerWallets, +) if TYPE_CHECKING: from generalresearch.models.thl.contest.contest import ContestWinner @@ -68,7 +73,6 @@ logger.setLevel(logging.INFO) class ThlLedgerManager(LedgerManager): - def get_account_or_create_user_wallet(self, user: User) -> LedgerAccount: """ TODO: In the future we could create a user wallet account with a @@ -474,7 +478,7 @@ class ThlLedgerManager(LedgerManager): amount=user_pay, ) ) - if user.product_id == JAMES_BILLINGS_BPID: + if user.product.user_wallet_config.failed_attempt_credit_enabled: settlement_amount = self.get_user_attempt_credit_to_settle( user=user, user_pay=user_pay ) @@ -1536,9 +1540,7 @@ class ThlLedgerManager(LedgerManager): assert user is not None, "Session must have a user" product = user.product assert product is not None, "user.prefetch_product()" - assert product.user_wallet_enabled, ( - "Product does not have user_wallet enabled" - ) + assert product.user_wallet_enabled, "Product does not have user_wallet enabled" config = product.user_wallet_config assert config.failed_attempt_credit_enabled, ( "Product does not have failed_attempt_credit enabled" @@ -1546,6 +1548,9 @@ class ThlLedgerManager(LedgerManager): amount = USDCent(round(config.failed_attempt_credit * 100)) assert session.status == Status.FAIL, "Attempt credit requires a failed session" + assert session.is_attempt_credit_eligible, ( + "Session is not eligible for attempt credit" + ) tag = f"{self.currency.value}:{TransactionType.USER_ATTEMPT_CREDIT.value}:{session.uuid}" condition = generate_condition_tag_exists(tag) @@ -2052,6 +2057,77 @@ class ThlLedgerManager(LedgerManager): account = self.get_account_or_create_user_attempt_credit(user) return self.get_account_balance(account) + def get_user_wallets(self, user: User) -> UserLedgerWallets: + """Return every ledger wallet owned by a user, across currencies.""" + assert user.user_id, "User must be saved" + user.prefetch_product(self.pg_config) + payout_format = user.product.payout_config.payout_format + assert payout_format is not None, "Product must have a payout format" + + rows = self.pg_config.execute_sql_query( + query=""" + SELECT + uuid, display_name, qualified_name, account_type, + normal_balance, reference_type, reference_uuid, currency + FROM ledger_account + WHERE reference_type = 'user' + AND reference_uuid = %(reference_uuid)s + AND account_type = ANY(%(account_types)s) + ORDER BY currency, account_type, qualified_name; + """, + params={ + "reference_uuid": user.uuid, + "account_types": [ + AccountType.USER_WALLET.value, + AccountType.USER_ATTEMPT_CREDIT.value, + ], + }, + ) + accounts = [LedgerAccount.model_validate(row) for row in rows] + + wallets = [] + for account in accounts: + if ( + user.product_id == JAMES_BILLINGS_BPID + and account.account_type == AccountType.USER_WALLET + ): + balance = self.get_account_balance_timerange( + account=account, + time_start=JAMES_BILLINGS_TX_CUTOFF, + ) + else: + balance = self.get_account_balance(account) + + if account.account_type == AccountType.USER_ATTEMPT_CREDIT: + redeemable_balance = 0 + elif account.currency == self.currency.value: + redeemable_balance = self.get_user_redeemable_wallet_balance( + user=user, + user_wallet_balance=balance, + ) + else: + # The session-based USD reserve calculation cannot be applied + # to another ledger currency. + redeemable_balance = max(balance, 0) + + wallets.append( + UserLedgerWallet( + account_uuid=account.uuid, + account_type=account.account_type, + currency=account.currency, + display_name=account.display_name, + amount=balance, + redeemable_amount=redeemable_balance, + payout_format=payout_format, + amount_string=format_payout_format(payout_format, balance), + redeemable_amount_string=format_payout_format( + payout_format, redeemable_balance + ), + ) + ) + + return UserLedgerWallets(wallets=wallets) + def get_user_attempt_credit_to_settle( self, user: User, diff --git a/generalresearch/models/thl/session.py b/generalresearch/models/thl/session.py index 17142f3..a270257 100644 --- a/generalresearch/models/thl/session.py +++ b/generalresearch/models/thl/session.py @@ -823,6 +823,29 @@ class Session(BaseModel): self.model_config["validate_assignment"] = True self.__class__.model_validate(self) + @property + def is_attempt_credit_eligible(self) -> bool: + """Return whether this session qualifies for attempt credit. + The status of the BP's user_wallet_config.failed_attempt_credit_enabled does + not matter here. + """ + min_session_length = timedelta(minutes=1) + + ineligible_status_codes = { + StatusCode1.SESSION_START_FAIL, + StatusCode1.SESSION_START_QUALITY_FAIL, + StatusCode1.SESSION_CONTINUE_QUALITY_FAIL, + StatusCode1.BUYER_QUALITY_FAIL, + StatusCode1.PS_BLOCKED, + StatusCode1.PS_QUALITY + } + return ( + self.status == Status.FAIL + and self.status_code_1 not in ineligible_status_codes + and self.finished is not None + and self.elapsed >= min_session_length + ) + def model_dump_mysql( self, *args, **kwargs ) -> dict[str, str | int | datetime | float | None]: diff --git a/generalresearch/models/thl/wallet/user_wallet.py b/generalresearch/models/thl/wallet/user_wallet.py index dbe66fa..0671d38 100644 --- a/generalresearch/models/thl/wallet/user_wallet.py +++ b/generalresearch/models/thl/wallet/user_wallet.py @@ -1,10 +1,13 @@ from __future__ import annotations import logging +from typing import Literal from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt +from generalresearch.models.custom_types import UUIDStr from generalresearch.models.legacy.api_status import StatusResponse +from generalresearch.models.thl.ledger import AccountType from generalresearch.models.thl.payout_format import ( PayoutFormatField, PayoutFormatType, @@ -40,3 +43,25 @@ class UserWalletBalance(BaseModel): class UserWalletBalanceResponse(StatusResponse): wallet: UserWalletBalance = Field() + + +class UserLedgerWallet(UserWalletBalance): + """A user-owned ledger account exposed by the wallets endpoint.""" + + account_uuid: UUIDStr = Field( + description="A unique identifier for this Ledger Account", + examples=["c3c3566b5b1b4961b63a5670a2dc923d"], + ) + account_type: Literal[ + AccountType.USER_WALLET, + AccountType.USER_ATTEMPT_CREDIT, + ] + currency: str = Field(default="USD", max_length=32) + display_name: str = Field( + max_length=64, + description="Human-readable description of the Ledger Account", + ) + + +class UserLedgerWallets(BaseModel): + wallets: list[UserLedgerWallet] = Field(default_factory=list) |
