diff options
| author | stuppie | 2026-09-02 16:36:53 -0600 |
|---|---|---|
| committer | stuppie | 2026-09-02 16:36:53 -0600 |
| commit | 8d1c0c6b98c403186516f8e6bcadf0be5bcf7a84 (patch) | |
| tree | 550b6364c1a7c0efbfec1f23edc8a8476abed72f | |
| parent | dc79688a2ddc3b50b5df5c42323a4d237be2c47a (diff) | |
| download | generalresearch-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.py | 12 | ||||
| -rw-r--r-- | generalresearch/managers/thl/ledger_manager/thl_ledger.py | 114 | ||||
| -rw-r--r-- | generalresearch/managers/thl/session.py | 2 | ||||
| -rw-r--r-- | generalresearch/managers/thl/wallet/__init__.py | 21 | ||||
| -rw-r--r-- | generalresearch/managers/thl/wallet/approve.py | 16 | ||||
| -rw-r--r-- | generalresearch/models/thl/product.py | 7 | ||||
| -rw-r--r-- | generalresearch/models/thl/wallet/user_wallet.py | 15 | ||||
| -rw-r--r-- | tests/models/thl/test_product_userwalletconfig.py | 6 |
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() |
