""" Hyperf PHP API Client for B2B Shopping AI Assistant """ from typing import Any, Optional from dataclasses import dataclass from enum import Enum import httpx from config import settings from utils.logger import get_logger logger = get_logger(__name__) class APIError(Exception): """API error with code and message""" def __init__(self, code: int, message: str, data: Optional[Any] = None): self.code = code self.message = message self.data = data super().__init__(f"[{code}] {message}") @dataclass class APIResponse: """Standardized API response""" code: int message: str data: Any meta: Optional[dict[str, Any]] = None @property def success(self) -> bool: return self.code == 0 class HyperfClient: """Hyperf PHP API Client""" def __init__( self, api_url: Optional[str] = None, api_token: Optional[str] = None ): """Initialize Hyperf client Args: api_url: Hyperf API base URL, defaults to settings api_token: API access token, defaults to settings """ self.api_url = (api_url or settings.hyperf_api_url).rstrip("/") self.api_token = api_token or settings.hyperf_api_token self._client: Optional[httpx.AsyncClient] = None logger.info("Hyperf client initialized", api_url=self.api_url) async def _get_client(self) -> httpx.AsyncClient: """Get or create HTTP client""" if self._client is None: self._client = httpx.AsyncClient( base_url=f"{self.api_url}/api/v1", headers={ "Authorization": f"Bearer {self.api_token}", "Content-Type": "application/json", "Accept": "application/json" }, timeout=30.0 ) return self._client async def close(self) -> None: """Close HTTP client""" if self._client: await self._client.aclose() self._client = None async def _request( self, method: str, endpoint: str, params: Optional[dict[str, Any]] = None, json: Optional[dict[str, Any]] = None, headers: Optional[dict[str, str]] = None ) -> APIResponse: """Make API request Args: method: HTTP method endpoint: API endpoint params: Query parameters json: JSON body headers: Additional headers Returns: Parsed API response Raises: APIError: If API returns error """ client = await self._get_client() # Merge headers request_headers = {} if headers: request_headers.update(headers) logger.debug( "API request", method=method, endpoint=endpoint ) try: response = await client.request( method=method, url=endpoint, params=params, json=json, headers=request_headers ) response.raise_for_status() data = response.json() result = APIResponse( code=data.get("code", 0), message=data.get("message", "success"), data=data.get("data"), meta=data.get("meta") ) if not result.success: raise APIError(result.code, result.message, result.data) logger.debug( "API response", endpoint=endpoint, code=result.code ) return result except httpx.HTTPStatusError as e: logger.error( "HTTP error", endpoint=endpoint, status_code=e.response.status_code ) raise APIError( e.response.status_code, f"HTTP error: {e.response.status_code}" ) except Exception as e: logger.error("API request failed", endpoint=endpoint, error=str(e)) raise async def get( self, endpoint: str, params: Optional[dict[str, Any]] = None, **kwargs: Any ) -> APIResponse: """GET request""" return await self._request("GET", endpoint, params=params, **kwargs) async def post( self, endpoint: str, json: Optional[dict[str, Any]] = None, **kwargs: Any ) -> APIResponse: """POST request""" return await self._request("POST", endpoint, json=json, **kwargs) async def put( self, endpoint: str, json: Optional[dict[str, Any]] = None, **kwargs: Any ) -> APIResponse: """PUT request""" return await self._request("PUT", endpoint, json=json, **kwargs) async def delete( self, endpoint: str, **kwargs: Any ) -> APIResponse: """DELETE request""" return await self._request("DELETE", endpoint, **kwargs) # ============ Order APIs ============ async def query_orders( self, user_id: str, account_id: str, order_id: Optional[str] = None, status: Optional[str] = None, date_start: Optional[str] = None, date_end: Optional[str] = None, page: int = 1, page_size: int = 20 ) -> APIResponse: """Query orders Args: user_id: User ID account_id: Account ID order_id: Optional specific order ID status: Optional order status filter date_start: Optional start date (YYYY-MM-DD) date_end: Optional end date (YYYY-MM-DD) page: Page number page_size: Items per page Returns: Orders list response """ payload = { "user_id": user_id, "account_id": account_id, "page": page, "page_size": page_size } if order_id: payload["order_id"] = order_id if status: payload["status"] = status if date_start: payload["date_range"] = {"start": date_start} if date_end: payload.setdefault("date_range", {})["end"] = date_end return await self.post("/orders/query", json=payload) async def get_logistics( self, order_id: str, tracking_number: Optional[str] = None ) -> APIResponse: """Get order logistics information Args: order_id: Order ID tracking_number: Optional tracking number Returns: Logistics tracking response """ params = {} if tracking_number: params["tracking_number"] = tracking_number return await self.get(f"/orders/{order_id}/logistics", params=params) async def modify_order( self, order_id: str, user_id: str, modifications: dict[str, Any] ) -> APIResponse: """Modify order Args: order_id: Order ID user_id: User ID for permission check modifications: Changes to apply Returns: Modified order response """ return await self.put( f"/orders/{order_id}/modify", json={ "user_id": user_id, "modifications": modifications } ) async def cancel_order( self, order_id: str, user_id: str, reason: str ) -> APIResponse: """Cancel order Args: order_id: Order ID user_id: User ID for permission check reason: Cancellation reason Returns: Cancellation result with refund info """ return await self.post( f"/orders/{order_id}/cancel", json={ "user_id": user_id, "reason": reason } ) # ============ Product APIs ============ async def search_products( self, query: str, filters: Optional[dict[str, Any]] = None, sort: str = "relevance", page: int = 1, page_size: int = 20 ) -> APIResponse: """Search products Args: query: Search query filters: Optional filters (category, price_range, brand, etc.) sort: Sort order page: Page number page_size: Items per page Returns: Products list response """ payload = { "query": query, "sort": sort, "page": page, "page_size": page_size } if filters: payload["filters"] = filters return await self.post("/products/search", json=payload) async def get_product(self, product_id: str) -> APIResponse: """Get product details Args: product_id: Product ID Returns: Product details response """ return await self.get(f"/products/{product_id}") async def get_recommendations( self, user_id: str, account_id: str, context: Optional[dict[str, Any]] = None, limit: int = 10 ) -> APIResponse: """Get product recommendations Args: user_id: User ID account_id: Account ID context: Optional context (recent views, current query) limit: Number of recommendations Returns: Recommendations response """ payload = { "user_id": user_id, "account_id": account_id, "limit": limit } if context: payload["context"] = context return await self.post("/products/recommend", json=payload) async def get_quote( self, product_id: str, quantity: int, account_id: str, delivery_address: Optional[dict[str, str]] = None ) -> APIResponse: """Get B2B price quote Args: product_id: Product ID quantity: Quantity account_id: Account ID for pricing tier delivery_address: Optional delivery address Returns: Quote response with pricing details """ payload = { "product_id": product_id, "quantity": quantity, "account_id": account_id } if delivery_address: payload["delivery_address"] = delivery_address return await self.post("/products/quote", json=payload) # ============ Aftersale APIs ============ async def apply_return( self, order_id: str, user_id: str, items: list[dict[str, Any]], description: str, images: Optional[list[str]] = None ) -> APIResponse: """Apply for return Args: order_id: Order ID user_id: User ID items: Items to return with quantity and reason description: Description of issue images: Optional image URLs Returns: Return application response """ payload = { "order_id": order_id, "user_id": user_id, "items": items, "description": description } if images: payload["images"] = images return await self.post("/aftersales/return", json=payload) async def apply_exchange( self, order_id: str, user_id: str, items: list[dict[str, Any]], description: str ) -> APIResponse: """Apply for exchange Args: order_id: Order ID user_id: User ID items: Items to exchange with reason description: Description of issue Returns: Exchange application response """ return await self.post( "/aftersales/exchange", json={ "order_id": order_id, "user_id": user_id, "items": items, "description": description } ) async def create_complaint( self, user_id: str, complaint_type: str, title: str, description: str, related_order_id: Optional[str] = None, attachments: Optional[list[str]] = None ) -> APIResponse: """Create complaint Args: user_id: User ID complaint_type: Type of complaint title: Complaint title description: Detailed description related_order_id: Optional related order attachments: Optional attachment URLs Returns: Complaint creation response """ payload = { "user_id": user_id, "type": complaint_type, "title": title, "description": description } if related_order_id: payload["related_order_id"] = related_order_id if attachments: payload["attachments"] = attachments return await self.post("/aftersales/complaint", json=payload) async def query_aftersales( self, user_id: str, aftersale_id: Optional[str] = None ) -> APIResponse: """Query aftersale records Args: user_id: User ID aftersale_id: Optional specific aftersale ID Returns: Aftersale records response """ params = {"user_id": user_id} if aftersale_id: params["aftersale_id"] = aftersale_id return await self.get("/aftersales/query", params=params) # Global Hyperf client instance hyperf_client: Optional[HyperfClient] = None def get_hyperf_client() -> HyperfClient: """Get or create global Hyperf client instance""" global hyperf_client if hyperf_client is None: hyperf_client = HyperfClient() return hyperf_client