"""Airwallex API service layer.""" import json import logging from typing import Any, Dict, List, Optional from sqlalchemy.orm import Session from fastapi import HTTPException from app.models.db_models import SystemSetting from app.proxy_client import ProxiedAirwallexClient logger = logging.getLogger(__name__) # Cached client instance _client_instance: Optional[ProxiedAirwallexClient] = None _client_config_hash: Optional[str] = None def _get_setting(db: Session, key: str, default: str = "") -> str: """Get a system setting value.""" setting = db.query(SystemSetting).filter(SystemSetting.key == key).first() return setting.value if setting else default def _build_proxy_url(db: Session) -> Optional[str]: """Build proxy URL from settings. Supports http and socks5.""" proxy_ip = _get_setting(db, "proxy_ip") proxy_port = _get_setting(db, "proxy_port") if not proxy_ip or not proxy_port: return None proxy_type = _get_setting(db, "proxy_type", "socks5") # default socks5 scheme = "socks5" if proxy_type == "socks5" else "http" proxy_user = _get_setting(db, "proxy_username") proxy_pass = _get_setting(db, "proxy_password") if proxy_user and proxy_pass: return f"{scheme}://{proxy_user}:{proxy_pass}@{proxy_ip}:{proxy_port}" return f"{scheme}://{proxy_ip}:{proxy_port}" def get_client(db: Session) -> ProxiedAirwallexClient: """Get or create an Airwallex client with current settings.""" global _client_instance, _client_config_hash client_id = _get_setting(db, "airwallex_client_id") api_key = _get_setting(db, "airwallex_api_key") base_url = _get_setting(db, "airwallex_base_url", "https://api-demo.airwallex.com/") login_as = _get_setting(db, "airwallex_login_as") proxy_url = _build_proxy_url(db) if not client_id or not api_key: raise HTTPException(status_code=400, detail="Airwallex 凭证未配置,请在系统设置中填写 Client ID 和 API Key") config_hash = f"{client_id}:{api_key}:{base_url}:{login_as}:{proxy_url}" if _client_instance and _client_config_hash == config_hash: return _client_instance # Close old client if _client_instance: try: _client_instance.close() except Exception: pass # auth_url must match base_url domain auth_url = base_url.rstrip("/") + "/api/v1/authentication/login" _client_instance = ProxiedAirwallexClient( proxy_url=proxy_url, login_as=login_as or None, client_id=client_id, api_key=api_key, base_url=base_url, auth_url=auth_url, ) _client_config_hash = config_hash return _client_instance def ensure_authenticated(db: Session) -> ProxiedAirwallexClient: """Get client and ensure it's authenticated.""" client = get_client(db) try: client.authenticate() except HTTPException: raise except Exception as e: error_msg = str(e) if hasattr(e, "response"): try: detail = e.response.json() error_msg = f"Airwallex API 错误 ({e.response.status_code}): {detail.get('message', detail)}" except Exception: error_msg = f"Airwallex API 错误 ({e.response.status_code}): {e.response.text[:200]}" raise HTTPException(status_code=400, detail=error_msg) return client # ─── Card operations ──────────────────────────────────────────────── def list_cards( db: Session, page_num: int = 0, page_size: int = 20, cardholder_id: Optional[str] = None, status: Optional[str] = None, ) -> Dict[str, Any]: """List cards with optional filters.""" client = ensure_authenticated(db) params: Dict[str, Any] = {"page_num": page_num, "page_size": page_size} if cardholder_id: params["cardholder_id"] = cardholder_id if status: params["status"] = status cards = client.issuing_card.list_with_filters(**params) return { "items": [c.to_dict() if hasattr(c, "to_dict") else c.__dict__ for c in cards], "page_num": page_num, "page_size": page_size, "has_more": len(cards) == page_size, } def create_card(db: Session, card_data: Dict[str, Any]) -> Dict[str, Any]: """Create a new card.""" from airwallex.models.issuing_card import CardCreateRequest client = ensure_authenticated(db) request = CardCreateRequest(**card_data) card = client.issuing_card.create_card(request) return card.to_dict() if hasattr(card, "to_dict") else card.__dict__ def get_card(db: Session, card_id: str) -> Dict[str, Any]: """Get card by ID.""" client = ensure_authenticated(db) card = client.issuing_card.get(card_id) return card.to_dict() if hasattr(card, "to_dict") else card.__dict__ def get_card_details(db: Session, card_id: str) -> Dict[str, Any]: """Get sensitive card details (card number, CVV, expiry).""" client = ensure_authenticated(db) details = client.issuing_card.get_card_details(card_id) return details.to_dict() if hasattr(details, "to_dict") else details.__dict__ def update_card(db: Session, card_id: str, update_data: Dict[str, Any]) -> Dict[str, Any]: """Update card attributes.""" from airwallex.models.issuing_card import CardUpdateRequest client = ensure_authenticated(db) request = CardUpdateRequest(**update_data) card = client.issuing_card.update_card(card_id, request) return card.to_dict() if hasattr(card, "to_dict") else card.__dict__ # ─── Cardholder operations ────────────────────────────────────────── def list_cardholders( db: Session, page_num: int = 0, page_size: int = 20, ) -> Dict[str, Any]: """List cardholders.""" client = ensure_authenticated(db) cardholders = client.issuing_cardholder.list(page_num=page_num, page_size=page_size) return { "items": [c.to_dict() if hasattr(c, "to_dict") else c.__dict__ for c in cardholders], "page_num": page_num, "page_size": page_size, "has_more": len(cardholders) == page_size, } def create_cardholder(db: Session, cardholder_data: Dict[str, Any]) -> Dict[str, Any]: """Create a new cardholder.""" from airwallex.models.issuing_cardholder import CardholderCreateRequest client = ensure_authenticated(db) request = CardholderCreateRequest(**cardholder_data) cardholder = client.issuing_cardholder.create_cardholder(request) return cardholder.to_dict() if hasattr(cardholder, "to_dict") else cardholder.__dict__ # ─── Transaction operations ───────────────────────────────────────── def list_transactions( db: Session, page_num: int = 0, page_size: int = 20, card_id: Optional[str] = None, from_created_at: Optional[str] = None, to_created_at: Optional[str] = None, ) -> Dict[str, Any]: """List transactions.""" client = ensure_authenticated(db) params: Dict[str, Any] = {"page_num": page_num, "page_size": page_size} if card_id: params["card_id"] = card_id if from_created_at: params["from_created_at"] = from_created_at if to_created_at: params["to_created_at"] = to_created_at txns = client.issuing_transaction.list_with_filters(**params) return { "items": [t.to_dict() if hasattr(t, "to_dict") else t.__dict__ for t in txns], "page_num": page_num, "page_size": page_size, "has_more": len(txns) == page_size, } def list_authorizations( db: Session, page_num: int = 0, page_size: int = 20, card_id: Optional[str] = None, status: Optional[str] = None, from_created_at: Optional[str] = None, to_created_at: Optional[str] = None, ) -> Dict[str, Any]: """List authorizations.""" client = ensure_authenticated(db) params: Dict[str, Any] = {"page_num": page_num, "page_size": page_size} if card_id: params["card_id"] = card_id if status: params["status"] = status if from_created_at: params["from_created_at"] = from_created_at if to_created_at: params["to_created_at"] = to_created_at auths = client.issuing_authorization.list_with_filters(**params) return { "items": [a.to_dict() if hasattr(a, "to_dict") else a.__dict__ for a in auths], "page_num": page_num, "page_size": page_size, "has_more": len(auths) == page_size, } # ─── Account / Balance ────────────────────────────────────────────── def get_balance(db: Session) -> Optional[Dict[str, Any]]: """Get account balance. Returns None if not available.""" try: client = ensure_authenticated(db) # Try to get global account balance via the balances endpoint response = client._request("GET", "api/v1/balances/current") return response.json() except Exception as e: logger.warning("Failed to fetch balance: %s", e) return None # ─── Config ────────────────────────────────────────────────────────── def get_issuing_config(db: Session) -> Optional[Dict[str, Any]]: """Get issuing configuration.""" try: client = ensure_authenticated(db) config = client.issuing_config.get_config() return config.to_dict() if hasattr(config, "to_dict") else config.__dict__ except Exception as e: logger.warning("Failed to fetch issuing config: %s", e) return None def test_connection(db: Session) -> Dict[str, Any]: """Test Airwallex API connection with detailed error info.""" try: client = get_client(db) client.authenticate() return {"success": True, "message": "连接成功,认证通过"} except HTTPException as e: return {"success": False, "message": e.detail} except Exception as e: error_msg = str(e) # Try to extract more detail from httpx response errors if hasattr(e, "response"): try: detail = e.response.json() error_msg = f"HTTP {e.response.status_code}: {detail.get('message', detail)}" except Exception: error_msg = f"HTTP {e.response.status_code}: {e.response.text[:200]}" return {"success": False, "message": error_msg} def test_proxy(db: Session) -> Dict[str, Any]: """Test proxy connectivity and return the outbound IP address.""" import httpx proxy_url = _build_proxy_url(db) result: Dict[str, Any] = {"proxy_configured": bool(proxy_url), "proxy_url": None} if proxy_url: # Mask password in display masked = proxy_url if "@" in proxy_url: prefix, rest = proxy_url.rsplit("@", 1) if ":" in prefix: scheme_user = prefix.rsplit(":", 1)[0] masked = f"{scheme_user}:****@{rest}" result["proxy_url"] = masked # Query outbound IP + country — with proxy if configured, otherwise direct for label, use_proxy in [("proxy", True), ("direct", False)]: if label == "proxy" and not proxy_url: continue try: client_kwargs: Dict[str, Any] = {"timeout": 10} if use_proxy and proxy_url: client_kwargs["proxy"] = proxy_url with httpx.Client(**client_kwargs) as client: # Use ip-api.com for IP + country info resp = client.get("http://ip-api.com/json/?fields=query,country,countryCode,city,isp") ip_data = resp.json() result[f"{label}_ip"] = ip_data.get("query", "unknown") result[f"{label}_country"] = ip_data.get("country", "unknown") result[f"{label}_country_code"] = ip_data.get("countryCode", "") result[f"{label}_city"] = ip_data.get("city", "") result[f"{label}_isp"] = ip_data.get("isp", "") result[f"{label}_status"] = "ok" except Exception as e: result[f"{label}_ip"] = None result[f"{label}_country"] = None result[f"{label}_status"] = f"failed: {str(e)[:150]}" result["success"] = result.get("proxy_status", result.get("direct_status")) == "ok" return result