From 0144d37b3e9c38e5acecc1f9f1750b56b3de9749 Mon Sep 17 00:00:00 2001 From: stuppie Date: Fri, 11 Sep 2026 12:15:24 -0600 Subject: wip: moving cashout method stuff over from grpc methods --- generalresearch/managers/thl/cashout_method.py | 76 ++++++- generalresearch/managers/thl/wallet/tango.py | 231 +++++++++++---------- generalresearch/managers/thl/wallet/tango_admin.py | 193 +++++++++++++++++ generalresearch/managers/thl/wallet/tango_tasks.py | 127 +++++++++++ .../models/thl/wallet/cashout_method.py | 4 +- generalresearch/models/thl/wallet/definitions.py | 2 + 6 files changed, 512 insertions(+), 121 deletions(-) create mode 100644 generalresearch/managers/thl/wallet/tango_admin.py create mode 100644 generalresearch/managers/thl/wallet/tango_tasks.py diff --git a/generalresearch/managers/thl/cashout_method.py b/generalresearch/managers/thl/cashout_method.py index 910d37b..dad1ef1 100644 --- a/generalresearch/managers/thl/cashout_method.py +++ b/generalresearch/managers/thl/cashout_method.py @@ -8,9 +8,14 @@ from uuid import UUID, uuid4 from pydantic import NonNegativeInt +from generalresearch.currency import USDCent from generalresearch.managers.base import PostgresManager from generalresearch.models.thl.user_ref import UserRef -from generalresearch.models.thl.wallet.definitions import PayoutType +from generalresearch.models.thl.wallet.definitions import ( + SUPPORTED_CURRENCIES, + Currency, + PayoutType, +) if TYPE_CHECKING: from generalresearch.models.thl.user import User @@ -86,7 +91,7 @@ class CashoutMethodManager(PostgresManager): name="Cash in Mail", description="USPS delivery of cash", id=uuid4().hex, - currency="USD", + currency=Currency.USD, image_url="https://www.shutterstock.com/shutterstock/photos/2175413929/display_1500/stock-vector-opened" "-envelope-with-money-dollar-bills-salary-earning-and-savings-concept-d-web-vector-2175413929.jpg", min_value=500, # $5.00 @@ -131,7 +136,7 @@ class CashoutMethodManager(PostgresManager): name="PayPal", description="Cashout via PayPal", id=uuid4().hex, - currency="USD", + currency=Currency.USD, image_url="https://cdn.mmfwcl.com/images/brands/p439786-1200w-326ppi.png", min_value=100, # $1.00 max_value=25_000, # $250.00 @@ -280,8 +285,70 @@ class CashoutMethodManager(PostgresManager): if (x.type == PayoutType.AMT and product.user_wallet_config.amt) or (x.type != PayoutType.AMT) ] + # This is b/c there might be some Tango cards in here for currency we don't support + cms = [ + x + for x in cms + if x.original_currency is None + or x.original_currency in SUPPORTED_CURRENCIES + ] return cms + def get_user_cashout_methods( + self, + user: User, + country_iso: str, + usd_exchange_rate: dict[Currency, float], + ): + """ + Get the cashout methods allowed for this user. Does not check financial stuff at all. + This checks the user's country for filtering purposes. + Gets cashoutmethods from accounting_cashoutmethod, along with a consistent UUID. + If BP has a min_cashout, modifies the cashout method's min_value based on the BP's min cashout. + :return: Dict with keys: the UIID, values: Dict with keys: 'id', 'provider', 'ext_id', 'data', + where 'data' is provider-specific JSON data containing details about the cashout method. + + The available cashout methods is based off the user's latest IP address -> country, and + also (in the future) a risk assessment (maybe riskier user aren't allowed certain + methods). Also, a user may have different minimums based off "stuff". + # country_iso = get_user_latest_country(user_iph_manager, user) or "us" + If a user requests their cashout methods before they ever enter a survey, we won't have + saved their IP address, and this will fail. I think this call should just default to US. + assert country_iso, "unknown country from IP address" + """ + + # BP can set their own min in USD or equivalent + user.prefetch_product(pg_config=self.pg_config) + product = user.product + min_value_usd = product.user_wallet_config.min_cashout or 0 + min_value = USDCent(round(min_value_usd * 100)) + + cms = self.get_cashout_methods(user=user) + + for x in cms: + # assets in non-USD need to be converted to USD here + if x.original_currency is not None: + if x.original_currency == Currency.USD: + x.usd_exchange_rate = 1.0 + 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.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) + + cms = [ + cm + for cm in cms + if ( + cm.type == PayoutType.TANGO and country_iso.lower() in cm.data.countries + ) + or cm.type != PayoutType.TANGO + ] + + return {x.id: x for x in cms} + @staticmethod def format_from_db(x: dict[str, Any], user: User | None = None) -> CashoutMethod: x["id"] = UUID(x["id"]).hex @@ -302,5 +369,6 @@ class CashoutMethodManager(PostgresManager): x["data"]["type"] = x["type"] if user and x["type"] in {PayoutType.PAYPAL, PayoutType.CASH_IN_MAIL}: x["user"] = user.to_user_ref() - + x["original_currency"] = x.get("currency") or Currency.USD + x["currency"] = Currency.USD return CashoutMethod.model_validate(x) diff --git a/generalresearch/managers/thl/wallet/tango.py b/generalresearch/managers/thl/wallet/tango.py index 038f67f..aa780c3 100644 --- a/generalresearch/managers/thl/wallet/tango.py +++ b/generalresearch/managers/thl/wallet/tango.py @@ -1,127 +1,128 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any - -from generalresearch.config import ( - is_debug, +from datetime import timedelta +from decimal import Decimal +from threading import Lock +from typing import Any + +from cachetools import TTLCache, cachedmethod + +from generalresearch.currency import USDCent +from generalresearch.managers.thl.cashout_method import CashoutMethodManager +from generalresearch.managers.thl.tango_api import TangoClient +from generalresearch.models.thl.wallet.definitions import ( + CURRENCY_FORMATTER, + Currency, + PayoutType, ) -from generalresearch.managers.thl.tango_api import TangoOrderRequest -from generalresearch.models.thl.definitions import PayoutStatus - -if TYPE_CHECKING: - from generalresearch.managers.thl.ledger_manager.thl_ledger import ( - ThlLedgerManager, - ) - from generalresearch.managers.thl.payout import PayoutEventManager - from generalresearch.managers.thl.tango_api import TangoClient - from generalresearch.models.thl.payout import UserPayoutEvent - from generalresearch.models.thl.user import User - - -def complete_tango_order( - user: User, - payout_event: UserPayoutEvent, - payout_event_manager: PayoutEventManager, - ledger_manager: ThlLedgerManager, - tango_client: TangoClient, -): - """ - We approved the Tango card redemption. Actually request the card. - (Note: we're skipping the PENDING -> APPROVED -> COMPLETE order for tango. - When a tango request gets APPROVED, we COMPLETE it (or FAIL!) in the - same step) - """ - assert payout_event.status in { - PayoutStatus.PENDING, - PayoutStatus.FAILED, - }, "attempting to manage payout that is not pending (or you can retry a failed order)" - request = payout_event.request_data - ref_id = request["externalRefID"] - # amount_usd = Decimal(payout_event.request_data["amount_usd"]) - # Note: tango uses the ref_id to uniquify orders, so locking is not actually needed as long - # as the ref_id is the same. - try: - order = create_tango_order( - request_data=payout_event.request_data, - ref_id=ref_id, - tango_client=tango_client, +class TangoManager: + def __init__( + self, + tango_client: TangoClient, + tango_account_id: str, + tango_customer_id: str, + cashout_method_manager: CashoutMethodManager, + ) -> None: + self.tango_client = tango_client + self.tango_account_id = tango_account_id + self.tango_customer_id = tango_customer_id + self.cashout_method_manager = cashout_method_manager + self.supported_currencies = frozenset({currency.value for currency in Currency}) + self._exchange_rate_cache = TTLCache( + maxsize=1, ttl=timedelta(minutes=30).total_seconds() ) + self._name_cache = TTLCache( + maxsize=1000, ttl=timedelta(minutes=30).total_seconds() + ) + self._exchange_rate_lock = Lock() + self._name_lock = Lock() - except AssertionError: - # todo: its possible the order went through, but something else was wrong - # we should try to retrieve the order by its ref_id and confirm it really - # failed... - payout_event_manager.update(payout_event, status=PayoutStatus.FAILED) - return payout_event - - # update TangoPayoutEvent with the order data - payout_event_manager.update( - payout_event, - status=order["status"], - ext_ref_id=order["referenceOrderID"], - order_data=order, + @cachedmethod( + cache=lambda self: self._exchange_rate_cache, + lock=lambda self: self._exchange_rate_lock, ) - - ledger_manager.create_tx_user_payout_complete(user, payout_event=payout_event) - - return payout_event - - -def create_tango_order( - request_data: dict[str, Any], ref_id: str, tango_client: TangoClient -) -> dict[str, Any]: - """ - Create a tango gift card order. - Throws exception if anything is not right. - - https://integration-www.tangocard.com/raas_api_console/v2/ - - https://www.apimatic.io/apidocs/tangocard/v/2_3_4#/python - - :param utid: Card identifier - :param amount: requested card value in USD - :param ref_id: TangoPayoutEvent.uuid - :return: - """ - # make sure we don't create more than one tango order for a single PayoutEvent - assert tango_client.get_order_if_exists(ref_id) is None - amount = request_data["amount"] - request_data.pop("amount_usd", None) - request_data.pop("description", None) - - if is_debug(): + def get_exchange_rates(self) -> dict[Currency, float]: + """Return supported foreign-currency-to-USD Tango exchange rates.""" + rates = self.tango_client.get_exchange_rates()["exchangeRates"] return { - "status": "COMPLETE", - "referenceOrderID": "test", - "reward": { - "credentials": { - "Security Code": "XXXX-XXXX", - "Redemption URL": "https://codes.rewardcodes.com/r2/1/XXXX", - }, - "credentialList": [ - { - "type": "text", - "label": "Security Code", - "value": "XXXX-XXXX", - }, - { - "type": "url", - "label": "Redemption URL", - "value": "https://codes.rewardcodes.com/r2/1/XXXX", - }, - ], - "redemptionInstructions": "do your thang fam", - }, + Currency(rate["baseCurrency"]): rate["baseFx"] + for rate in rates + if rate["rewardCurrency"] == "USD" + and rate["baseCurrency"] in self.supported_currencies } - request = TangoOrderRequest.model_validate(request_data) - order = tango_client.create_order(request) - - amount_f: float = float(amount) - assert order["status"] == "COMPLETE" - assert abs(order["amountCharged"]["total"] - amount_f) < 0.0200001 - assert order["amountCharged"]["currencyCode"] == "USD" - if order["denomination"]["currencyCode"] == "USD": - assert order["denomination"]["value"] == amount_f + def get_order_detail(self, tango_order_id: str) -> dict[str, Any]: + return self.tango_client.get_order(tango_order_id) + + def make_request( + self, amount_usd: Decimal, cashout_method: Any, external_ref_id: str + ) -> dict[str, Any]: + """Build the data needed to place a Tango order.""" + assert type(amount_usd) is Decimal + utid = cashout_method.data.utid + amount: Decimal | float = amount_usd + currency = cashout_method.original_currency + currency_code = getattr(currency, "value", currency) + if currency_code and currency_code != "USD": + amount = round(float(amount) / self.get_exchange_rates()[currency_code], 2) + return { + "accountIdentifier": self.tango_account_id, + "customerIdentifier": self.tango_customer_id, + "utid": utid, + "amount": str(amount), + "amount_usd": str(amount_usd), + "campaign": "300large", + "sendEmail": False, + "externalRefID": external_ref_id, + "description": self.get_name(utid), + } - return order + @cachedmethod( + cache=lambda self: self._name_cache, + lock=lambda self: self._name_lock, + ) + def get_name(self, utid: str) -> str: + methods = self.cashout_method_manager.filter( + ext_id=utid, payout_types=[PayoutType.TANGO], is_live=None + ) + cashout_method = next(iter(methods), None) + return cashout_method.name if cashout_method else "Tango Gift Card" + + def get_expected_redemption_value( + self, cashout_method_id: str, amount: USDCent + ) -> tuple[int, Currency]: + """ + Convert USD cents to a Tango card's smallest currency unit. + for e.g.: A user wants a variable value CAD visa card. He redeems $10 USD (amount=1000), + this function returns the amount that will be redeemed in CAD. + :param cashout_method_id: ID of tango card. expected to be non USD. If USD, just returns the amount. + :param amount: amount the user is redeeming from their wallet in USD integer cents. + :return: amount that will be redeemed through tango on their foreign card, in the card's + (specified by the UTID) foreign currency (in integer units of the lowest denomination) + # example CAD visa: U121653 + """ + assert type(amount) is USDCent + res = self.cashout_method_manager.filter(uuid=cashout_method_id, is_live=True) + if not res: + raise ValueError(f"no cashout method found for {cashout_method_id!r}") + cashout_method = res[0] + assert cashout_method.type == PayoutType.TANGO + assert cashout_method.original_currency is not None + + currency = cashout_method.original_currency + if currency == Currency.USD: + return int(amount), currency + + foreign_amount = round(int(amount) / self.get_exchange_rates()[currency]) + return foreign_amount, currency + + def format_currency(self, amount: int, currency: Currency): + return CURRENCY_FORMATTER[currency](amount) + + def clear_caches(self) -> None: + with self._exchange_rate_lock: + self._exchange_rate_cache.clear() + with self._name_lock: + self._name_cache.clear() diff --git a/generalresearch/managers/thl/wallet/tango_admin.py b/generalresearch/managers/thl/wallet/tango_admin.py new file mode 100644 index 0000000..579b21f --- /dev/null +++ b/generalresearch/managers/thl/wallet/tango_admin.py @@ -0,0 +1,193 @@ +import re + +from generalresearch.managers.base import PostgresManager +from generalresearch.managers.thl.tango_api import TangoClient +from generalresearch.models.thl.wallet.definitions import ( + CURRENCY_MAX_VALUE, +) + + +class TangoAdmin(PostgresManager): + def __init__(self, *, tango_client: TangoClient, tango_account_id: str, **kwargs): + super().__init__(**kwargs) + self.tango_client = tango_client + self.tango_account_id = tango_account_id + + def get_account_balance(self) -> float: + account = self.tango_client.get_account( + account_identifier=self.tango_account_id + ) + return account["currentBalance"] + + @staticmethod + def is_available_tango_card(item: dict): + """ + We're supporting cards that meet the following conditions or checks: + - see below + - non USD card support variable value only (todo: handling conversion of fixed amounts) + """ + from generalresearch.models.thl.wallet.definitions import SUPPORTED_CURRENCIES + + reward_types = ["cash equivalent", "gift card"] # these are the only options + value_types = ["VARIABLE_VALUE", "FIXED_VALUE"] # these are the only options + ignore_cards = { + "U557938", # Southwest® Gift Card $25, doesn't work...? + } + return bool( + item["rewardType"] in reward_types + and item["status"] == "active" + and item["valueType"] in value_types + and item["utid"] not in ignore_cards + and item["currencyCode"] in SUPPORTED_CURRENCIES + and ( + item["currencyCode"] == "USD" + or item["currencyCode"] != "USD" + and item["valueType"] == "VARIABLE_VALUE" + ) + ) + + @staticmethod + def format_tango_card(item: dict, brand: dict): + """ + For non-USD, we support variable value only. We aren't doing any currency conversion here, for + them. To handle the $250USD max value, I'm setting a hard limit in each currency. These are + for our supported, stable currencies only, so this shouldn't be a problem.... + """ + tango_card_info = { + # 'currency': 'USD', + "provider": "TANGO", + "cashout_method_type": "GIFT_CARD", + } + brand_dict = { + "image_url": brand["imageUrls"]["1200w-326ppi"], + "description": brand["description"], + "disclaimer": brand["disclaimer"], + "terms": brand["terms"], + } + value_type_map = {"VARIABLE_VALUE": "variable", "FIXED_VALUE": "fixed"} + item["rewardName"] = re.sub(r"\s+", " ", item["rewardName"]) + # if this is a fixed value, non USD card, we need to save the original value before + # we convert it to USD, b/c we need that value to make the request + if item["valueType"] == "FIXED_VALUE" and item["currencyCode"] != "USD": + raise NotImplementedError("FIXED VALUE non-USD not supported") + if item["valueType"] == "VARIABLE_VALUE" and item["currencyCode"] != "USD": + pass + if item["valueType"] == "FIXED_VALUE": + item["minValue"] = item["faceValue"] + item["maxValue"] = item["faceValue"] + + card = { + "name": item["rewardName"], + "utid": item["utid"], + "type": item["rewardType"], + # 'min_value_usd': round(item.min_value, 2), + # 'max_value_usd': round(min(item.max_value, MAX_VALUE[item.currency_code]), 2), + "min_value": round(100 * float(item["minValue"])), + "max_value": min( + round(100 * float(item["maxValue"])), + CURRENCY_MAX_VALUE[item["currencyCode"]] * 100, + ), + "countries": item["countries"], + "value_type": value_type_map[item["valueType"]], + # This is the currency the reward is based in. We'll need this to convert the request + # to USD when the user requests it. + "currency": item["currencyCode"], + } + card.update(brand_dict) + card.update(tango_card_info) + return card + + def get_tango_cards(self) -> list: + """ + This is called to populate the accounting_cashoutmethod table ONLY. It is not LIVE. + For non-USD cards, the $ fields are in foreign currency because this is stored in + our DB for lookup purposes and has to be converted when requested. + """ + # usd_exchange_rate = get_tango_exchange_rates() + cards: list[dict] = [] + catalog = self.tango_client.get_catalog() + brands = [b for b in catalog["brands"] if b["status"] == "active"] + for brand in brands: + for item in brand["items"]: + if self.is_available_tango_card(item): + print( + item["currencyCode"], + item["rewardName"], + item["valueType"], + ( + f"{item['minValue']}-{item['maxValue']}" + if "minValue" in item + else item["faceValue"] + ), + ) + cards.append(self.format_tango_card(item, brand)) + return cards + +# +# def update_tango_cashout_method_db(): +# # Synchronizes Tango cashout methods/gift cards with the accounting_cashoutmethod table. +# # Should only need to be run sporadically. +# # Note: Nothing calls this at the moment. It was run once manually. +# now = datetime.utcnow() +# tango_cards = get_tango_cards() +# provider = "TANGO" +# +# db_res = THL_WEB_RW.execute_sql_query( +# f""" +# SELECT `id`, `provider`, `ext_id` FROM `{THL_WEB_RW.db}`.`accounting_cashoutmethod`""" +# ) +# existing_ids = {(provider, x["ext_id"]): x["id"] for x in db_res} +# +# fields = ["id", "last_updated", "is_live", "provider", "ext_id", "name", "data"] +# values = [ +# [ +# existing_ids.get((provider, x["utid"]), uuid.uuid4().hex), +# now, +# True, +# provider, +# x["utid"], +# x["name"], +# json.dumps(x), +# ] +# for x in tango_cards +# ] +# +# THL_WEB_RW.bulk_update("accounting_cashoutmethod", fields, values) +# +# +# def check_and_refill_balance() -> None: +# # dont do this more often than once a week!!! +# REFILL_AMOUNT = 5000 +# LOW_BALANCE = 1000 +# balance = get_account_balance() +# +# if balance < LOW_BALANCE: +# SC.chat_postMessage( +# channel=settings.SLACK_CHANNEL, +# text=f"Refilling Tango account. current balance: {balance}", +# ) +# depost_request = { +# "accountIdentifier": settings.TANGO_ACCOUNT_ID, +# "amount": REFILL_AMOUNT, +# "creditCardToken": settings.TANGO_CREDITCARD_TOKEN, +# "customerIdentifier": settings.TANGO_CUSTOMER_ID, +# } +# response = None +# try: +# response = api_client.fund.add_funds(depost_request) +# assert response.status == "SUCCESS" +# response = json.loads(APIHelper.json_serialize(response)) +# except Exception as e: +# SC.chat_postMessage( +# channel=settings.SLACK_CHANNEL, +# text=f"Tango refill error: {e}. Response: {response}", +# ) +# SC.chat_postMessage( +# channel=settings.SLACK_CHANNEL, +# text=f"Tango refill success!: Response: {response}", +# ) +# elif balance < (LOW_BALANCE * 1.5): +# SC.chat_postMessage( +# channel=settings.SLACK_CHANNEL, +# text=f"Tango balance low. current balance: {balance}", +# ) diff --git a/generalresearch/managers/thl/wallet/tango_tasks.py b/generalresearch/managers/thl/wallet/tango_tasks.py new file mode 100644 index 0000000..038f67f --- /dev/null +++ b/generalresearch/managers/thl/wallet/tango_tasks.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from generalresearch.config import ( + is_debug, +) +from generalresearch.managers.thl.tango_api import TangoOrderRequest +from generalresearch.models.thl.definitions import PayoutStatus + +if TYPE_CHECKING: + from generalresearch.managers.thl.ledger_manager.thl_ledger import ( + ThlLedgerManager, + ) + from generalresearch.managers.thl.payout import PayoutEventManager + from generalresearch.managers.thl.tango_api import TangoClient + from generalresearch.models.thl.payout import UserPayoutEvent + from generalresearch.models.thl.user import User + + +def complete_tango_order( + user: User, + payout_event: UserPayoutEvent, + payout_event_manager: PayoutEventManager, + ledger_manager: ThlLedgerManager, + tango_client: TangoClient, +): + """ + We approved the Tango card redemption. Actually request the card. + + (Note: we're skipping the PENDING -> APPROVED -> COMPLETE order for tango. + When a tango request gets APPROVED, we COMPLETE it (or FAIL!) in the + same step) + """ + assert payout_event.status in { + PayoutStatus.PENDING, + PayoutStatus.FAILED, + }, "attempting to manage payout that is not pending (or you can retry a failed order)" + request = payout_event.request_data + ref_id = request["externalRefID"] + # amount_usd = Decimal(payout_event.request_data["amount_usd"]) + + # Note: tango uses the ref_id to uniquify orders, so locking is not actually needed as long + # as the ref_id is the same. + try: + order = create_tango_order( + request_data=payout_event.request_data, + ref_id=ref_id, + tango_client=tango_client, + ) + + except AssertionError: + # todo: its possible the order went through, but something else was wrong + # we should try to retrieve the order by its ref_id and confirm it really + # failed... + payout_event_manager.update(payout_event, status=PayoutStatus.FAILED) + return payout_event + + # update TangoPayoutEvent with the order data + payout_event_manager.update( + payout_event, + status=order["status"], + ext_ref_id=order["referenceOrderID"], + order_data=order, + ) + + ledger_manager.create_tx_user_payout_complete(user, payout_event=payout_event) + + return payout_event + + +def create_tango_order( + request_data: dict[str, Any], ref_id: str, tango_client: TangoClient +) -> dict[str, Any]: + """ + Create a tango gift card order. + Throws exception if anything is not right. + - https://integration-www.tangocard.com/raas_api_console/v2/ + - https://www.apimatic.io/apidocs/tangocard/v/2_3_4#/python + + :param utid: Card identifier + :param amount: requested card value in USD + :param ref_id: TangoPayoutEvent.uuid + :return: + """ + # make sure we don't create more than one tango order for a single PayoutEvent + assert tango_client.get_order_if_exists(ref_id) is None + amount = request_data["amount"] + request_data.pop("amount_usd", None) + request_data.pop("description", None) + + if is_debug(): + return { + "status": "COMPLETE", + "referenceOrderID": "test", + "reward": { + "credentials": { + "Security Code": "XXXX-XXXX", + "Redemption URL": "https://codes.rewardcodes.com/r2/1/XXXX", + }, + "credentialList": [ + { + "type": "text", + "label": "Security Code", + "value": "XXXX-XXXX", + }, + { + "type": "url", + "label": "Redemption URL", + "value": "https://codes.rewardcodes.com/r2/1/XXXX", + }, + ], + "redemptionInstructions": "do your thang fam", + }, + } + + request = TangoOrderRequest.model_validate(request_data) + order = tango_client.create_order(request) + + amount_f: float = float(amount) + assert order["status"] == "COMPLETE" + assert abs(order["amountCharged"]["total"] - amount_f) < 0.0200001 + assert order["amountCharged"]["currencyCode"] == "USD" + if order["denomination"]["currencyCode"] == "USD": + assert order["denomination"]["value"] == amount_f + + return order diff --git a/generalresearch/models/thl/wallet/cashout_method.py b/generalresearch/models/thl/wallet/cashout_method.py index 6ad0bf2..1e96818 100644 --- a/generalresearch/models/thl/wallet/cashout_method.py +++ b/generalresearch/models/thl/wallet/cashout_method.py @@ -58,8 +58,8 @@ class CashoutMethodBase(BaseModel): id: UUIDStr = Field(description="Unique ID for this cashout method") - currency: Literal["USD"] = Field( - default="USD", + currency: Literal[Currency.USD] = Field( + default=Currency.USD, description="The currency of the cashout. Only USD is supported.", ) original_currency: Currency | None = Field( diff --git a/generalresearch/models/thl/wallet/definitions.py b/generalresearch/models/thl/wallet/definitions.py index 2d1eb8d..4d9729b 100644 --- a/generalresearch/models/thl/wallet/definitions.py +++ b/generalresearch/models/thl/wallet/definitions.py @@ -57,6 +57,8 @@ class Currency(StrEnum): # Mexican Peso MXN = "MXN" +SUPPORTED_CURRENCIES = {currency.value for currency in Currency} + CURRENCY_FORMATTER = { "USD": lambda x: f"${x / 100:,.2f}", -- cgit v1.2.3