aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--generalresearch/managers/thl/tango_api.py135
-rw-r--r--generalresearch/managers/thl/wallet/__init__.py5
-rw-r--r--generalresearch/managers/thl/wallet/tango.py78
-rw-r--r--generalresearch/models/thl/ledger.py2
4 files changed, 162 insertions, 58 deletions
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)