bmc_hub/app/system/backend/sync_router.py

454 lines
18 KiB
Python
Raw Normal View History

"""
System Sync Router
API endpoints for syncing data between vTiger, e-conomic and Hub
"""
import logging
from fastapi import APIRouter, HTTPException
from typing import Dict, Any
from app.core.database import execute_query
from app.services.vtiger_service import get_vtiger_service
import re
logger = logging.getLogger(__name__)
router = APIRouter()
def normalize_name(name: str) -> str:
"""Normalize company name for matching"""
if not name:
return ""
# Remove common suffixes and punctuation
name = re.sub(r'\b(A/S|ApS|I/S|IVS|v/)\b', '', name, flags=re.IGNORECASE)
name = re.sub(r'[^\w\s]', '', name) # Remove punctuation
return name.lower().strip()
@router.post("/sync/vtiger")
async def sync_from_vtiger() -> Dict[str, Any]:
"""
Sync companies from vTiger Accounts module
Matches by CVR number or normalized company name
"""
try:
logger.info("🔄 Starting vTiger accounts sync...")
vtiger = get_vtiger_service()
# Query vTiger for all accounts with CVR or name
query = "SELECT id, accountname, email1, siccode, cf_accounts_cvr, website, bill_city, bill_code, bill_country FROM Accounts LIMIT 1000;"
accounts = await vtiger.query(query)
logger.info(f"📥 Fetched {len(accounts)} accounts from vTiger")
created_count = 0
updated_count = 0
skipped_count = 0
for account in accounts:
vtiger_id = account.get('id')
name = account.get('accountname', '').strip()
cvr = account.get('cf_accounts_cvr') or account.get('siccode')
economic_customer_number = None # Will be set by e-conomic sync
if not name:
skipped_count += 1
logger.debug(f"⏭️ Sprunget over: Tomt firmanavn (ID: {vtiger_id})")
continue
# Clean CVR number
if cvr:
cvr = re.sub(r'\D', '', str(cvr))[:8] # Remove non-digits, max 8 chars
if len(cvr) != 8:
cvr = None
# Try to find existing customer by vTiger ID or CVR
existing = None
if vtiger_id:
existing = execute_query(
"SELECT id FROM customers WHERE vtiger_id = %s",
(vtiger_id,)
)
if not existing and cvr:
existing = execute_query(
"SELECT id FROM customers WHERE cvr_number = %s",
(cvr,)
)
if not existing:
# Match by normalized name
normalized = normalize_name(name)
all_customers = execute_query("SELECT id, name FROM customers")
for customer in all_customers:
if normalize_name(customer['name']) == normalized:
existing = [customer]
break
if existing:
# Update existing customer
update_fields = []
params = []
if vtiger_id:
update_fields.append("vtiger_id = %s")
params.append(vtiger_id)
if cvr:
update_fields.append("cvr_number = %s")
params.append(cvr)
if economic_customer_number:
update_fields.append("economic_customer_number = %s")
params.append(int(economic_customer_number))
update_fields.append("last_synced_at = NOW()")
if update_fields:
params.append(existing[0]['id'])
query = f"UPDATE customers SET {', '.join(update_fields)} WHERE id = %s"
execute_query(query, tuple(params))
updated_count += 1
logger.info(f"✏️ Opdateret: {name} (CVR: {cvr or 'ingen'}) - Felter: {', '.join([f.split(' = ')[0] for f in update_fields if 'last_synced' not in f])}")
else:
# Create new customer
insert_query = """
INSERT INTO customers
(name, vtiger_id, cvr_number, economic_customer_number,
email_domain, city, postal_code, country, website, last_synced_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
RETURNING id
"""
email_domain = account.get('email1', '').split('@')[-1] if '@' in account.get('email1', '') else None
city = account.get('bill_city')
postal_code = account.get('bill_code')
country = account.get('bill_country') or 'DK'
website = account.get('website1')
result = execute_query(insert_query, (
name,
vtiger_id,
cvr,
int(economic_customer_number) if economic_customer_number else None,
email_domain,
city,
postal_code,
country,
website
))
if result:
created_count += 1
logger.info(f"✨ Oprettet: {name} (CVR: {cvr or 'ingen'}, By: {city or 'ukendt'})")
logger.info(f"✅ vTiger sync fuldført: {created_count} oprettet, {updated_count} opdateret, {skipped_count} sprunget over af {len(accounts)} totalt")
return {
"status": "success",
"created": created_count,
"updated": updated_count,
"skipped": skipped_count,
"total_processed": len(accounts)
}
except Exception as e:
logger.error(f"❌ vTiger sync error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/vtiger-contacts")
async def sync_vtiger_contacts() -> Dict[str, Any]:
"""
Sync contacts from vTiger Contacts module
Links to existing Hub customers by Account ID
"""
try:
logger.info("🔄 Starting vTiger contacts sync...")
vtiger = get_vtiger_service()
# Query vTiger for contacts
query = "SELECT id, firstname, lastname, email, phone, mobile, title, department, account_id FROM Contacts LIMIT 1000;"
contacts = await vtiger.query(query)
logger.info(f"📥 Fetched {len(contacts)} contacts from vTiger")
created_count = 0
updated_count = 0
skipped_count = 0
for contact in contacts:
vtiger_contact_id = contact.get('id')
first_name = contact.get('firstname', '').strip()
last_name = contact.get('lastname', '').strip()
if not (first_name or last_name):
skipped_count += 1
logger.debug(f"⏭️ Sprunget over: Intet navn (ID: {vtiger_contact_id})")
continue
# Find existing contact by vTiger ID
existing = None
if vtiger_contact_id:
existing = execute_query(
"SELECT id FROM contacts WHERE vtiger_id = %s",
(vtiger_contact_id,)
)
contact_data = {
'first_name': first_name,
'last_name': last_name,
'email': contact.get('email'),
'phone': contact.get('phone'),
'mobile': contact.get('mobile'),
'title': contact.get('title'),
'department': contact.get('department'),
'vtiger_id': vtiger_contact_id
}
if existing:
# Update existing contact
update_query = """
UPDATE contacts
SET first_name = %s, last_name = %s, email = %s, phone = %s,
mobile = %s, title = %s, department = %s, updated_at = NOW()
WHERE id = %s
"""
execute_query(update_query, (
contact_data['first_name'],
contact_data['last_name'],
contact_data['email'],
contact_data['phone'],
contact_data['mobile'],
contact_data['title'],
contact_data['department'],
existing[0]['id']
))
updated_count += 1
contact_id = existing[0]['id']
logger.info(f"✏️ Opdateret kontakt: {first_name} {last_name} (Email: {contact_data['email'] or 'ingen'})")
else:
# Create new contact
insert_query = """
INSERT INTO contacts
(first_name, last_name, email, phone, mobile, title, department, vtiger_id)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
"""
result = execute_query(insert_query, (
contact_data['first_name'],
contact_data['last_name'],
contact_data['email'],
contact_data['phone'],
contact_data['mobile'],
contact_data['title'],
contact_data['department'],
contact_data['vtiger_id']
))
if result:
created_count += 1
contact_id = result[0]['id']
logger.info(f"✨ Oprettet kontakt: {first_name} {last_name} (Email: {contact_data['email'] or 'ingen'})")
else:
skipped_count += 1
logger.warning(f"⚠️ Kunne ikke oprette kontakt: {first_name} {last_name}")
continue
# Link contact to customer if account_id exists
account_id = contact.get('account_id')
if account_id and contact_id:
# Find customer by vTiger account ID
customer = execute_query(
"SELECT id FROM customers WHERE vtiger_id = %s",
(account_id,)
)
if customer:
customer_name_result = execute_query("SELECT name FROM customers WHERE id = %s", (customer[0]['id'],))
customer_name = customer_name_result[0]['name'] if customer_name_result else 'ukendt'
# Check if relationship exists
existing_rel = execute_query(
"SELECT id FROM contact_companies WHERE contact_id = %s AND customer_id = %s",
(contact_id, customer[0]['id'])
)
if not existing_rel:
# Create relationship
execute_query(
"INSERT INTO contact_companies (contact_id, customer_id, is_primary) VALUES (%s, %s, false)",
(contact_id, customer[0]['id'])
)
logger.info(f"🔗 Linket kontakt {first_name} {last_name} til firma: {customer_name}")
logger.info(f"✅ vTiger kontakt sync fuldført: {created_count} oprettet, {updated_count} opdateret, {skipped_count} sprunget over af {len(contacts)} totalt")
return {
"status": "success",
"created": created_count,
"updated": updated_count,
"skipped": skipped_count,
"total_processed": len(contacts)
}
except Exception as e:
logger.error(f"❌ vTiger contacts sync error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/economic")
async def sync_from_economic() -> Dict[str, Any]:
"""
Sync customer numbers from e-conomic
Matches Hub customers to e-conomic by CVR number or normalized name
"""
try:
logger.info("🔄 Starting e-conomic sync...")
from app.services.economic_service import EconomicService
economic = EconomicService()
# Get all customers from e-conomic (max 1000 per page)
all_customers = []
page = 0
while True:
customers = await economic.get_customers(page=page, page_size=1000)
if not customers:
break
all_customers.extend(customers)
page += 1
if len(customers) < 1000: # Last page
break
economic_customers = all_customers
logger.info(f"📥 Fetched {len(economic_customers)} customers from e-conomic ({page} pages)")
matched_count = 0
not_matched_count = 0
for eco_customer in economic_customers:
customer_number = eco_customer.get('customerNumber')
cvr = eco_customer.get('corporateIdentificationNumber')
name = eco_customer.get('name', '')
if not customer_number:
continue
# Clean CVR
if cvr:
cvr = re.sub(r'\D', '', str(cvr))[:8]
if len(cvr) != 8:
cvr = None
# Try to match by CVR first
matched = None
if cvr:
matched = execute_query(
"SELECT id, name FROM customers WHERE cvr_number = %s",
(cvr,)
)
# If no CVR match, try normalized name (only for customers without economic number)
if not matched and name:
normalized = normalize_name(name)
hub_customers = execute_query("SELECT id, name, economic_customer_number FROM customers WHERE economic_customer_number IS NULL")
for hub_customer in hub_customers:
if normalize_name(hub_customer['name']) == normalized:
matched = [hub_customer]
break
if matched:
# Update Hub customer with e-conomic number
current_number = matched[0].get('economic_customer_number')
if current_number is None:
execute_query(
"UPDATE customers SET economic_customer_number = %s, last_synced_at = NOW() WHERE id = %s",
(customer_number, matched[0]['id'])
)
matched_count += 1
logger.info(f"🔗 Matchet: {matched[0]['name']} → e-conomic kunde #{customer_number} (CVR: {cvr or 'navn-match'})")
else:
# Already has number, just update sync timestamp
execute_query("UPDATE customers SET last_synced_at = NOW() WHERE id = %s", (matched[0]['id'],))
logger.debug(f"✓ Verificeret: {matched[0]['name']} → #{customer_number}")
else:
not_matched_count += 1
logger.debug(f"❌ Ikke matchet: {name} (CVR: {cvr or 'ingen'})")
logger.info(f"✅ e-conomic sync fuldført: {matched_count} matchet, {not_matched_count} ikke matchet af {len(economic_customers)} totalt")
return {
"status": "success",
"matched": matched_count,
"not_matched": not_matched_count,
"total_processed": len(economic_customers)
}
except Exception as e:
logger.error(f"❌ e-conomic sync error: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/cvr-to-economic")
async def sync_cvr_to_economic() -> Dict[str, Any]:
"""
Find customers in Hub with CVR but without e-conomic customer number
Search e-conomic for matching CVR and update Hub
"""
try:
logger.info("🔄 Starting CVR to e-conomic sync...")
from app.services.economic_service import EconomicService
economic = EconomicService()
# Find customers with CVR but no economic_customer_number
customers = execute_query("""
SELECT id, name, cvr_number
FROM customers
WHERE cvr_number IS NOT NULL
AND cvr_number != ''
AND economic_customer_number IS NULL
LIMIT 100
""")
logger.info(f"📥 Found {len(customers)} customers with CVR but no e-conomic number")
found_count = 0
linked_count = 0
for customer in customers:
cvr = customer['cvr_number']
# Search e-conomic for this CVR
eco_customer = await economic.search_customer_by_cvr(cvr)
if eco_customer:
customer_number = eco_customer.get('customerNumber')
if customer_number:
# Update Hub customer
execute_query(
"UPDATE customers SET economic_customer_number = %s, last_synced_at = NOW() WHERE id = %s",
(customer_number, customer['id'])
)
found_count += 1
linked_count += 1
logger.info(f"✅ Fundet og linket: {customer['name']} (CVR: {cvr}) → e-conomic kunde #{customer_number}")
else:
found_count += 1
logger.warning(f"⚠️ Fundet men mangler kundenummer: {customer['name']} (CVR: {cvr})")
logger.info(f"✅ CVR søgning fuldført: {found_count} fundet, {linked_count} linket af {len(customers)} kontrolleret")
return {
"status": "not_implemented",
"message": "Requires e-conomic API search by CVR functionality",
"found": 0,
"candidates": len(customers)
}
except Exception as e:
logger.error(f"❌ CVR to e-conomic sync error: {e}")
raise HTTPException(status_code=500, detail=str(e))