2025-12-06 02:22:01 +01:00
"""
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
2025-12-11 23:14:20 +01:00
@router.get ( " /customers/ {customer_id} /subscriptions " )
async def get_customer_subscriptions ( customer_id : int ) :
"""
Get subscriptions and sales orders for a customer
Returns data from vTiger :
1. Recurring Sales Orders ( enable_recurring = 1 )
2. Sales Orders with recurring_frequency ( open status )
3. Recent Invoices for context
"""
from app . services . vtiger_service import get_vtiger_service
# Get customer with vTiger ID
customer = execute_query (
" SELECT id, name, vtiger_id FROM customers WHERE id = %s " ,
( customer_id , ) ,
fetchone = True
)
if not customer :
raise HTTPException ( status_code = 404 , detail = " Customer not found " )
vtiger_id = customer . get ( ' vtiger_id ' )
if not vtiger_id :
logger . warning ( f " ⚠️ Customer { customer_id } has no vTiger ID " )
return {
" status " : " no_vtiger_link " ,
" message " : " Kunde er ikke synkroniseret med vTiger " ,
" recurring_orders " : [ ] ,
" sales_orders " : [ ] ,
" invoices " : [ ]
}
try :
vtiger = get_vtiger_service ( )
# Fetch all sales orders
logger . info ( f " 🔍 Fetching sales orders for vTiger account { vtiger_id } " )
all_orders = await vtiger . get_customer_sales_orders ( vtiger_id )
# Fetch subscriptions from vTiger
logger . info ( f " 🔍 Fetching subscriptions for vTiger account { vtiger_id } " )
subscriptions = await vtiger . get_customer_subscriptions ( vtiger_id )
# Filter sales orders into categories
recurring_orders = [ ]
frequency_orders = [ ]
all_open_orders = [ ]
for order in all_orders :
# Skip closed/cancelled orders
status = order . get ( ' sostatus ' , ' ' ) . lower ( )
if status in [ ' closed ' , ' cancelled ' ] :
continue
all_open_orders . append ( order )
# Check if recurring is enabled
enable_recurring = order . get ( ' enable_recurring ' )
recurring_frequency = order . get ( ' recurring_frequency ' , ' ' ) . strip ( )
if enable_recurring == ' 1 ' or enable_recurring == 1 :
recurring_orders . append ( order )
elif recurring_frequency :
frequency_orders . append ( order )
# Filter subscriptions by status
active_subscriptions = [ ]
expired_subscriptions = [ ]
for sub in subscriptions :
status = sub . get ( ' sub_status ' , ' ' ) . lower ( )
if status in [ ' cancelled ' , ' expired ' ] :
expired_subscriptions . append ( sub )
else :
active_subscriptions . append ( sub )
# Fetch BMC Office subscriptions from local database
bmc_office_query = """
SELECT * FROM bmc_office_subscription_totals
WHERE customer_id = % s AND active = true
ORDER BY start_date DESC
"""
bmc_office_subs = execute_query ( bmc_office_query , ( customer_id , ) ) or [ ]
logger . info ( f " ✅ Found { len ( recurring_orders ) } recurring orders, { len ( frequency_orders ) } frequency orders, { len ( all_open_orders ) } total open orders, { len ( active_subscriptions ) } active subscriptions, { len ( bmc_office_subs ) } BMC Office subscriptions " )
return {
" status " : " success " ,
" customer_id " : customer_id ,
" customer_name " : customer [ ' name ' ] ,
" vtiger_id " : vtiger_id ,
" recurring_orders " : recurring_orders ,
" sales_orders " : all_open_orders , # Show ALL open sales orders
" subscriptions " : active_subscriptions , # Active subscriptions from vTiger Subscriptions module
" expired_subscriptions " : expired_subscriptions ,
" bmc_office_subscriptions " : bmc_office_subs , # Local BMC Office subscriptions
" last_updated " : " real-time "
}
except Exception as e :
logger . error ( f " ❌ Error fetching subscriptions: { e } " )
raise HTTPException ( status_code = 500 , detail = f " Failed to fetch subscriptions: { str ( e ) } " )