#!/usr/bin/env python3 """ Relink Hub Customers to e-conomic Customer Numbers =================================================== Dette script matcher Hub kunder med e-conomic kunder baseret på NAVN matching og opdaterer economic_customer_number feltet. VIGTIG: Dette script ændrer IKKE data i e-conomic - det opdaterer kun Hub's links. Usage: python scripts/relink_economic_customers.py [--dry-run] [--force] Options: --dry-run Vis hvad der ville blive ændret uden at gemme --force Overskriv eksisterende links (standard: skip hvis allerede linket) Note: Matcher på kundenavn (case-insensitive, fjerner mellemrum/punktum) """ import sys import asyncio import logging from typing import Dict, List, Optional import argparse import aiohttp sys.path.insert(0, '/app') from app.core.config import settings from app.core.database import execute_query, execute_update, init_db logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) class EconomicRelinkService: """Service til at relinke Hub kunder til e-conomic""" def __init__(self, dry_run: bool = False, force: bool = False): self.api_url = settings.ECONOMIC_API_URL self.app_secret_token = settings.ECONOMIC_APP_SECRET_TOKEN self.agreement_grant_token = settings.ECONOMIC_AGREEMENT_GRANT_TOKEN self.dry_run = dry_run self.force = force if dry_run: logger.info("🔍 DRY RUN MODE - ingen ændringer gemmes") if force: logger.info("⚠️ FORCE MODE - overskriver eksisterende links") async def fetch_economic_customers(self) -> List[Dict]: """Hent alle kunder fra e-conomic API""" logger.info("📥 Henter kunder fra e-conomic...") headers = { 'X-AppSecretToken': self.app_secret_token, 'X-AgreementGrantToken': self.agreement_grant_token, 'Content-Type': 'application/json' } all_customers = [] page = 0 page_size = 1000 async with aiohttp.ClientSession() as session: while True: url = f"{self.api_url}/customers?skippages={page}&pagesize={page_size}" try: async with session.get(url, headers=headers) as response: if response.status != 200: error_text = await response.text() logger.error(f"❌ e-conomic API fejl: {response.status} - {error_text}") break data = await response.json() customers = data.get('collection', []) if not customers: break all_customers.extend(customers) logger.info(f" Hentet side {page + 1}: {len(customers)} kunder") if len(customers) < page_size: break page += 1 except Exception as e: logger.error(f"❌ Fejl ved hentning fra e-conomic: {e}") break logger.info(f"✅ Hentet {len(all_customers)} kunder fra e-conomic") return all_customers def get_hub_customers(self) -> List[Dict]: """Hent alle Hub kunder""" logger.info("📥 Henter kunder fra Hub database...") query = """ SELECT id, name, cvr_number, economic_customer_number FROM customers WHERE name IS NOT NULL AND name != '' ORDER BY name """ customers = execute_query(query) logger.info(f"✅ Hentet {len(customers)} Hub kunder") return customers def normalize_name(self, name: str) -> str: """Normaliser kundenavn for matching""" if not name: return "" # Lowercase, fjern punktum, mellemrum, A/S, ApS osv name = name.lower() name = name.replace('a/s', '').replace('aps', '').replace('i/s', '') name = name.replace('.', '').replace(',', '').replace('-', '') name = ''.join(name.split()) # Fjern alle mellemrum return name def build_name_mapping(self, economic_customers: List[Dict]) -> Dict[str, int]: """ Byg mapping fra normaliseret kundenavn til e-conomic customerNumber. """ name_map = {} for customer in economic_customers: customer_number = customer.get('customerNumber') customer_name = customer.get('name', '').strip() if not customer_number or not customer_name: continue normalized = self.normalize_name(customer_name) if normalized: if normalized in name_map: logger.warning(f"⚠️ Duplikat navn '{customer_name}' i e-conomic (kunde {customer_number} og {name_map[normalized]})") else: name_map[normalized] = customer_number logger.info(f"✅ Bygget navn mapping med {len(name_map)} unikke navne") return name_map async def relink_customers(self): """Hovedfunktion - relink alle kunder""" logger.info("🚀 Starter relink proces...") logger.info("=" * 60) # Hent data economic_customers = await self.fetch_economic_customers() if not economic_customers: logger.error("❌ Kunne ikke hente e-conomic kunder - afbryder") return hub_customers = self.get_hub_customers() if not hub_customers: logger.warning("⚠️ Ingen Hub kunder fundet") return # Byg navn mapping name_map = self.build_name_mapping(economic_customers) # Match og opdater logger.info("") logger.info("🔗 Matcher og opdaterer links...") logger.info("=" * 60) stats = { 'matched': 0, 'updated': 0, 'skipped_already_linked': 0, 'skipped_no_match': 0, 'errors': 0 } for hub_customer in hub_customers: hub_id = hub_customer['id'] hub_name = hub_customer['name'] current_economic_number = hub_customer.get('economic_customer_number') # Normaliser navn og find match normalized_name = self.normalize_name(hub_name) if not normalized_name: continue # Find match i e-conomic economic_number = name_map.get(normalized_name) if not economic_number: stats['skipped_no_match'] += 1 logger.debug(f" ⏭️ {hub_name} - ingen match i e-conomic") continue stats['matched'] += 1 # Check om allerede linket if current_economic_number and not self.force: if current_economic_number == economic_number: stats['skipped_already_linked'] += 1 logger.debug(f" ✓ {hub_name} allerede linket til {economic_number}") else: stats['skipped_already_linked'] += 1 logger.warning(f" ⚠️ {hub_name} allerede linket til {current_economic_number} (ville være {economic_number}) - brug --force") continue # Opdater link if self.dry_run: logger.info(f" 🔍 {hub_name} → e-conomic kunde {economic_number}") stats['updated'] += 1 else: try: execute_update( "UPDATE customers SET economic_customer_number = %s WHERE id = %s", (economic_number, hub_id) ) logger.info(f" ✅ {hub_name} → e-conomic kunde {economic_number}") stats['updated'] += 1 except Exception as e: logger.error(f" ❌ Fejl ved opdatering af {hub_name}: {e}") stats['errors'] += 1 # Vis statistik logger.info("") logger.info("=" * 60) logger.info("📊 RESULTAT:") logger.info(f" Kunder matchet: {stats['matched']}") logger.info(f" Links opdateret: {stats['updated']}") logger.info(f" Allerede linket (skipped): {stats['skipped_already_linked']}") logger.info(f" Ingen match i e-conomic: {stats['skipped_no_match']}") logger.info(f" Fejl: {stats['errors']}") logger.info("=" * 60) if self.dry_run: logger.info("🔍 DRY RUN - ingen ændringer blev gemt") logger.info(" Kør uden --dry-run for at gemme ændringer") async def main(): parser = argparse.ArgumentParser( description='Relink Hub kunder til e-conomic baseret på CVR-nummer' ) parser.add_argument( '--dry-run', action='store_true', help='Vis hvad der ville blive ændret uden at gemme' ) parser.add_argument( '--force', action='store_true', help='Overskriv eksisterende links' ) args = parser.parse_args() # Initialize database connection init_db() service = EconomicRelinkService(dry_run=args.dry_run, force=args.force) await service.relink_customers() if __name__ == '__main__': asyncio.run(main())