""" CVR service for looking up Danish company information. Primary provider: FirmaAPI (authenticated). Legacy fallback: cvrapi.dk when no FirmaAPI key is configured. """ import asyncio import aiohttp import logging from typing import Optional, Dict from app.core.config import settings logger = logging.getLogger(__name__) class CVRService: """Service for CVR lookups using FirmaAPI (or legacy fallback).""" LEGACY_BASE_URL = "https://cvrapi.dk/api" @property def firmaapi_base_url(self) -> str: return settings.FIRMAAPI_BASE_URL.rstrip("/") @property def firmaapi_timeout(self) -> aiohttp.ClientTimeout: return aiohttp.ClientTimeout(total=settings.FIRMAAPI_TIMEOUT_SECONDS) @property def has_firmaapi_key(self) -> bool: return bool((settings.FIRMAAPI_API_KEY or "").strip()) def _firmaapi_headers(self) -> Dict[str, str]: api_key = (settings.FIRMAAPI_API_KEY or "").strip() return { "Authorization": f"Bearer {api_key}", "Accept": "application/json", } @staticmethod def _normalize_payload(payload: Dict) -> Dict: return { "cvr": payload.get("cvr") or payload.get("vat"), "name": payload.get("name"), "address": payload.get("address"), "city": payload.get("city"), "zipcode": payload.get("zipcode"), "postal_code": payload.get("zipcode") or payload.get("postal_code"), "country": payload.get("country") or "DK", "phone": payload.get("phone"), "email": payload.get("email"), "website": payload.get("website"), "status": payload.get("status"), "source": "firmaapi" if payload.get("meta", {}).get("source") == "FirmaAPI" else payload.get("source", "firmaapi"), } async def lookup_by_name(self, company_name: str) -> Optional[Dict]: """ Lookup company by name using CVR.dk API Args: company_name: Company name to search for Returns: Company data dict or None if not found """ if not company_name or len(company_name) < 3: return None # Clean company name clean_name = company_name.strip() try: if self.has_firmaapi_key: async with aiohttp.ClientSession() as session: async with session.get( f"{self.firmaapi_base_url}/company/search", params={"q": clean_name, "limit": 1}, headers=self._firmaapi_headers(), timeout=self.firmaapi_timeout, ) as response: if response.status == 200: data = await response.json() results = data.get("results") or [] if results: match = results[0] logger.info("✅ Found CVR %s for '%s' via FirmaAPI", match.get("cvr"), company_name) return self._normalize_payload(match) return None if response.status == 404: return None detail = await response.text() logger.error("❌ FirmaAPI name lookup error %s for '%s': %s", response.status, company_name, detail[:240]) return None # Legacy fallback without API key params = {"search": clean_name, "country": "dk"} async with aiohttp.ClientSession() as session: async with session.get( f"{self.LEGACY_BASE_URL}", params=params, timeout=aiohttp.ClientTimeout(total=10), ) as response: if response.status == 200: data = await response.json() if data and "vat" in data: return self._normalize_payload(data) return None except asyncio.TimeoutError: logger.error(f"⏱️ CVR API timeout for '{company_name}'") return None except Exception as e: logger.error(f"❌ CVR lookup error for '{company_name}': {e}") return None async def lookup_by_cvr(self, cvr_number: str) -> Optional[Dict]: """ Lookup company by CVR number Args: cvr_number: CVR number (8 digits) Returns: Company data dict or None if not found """ if not cvr_number: return None # Extract only digits cvr_clean = ''.join(filter(str.isdigit, str(cvr_number))) if len(cvr_clean) != 8: logger.warning(f"⚠️ Invalid CVR number format: {cvr_number}") return None try: if self.has_firmaapi_key: async with aiohttp.ClientSession() as session: async with session.get( f"{self.firmaapi_base_url}/company/{cvr_clean}", headers=self._firmaapi_headers(), timeout=self.firmaapi_timeout, ) as response: if response.status == 200: data = await response.json() logger.info("✅ Validated CVR %s via FirmaAPI", cvr_clean) return self._normalize_payload(data) if response.status in (400, 404): return None detail = await response.text() logger.error("❌ FirmaAPI CVR lookup error %s for %s: %s", response.status, cvr_clean, detail[:240]) return None # Legacy fallback without API key async with aiohttp.ClientSession() as session: async with session.get( f"{self.LEGACY_BASE_URL}", params={"vat": cvr_clean, "country": "dk"}, timeout=aiohttp.ClientTimeout(total=10), ) as response: if response.status == 200: data = await response.json() if data and "vat" in data: logger.info("✅ Validated CVR %s via legacy CVR API", cvr_clean) return self._normalize_payload(data) return None except Exception as e: logger.error(f"❌ CVR validation error for {cvr_number}: {e}") return None def get_cvr_service() -> CVRService: """Get CVR service instance""" return CVRService()