aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuppie2026-09-02 16:36:53 -0600
committerstuppie2026-09-02 16:36:53 -0600
commit8d1c0c6b98c403186516f8e6bcadf0be5bcf7a84 (patch)
tree550b6364c1a7c0efbfec1f23edc8a8476abed72f
parentdc79688a2ddc3b50b5df5c42323a4d237be2c47a (diff)
downloadgeneralresearch-8d1c0c6b98c403186516f8e6bcadf0be5bcf7a84.tar.gz
generalresearch-8d1c0c6b98c403186516f8e6bcadf0be5bcf7a84.zip
add a balance_type product config, Add UserDisplayedWalletBalance model and implement. Remove same amt stuff
-rw-r--r--generalresearch/managers/thl/cashout_method.py12
-rw-r--r--generalresearch/managers/thl/ledger_manager/thl_ledger.py114
-rw-r--r--generalresearch/managers/thl/session.py2
-rw-r--r--generalresearch/managers/thl/wallet/__init__.py21
-rw-r--r--generalresearch/managers/thl/wallet/approve.py16
-rw-r--r--generalresearch/models/thl/product.py7
-rw-r--r--generalresearch/models/thl/wallet/user_wallet.py15
-rw-r--r--tests/models/thl/test_product_userwalletconfig.py6
8 files changed, 97 insertions, 96 deletions
diff --git a/generalresearch/managers/thl/cashout_method.py b/generalresearch/managers/thl/cashout_method.py
index 7365878..91d6a42 100644
--- a/generalresearch/managers/thl/cashout_method.py
+++ b/generalresearch/managers/thl/cashout_method.py
@@ -243,9 +243,6 @@ class CashoutMethodManager(PostgresManager):
product = user.product
supported_payout_types = copy(product.user_wallet_config.supported_payout_types)
- if product.user_wallet_config.amt:
- supported_payout_types.add(PayoutType.AMT)
-
user_scoped_payout_types = [PayoutType.PAYPAL, PayoutType.CASH_IN_MAIL]
params = {
"user_scoped_payout_types": [x.value for x in user_scoped_payout_types],
@@ -268,15 +265,6 @@ class CashoutMethodManager(PostgresManager):
raise ValueError(f"Unexpectedly large number of cashout_methods: {user=}")
cms = [self.format_from_db(x, user=user) for x in res]
-
- # Only allow AMT if the BP is marked as AMT (already should have been
- # filtered in query)
- cms = [
- x
- for x in cms
- if (x.type == PayoutType.AMT and product.user_wallet_config.amt)
- or (x.type != PayoutType.AMT)
- ]
return cms
@staticmethod
diff --git a/generalresearch/managers/thl/ledger_manager/thl_ledger.py b/generalresearch/managers/thl/ledger_manager/thl_ledger.py
index 2d7a588..aac47ed 100644
--- a/generalresearch/managers/thl/ledger_manager/thl_ledger.py
+++ b/generalresearch/managers/thl/ledger_manager/thl_ledger.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import logging
+from collections import defaultdict
from collections.abc import Callable, Collection
from datetime import datetime, timedelta, timezone
from decimal import Decimal
@@ -57,16 +58,17 @@ 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 BPUIDStr, User
+from generalresearch.models.thl.user import User
from generalresearch.models.thl.wallet import PayoutType
from generalresearch.models.thl.wallet.user_wallet import (
+ UserDisplayedWalletBalance,
UserLedgerWallet,
UserLedgerWallets,
)
if TYPE_CHECKING:
- from generalresearch.models.thl.contest.contest import ContestWinner
from generalresearch.managers.thl.session import SessionManager
+ from generalresearch.models.thl.contest.contest import ContestWinner
logging.basicConfig()
@@ -1050,8 +1052,8 @@ class ThlLedgerManager(LedgerManager):
user: User,
payout_event: UserPayoutEvent,
created: datetime | None = None,
- skip_flag_check: bool | None = False,
- skip_wallet_balance_check: bool | None = False,
+ skip_flag_check: bool = False,
+ skip_wallet_balance_check: bool = False,
) -> LedgerTransaction:
"""
The funds move from the user's wallet into the BP's "pending"
@@ -1070,28 +1072,12 @@ class ThlLedgerManager(LedgerManager):
amt_str = f"${int(amount) / 100:,.2f}"
descriptions = {
- PayoutType.AMT_HIT: f"User Payout AMT Assignment Request {amt_str}",
- PayoutType.AMT_BONUS: f"User Payout AMT Bonus Request {amt_str}",
PayoutType.PAYPAL: f"User Payout Paypal Request {amt_str}",
PayoutType.CASH_IN_MAIL: f"User Payout Cash Request {amt_str}",
PayoutType.TANGO: f"User Payout Tango Request {amt_str}",
}
description = descriptions[payout_event.payout_type]
- if payout_event.payout_type in {
- PayoutType.AMT_HIT,
- PayoutType.AMT_BONUS,
- }:
- """
- This is for AMT accounts only (currently JB). This is the
- payment of a either 1) 1c or 5c (typically) assignment or 2) a
- bonus for task complete to the user. The 20% commission will
- be taken from the BP's wallet once the tx is completed.
- """
- assert user.product.user_wallet_amt, (
- "Can only call this on an AMT-enabled BPs"
- )
-
f = lambda: self.create_tx_user_payout_request_(
user=user,
payout_event=payout_event,
@@ -1100,10 +1086,6 @@ class ThlLedgerManager(LedgerManager):
)
min_balance: int | None = int(amount)
- if payout_event.payout_type == PayoutType.AMT_HIT:
- # We allow the user's balance to reach up to -$1.00.
- min_balance = -100 + amount
-
if skip_wallet_balance_check:
min_balance = None
@@ -1605,9 +1587,7 @@ class ThlLedgerManager(LedgerManager):
and is eligible for an attempt credit, then the credit is automatically
given."""
- session = session_manager.get_latest_for_user(
- user_id=user.user_id
- )
+ session = session_manager.get_latest_for_user(user_id=user.user_id)
if session is None:
raise ValueError("User has no session to claim attempt credit for")
if session.status is not None:
@@ -2090,6 +2070,10 @@ class ThlLedgerManager(LedgerManager):
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"
+ user_account_types = {
+ AccountType.USER_WALLET.value,
+ AccountType.USER_ATTEMPT_CREDIT.value,
+ }
rows = self.pg_config.execute_sql_query(
query="""
@@ -2104,15 +2088,13 @@ class ThlLedgerManager(LedgerManager):
""",
params={
"reference_uuid": user.uuid,
- "account_types": [
- AccountType.USER_WALLET.value,
- AccountType.USER_ATTEMPT_CREDIT.value,
- ],
+ "account_types": list(user_account_types),
},
)
accounts = [LedgerAccount.model_validate(row) for row in rows]
wallets = []
+ displayed_amounts: dict[str, int] = defaultdict(int)
for account in accounts:
if (
user.product_id == JAMES_BILLINGS_BPID
@@ -2133,27 +2115,63 @@ class ThlLedgerManager(LedgerManager):
user_wallet_balance=balance,
)
else:
- # The session-based USD reserve calculation cannot be applied
- # to another ledger currency.
+ # We don't have redeemable logic for other currencies
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
- ),
- )
+ # A Product has only one payout_format. It is not clear which
+ # currency it is to be applied to. If we have a non USD currency,
+ # we'd need multiple payout formats.
+ account_payout_format = (
+ payout_format if account.currency == self.currency.value else None
+ )
+
+ wallet = 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=account_payout_format,
+ amount_string=(
+ format_payout_format(account_payout_format, balance)
+ if account_payout_format is not None
+ else None
+ ),
+ redeemable_amount_string=(
+ format_payout_format(account_payout_format, redeemable_balance)
+ if account_payout_format is not None
+ else None
+ ),
)
+ wallets.append(wallet)
- return UserLedgerWallets(wallets=wallets)
+ if (
+ account.account_type == AccountType.USER_ATTEMPT_CREDIT
+ or user.product.user_wallet_config.balance_type == "wallet_balance"
+ ):
+ displayed_amount = wallet.amount
+ else:
+ displayed_amount = wallet.redeemable_amount
+ displayed_amounts[account.currency] += displayed_amount
+
+ displayed_balances = [
+ UserDisplayedWalletBalance(
+ currency=currency,
+ amount=amount,
+ amount_string=(
+ format_payout_format(payout_format, amount)
+ if currency == self.currency.value
+ else None
+ ),
+ )
+ for currency, amount in sorted(displayed_amounts.items())
+ ]
+
+ return UserLedgerWallets(
+ wallets=wallets,
+ displayed_balances=displayed_balances,
+ )
def get_user_attempt_credit_to_settle(
self,
diff --git a/generalresearch/managers/thl/session.py b/generalresearch/managers/thl/session.py
index 8834f8d..9d3661c 100644
--- a/generalresearch/managers/thl/session.py
+++ b/generalresearch/managers/thl/session.py
@@ -116,7 +116,7 @@ class SessionManager(PostgresManager):
def get_latest_for_user(self, user_id: int) -> Session | None:
"""Return the most recently started session for a product user."""
- res = self.filter_paginated(user_id=user_id, order_by="-started", size=1)
+ res, _ = self.filter_paginated(user_id=user_id, order_by="-started", size=1)
if res:
return res[0]
diff --git a/generalresearch/managers/thl/wallet/__init__.py b/generalresearch/managers/thl/wallet/__init__.py
index b063e54..70fcacf 100644
--- a/generalresearch/managers/thl/wallet/__init__.py
+++ b/generalresearch/managers/thl/wallet/__init__.py
@@ -14,7 +14,6 @@ from generalresearch.managers.thl.user_manager.user_manager import (
)
from generalresearch.managers.thl.userhealth import UserIpHistoryManager
from generalresearch.managers.thl.wallet.approve import (
- approve_amt_cashout,
approve_paypal_order,
)
from generalresearch.models.thl.definitions import PayoutStatus
@@ -32,8 +31,8 @@ def manage_pending_cashout(
user_ip_history_manager: UserIpHistoryManager,
user_manager: UserManager,
ledger_manager: ThlLedgerManager,
- order_data: Optional[Union[Dict[str, Any], CashMailOrderData]] = None,
- tango_client: Optional[TangoClient] = None,
+ order_data: dict[str, Any] | CashMailOrderData | None = None,
+ tango_client: TangoClient | None = None,
) -> UserPayoutEvent:
"""
Called by a UI actions performed by Todd. This rejects/approves/cancels
@@ -89,14 +88,6 @@ def manage_pending_cashout(
payout_event=pe, payout_event_manager=payout_event_manager
)
- elif pe.payout_type in {PayoutType.AMT_BONUS, PayoutType.AMT_HIT}:
- approve_amt_cashout(
- user=user,
- payout_event=pe,
- payout_event_manager=payout_event_manager,
- ledger_manager=ledger_manager,
- )
-
elif pe.payout_type == PayoutType.CASH_IN_MAIL:
assert order_data, "must pass order_data"
payout_event_manager.update(
@@ -114,14 +105,8 @@ def manage_pending_cashout(
return pe
elif new_status == PayoutStatus.COMPLETE:
- # Used only for AMT/dummy cashouts that are actually paid out not
- # by us. They are informing us that the cashout was successfully
- # sent to the user
- if pe.payout_type in {PayoutType.AMT_BONUS, PayoutType.AMT_HIT}:
- # We already do this under approve_amt_cashout()
- pass
- elif pe.payout_type == PayoutType.PAYPAL:
+ if pe.payout_type == PayoutType.PAYPAL:
# This is an issue here in that we actually don't know what the
# fee is until it is sent and we read it back from paypal's csv
# result. We have to just run this with a custom script, which
diff --git a/generalresearch/managers/thl/wallet/approve.py b/generalresearch/managers/thl/wallet/approve.py
index 4a7ae5e..012b406 100644
--- a/generalresearch/managers/thl/wallet/approve.py
+++ b/generalresearch/managers/thl/wallet/approve.py
@@ -34,19 +34,3 @@ def approve_paypal_order(
return payout_event
-def approve_amt_cashout(
- user: User,
- payout_event: UserPayoutEvent,
- ledger_manager: ThlLedgerManager,
- payout_event_manager: PayoutEventManager,
-) -> None:
- """
- This is going to be paid out by the requester (the jb-lambdas) as an AMT bonus.
- """
- assert payout_event.status in {
- PayoutStatus.PENDING,
- PayoutStatus.FAILED,
- }, "attempting to manage payout that is not pending (or you can retry a failed order)"
-
- payout_event_manager.update(payout_event, status=PayoutStatus.APPROVED)
- ledger_manager.create_tx_user_payout_complete(user, payout_event=payout_event)
diff --git a/generalresearch/models/thl/product.py b/generalresearch/models/thl/product.py
index 3faacb4..a179142 100644
--- a/generalresearch/models/thl/product.py
+++ b/generalresearch/models/thl/product.py
@@ -413,7 +413,12 @@ class UserWalletConfig(BaseModel):
default=False, description="If enabled, the users' wallets are managed."
)
- # This field could go in supported_payout_types ---v
+ balance_type: Literal["wallet_balance", "redeemable_balance"] = Field(
+ default="wallet_balance",
+ description="The balance used as the user's displayed balance.",
+ )
+
+ # Deprecated
amt: bool = Field(default=False, description="Uses Amazon Mechanical Turk")
supported_payout_types: set[PayoutType] = Field(
diff --git a/generalresearch/models/thl/wallet/user_wallet.py b/generalresearch/models/thl/wallet/user_wallet.py
index 0671d38..0a66fc2 100644
--- a/generalresearch/models/thl/wallet/user_wallet.py
+++ b/generalresearch/models/thl/wallet/user_wallet.py
@@ -48,6 +48,10 @@ class UserWalletBalanceResponse(StatusResponse):
class UserLedgerWallet(UserWalletBalance):
"""A user-owned ledger account exposed by the wallets endpoint."""
+ payout_format: PayoutFormatType | None = Field(default=None)
+ amount_string: str | None = Field(default=None)
+ redeemable_amount_string: str | None = Field(default=None)
+
account_uuid: UUIDStr = Field(
description="A unique identifier for this Ledger Account",
examples=["c3c3566b5b1b4961b63a5670a2dc923d"],
@@ -63,5 +67,16 @@ class UserLedgerWallet(UserWalletBalance):
)
+class UserDisplayedWalletBalance(BaseModel):
+ """Combined user-visible balance for one ledger currency."""
+
+ currency: str = Field(max_length=32)
+ amount: int = Field(
+ description="Displayed balance in the currency's smallest ledger unit."
+ )
+ amount_string: str | None = None
+
+
class UserLedgerWallets(BaseModel):
wallets: list[UserLedgerWallet] = Field(default_factory=list)
+ displayed_balances: list[UserDisplayedWalletBalance] = Field(default_factory=list)
diff --git a/tests/models/thl/test_product_userwalletconfig.py b/tests/models/thl/test_product_userwalletconfig.py
index 4f6a6cc..614fc0a 100644
--- a/tests/models/thl/test_product_userwalletconfig.py
+++ b/tests/models/thl/test_product_userwalletconfig.py
@@ -21,6 +21,7 @@ class TestProductUserWalletConfig:
# Check the defaults
assert not instance.enabled
+ assert instance.balance_type == "wallet_balance"
assert not instance.amt
assert isinstance(instance.supported_payout_types, set)
@@ -28,6 +29,11 @@ class TestProductUserWalletConfig:
assert instance.min_cashout is None
+ def test_redeemable_balance_type(self):
+ instance = UserWalletConfig(balance_type="redeemable_balance")
+
+ assert instance.balance_type == "redeemable_balance"
+
def test_model_dump(self):
instance = UserWalletConfig()