""" 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...") # Note: This requires adding get_customers() method to economic_service.py # For now, return a placeholder response logger.warning("⚠️ e-conomic sync not fully implemented yet") return { "status": "not_implemented", "message": "e-conomic customer sync requires get_customers() method in economic_service.py", "matched": 0 } 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...") # 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") # Note: This requires e-conomic API search functionality # For now, return placeholder logger.warning("⚠️ CVR to e-conomic sync not fully implemented yet") 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))