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 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 , )
)
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 ]
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 ' )
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 } (CVR: { cvr or ' navn-match ' } ) " )
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
logger . debug ( f " ⏭️ Ikke fundet i Hub: { name } (CVR: { cvr or ' ingen ' } ) " )
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 ] :
"""
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 ( )
2025-12-22 11:39:07 +01:00
# Query vTiger for all contacts (no limit)
query = " SELECT id, firstname, lastname, email, phone, mobile, title, department, account_id FROM Contacts; "
2025-12-19 13:09:42 +01:00
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
2025-12-19 16:36:41 +01:00
logger . debug ( f " ⏭️ Sprunget over: Intet navn (ID: { vtiger_contact_id } ) " )
2025-12-19 13:09:42 +01:00
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 ' ]
2025-12-19 16:36:41 +01:00
logger . info ( f " ✏️ Opdateret kontakt: { first_name } { last_name } (Email: { contact_data [ ' email ' ] or ' ingen ' } ) " )
2025-12-19 13:09:42 +01:00
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 ' ]
2025-12-19 16:36:41 +01:00
logger . info ( f " ✨ Oprettet kontakt: { first_name } { last_name } (Email: { contact_data [ ' email ' ] or ' ingen ' } ) " )
2025-12-19 13:09:42 +01:00
else :
skipped_count + = 1
2025-12-19 16:36:41 +01:00
logger . warning ( f " ⚠️ Kunne ikke oprette kontakt: { first_name } { last_name } " )
2025-12-19 13:09:42 +01:00
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 :
2025-12-19 16:36:41 +01:00
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 '
2025-12-19 13:09:42 +01:00
# 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 ' ] )
)
2025-12-19 16:36:41 +01:00
logger . info ( f " 🔗 Linket kontakt { first_name } { last_name } til firma: { customer_name } " )
2025-12-19 13:09:42 +01:00
2025-12-19 16:36:41 +01:00
logger . info ( f " ✅ vTiger kontakt sync fuldført: { created_count } oprettet, { updated_count } opdateret, { skipped_count } sprunget over af { len ( contacts ) } totalt " )
2025-12-19 13:09:42 +01:00
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 ] :
"""
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 :
# 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 ' } ) " )
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 ) )