import logging from typing import Any, Dict, List import aiohttp from app.core.config import settings logger = logging.getLogger(__name__) class FedExApiClient: def __init__(self) -> None: self.base_url = (settings.FEDEX_BASE_URL or "").rstrip("/") self.timeout_seconds = max(5, int(settings.FEDEX_TIMEOUT_SECONDS or 20)) def _headers(self) -> Dict[str, str]: return { "Content-Type": "application/json", "X-API-KEY": settings.FEDEX_API_KEY or "", "X-API-SECRET": settings.FEDEX_API_SECRET or "", "X-FEDEX-ACCOUNT": settings.FEDEX_ACCOUNT_NUMBER or "", } async def create_shipment(self, payload: Dict[str, Any]) -> Dict[str, Any]: if not self.base_url: raise RuntimeError("FedEx base URL is not configured") url = f"{self.base_url}/shipments" logger.info("🚀 FedEx create shipment request sent") timeout = aiohttp.ClientTimeout(total=self.timeout_seconds) async with aiohttp.ClientSession(timeout=timeout) as session: async with session.post(url, json=payload, headers=self._headers()) as response: body = await response.text() if response.status >= 400: logger.error("❌ FedEx create shipment failed (%s): %s", response.status, body) raise RuntimeError(f"FedEx create shipment failed: HTTP {response.status}") return await response.json() async def get_tracking(self, tracking_number: str) -> Dict[str, Any]: if not self.base_url: raise RuntimeError("FedEx base URL is not configured") url = f"{self.base_url}/tracking/{tracking_number}" timeout = aiohttp.ClientTimeout(total=self.timeout_seconds) async with aiohttp.ClientSession(timeout=timeout) as session: async with session.get(url, headers=self._headers()) as response: body = await response.text() if response.status >= 400: logger.error("❌ FedEx tracking failed (%s): %s", response.status, body) raise RuntimeError(f"FedEx tracking failed: HTTP {response.status}") return await response.json() async def cancel_shipment(self, tracking_number: str) -> Dict[str, Any]: if not self.base_url: raise RuntimeError("FedEx base URL is not configured") url = f"{self.base_url}/shipments/{tracking_number}/cancel" timeout = aiohttp.ClientTimeout(total=self.timeout_seconds) async with aiohttp.ClientSession(timeout=timeout) as session: async with session.post(url, headers=self._headers()) as response: body = await response.text() if response.status >= 400: logger.error("❌ FedEx cancel failed (%s): %s", response.status, body) raise RuntimeError(f"FedEx cancel failed: HTTP {response.status}") return await response.json() def parse_tracking_events(payload: Dict[str, Any]) -> List[Dict[str, Any]]: raw_events = payload.get("events") or [] if not isinstance(raw_events, list): return [] normalized: List[Dict[str, Any]] = [] for event in raw_events: if not isinstance(event, dict): continue normalized.append( { "status": str(event.get("status") or "unknown"), "description": event.get("description"), "event_timestamp": event.get("event_timestamp") or event.get("timestamp"), "location_city": event.get("location_city") or event.get("city"), "location_country": event.get("location_country") or event.get("country"), } ) return normalized