BREAKING CHANGES: - vTiger sync: Never overwrites existing vtiger_id - Contact sync: REPLACES links instead of appending (idempotent) - E-conomic sync: Only updates fields it owns (address, city, postal, email_domain, website) - E-conomic sync: Does NOT overwrite name or cvr_number anymore ARCHITECTURE: - Each data source owns specific fields - Sync operations are now idempotent (can run multiple times) - Clear documentation of field ownership in sync_router.py - Contact links deleted and recreated on sync to match vTiger state FIXED: - Contact relationships now correct after re-sync - No more mixed customer data from different sources - Sorting contacts by company_count DESC (companies first)
163 lines
5.8 KiB
Python
163 lines
5.8 KiB
Python
"""
|
|
Contact API Router - Simplified (Read-Only)
|
|
Only GET endpoints for now
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from typing import Optional
|
|
from app.core.database import execute_query
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/contacts-debug")
|
|
async def debug_contacts():
|
|
"""Debug endpoint: Check contact-company links"""
|
|
try:
|
|
# Count links
|
|
links = execute_query("SELECT COUNT(*) as total FROM contact_companies")
|
|
|
|
# Get sample with links
|
|
sample = execute_query("""
|
|
SELECT
|
|
c.id, c.first_name, c.last_name,
|
|
COUNT(cc.customer_id) as company_count,
|
|
ARRAY_AGG(cu.name) as company_names
|
|
FROM contacts c
|
|
LEFT JOIN contact_companies cc ON c.id = cc.contact_id
|
|
LEFT JOIN customers cu ON cc.customer_id = cu.id
|
|
GROUP BY c.id, c.first_name, c.last_name
|
|
HAVING COUNT(cc.customer_id) > 0
|
|
LIMIT 10
|
|
""")
|
|
|
|
# Test the actual query used in get_contacts
|
|
test_query = """
|
|
SELECT
|
|
c.id, c.first_name, c.last_name,
|
|
COUNT(DISTINCT cc.customer_id) as company_count,
|
|
ARRAY_AGG(DISTINCT cu.name ORDER BY cu.name) FILTER (WHERE cu.name IS NOT NULL) as company_names
|
|
FROM contacts c
|
|
LEFT JOIN contact_companies cc ON c.id = cc.contact_id
|
|
LEFT JOIN customers cu ON cc.customer_id = cu.id
|
|
GROUP BY c.id, c.first_name, c.last_name
|
|
ORDER BY c.last_name, c.first_name
|
|
LIMIT 10
|
|
"""
|
|
test_result = execute_query(test_query)
|
|
|
|
return {
|
|
"total_links": links[0]['total'] if links else 0,
|
|
"sample_contacts_with_companies": sample or [],
|
|
"test_query_result": test_result or [],
|
|
"note": "If company_count is 0, the JOIN might not be working"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Debug failed: {e}", exc_info=True)
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/contacts")
|
|
async def get_contacts(
|
|
search: Optional[str] = None,
|
|
customer_id: Optional[int] = None,
|
|
is_active: Optional[bool] = None,
|
|
limit: int = Query(default=100, le=1000),
|
|
offset: int = Query(default=0, ge=0)
|
|
):
|
|
"""Get all contacts with optional filtering"""
|
|
try:
|
|
where_clauses = []
|
|
params = []
|
|
|
|
if search:
|
|
where_clauses.append("(c.first_name ILIKE %s OR c.last_name ILIKE %s OR c.email ILIKE %s)")
|
|
params.extend([f"%{search}%", f"%{search}%", f"%{search}%"])
|
|
|
|
if is_active is not None:
|
|
where_clauses.append("c.is_active = %s")
|
|
params.append(is_active)
|
|
|
|
where_sql = "WHERE " + " AND ".join(where_clauses) if where_clauses else ""
|
|
|
|
# Count total (needs alias c for consistency)
|
|
count_query = f"SELECT COUNT(*) as count FROM contacts c {where_sql}"
|
|
count_result = execute_query(count_query, tuple(params))
|
|
total = count_result[0]['count'] if count_result else 0
|
|
|
|
# Get contacts with company info
|
|
query = f"""
|
|
SELECT
|
|
c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile,
|
|
c.title, c.department, c.is_active, c.created_at, c.updated_at,
|
|
COUNT(DISTINCT cc.customer_id) as company_count,
|
|
ARRAY_AGG(DISTINCT cu.name ORDER BY cu.name) FILTER (WHERE cu.name IS NOT NULL) as company_names
|
|
FROM contacts c
|
|
LEFT JOIN contact_companies cc ON c.id = cc.contact_id
|
|
LEFT JOIN customers cu ON cc.customer_id = cu.id
|
|
{where_sql}
|
|
GROUP BY c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile,
|
|
c.title, c.department, c.is_active, c.created_at, c.updated_at
|
|
ORDER BY company_count DESC, c.last_name, c.first_name
|
|
LIMIT %s OFFSET %s
|
|
"""
|
|
params.extend([limit, offset])
|
|
contacts = execute_query(query, tuple(params))
|
|
|
|
return {
|
|
"total": total,
|
|
"contacts": contacts,
|
|
"limit": limit,
|
|
"offset": offset
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get contacts: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/contacts/{contact_id}")
|
|
async def get_contact(contact_id: int):
|
|
"""Get a single contact by ID with linked companies"""
|
|
try:
|
|
# Get contact info
|
|
query = """
|
|
SELECT
|
|
id, first_name, last_name, email, phone, mobile,
|
|
title, department, is_active, user_company, vtiger_id,
|
|
created_at, updated_at
|
|
FROM contacts
|
|
WHERE id = %s
|
|
"""
|
|
contacts = execute_query(query, (contact_id,))
|
|
|
|
if not contacts:
|
|
raise HTTPException(status_code=404, detail="Contact not found")
|
|
|
|
contact = contacts[0]
|
|
|
|
# Get linked companies
|
|
companies_query = """
|
|
SELECT
|
|
cu.id, cu.name, cu.cvr_number,
|
|
cc.is_primary, cc.role, cc.notes
|
|
FROM contact_companies cc
|
|
JOIN customers cu ON cc.customer_id = cu.id
|
|
WHERE cc.contact_id = %s
|
|
ORDER BY cc.is_primary DESC, cu.name
|
|
"""
|
|
companies = execute_query(companies_query, (contact_id,))
|
|
|
|
contact['companies'] = companies or []
|
|
|
|
return contact
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to get contact {contact_id}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|