bmc_hub/app/services/cvr_service.py

183 lines
6.8 KiB
Python
Raw Normal View History

"""
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()