From fd77cd429cc6205f9a8f2f4168298fe82d461383 Mon Sep 17 00:00:00 2001 From: stuppie Date: Fri, 26 Jun 2026 19:54:42 -0600 Subject: implement tangoapi directly. Pass it into manage_pending_cashout. Fix small bug for get_user_txs for tango txs --- generalresearch/managers/thl/tango_api.py | 135 ++++++++++++++++++++++++ generalresearch/managers/thl/wallet/__init__.py | 5 + generalresearch/managers/thl/wallet/tango.py | 78 ++++---------- generalresearch/models/thl/ledger.py | 2 + 4 files changed, 162 insertions(+), 58 deletions(-) create mode 100644 generalresearch/managers/thl/tango_api.py diff --git a/generalresearch/managers/thl/tango_api.py b/generalresearch/managers/thl/tango_api.py new file mode 100644 index 0000000..5c1706e --- /dev/null +++ b/generalresearch/managers/thl/tango_api.py @@ -0,0 +1,135 @@ +from decimal import Decimal +from typing import Any, Dict, List + +import requests +from pydantic import BaseModel + +TANGO_SANDBOX_URL = "https://integration-api.tangocard.com/raas/v2" +TANGO_PROD_URL = "https://api.tangocard.com/raas/v2" + + +class TangoError(RuntimeError): + pass + + +class TangoOrderRequest(BaseModel): + externalRefID: str + + customerIdentifier: str + accountIdentifier: str + + utid: str + sendEmail: bool + campaign: str + # In USD (e.g. '0.24' is 24 cents) + amount: Decimal + + +class TangoClient: + def __init__( + self, + *, + platform_name: str, + platform_key: str, + base_url: str = TANGO_SANDBOX_URL, + timeout: float = 30.0, + ) -> None: + self.base_url = base_url.rstrip("/") + self.timeout = timeout + + self.session = requests.Session() + self.session.auth = (platform_name, platform_key) + self.session.headers.update( + { + "Accept": "application/json", + "Content-Type": "application/json", + } + ) + + def _request( + self, + method: str, + path: str, + *, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, + ) -> Any: + response = self.session.request( + method=method, + url=f"{self.base_url}{path}", + params=params, + json=json, + timeout=self.timeout, + ) + + if response.ok: + if not response.content: + return None + return response.json() + + request_id = response.headers.get("X-REQUEST-ID") + try: + body = response.json() + except ValueError: + body = response.text + + raise TangoError( + f"Tango API error {response.status_code}" + + (f" X-REQUEST-ID={request_id}" if request_id else "") + + f": {body}" + ) + + def get_exchange_rates(self, reward_currency: str = "USD") -> Any: + return self._request( + "GET", + "/exchangerates", + params={"rewardCurrency": reward_currency}, + ) + + def get_customers(self) -> Any: + return self._request("GET", "/customers") + + def get_accounts(self, customer_identifier: str) -> Any: + return self._request( + "GET", + f"/customers/{customer_identifier}/accounts", + ) + + def get_account( + self, + account_identifier: str, + ) -> Any: + return self._request( + "GET", + f"/accounts/{account_identifier}", + ) + + def get_catalog(self) -> Dict[str, List[Dict[str, Any]]]: + """ + Replacement for: + api_client.catalog.get_catalog() + """ + return self._request("GET", "/catalogs") + + def list_orders(self, **filters: Any) -> list[dict[str, Any]]: + return self._request("GET", "/orders", params=filters or None) + + def get_order(self, reference_order_id: str) -> dict[str, Any]: + return self._request("GET", f"/orders/{reference_order_id}") + + def get_order_if_exists(self, reference_order_id: str) -> dict[str, Any] | None: + try: + return self.get_order(reference_order_id) + except TangoError as e: + if "The order you requested cannot be found" not in e.args[0]: + raise e + return None + + def create_order(self, order: TangoOrderRequest) -> dict[str, Any]: + payload = order.model_dump(mode="json", exclude_none=True) + + # Pydantic serializes Decimal as string in JSON mode. Tango docs say `amount` + # is a double, so convert for the outgoing payload. + payload["amount"] = float(order.amount) + + return self._request("POST", "/orders", json=payload) diff --git a/generalresearch/managers/thl/wallet/__init__.py b/generalresearch/managers/thl/wallet/__init__.py index 0826263..b063e54 100644 --- a/generalresearch/managers/thl/wallet/__init__.py +++ b/generalresearch/managers/thl/wallet/__init__.py @@ -8,6 +8,7 @@ from generalresearch.managers.thl.payout import ( PayoutEventManager, UserPayoutEventManager, ) +from generalresearch.managers.thl.tango_api import TangoClient from generalresearch.managers.thl.user_manager.user_manager import ( UserManager, ) @@ -32,6 +33,7 @@ def manage_pending_cashout( user_manager: UserManager, ledger_manager: ThlLedgerManager, order_data: Optional[Union[Dict[str, Any], CashMailOrderData]] = None, + tango_client: Optional[TangoClient] = None, ) -> UserPayoutEvent: """ Called by a UI actions performed by Todd. This rejects/approves/cancels @@ -72,11 +74,14 @@ def manage_pending_cashout( complete_tango_order, ) + assert tango_client is not None + complete_tango_order( user=user, payout_event=pe, payout_event_manager=payout_event_manager, ledger_manager=ledger_manager, + tango_client=tango_client, ) elif pe.payout_type == PayoutType.PAYPAL: diff --git a/generalresearch/managers/thl/wallet/tango.py b/generalresearch/managers/thl/wallet/tango.py index 7665ef0..2f2dc52 100644 --- a/generalresearch/managers/thl/wallet/tango.py +++ b/generalresearch/managers/thl/wallet/tango.py @@ -7,33 +7,18 @@ 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, TangoOrderRequest from generalresearch.models.thl.definitions import PayoutStatus from generalresearch.models.thl.payout import UserPayoutEvent from generalresearch.models.thl.user import User -# from raas.api_helper import APIHelper -# from raas.exceptions.raas_client_exception import RaasClientException -# from raas.raas_client import RaasClient - -# RaasClient.config.environment = 1 -# api_client = RaasClient( -# platform_name=TANGO_PLATFORM_NAME, platform_key=TANGO_PLATFORM_KEY -# ) -# it really annoyingly logs the entire http response. turn it off -# api_client.catalog.logger.setLevel(logging.INFO) -# api_client.exchange_rates.logger.setLevel(logging.INFO) -# api_client.orders.logger.setLevel(logging.INFO) -# api_client.status.logger.setLevel(logging.INFO) -# api_client.accounts.logger.setLevel(logging.INFO) -# api_client.customers.logger.setLevel(logging.INFO) -# api_client.fund.logger.setLevel(logging.INFO) - 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. @@ -54,7 +39,9 @@ def complete_tango_order( # as the ref_id is the same. try: order = create_tango_order( - request_data=payout_event.request_data, ref_id=ref_id + request_data=payout_event.request_data, + ref_id=ref_id, + tango_client=tango_client, ) except Exception as e: @@ -77,20 +64,9 @@ def complete_tango_order( return payout_event -def get_tango_order(ref_id: str): - """ - Retrieve a tango order by its external ref ID. - We should have set it to the TangoPayoutEvent instance uuid associated - with this Tango order (lowercase no dashes). - :return: the json order data or None if doesn't exist - """ - raise NotImplementedError("convert to requests") - # orders = api_client.orders.get_orders({"external_ref_id": ref_id}).orders - # if orders: - # return json.loads(APIHelper.json_serialize(orders[0])) - - -def create_tango_order(request_data: Dict[str, Any], ref_id: str) -> Dict[str, Any]: +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. @@ -103,7 +79,7 @@ def create_tango_order(request_data: Dict[str, Any], ref_id: str) -> Dict[str, A :return: """ # make sure we don't create more than one tango order for a single PayoutEvent - assert get_tango_order(ref_id) is None + 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) @@ -133,28 +109,14 @@ def create_tango_order(request_data: Dict[str, Any], ref_id: str) -> Dict[str, A }, } - raise NotImplementedError("convert to requests") - # try: - # order = api_client.orders.create_order(request_data) - # order = json.loads(APIHelper.json_serialize(order)) - # except RaasClientException as e: - # e = json.loads(APIHelper.json_serialize(e)) - # try: - # msgs = [x["message"] for x in e["errors"]] - # print(" | ".join(msgs)) - # except Exception: - # pass - # capture_exception() - # raise e - # except Exception as e: - # capture_exception() - # raise e - - # 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 + 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/ledger.py b/generalresearch/models/thl/ledger.py index 31679d2..9d473c5 100644 --- a/generalresearch/models/thl/ledger.py +++ b/generalresearch/models/thl/ledger.py @@ -335,6 +335,8 @@ class LedgerTransaction(BaseModel): d["description"] = "HIT Reward" elif payout_type == PayoutType.AMT_BONUS: d["description"] = "HIT Bonus" + elif payout_type == PayoutType.TANGO: + d["description"] = "Tango" else: raise ValueError(payout_type) return UserLedgerTransactionUserPayout.model_validate(d) -- cgit v1.2.3