diff options
| author | stuppie | 2026-09-21 14:22:04 -0600 |
|---|---|---|
| committer | stuppie | 2026-09-21 14:22:04 -0600 |
| commit | 96234863aa0c45fb98934faf3b35778f23bce8ef (patch) | |
| tree | ec46ee7b3b671bd660eb17a7d67afb668e9d7c5b | |
| parent | 414ca0b8d1b55214ca2931906280e5a9ef411e45 (diff) | |
| download | generalresearch-3.5.3.tar.gz generalresearch-3.5.3.zip | |
fix bug in min / max payout calculation for cashout methods, and add testsv3.5.3
| -rw-r--r-- | generalresearch/managers/thl/cashout_method.py | 22 | ||||
| -rw-r--r-- | generalresearch/models/thl/wallet/cashout_method.py | 26 | ||||
| -rw-r--r-- | pyproject.toml | 2 | ||||
| -rw-r--r-- | tests/managers/thl/test_wallet/test_get_user_cashout_methods.py | 91 |
4 files changed, 135 insertions, 6 deletions
diff --git a/generalresearch/managers/thl/cashout_method.py b/generalresearch/managers/thl/cashout_method.py index e83b16a..8a4236b 100644 --- a/generalresearch/managers/thl/cashout_method.py +++ b/generalresearch/managers/thl/cashout_method.py @@ -347,10 +347,26 @@ class CashoutMethodManager(PostgresManager): else: x.usd_exchange_rate = usd_exchange_rate[x.original_currency] # If the user has foreign cards available, we need to show their min_value in USD - x.min_value_usd = USDCent(round(x.min_value * x.usd_exchange_rate)) + x.min_value_usd = max( + USDCent(round(x.min_value * x.usd_exchange_rate)), + min_value, + ) x.max_value_usd = USDCent(round(x.max_value * x.usd_exchange_rate)) - # Adjust min_value for BP - x.min_value = max(x.min_value_usd, min_value) + # Keep min_value in the method's original currency and use the + # same conversion and rounding as TangoManager.make_request. + x.min_value = max( + x.min_value, + round(int(min_value) / x.usd_exchange_rate), + ) + + # A product minimum above the method maximum makes the method unusable. + cms = [ + x + for x in cms + if x.min_value_usd is None + or x.max_value_usd is None + or x.min_value_usd <= x.max_value_usd + ] return {x.id: x for x in cms} diff --git a/generalresearch/models/thl/wallet/cashout_method.py b/generalresearch/models/thl/wallet/cashout_method.py index 61a8992..37a5a74 100644 --- a/generalresearch/models/thl/wallet/cashout_method.py +++ b/generalresearch/models/thl/wallet/cashout_method.py @@ -119,6 +119,18 @@ class CashoutMethodBase(BaseModel): # return None # return self.min_value * self.usd_exchange_rate + @model_validator(mode="after") + def validate_value_ranges(self) -> Self: + if self.min_value > self.max_value: + raise ValueError("min_value must not exceed max_value") + if ( + self.min_value_usd is not None + and self.max_value_usd is not None + and self.min_value_usd > self.max_value_usd + ): + raise ValueError("min_value_usd must not exceed max_value_usd") + return self + def validate_requested_amount(self, amount: USDCent): """ Check if 'amount' is a valid amount that can be requested. @@ -127,10 +139,20 @@ class CashoutMethodBase(BaseModel): amount = int(amount) if amount <= 0: raise ValueError("Amount must be positive") - if not self.min_value <= amount <= self.max_value: + if self.original_currency not in {None, Currency.USD} and ( + self.min_value_usd is None or self.max_value_usd is None + ): + raise ValueError("USD limits are required for a foreign cashout method") + min_value = ( + self.min_value if self.min_value_usd is None else self.min_value_usd + ) + max_value = ( + self.max_value if self.max_value_usd is None else self.max_value_usd + ) + if not min_value <= amount <= max_value: raise ValueError( f"Invalid amount requested: ${amount / 100:.2f}. Must be between" - f" ${int(self.min_value) / 100:.2f} and ${int(self.max_value) / 100:.2f}" + f" ${int(min_value) / 100:.2f} and ${int(max_value) / 100:.2f}" ) if self.type == PayoutType.CASH_IN_MAIL and amount % 500 != 0: raise ValueError("Amount must be in increments of $5.00") diff --git a/pyproject.toml b/pyproject.toml index 9af44b4..97d25ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "generalresearch" -version = "3.5.2" +version = "3.5.3" description = "Python Utilities for General Research" readme = "README.md" requires-python = ">=3.14" diff --git a/tests/managers/thl/test_wallet/test_get_user_cashout_methods.py b/tests/managers/thl/test_wallet/test_get_user_cashout_methods.py new file mode 100644 index 0000000..de1fbec --- /dev/null +++ b/tests/managers/thl/test_wallet/test_get_user_cashout_methods.py @@ -0,0 +1,91 @@ +from collections.abc import Callable +from decimal import Decimal +from typing import TYPE_CHECKING + +import pytest + +from generalresearch.currency import USDCent +from generalresearch.models.thl.wallet.definitions import Currency + +if TYPE_CHECKING: + from generalresearch.managers.thl.cashout_method import CashoutMethodManager + from generalresearch.models.thl.user import User + from generalresearch.models.thl.wallet.cashout_method import CashoutMethod + +def user_with_ten_dollar_minimum(user_with_wallet: User) -> User: + user_with_wallet.product.user_wallet_config.min_cashout = Decimal("10.00") + return user_with_wallet + + +def test_usd_tango_cashout_method_uses_product_minimum( + cashout_method_manager: CashoutMethodManager, + user_with_wallet: User, + delete_cashoutmethod_db: Callable[..., None], + example_tango_cashout_methods: list[CashoutMethod], +) -> None: + delete_cashoutmethod_db() + method = example_tango_cashout_methods[0].model_copy( + update={ + "data": example_tango_cashout_methods[0].data.model_copy( + update={"value_type": "variable"} + ), + "min_value": 5_00, + "max_value": 25_00, + } + ) + cashout_method_manager.create(method) + + methods = cashout_method_manager.get_user_cashout_methods( + user=user_with_ten_dollar_minimum(user_with_wallet), + country_iso="us", + usd_exchange_rate={}, + ) + result = methods[method.id] + + assert result.usd_exchange_rate == 1.0 + assert result.min_value == 10_00 + assert result.min_value_usd == USDCent(10_00) + assert result.max_value == 25_00 + assert result.max_value_usd == USDCent(25_00) + assert result.validate_requested_amount(USDCent(10_00)) + with pytest.raises(ValueError, match="Must be between"): + result.validate_requested_amount(USDCent(9_99)) + with pytest.raises(ValueError, match="Must be between"): + result.validate_requested_amount(USDCent(25_01)) + + +def test_eur_tango_cashout_method_converts_product_minimum( + cashout_method_manager: CashoutMethodManager, + user_with_wallet: User, + delete_cashoutmethod_db: Callable[..., None], + example_tango_cashout_methods: list[CashoutMethod], +) -> None: + delete_cashoutmethod_db() + method = example_tango_cashout_methods[1] + cashout_method_manager.create(method) + eur_to_usd = 1.14792 + + methods = cashout_method_manager.get_user_cashout_methods( + user=user_with_ten_dollar_minimum(user_with_wallet), + country_iso="it", + usd_exchange_rate={Currency.EUR: eur_to_usd}, + ) + result = methods[method.id] + + assert result.usd_exchange_rate == eur_to_usd + assert result.min_value == round(10_00 / eur_to_usd) + assert result.min_value_usd == USDCent(10_00) + assert result.max_value == 100_00 + assert result.max_value_usd == USDCent(round(100_00 * eur_to_usd)) + + # $10.00 min = 8.71 EUR which is over the card minimum + assert result.validate_requested_amount(USDCent(10_00)) + # $101 USD = 87 EURO which is below $100 max + assert result.validate_requested_amount(USDCent(101_00)) + + # $9.99 is below the BP's USD min + with pytest.raises(ValueError, match="Must be between"): + result.validate_requested_amount(USDCent(9_99)) + # $115 USD = 100.18 EURO which is over the card's max + with pytest.raises(ValueError, match="Must be between"): + result.validate_requested_amount(USDCent(115_00)) |
