""" Customers Router API endpoints for customer management Adapted from OmniSync for BMC Hub """ from fastapi import APIRouter, HTTPException, Query from typing import List, Optional, Dict from pydantic import BaseModel import logging from app.core.database import execute_query, execute_insert, execute_update from app.services.cvr_service import get_cvr_service logger = logging.getLogger(__name__) router = APIRouter() # Pydantic Models class CustomerBase(BaseModel): name: str cvr_number: Optional[str] = None email: Optional[str] = None phone: Optional[str] = None address: Optional[str] = None city: Optional[str] = None postal_code: Optional[str] = None country: Optional[str] = "DK" website: Optional[str] = None is_active: Optional[bool] = True invoice_email: Optional[str] = None mobile_phone: Optional[str] = None class CustomerCreate(CustomerBase): pass class CustomerUpdate(BaseModel): name: Optional[str] = None cvr_number: Optional[str] = None email: Optional[str] = None phone: Optional[str] = None address: Optional[str] = None city: Optional[str] = None postal_code: Optional[str] = None country: Optional[str] = None website: Optional[str] = None is_active: Optional[bool] = None invoice_email: Optional[str] = None mobile_phone: Optional[str] = None class ContactCreate(BaseModel): first_name: str last_name: str email: Optional[str] = None phone: Optional[str] = None mobile: Optional[str] = None title: Optional[str] = None department: Optional[str] = None is_primary: Optional[bool] = False role: Optional[str] = None @router.get("/customers") async def list_customers( limit: int = Query(default=50, ge=1, le=1000), offset: int = Query(default=0, ge=0), search: Optional[str] = Query(default=None), source: Optional[str] = Query(default=None), # 'vtiger', 'local', or None is_active: Optional[bool] = Query(default=None) ): """ List customers with pagination and filtering Args: limit: Maximum number of customers to return offset: Number of customers to skip search: Search term for name, email, cvr, phone, city source: Filter by source ('vtiger' or 'local') is_active: Filter by active status """ # Build query query = """ SELECT c.*, COUNT(DISTINCT cc.contact_id) as contact_count FROM customers c LEFT JOIN contact_companies cc ON cc.customer_id = c.id WHERE 1=1 """ params = [] # Add search filter if search: query += """ AND ( c.name ILIKE %s OR c.email ILIKE %s OR c.cvr_number ILIKE %s OR c.phone ILIKE %s OR c.city ILIKE %s )""" search_term = f"%{search}%" params.extend([search_term] * 5) # Add source filter if source == 'vtiger': query += " AND c.vtiger_id IS NOT NULL" elif source == 'local': query += " AND c.vtiger_id IS NULL" # Add active filter if is_active is not None: query += " AND c.is_active = %s" params.append(is_active) query += """ GROUP BY c.id ORDER BY c.name LIMIT %s OFFSET %s """ params.extend([limit, offset]) rows = execute_query(query, tuple(params)) # Get total count count_query = "SELECT COUNT(*) as total FROM customers WHERE 1=1" count_params = [] if search: count_query += """ AND ( name ILIKE %s OR email ILIKE %s OR cvr_number ILIKE %s OR phone ILIKE %s OR city ILIKE %s )""" count_params.extend([search_term] * 5) if source == 'vtiger': count_query += " AND vtiger_id IS NOT NULL" elif source == 'local': count_query += " AND vtiger_id IS NULL" if is_active is not None: count_query += " AND is_active = %s" count_params.append(is_active) count_result = execute_query(count_query, tuple(count_params), fetchone=True) total = count_result['total'] if count_result else 0 return { "customers": rows or [], "total": total, "limit": limit, "offset": offset } @router.get("/customers/{customer_id}") async def get_customer(customer_id: int): """Get single customer by ID with contact count""" # Get customer customer = execute_query( "SELECT * FROM customers WHERE id = %s", (customer_id,), fetchone=True ) if not customer: raise HTTPException(status_code=404, detail="Customer not found") # Get contact count contact_count_result = execute_query( "SELECT COUNT(*) as count FROM contact_companies WHERE customer_id = %s", (customer_id,), fetchone=True ) contact_count = contact_count_result['count'] if contact_count_result else 0 return { **customer, 'contact_count': contact_count } @router.post("/customers") async def create_customer(customer: CustomerCreate): """Create a new customer""" try: customer_id = execute_insert( """INSERT INTO customers (name, cvr_number, email, phone, address, city, postal_code, country, website, is_active, invoice_email, mobile_phone) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id""", ( customer.name, customer.cvr_number, customer.email, customer.phone, customer.address, customer.city, customer.postal_code, customer.country, customer.website, customer.is_active, customer.invoice_email, customer.mobile_phone ) ) logger.info(f"✅ Created customer {customer_id}: {customer.name}") # Fetch and return created customer created = execute_query( "SELECT * FROM customers WHERE id = %s", (customer_id,), fetchone=True ) return created except Exception as e: logger.error(f"❌ Failed to create customer: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.put("/customers/{customer_id}") async def update_customer(customer_id: int, update: CustomerUpdate): """Update customer information""" # Verify customer exists existing = execute_query( "SELECT id FROM customers WHERE id = %s", (customer_id,), fetchone=True ) if not existing: raise HTTPException(status_code=404, detail="Customer not found") # Build dynamic UPDATE query updates = [] params = [] update_dict = update.dict(exclude_unset=True) for field, value in update_dict.items(): updates.append(f"{field} = %s") params.append(value) if not updates: raise HTTPException(status_code=400, detail="No fields to update") params.append(customer_id) query = f"UPDATE customers SET {', '.join(updates)}, updated_at = CURRENT_TIMESTAMP WHERE id = %s" try: execute_update(query, tuple(params)) logger.info(f"✅ Updated customer {customer_id}") # Fetch and return updated customer updated = execute_query( "SELECT * FROM customers WHERE id = %s", (customer_id,), fetchone=True ) return updated except Exception as e: logger.error(f"❌ Failed to update customer {customer_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/customers/{customer_id}/contacts") async def get_customer_contacts(customer_id: int): """Get all contacts for a specific customer""" rows = execute_query(""" SELECT c.*, cc.is_primary, cc.role, cc.notes FROM contacts c JOIN contact_companies cc ON c.id = cc.contact_id WHERE cc.customer_id = %s AND c.is_active = TRUE ORDER BY cc.is_primary DESC, c.first_name, c.last_name """, (customer_id,)) return rows or [] @router.post("/customers/{customer_id}/contacts") async def create_customer_contact(customer_id: int, contact: ContactCreate): """Create a new contact for a customer""" # Verify customer exists customer = execute_query( "SELECT id FROM customers WHERE id = %s", (customer_id,), fetchone=True ) if not customer: raise HTTPException(status_code=404, detail="Customer not found") try: # Create contact contact_id = execute_insert( """INSERT INTO contacts (first_name, last_name, email, phone, mobile, title, department) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id""", ( contact.first_name, contact.last_name, contact.email, contact.phone, contact.mobile, contact.title, contact.department ) ) # Link contact to customer execute_insert( """INSERT INTO contact_companies (contact_id, customer_id, is_primary, role) VALUES (%s, %s, %s, %s)""", (contact_id, customer_id, contact.is_primary, contact.role) ) logger.info(f"✅ Created contact {contact_id} for customer {customer_id}") # Fetch and return created contact created = execute_query( "SELECT * FROM contacts WHERE id = %s", (contact_id,), fetchone=True ) return created except Exception as e: logger.error(f"❌ Failed to create contact: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/cvr/{cvr_number}") async def lookup_cvr(cvr_number: str): """Lookup company information by CVR number""" cvr_service = get_cvr_service() result = await cvr_service.lookup_by_cvr(cvr_number) if not result: raise HTTPException(status_code=404, detail="CVR number not found") return result