bmc_hub/scripts/relink_economic_customers.py

274 lines
9.7 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""
Relink Hub Customers to e-conomic Customer Numbers
===================================================
Dette script matcher Hub kunder med e-conomic kunder baseret 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 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())