aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuppie2026-09-21 14:22:04 -0600
committerstuppie2026-09-21 14:22:04 -0600
commit96234863aa0c45fb98934faf3b35778f23bce8ef (patch)
treeec46ee7b3b671bd660eb17a7d67afb668e9d7c5b
parent414ca0b8d1b55214ca2931906280e5a9ef411e45 (diff)
downloadgeneralresearch-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.py22
-rw-r--r--generalresearch/models/thl/wallet/cashout_method.py26
-rw-r--r--pyproject.toml2
-rw-r--r--tests/managers/thl/test_wallet/test_get_user_cashout_methods.py91
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))