- Introduced a global search button and modal for enhanced user experience. - Added a new section for displaying email results in the global search modal. - Implemented functionality to fetch and display emails based on user queries. - Updated the UI to include a reminders button and improved accessibility features. fix: Update docker-compose to allow reload configuration - Changed ENABLE_RELOAD environment variable to default to true for easier development. chore: Update requirements for new dependencies - Added brother_ql, pyzbar, and pypdfium2 to requirements for label printing and PDF processing. feat: Implement Brother label printing service - Created a new service for printing labels using Brother QL printers. - Supports direct printing of case hardware labels with customizable layouts. feat: Add Vaultwarden service for credential management - Implemented a service to interact with Vaultwarden for secure credential storage and retrieval. sql: Add migrations for email thread keys and document tokens - Created migrations to backfill email thread keys and manage document tokens for work orders. - Introduced new tables and updated existing structures to support token-based linking of scanned documents. sql: Import links into the database - Added a script to import a predefined set of links into the database with associated categories.
183 lines
6.8 KiB
Python
183 lines
6.8 KiB
Python
"""
|
|
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()
|