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 ] :
"""
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
2025-12-19 15:34:49 +01:00
query = " SELECT id, accountname, email1, siccode, cf_accounts_cvr, website, bill_city, bill_code, bill_country FROM Accounts LIMIT 1000; "
2025-12-19 13:09:42 +01:00
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 ' )
2025-12-19 15:34:49 +01:00
economic_customer_number = None # Will be set by e-conomic sync
2025-12-19 13:09:42 +01:00
if not name :
skipped_count + = 1
2025-12-19 16:36:41 +01:00
logger . debug ( f " ⏭️ Sprunget over: Tomt firmanavn (ID: { vtiger_id } ) " )
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
# 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
2025-12-19 16:36:41 +01:00
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 ] ) } " )
2025-12-19 13:09:42 +01:00
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
2025-12-19 16:36:41 +01:00
logger . info ( f " ✨ Oprettet: { name } (CVR: { cvr or ' ingen ' } , By: { city or ' ukendt ' } ) " )
2025-12-19 13:09:42 +01:00
2025-12-19 16:36:41 +01:00
logger . info ( f " ✅ vTiger sync fuldført: { created_count } oprettet, { updated_count } opdateret, { skipped_count } sprunget over af { len ( accounts ) } totalt " )
2025-12-19 13:09:42 +01:00
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
2025-12-19 15:34:49 +01:00
query = " SELECT id, firstname, lastname, email, phone, mobile, title, department, account_id FROM Contacts LIMIT 1000; "
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 ] :
"""
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... " )
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:41:11 +01:00
# Get all customers from e-conomic
economic_customers = await economic . get_customers ( page = 0 , page_size = 10000 )
logger . info ( f " 📥 Fetched { len ( economic_customers ) } customers from e-conomic " )
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
if not matched and name :
normalized = normalize_name ( name )
all_customers = execute_query ( " SELECT id, name FROM customers WHERE economic_customer_number IS NULL " )
for hub_customer in all_customers :
if normalize_name ( hub_customer [ ' name ' ] ) == normalized :
matched = [ hub_customer ]
break
if matched :
# Update Hub customer with e-conomic number
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 :
not_matched_count + = 1
logger . info ( f " ✅ e-conomic sync fuldført: { matched_count } matchet, { not_matched_count } ikke matchet af { len ( economic_customers ) } totalt " )
2025-12-19 13:09:42 +01:00
return {
2025-12-19 16:41:11 +01:00
" status " : " success " ,
" matched " : matched_count ,
" not_matched " : not_matched_count ,
" 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 ) )