2025-12-19 13:09:42 +01:00
"""
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 ] :
"""
2025-12-22 13:02:24 +01:00
Link vTiger accounts to existing Hub customers
Matches by CVR or normalized name , updates vtiger_id
2025-12-19 13:09:42 +01:00
"""
try :
2025-12-22 13:02:24 +01:00
logger . info ( " 🔄 Starting vTiger link sync... " )
2025-12-19 13:09:42 +01:00
vtiger = get_vtiger_service ( )
2025-12-22 12:59:12 +01:00
# Fetch ALL accounts - vTiger query API doesn't support LIMIT/OFFSET
# Instead, use recursive queries based on ID to get all records
2025-12-22 12:53:11 +01:00
all_accounts = [ ]
2025-12-22 12:59:12 +01:00
last_id = None
batch_size = 100 # vTiger typically returns ~100 records per query
2025-12-19 13:09:42 +01:00
2025-12-22 12:53:11 +01:00
while True :
2025-12-22 12:59:12 +01:00
# 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 } ; "
2025-12-22 12:53:11 +01:00
batch = await vtiger . query ( query )
if not batch or len ( batch ) == 0 :
break
all_accounts . extend ( batch )
2025-12-22 12:59:12 +01:00
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 ) } ) " )
2025-12-22 12:53:11 +01:00
# 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
2025-12-19 13:09:42 +01:00
2025-12-22 13:02:24 +01:00
linked_count = 0
2025-12-19 13:09:42 +01:00
updated_count = 0
2025-12-22 13:02:24 +01:00
not_found_count = 0
2025-12-19 13:09:42 +01:00
for account in accounts :
vtiger_id = account . get ( ' id ' )
name = account . get ( ' accountname ' , ' ' ) . strip ( )
cvr = account . get ( ' cf_accounts_cvr ' ) or account . get ( ' siccode ' )
2025-12-22 13:02:24 +01:00
if not name or not vtiger_id :
not_found_count + = 1
2025-12-19 13:09:42 +01:00
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
2025-12-22 13:02:24 +01:00
# Find existing Hub customer by CVR or normalized name
2025-12-19 13:09:42 +01:00
existing = None
2025-12-22 14:13:44 +01:00
match_method = None
2025-12-22 13:02:24 +01:00
if cvr :
2025-12-19 13:09:42 +01:00
existing = execute_query (
2025-12-22 13:02:24 +01:00
" SELECT id, name, vtiger_id FROM customers WHERE cvr_number = %s " ,
2025-12-19 13:09:42 +01:00
( cvr , )
)
2025-12-22 14:13:44 +01:00
if existing :
match_method = " CVR "
2025-12-19 13:09:42 +01:00
if not existing :
# Match by normalized name
normalized = normalize_name ( name )
2025-12-22 13:02:24 +01:00
all_customers = execute_query ( " SELECT id, name, vtiger_id FROM customers " )
2025-12-19 13:09:42 +01:00
for customer in all_customers :
if normalize_name ( customer [ ' name ' ] ) == normalized :
existing = [ customer ]
2025-12-22 14:13:44 +01:00
match_method = " navn "
2025-12-19 13:09:42 +01:00
break
if existing :
2025-12-22 13:02:24 +01:00
# Link vTiger ID to existing customer
current_vtiger_id = existing [ 0 ] . get ( ' vtiger_id ' )
2025-12-22 14:15:17 +01:00
# 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
2025-12-22 13:02:24 +01:00
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
2025-12-22 14:13:44 +01:00
logger . info ( f " 🔗 Linket: { existing [ 0 ] [ ' name ' ] } → vTiger # { vtiger_id } (match: { match_method } , CVR: { cvr or ' ingen ' } ) " )
2025-12-22 13:02:24 +01:00
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 ' ] )
)
2025-12-19 13:09:42 +01:00
updated_count + = 1
2025-12-22 13:02:24 +01:00
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 ' ] , ) )
2025-12-19 13:09:42 +01:00
else :
2025-12-22 13:02:24 +01:00
not_found_count + = 1
2025-12-22 14:13:44 +01:00
logger . debug ( f " ⏭️ Ikke fundet i Hub: ' { name } ' (CVR: { cvr or ' ingen ' } , normalized: ' { normalize_name ( name ) } ' ) " )
2025-12-19 13:09:42 +01:00
2025-12-22 13:02:24 +01:00
logger . info ( f " ✅ vTiger link sync fuldført: { linked_count } nye linket, { updated_count } opdateret, { not_found_count } ikke fundet af { len ( accounts ) } totalt " )
2025-12-19 13:09:42 +01:00
return {
" status " : " success " ,
2025-12-22 13:02:24 +01:00
" linked " : linked_count ,
2025-12-19 13:09:42 +01:00
" updated " : updated_count ,
2025-12-22 13:02:24 +01:00
" not_found " : not_found_count ,
2025-12-19 13:09:42 +01:00
" 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 ] :
"""
2025-12-22 15:05:40 +01:00
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
2025-12-19 13:09:42 +01:00
"""
try :
2025-12-22 15:05:40 +01:00
logger . info ( " 🔄 Starting vTiger contacts sync (SIMPEL VERSION)... " )
2025-12-19 13:09:42 +01:00
vtiger = get_vtiger_service ( )
2025-12-22 15:05:40 +01:00
# ===== STEP 1: FETCH ALL CONTACTS =====
logger . info ( " 📥 STEP 1: Fetching contacts from vTiger... " )
2025-12-22 13:24:41 +01:00
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 ' )
2025-12-22 15:05:40 +01:00
logger . info ( f " → Batch: { len ( batch ) } contacts (ID: { last_id } , total: { len ( all_contacts ) } ) " )
2025-12-22 13:24:41 +01:00
if len ( batch ) < batch_size :
break
2025-12-19 13:09:42 +01:00
2025-12-22 15:05:40 +01:00
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... " )
2025-12-19 13:09:42 +01:00
created_count = 0
updated_count = 0
skipped_count = 0
2025-12-22 13:24:41 +01:00
linked_count = 0
2025-12-22 15:20:29 +01:00
customers_created_count = 0
2025-12-22 15:05:40 +01:00
debug_count = 0
2025-12-19 13:09:42 +01:00
2025-12-22 15:05:40 +01:00
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... " )
2025-12-19 13:09:42 +01:00
2025-12-22 15:05:40 +01:00
# Must have name
if not first_name and not last_name :
2025-12-19 13:09:42 +01:00
skipped_count + = 1
continue
2025-12-22 15:05:40 +01:00
# --- STEP 2A: Check if contact exists ---
existing = execute_query (
" SELECT id FROM contacts WHERE vtiger_id = %s " ,
( vtiger_id , )
) if vtiger_id else None
2025-12-19 13:09:42 +01:00
2025-12-22 15:05:40 +01:00
# --- STEP 2B: Create or update contact ---
contact_id = None
2025-12-19 13:09:42 +01:00
if existing :
2025-12-22 15:05:40 +01:00
# UPDATE existing
execute_query ( """
2025-12-19 13:09:42 +01:00
UPDATE contacts
2025-12-22 15:05:40 +01:00
SET first_name = % s , last_name = % s , email = % s ,
phone = % s , mobile = % s , title = % s , department = % s ,
updated_at = NOW ( )
2025-12-19 13:09:42 +01:00
WHERE id = % s
2025-12-22 15:05:40 +01:00
""" , (
first_name , last_name , email ,
contact . get ( ' phone ' ) , contact . get ( ' mobile ' ) ,
contact . get ( ' title ' ) , contact . get ( ' department ' ) ,
2025-12-19 13:09:42 +01:00
existing [ 0 ] [ ' id ' ]
) )
contact_id = existing [ 0 ] [ ' id ' ]
2025-12-22 15:05:40 +01:00
updated_count + = 1
2025-12-19 13:09:42 +01:00
else :
2025-12-22 15:05:40 +01:00
# CREATE new
result = execute_query ( """
2025-12-19 13:09:42 +01:00
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
2025-12-22 15:05:40 +01:00
""" , (
first_name , last_name , email ,
contact . get ( ' phone ' ) , contact . get ( ' mobile ' ) ,
contact . get ( ' title ' ) , contact . get ( ' department ' ) ,
vtiger_id
2025-12-19 13:09:42 +01:00
) )
2025-12-22 15:05:40 +01:00
if result and len ( result ) > 0 :
2025-12-19 13:09:42 +01:00
contact_id = result [ 0 ] [ ' id ' ]
2025-12-22 15:05:40 +01:00
created_count + = 1
2025-12-19 13:09:42 +01:00
else :
skipped_count + = 1
continue
2025-12-22 15:05:40 +01:00
# --- 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
2025-12-22 14:34:07 +01:00
2025-12-22 15:05:40 +01:00
# --- STEP 3: LINK TO CUSTOMER ---
2025-12-22 14:41:44 +01:00
if not account_id :
2025-12-22 15:05:40 +01:00
# 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 " )
2025-12-22 14:41:44 +01:00
continue
2025-12-22 15:05:40 +01:00
# Find Hub customer with matching vtiger_id
customer_result = execute_query (
2025-12-22 14:41:44 +01:00
" SELECT id, name FROM customers WHERE vtiger_id = %s " ,
( account_id , )
)
2025-12-22 15:05:40 +01:00
if not customer_result or len ( customer_result ) == 0 :
2025-12-22 15:20:29 +01:00
# FALLBACK: Create customer from vTiger account if not found
try :
account_query = f " SELECT id, accountname, email1, siccode, cf_accounts_cvr, website, bill_city, bill_code, bill_country FROM Accounts WHERE id = ' { account_id } ' ; "
account_data = await vtiger . query ( account_query )
if account_data and len ( account_data ) > 0 :
account = account_data [ 0 ]
account_name = account . get ( ' accountname ' , ' ' ) . strip ( )
cvr = account . get ( ' cf_accounts_cvr ' ) or account . get ( ' siccode ' )
if cvr :
cvr = re . sub ( r ' \ D ' , ' ' , str ( cvr ) ) [ : 8 ]
if len ( cvr ) != 8 :
cvr = None
# Check if customer already exists by CVR (to avoid duplicates)
if cvr :
existing_by_cvr = execute_query (
" SELECT id, name FROM customers WHERE cvr_number = %s " ,
( cvr , )
)
if existing_by_cvr :
# Found by CVR - update vtiger_id and use this customer
execute_query (
" UPDATE customers SET vtiger_id = %s , last_synced_at = NOW() WHERE id = %s " ,
( account_id , existing_by_cvr [ 0 ] [ ' id ' ] )
)
customer_id = existing_by_cvr [ 0 ] [ ' id ' ]
customer_name = existing_by_cvr [ 0 ] [ ' name ' ]
logger . info ( f " ✅ Matched by CVR and updated vtiger_id: { customer_name } → { account_id } " )
else :
# Create new customer from vTiger
country = ( account . get ( ' bill_country ' ) or ' DK ' ) . strip ( ) . upper ( ) [ : 2 ]
new_customer = execute_query ( """
INSERT INTO customers
( name , cvr_number , email , website , city , postal_code , country , vtiger_id , last_synced_at )
VALUES ( % s , % s , % s , % s , % s , % s , % s , % s , NOW ( ) )
RETURNING id , name
""" , (
account_name ,
cvr ,
account . get ( ' email1 ' ) ,
account . get ( ' website ' ) ,
account . get ( ' bill_city ' ) ,
account . get ( ' bill_code ' ) ,
country ,
account_id
) )
if new_customer :
customer_id = new_customer [ 0 ] [ ' id ' ]
customer_name = new_customer [ 0 ] [ ' name ' ]
customers_created_count + = 1
logger . info ( f " ✨ Created customer from vTiger: { customer_name } (vtiger_id= { account_id } ) " )
else :
if debug_count < = 20 :
logger . warning ( f " ↳ Failed to create customer from account_id= { account_id } " )
continue
else :
# No CVR - create customer anyway (might be foreign company)
country = ( account . get ( ' bill_country ' ) or ' DK ' ) . strip ( ) . upper ( ) [ : 2 ]
new_customer = execute_query ( """
INSERT INTO customers
( name , email , website , city , postal_code , country , vtiger_id , last_synced_at )
VALUES ( % s , % s , % s , % s , % s , % s , % s , NOW ( ) )
RETURNING id , name
""" , (
account_name ,
account . get ( ' email1 ' ) ,
account . get ( ' website ' ) ,
account . get ( ' bill_city ' ) ,
account . get ( ' bill_code ' ) ,
country ,
account_id
) )
if new_customer :
customer_id = new_customer [ 0 ] [ ' id ' ]
customer_name = new_customer [ 0 ] [ ' name ' ]
customers_created_count + = 1
logger . info ( f " ✨ Created customer from vTiger (no CVR): { customer_name } (vtiger_id= { account_id } ) " )
else :
if debug_count < = 20 :
logger . warning ( f " ↳ Failed to create customer from account_id= { account_id } " )
continue
else :
if debug_count < = 20 :
logger . warning ( f " ↳ Account not found in vTiger: { account_id } " )
continue
except Exception as e :
logger . error ( f " ❌ Failed to fetch/create customer from vTiger account { account_id } : { e } " )
continue
else :
customer_id = customer_result [ 0 ] [ ' id ' ]
customer_name = customer_result [ 0 ] [ ' name ' ]
2025-12-22 15:05:40 +01:00
# 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 } " )
2025-12-19 13:09:42 +01:00
2025-12-22 15:20:29 +01:00
logger . info ( f " ✅ SYNC COMPLETE: created= { created_count } , updated= { updated_count } , linked= { linked_count } , customers_created= { customers_created_count } , skipped= { skipped_count } , total= { len ( all_contacts ) } " )
2025-12-19 13:09:42 +01:00
return {
" status " : " success " ,
" created " : created_count ,
" updated " : updated_count ,
2025-12-22 13:24:41 +01:00
" linked " : linked_count ,
2025-12-22 15:20:29 +01:00
" customers_created " : customers_created_count ,
2025-12-19 13:09:42 +01:00
" skipped " : skipped_count ,
2025-12-22 15:05:40 +01:00
" total_processed " : len ( all_contacts )
2025-12-19 13:09:42 +01:00
}
except Exception as e :
2025-12-22 15:05:40 +01:00
logger . error ( f " ❌ vTiger contacts sync error: { e } " , exc_info = True )
2025-12-19 13:09:42 +01:00
raise HTTPException ( status_code = 500 , detail = str ( e ) )
@router.post ( " /sync/economic " )
async def sync_from_economic ( ) - > Dict [ str , Any ] :
"""
2025-12-22 13:02:24 +01:00
Sync customers from e - conomic ( PRIMARY SOURCE )
Creates / updates Hub customers with e - conomic data
2025-12-19 13:09:42 +01:00
"""
try :
2025-12-22 13:02:24 +01:00
logger . info ( " 🔄 Starting e-conomic customer sync (PRIMARY SOURCE)... " )
2025-12-19 13:09:42 +01:00
2025-12-19 16:41:11 +01:00
from app . services . economic_service import EconomicService
economic = EconomicService ( )
2025-12-19 13:09:42 +01:00
2025-12-19 16:53:39 +01:00
# 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) " )
2025-12-19 16:41:11 +01:00
2025-12-22 13:02:24 +01:00
created_count = 0
updated_count = 0
skipped_count = 0
2025-12-19 16:41:11 +01:00
for eco_customer in economic_customers :
customer_number = eco_customer . get ( ' customerNumber ' )
cvr = eco_customer . get ( ' corporateIdentificationNumber ' )
2025-12-22 13:02:24 +01:00
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 ' , ' ' )
2025-12-19 16:41:11 +01:00
2025-12-22 13:02:24 +01:00
if not customer_number or not name :
skipped_count + = 1
2025-12-19 16:41:11 +01:00
continue
# Clean CVR
if cvr :
cvr = re . sub ( r ' \ D ' , ' ' , str ( cvr ) ) [ : 8 ]
if len ( cvr ) != 8 :
cvr = None
2025-12-22 13:17:03 +01:00
# 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 '
2025-12-22 13:02:24 +01:00
# Extract email domain
email_domain = email . split ( ' @ ' ) [ - 1 ] if ' @ ' in email else None
2025-12-19 16:41:11 +01:00
2025-12-22 13:18:36 +01:00
# Check if customer exists by economic_customer_number OR CVR
2025-12-22 13:02:24 +01:00
existing = execute_query (
" SELECT id FROM customers WHERE economic_customer_number = %s " ,
( customer_number , )
)
2025-12-19 16:41:11 +01:00
2025-12-22 13:18:36 +01:00
# 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 , )
)
2025-12-22 13:02:24 +01:00
if existing :
2025-12-23 15:22:55 +01:00
# Update existing customer (always sync economic_customer_number from e-conomic)
2025-12-22 13:02:24 +01:00
update_query = """
UPDATE customers SET
name = % s ,
2025-12-23 15:22:55 +01:00
economic_customer_number = % s ,
2025-12-22 13:02:24 +01:00
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 , (
2025-12-23 15:22:55 +01:00
name , customer_number , cvr , email_domain , city , zip_code , country , website , existing [ 0 ] [ ' id ' ]
2025-12-22 13:02:24 +01:00
) )
updated_count + = 1
logger . info ( f " ✏️ Opdateret: { name } (e-conomic # { customer_number } , CVR: { cvr or ' ingen ' } ) " )
2025-12-19 16:41:11 +01:00
else :
2025-12-22 13:02:24 +01:00
# 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
2025-12-19 16:41:11 +01:00
2025-12-22 13:02:24 +01:00
logger . info ( f " ✅ e-conomic sync fuldført: { created_count } oprettet, { updated_count } opdateret, { skipped_count } sprunget over af { len ( economic_customers ) } totalt " )
2025-12-19 13:09:42 +01:00
return {
2025-12-19 16:41:11 +01:00
" status " : " success " ,
2025-12-22 13:02:24 +01:00
" created " : created_count ,
" updated " : updated_count ,
" skipped " : skipped_count ,
2025-12-19 16:41:11 +01:00
" total_processed " : len ( economic_customers )
2025-12-19 13:09:42 +01:00
}
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... " )
2025-12-19 16:41:11 +01:00
from app . services . economic_service import EconomicService
economic = EconomicService ( )
2025-12-19 13:09:42 +01:00
# 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 " )
2025-12-19 16:41:11 +01:00
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 } ) " )
2025-12-19 13:09:42 +01:00
2025-12-19 16:41:11 +01:00
logger . info ( f " ✅ CVR søgning fuldført: { found_count } fundet, { linked_count } linket af { len ( customers ) } kontrolleret " )
2025-12-19 13:09:42 +01:00
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 ) )
2025-12-22 15:14:31 +01:00
@router.get ( " /sync/diagnostics " )
async def sync_diagnostics ( ) - > Dict [ str , Any ] :
"""
Diagnostics : Check contact linking coverage
Shows why contacts aren ' t linking to customers
"""
try :
logger . info ( " 🔍 Running sync diagnostics... " )
# Check Hub data
hub_stats = execute_query ( """
SELECT
COUNT ( * ) as total_customers ,
COUNT ( vtiger_id ) as customers_with_vtiger
FROM customers
""" )
vtiger_ids_in_hub = execute_query (
" SELECT vtiger_id FROM customers WHERE vtiger_id IS NOT NULL "
)
hub_vtiger_set = set ( row [ ' vtiger_id ' ] for row in vtiger_ids_in_hub )
contact_stats = execute_query ( """
SELECT
COUNT ( * ) as total_contacts ,
COUNT ( vtiger_id ) as contacts_with_vtiger
FROM contacts
""" )
link_stats = execute_query ( """
SELECT COUNT ( * ) as total_links
FROM contact_companies
""" )
# Fetch sample contacts from vTiger
vtiger = get_vtiger_service ( )
query = " SELECT id, firstname, lastname, account_id FROM Contacts ORDER BY id LIMIT 200; "
contacts = await vtiger . query ( query )
# Analyze
with_account = [ c for c in contacts if c . get ( ' account_id ' ) ]
account_ids = set ( c [ ' account_id ' ] for c in with_account )
matched_accounts = [ aid for aid in account_ids if aid in hub_vtiger_set ]
unmatched_accounts = [ aid for aid in account_ids if aid not in hub_vtiger_set ]
result = {
" hub " : {
" total_customers " : hub_stats [ 0 ] [ ' total_customers ' ] ,
" customers_with_vtiger_id " : hub_stats [ 0 ] [ ' customers_with_vtiger ' ] ,
" unique_vtiger_ids " : len ( hub_vtiger_set ) ,
" total_contacts " : contact_stats [ 0 ] [ ' total_contacts ' ] ,
" contacts_with_vtiger_id " : contact_stats [ 0 ] [ ' contacts_with_vtiger ' ] ,
" total_links " : link_stats [ 0 ] [ ' total_links ' ]
} ,
" vtiger_sample " : {
" sample_size " : len ( contacts ) ,
" contacts_with_account_id " : len ( with_account ) ,
" unique_account_ids " : len ( account_ids ) ,
" account_ids_found_in_hub " : len ( matched_accounts ) ,
" account_ids_missing_in_hub " : len ( unmatched_accounts ) ,
" match_rate_percent " : round ( ( len ( matched_accounts ) / len ( account_ids ) * 100 ) , 1 ) if account_ids else 0
} ,
" examples " : {
" matched_account_ids " : matched_accounts [ : 5 ] ,
" unmatched_account_ids " : unmatched_accounts [ : 10 ]
} ,
" recommendation " : " "
}
# Add recommendation
match_rate = result [ ' vtiger_sample ' ] [ ' match_rate_percent ' ]
if match_rate < 50 :
result [ ' recommendation ' ] = f " ⚠️ LOW MATCH RATE ( { match_rate } %) - Kør først /sync/vtiger for at linke flere accounts "
elif match_rate < 90 :
result [ ' recommendation ' ] = f " ⚡ MEDIUM MATCH RATE ( { match_rate } %) - Nogen accounts mangler stadig at blive linket "
else :
result [ ' recommendation ' ] = f " ✅ HIGH MATCH RATE ( { match_rate } %) - De fleste contacts kan linkes "
logger . info ( f " ✅ Diagnostics complete: { result [ ' recommendation ' ] } " )
return result
except Exception as e :
logger . error ( f " ❌ Diagnostics error: { e } " , exc_info = True )
raise HTTPException ( status_code = 500 , detail = str ( e ) )