474 lines
19 KiB
Python
474 lines
19 KiB
Python
"""
|
|
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
|
|
if cvr:
|
|
existing = execute_query(
|
|
"SELECT id, name, vtiger_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, vtiger_id FROM customers")
|
|
for customer in all_customers:
|
|
if normalize_name(customer['name']) == normalized:
|
|
existing = [customer]
|
|
break
|
|
|
|
if existing:
|
|
# 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'])
|
|
)
|
|
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'})")
|
|
|
|
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]:
|
|
"""
|
|
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 all contacts (no limit)
|
|
query = "SELECT id, firstname, lastname, email, phone, mobile, title, department, account_id FROM Contacts;"
|
|
|
|
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 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))
|