aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuppie2026-09-02 12:36:00 -0600
committerstuppie2026-09-02 12:36:00 -0600
commitce5a2d02389b0879598dc5c5fbab989ea0913028 (patch)
tree5433119a9a8f13c93d1ad2fac1a9591978e5de01
parent4e3fbea26ca1aeee7459d6026b8403c862f85fca (diff)
downloadgeneralresearch-ce5a2d02389b0879598dc5c5fbab989ea0913028.tar.gz
generalresearch-ce5a2d02389b0879598dc5c5fbab989ea0913028.zip
add is_attempt_credit_eligible, get_user_wallets
-rw-r--r--generalresearch/managers/thl/ledger_manager/thl_ledger.py86
-rw-r--r--generalresearch/models/thl/session.py23
-rw-r--r--generalresearch/models/thl/wallet/user_wallet.py25
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)