""" 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]: """ Link vTiger accounts to existing Hub customers Matches by CVR or normalized name, updates vtiger_id """ try: logger.info("๐Ÿ”„ Starting vTiger link sync...") vtiger = get_vtiger_service() # Fetch ALL accounts - vTiger query API doesn't support LIMIT/OFFSET # Instead, use recursive queries based on ID to get all records all_accounts = [] last_id = None batch_size = 100 # vTiger typically returns ~100 records per query while True: # Build query with ID filter to paginate if last_id is None: query = f"SELECT id, accountname, email1, siccode, cf_accounts_cvr, website, bill_city, bill_code, bill_country FROM Accounts ORDER BY id LIMIT {batch_size};" else: # Get records with ID > last_id to continue pagination query = f"SELECT id, accountname, email1, siccode, cf_accounts_cvr, website, bill_city, bill_code, bill_country FROM Accounts WHERE id > '{last_id}' ORDER BY id LIMIT {batch_size};" batch = await vtiger.query(query) if not batch or len(batch) == 0: break all_accounts.extend(batch) last_id = batch[-1].get('id') # Track last ID for next query logger.info(f"๐Ÿ“ฅ Fetched batch: {len(batch)} accounts (last ID: {last_id}, total: {len(all_accounts)})") # If we got less than batch_size, we've reached the end if len(batch) < batch_size: break logger.info(f"๐Ÿ“ฅ Fetched total of {len(all_accounts)} accounts from vTiger") accounts = all_accounts linked_count = 0 updated_count = 0 not_found_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') if not name or not vtiger_id: not_found_count += 1 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 # Find existing Hub customer by CVR or normalized name existing = None match_method = None if cvr: existing = execute_query( "SELECT id, name, vtiger_id FROM customers WHERE cvr_number = %s", (cvr,) ) if existing: match_method = "CVR" if not existing: # Match by normalized name normalized = normalize_name(name) all_customers = execute_query("SELECT id, name, vtiger_id FROM customers") for customer in all_customers: if normalize_name(customer['name']) == normalized: existing = [customer] match_method = "navn" break if existing: # Link vTiger ID to existing customer current_vtiger_id = existing[0].get('vtiger_id') # Check if this vtiger_id is already assigned to another customer vtiger_owner = execute_query( "SELECT id, name FROM customers WHERE vtiger_id = %s", (vtiger_id,) ) if vtiger_owner and vtiger_owner[0]['id'] != existing[0]['id']: # vTiger ID already belongs to another customer - skip logger.warning(f"โš ๏ธ vTiger #{vtiger_id} allerede tilknyttet '{vtiger_owner[0]['name']}' - springer over '{existing[0]['name']}'") not_found_count += 1 continue if current_vtiger_id is None: execute_query( "UPDATE customers SET vtiger_id = %s, last_synced_at = NOW() WHERE id = %s", (vtiger_id, existing[0]['id']) ) linked_count += 1 logger.info(f"๐Ÿ”— Linket: {existing[0]['name']} โ†’ vTiger #{vtiger_id} (match: {match_method}, CVR: {cvr or 'ingen'})") elif current_vtiger_id != vtiger_id: # Update if different vTiger ID execute_query( "UPDATE customers SET vtiger_id = %s, last_synced_at = NOW() WHERE id = %s", (vtiger_id, existing[0]['id']) ) updated_count += 1 logger.info(f"โœ๏ธ Opdateret vTiger ID: {existing[0]['name']} โ†’ {vtiger_id} (var: {current_vtiger_id})") else: # Already linked, just update timestamp execute_query("UPDATE customers SET last_synced_at = NOW() WHERE id = %s", (existing[0]['id'],)) else: not_found_count += 1 logger.debug(f"โญ๏ธ Ikke fundet i Hub: '{name}' (CVR: {cvr or 'ingen'}, normalized: '{normalize_name(name)}')") logger.info(f"โœ… vTiger link sync fuldfรธrt: {linked_count} nye linket, {updated_count} opdateret, {not_found_count} ikke fundet af {len(accounts)} totalt") return { "status": "success", "linked": linked_count, "updated": updated_count, "not_found": not_found_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]: """ SIMPEL TILGANG - Sync contacts from vTiger and link to customers Step 1: Fetch all contacts from vTiger Step 2: Create/update contact in Hub Step 3: Link to customer IF account_id matches customer.vtiger_id """ try: logger.info("๐Ÿ”„ Starting vTiger contacts sync (SIMPEL VERSION)...") vtiger = get_vtiger_service() # ===== STEP 1: FETCH ALL CONTACTS ===== logger.info("๐Ÿ“ฅ STEP 1: Fetching contacts from vTiger...") all_contacts = [] last_id = None batch_size = 100 while True: if last_id is None: query = f"SELECT id, firstname, lastname, email, phone, mobile, title, department, account_id FROM Contacts ORDER BY id LIMIT {batch_size};" else: query = f"SELECT id, firstname, lastname, email, phone, mobile, title, department, account_id FROM Contacts WHERE id > '{last_id}' ORDER BY id LIMIT {batch_size};" batch = await vtiger.query(query) if not batch or len(batch) == 0: break all_contacts.extend(batch) last_id = batch[-1].get('id') logger.info(f" โ†’ Batch: {len(batch)} contacts (ID: {last_id}, total: {len(all_contacts)})") if len(batch) < batch_size: break logger.info(f"โœ… STEP 1 DONE: {len(all_contacts)} contacts fetched") # ===== STEP 2 & 3: PROCESS EACH CONTACT ===== logger.info("๐Ÿ”„ STEP 2 & 3: Creating/updating contacts and linking to customers...") created_count = 0 updated_count = 0 skipped_count = 0 linked_count = 0 debug_count = 0 for i, contact in enumerate(all_contacts, 1): vtiger_id = contact.get('id') first_name = (contact.get('firstname') or '').strip() last_name = (contact.get('lastname') or '').strip() email = contact.get('email') account_id = contact.get('account_id') # This is the vTiger Account ID # Show progress every 100 contacts if i % 100 == 0: logger.info(f" โ†’ Progress: {i}/{len(all_contacts)} contacts processed...") # Must have name if not first_name and not last_name: skipped_count += 1 continue # --- STEP 2A: Check if contact exists --- existing = execute_query( "SELECT id FROM contacts WHERE vtiger_id = %s", (vtiger_id,) ) if vtiger_id else None # --- STEP 2B: Create or update contact --- contact_id = None if existing: # UPDATE existing execute_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 """, ( first_name, last_name, email, contact.get('phone'), contact.get('mobile'), contact.get('title'), contact.get('department'), existing[0]['id'] )) contact_id = existing[0]['id'] updated_count += 1 else: # CREATE new result = execute_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 """, ( first_name, last_name, email, contact.get('phone'), contact.get('mobile'), contact.get('title'), contact.get('department'), vtiger_id )) if result and len(result) > 0: contact_id = result[0]['id'] created_count += 1 else: skipped_count += 1 continue # --- DEBUG LOGGING FOR FIRST 20 CONTACTS --- if debug_count < 20: logger.warning(f"๐Ÿ” DEBUG {debug_count+1}: Contact '{first_name} {last_name}' (vtiger_id={vtiger_id}, contact_id={contact_id}, account_id={account_id})") debug_count += 1 # --- STEP 3: LINK TO CUSTOMER --- if not account_id: # No account_id means contact is not linked to any account in vTiger if debug_count <= 20: logger.warning(f" โ†ณ No account_id - cannot link") continue # Find Hub customer with matching vtiger_id customer_result = execute_query( "SELECT id, name FROM customers WHERE vtiger_id = %s", (account_id,) ) if not customer_result or len(customer_result) == 0: # No matching customer found if debug_count <= 20: logger.warning(f" โ†ณ No customer found with vtiger_id={account_id}") continue customer_id = customer_result[0]['id'] customer_name = customer_result[0]['name'] # Check if link already exists existing_link = execute_query( "SELECT id FROM contact_companies WHERE contact_id = %s AND customer_id = %s", (contact_id, customer_id) ) if existing_link: # Already linked if debug_count <= 20: logger.warning(f" โ†ณ Already linked to '{customer_name}'") continue # CREATE LINK execute_query( "INSERT INTO contact_companies (contact_id, customer_id, is_primary) VALUES (%s, %s, false)", (contact_id, customer_id) ) linked_count += 1 if linked_count <= 10: # Log first 10 successful links logger.info(f"๐Ÿ”— LINKED: {first_name} {last_name} โ†’ {customer_name}") logger.info(f"โœ… SYNC COMPLETE: created={created_count}, updated={updated_count}, linked={linked_count}, skipped={skipped_count}, total={len(all_contacts)}") return { "status": "success", "created": created_count, "updated": updated_count, "linked": linked_count, "skipped": skipped_count, "total_processed": len(all_contacts) } except Exception as e: logger.error(f"โŒ vTiger contacts sync error: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) @router.post("/sync/economic") async def sync_from_economic() -> Dict[str, Any]: """ Sync customers from e-conomic (PRIMARY SOURCE) Creates/updates Hub customers with e-conomic data """ try: logger.info("๐Ÿ”„ Starting e-conomic customer sync (PRIMARY SOURCE)...") 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)") created_count = 0 updated_count = 0 skipped_count = 0 for eco_customer in economic_customers: customer_number = eco_customer.get('customerNumber') cvr = eco_customer.get('corporateIdentificationNumber') name = eco_customer.get('name', '').strip() address = eco_customer.get('address', '') city = eco_customer.get('city', '') zip_code = eco_customer.get('zip', '') country = eco_customer.get('country', 'DK') email = eco_customer.get('email', '') website = eco_customer.get('website', '') if not customer_number or not name: skipped_count += 1 continue # Clean CVR if cvr: cvr = re.sub(r'\D', '', str(cvr))[:8] if len(cvr) != 8: cvr = None # Clean country code (max 2 chars for ISO codes) if country: country = str(country).strip().upper()[:2] if not country: country = 'DK' else: country = 'DK' # Extract email domain email_domain = email.split('@')[-1] if '@' in email else None # Check if customer exists by economic_customer_number OR CVR existing = execute_query( "SELECT id FROM customers WHERE economic_customer_number = %s", (customer_number,) ) # If not found by customer number, try CVR (to avoid duplicates) if not existing and cvr: existing = execute_query( "SELECT id FROM customers WHERE cvr_number = %s", (cvr,) ) if existing: # Update existing customer update_query = """ UPDATE customers SET name = %s, cvr_number = %s, email_domain = %s, city = %s, postal_code = %s, country = %s, website = %s, last_synced_at = NOW() WHERE id = %s """ execute_query(update_query, ( name, cvr, email_domain, city, zip_code, country, website, existing[0]['id'] )) updated_count += 1 logger.info(f"โœ๏ธ Opdateret: {name} (e-conomic #{customer_number}, CVR: {cvr or 'ingen'})") else: # Create new customer from e-conomic insert_query = """ INSERT INTO customers (name, economic_customer_number, cvr_number, email_domain, city, postal_code, country, website, last_synced_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW()) RETURNING id """ result = execute_query(insert_query, ( name, customer_number, cvr, email_domain, city, zip_code, country, website )) if result: created_count += 1 logger.info(f"โœจ Oprettet: {name} (e-conomic #{customer_number}, CVR: {cvr or 'ingen'})") else: skipped_count += 1 logger.info(f"โœ… e-conomic sync fuldfรธrt: {created_count} oprettet, {updated_count} opdateret, {skipped_count} sprunget over af {len(economic_customers)} totalt") return { "status": "success", "created": created_count, "updated": updated_count, "skipped": skipped_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))