2025-12-06 02:22:01 +01:00
|
|
|
"""
|
|
|
|
|
Contact API Router
|
|
|
|
|
Handles contact CRUD operations with multi-company support
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
|
|
|
from typing import Optional, List
|
|
|
|
|
from app.core.database import execute_query, execute_insert, execute_update
|
2026-02-11 23:51:21 +01:00
|
|
|
from app.core.contact_utils import get_contact_customer_ids, get_primary_customer_id
|
|
|
|
|
from app.customers.backend.router import (
|
|
|
|
|
get_customer_subscriptions,
|
|
|
|
|
lock_customer_subscriptions,
|
|
|
|
|
save_subscription_comment,
|
|
|
|
|
get_subscription_comment,
|
|
|
|
|
get_subscription_billing_matrix,
|
|
|
|
|
SubscriptionComment,
|
|
|
|
|
)
|
2025-12-06 02:22:01 +01:00
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
2025-12-22 15:43:47 +01:00
|
|
|
@router.get("/contacts-debug", response_model=dict)
|
2025-12-22 15:35:26 +01:00
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
2025-12-06 02:22:01 +01:00
|
|
|
@router.get("/contacts", response_model=dict)
|
|
|
|
|
async def get_contacts(
|
|
|
|
|
search: Optional[str] = None,
|
|
|
|
|
customer_id: Optional[int] = None,
|
|
|
|
|
is_active: Optional[bool] = None,
|
|
|
|
|
limit: int = Query(default=20, le=100),
|
|
|
|
|
offset: int = Query(default=0, ge=0)
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Get all contacts with optional filtering, search, and pagination
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Build WHERE clauses
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
if customer_id is not None:
|
|
|
|
|
where_clauses.append("EXISTS (SELECT 1 FROM contact_companies cc WHERE cc.contact_id = c.id AND cc.customer_id = %s)")
|
|
|
|
|
params.append(customer_id)
|
|
|
|
|
|
|
|
|
|
where_sql = "WHERE " + " AND ".join(where_clauses) if where_clauses else ""
|
|
|
|
|
|
|
|
|
|
# Count total
|
|
|
|
|
count_query = f"""
|
|
|
|
|
SELECT COUNT(DISTINCT c.id)
|
|
|
|
|
FROM contacts c
|
|
|
|
|
{where_sql}
|
|
|
|
|
"""
|
2025-12-16 15:36:11 +01:00
|
|
|
count_result = execute_query_single(count_query, tuple(params))
|
2025-12-06 02:22:01 +01:00
|
|
|
total = count_result['count'] if count_result else 0
|
|
|
|
|
|
|
|
|
|
# Get contacts with company count
|
|
|
|
|
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.vtiger_id,
|
|
|
|
|
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.vtiger_id, c.created_at, c.updated_at
|
|
|
|
|
ORDER BY c.last_name, c.first_name
|
|
|
|
|
LIMIT %s OFFSET %s
|
|
|
|
|
"""
|
|
|
|
|
params.extend([limit, offset])
|
|
|
|
|
|
2025-12-22 15:29:28 +01:00
|
|
|
contacts = execute_query(query, tuple(params)) # Returns all rows
|
2025-12-06 02:22:01 +01:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"contacts": contacts or [],
|
|
|
|
|
"total": total,
|
|
|
|
|
"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}", response_model=dict)
|
|
|
|
|
async def get_contact(contact_id: int):
|
|
|
|
|
"""
|
|
|
|
|
Get a single contact with all linked companies
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Get contact
|
|
|
|
|
contact_query = """
|
|
|
|
|
SELECT id, first_name, last_name, email, phone, mobile,
|
|
|
|
|
title, department, is_active, vtiger_id,
|
|
|
|
|
created_at, updated_at
|
|
|
|
|
FROM contacts
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
"""
|
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
|
|
|
contact_result = execute_query(contact_query, (contact_id,))
|
2025-12-06 02:22:01 +01:00
|
|
|
|
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
|
|
|
if not contact_result:
|
2025-12-06 02:22:01 +01:00
|
|
|
raise HTTPException(status_code=404, detail="Contact not found")
|
|
|
|
|
|
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
|
|
|
contact = contact_result[0]
|
|
|
|
|
|
2025-12-06 02:22:01 +01:00
|
|
|
# Get linked companies
|
|
|
|
|
companies_query = """
|
|
|
|
|
SELECT
|
|
|
|
|
cu.id, cu.name,
|
|
|
|
|
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
|
|
|
|
|
"""
|
feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00
|
|
|
companies = execute_query(companies_query, (contact_id,))
|
2025-12-06 02:22:01 +01:00
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
2025-12-17 16:38:08 +01:00
|
|
|
# POST/PUT/DELETE endpoints temporarily disabled - need proper models
|
|
|
|
|
# @router.post("/contacts", response_model=dict)
|
|
|
|
|
# async def create_contact(contact: ContactCreate):
|
|
|
|
|
# """
|
|
|
|
|
# Create a new contact and link to companies
|
|
|
|
|
# """
|
2025-12-06 02:22:01 +01:00
|
|
|
try:
|
|
|
|
|
# Insert contact
|
|
|
|
|
insert_query = """
|
|
|
|
|
INSERT INTO contacts (first_name, last_name, email, phone, mobile, title, department, is_active)
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
|
|
|
|
"""
|
|
|
|
|
contact_id = execute_insert(
|
|
|
|
|
insert_query,
|
|
|
|
|
(contact.first_name, contact.last_name, contact.email, contact.phone,
|
|
|
|
|
contact.mobile, contact.title, contact.department, contact.is_active)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Link to companies
|
|
|
|
|
if contact.company_ids:
|
|
|
|
|
for idx, customer_id in enumerate(contact.company_ids):
|
|
|
|
|
is_primary = idx == 0 and contact.is_primary
|
|
|
|
|
link_query = """
|
|
|
|
|
INSERT INTO contact_companies (contact_id, customer_id, is_primary, role, notes)
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s)
|
|
|
|
|
"""
|
|
|
|
|
execute_insert(
|
|
|
|
|
link_query,
|
|
|
|
|
(contact_id, customer_id, is_primary, contact.role, contact.notes)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Return created contact
|
|
|
|
|
return await get_contact(contact_id)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to create contact: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/contacts/{contact_id}", response_model=dict)
|
|
|
|
|
async def update_contact(contact_id: int, contact: ContactUpdate):
|
|
|
|
|
"""
|
|
|
|
|
Update a contact
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Check if contact exists
|
2025-12-16 15:36:11 +01:00
|
|
|
existing = execute_query("SELECT id FROM contacts WHERE id = %s", (contact_id,))
|
2025-12-06 02:22:01 +01:00
|
|
|
if not existing:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Contact not found")
|
|
|
|
|
|
|
|
|
|
# Build update query
|
|
|
|
|
update_fields = []
|
|
|
|
|
params = []
|
|
|
|
|
|
|
|
|
|
if contact.first_name is not None:
|
|
|
|
|
update_fields.append("first_name = %s")
|
|
|
|
|
params.append(contact.first_name)
|
|
|
|
|
|
|
|
|
|
if contact.last_name is not None:
|
|
|
|
|
update_fields.append("last_name = %s")
|
|
|
|
|
params.append(contact.last_name)
|
|
|
|
|
|
|
|
|
|
if contact.email is not None:
|
|
|
|
|
update_fields.append("email = %s")
|
|
|
|
|
params.append(contact.email)
|
|
|
|
|
|
|
|
|
|
if contact.phone is not None:
|
|
|
|
|
update_fields.append("phone = %s")
|
|
|
|
|
params.append(contact.phone)
|
|
|
|
|
|
|
|
|
|
if contact.mobile is not None:
|
|
|
|
|
update_fields.append("mobile = %s")
|
|
|
|
|
params.append(contact.mobile)
|
|
|
|
|
|
|
|
|
|
if contact.title is not None:
|
|
|
|
|
update_fields.append("title = %s")
|
|
|
|
|
params.append(contact.title)
|
|
|
|
|
|
|
|
|
|
if contact.department is not None:
|
|
|
|
|
update_fields.append("department = %s")
|
|
|
|
|
params.append(contact.department)
|
|
|
|
|
|
|
|
|
|
if contact.is_active is not None:
|
|
|
|
|
update_fields.append("is_active = %s")
|
|
|
|
|
params.append(contact.is_active)
|
|
|
|
|
|
|
|
|
|
if not update_fields:
|
|
|
|
|
return await get_contact(contact_id)
|
|
|
|
|
|
|
|
|
|
update_query = f"""
|
|
|
|
|
UPDATE contacts
|
|
|
|
|
SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
"""
|
|
|
|
|
params.append(contact_id)
|
|
|
|
|
|
|
|
|
|
execute_update(update_query, tuple(params))
|
|
|
|
|
|
|
|
|
|
return await get_contact(contact_id)
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to update contact {contact_id}: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/contacts/{contact_id}")
|
|
|
|
|
async def delete_contact(contact_id: int):
|
|
|
|
|
"""
|
|
|
|
|
Delete a contact (cascade deletes company links)
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
result = execute_update("DELETE FROM contacts WHERE id = %s", (contact_id,))
|
|
|
|
|
if result == 0:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Contact not found")
|
|
|
|
|
|
|
|
|
|
return {"message": "Contact deleted successfully"}
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to delete contact {contact_id}: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/contacts/{contact_id}/companies")
|
|
|
|
|
async def link_contact_to_company(contact_id: int, link: ContactCompanyLink):
|
|
|
|
|
"""
|
|
|
|
|
Link a contact to a company
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Check if contact exists
|
2025-12-16 15:36:11 +01:00
|
|
|
contact = execute_query_single("SELECT id FROM contacts WHERE id = %s", (contact_id,))
|
2025-12-06 02:22:01 +01:00
|
|
|
if not contact:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Contact not found")
|
|
|
|
|
|
|
|
|
|
# Check if company exists
|
2025-12-16 15:36:11 +01:00
|
|
|
customer = execute_query_single("SELECT id FROM customers WHERE id = %s", (link.customer_id,))
|
2025-12-06 02:22:01 +01:00
|
|
|
if not customer:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Customer not found")
|
|
|
|
|
|
|
|
|
|
# Insert link (ON CONFLICT updates)
|
|
|
|
|
query = """
|
|
|
|
|
INSERT INTO contact_companies (contact_id, customer_id, is_primary, role, notes)
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s)
|
|
|
|
|
ON CONFLICT (contact_id, customer_id)
|
|
|
|
|
DO UPDATE SET is_primary = EXCLUDED.is_primary, role = EXCLUDED.role, notes = EXCLUDED.notes
|
|
|
|
|
"""
|
|
|
|
|
execute_insert(query, (contact_id, link.customer_id, link.is_primary, link.role, link.notes))
|
|
|
|
|
|
|
|
|
|
return {"message": "Contact linked to company successfully"}
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to link contact to company: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/contacts/{contact_id}/companies/{customer_id}")
|
|
|
|
|
async def unlink_contact_from_company(contact_id: int, customer_id: int):
|
|
|
|
|
"""
|
|
|
|
|
Unlink a contact from a company
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
result = execute_update(
|
|
|
|
|
"DELETE FROM contact_companies WHERE contact_id = %s AND customer_id = %s",
|
|
|
|
|
(contact_id, customer_id)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if result == 0:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Link not found")
|
|
|
|
|
|
|
|
|
|
return {"message": "Contact unlinked from company successfully"}
|
|
|
|
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to unlink contact from company: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
2026-02-11 23:51:21 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/contacts/{contact_id}/related-contacts", response_model=dict)
|
|
|
|
|
async def get_related_contacts(contact_id: int):
|
|
|
|
|
"""
|
|
|
|
|
Get contacts from the same companies as the contact (excluding itself).
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
customer_ids = get_contact_customer_ids(contact_id)
|
|
|
|
|
if not customer_ids:
|
|
|
|
|
return {"contacts": []}
|
|
|
|
|
|
|
|
|
|
placeholders = ",".join(["%s"] * len(customer_ids))
|
|
|
|
|
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.vtiger_id,
|
|
|
|
|
c.created_at, c.updated_at,
|
|
|
|
|
ARRAY_AGG(DISTINCT cu.name ORDER BY cu.name) FILTER (WHERE cu.name IS NOT NULL) as company_names
|
|
|
|
|
FROM contacts c
|
|
|
|
|
JOIN contact_companies cc ON c.id = cc.contact_id
|
|
|
|
|
JOIN customers cu ON cc.customer_id = cu.id
|
|
|
|
|
WHERE cc.customer_id IN ({placeholders}) AND c.id <> %s
|
|
|
|
|
GROUP BY c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile,
|
|
|
|
|
c.title, c.department, c.is_active, c.vtiger_id, c.created_at, c.updated_at
|
|
|
|
|
ORDER BY c.last_name, c.first_name
|
|
|
|
|
"""
|
|
|
|
|
params = tuple(customer_ids + [contact_id])
|
|
|
|
|
results = execute_query(query, params) or []
|
|
|
|
|
return {"contacts": results}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to get related contacts for {contact_id}: {e}")
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/contacts/{contact_id}/subscriptions")
|
|
|
|
|
async def get_contact_subscriptions(contact_id: int):
|
|
|
|
|
customer_id = get_primary_customer_id(contact_id)
|
|
|
|
|
if not customer_id:
|
|
|
|
|
return {
|
|
|
|
|
"status": "no_linked_customer",
|
|
|
|
|
"message": "Kontakt er ikke tilknyttet et firma",
|
|
|
|
|
"recurring_orders": [],
|
|
|
|
|
"sales_orders": [],
|
|
|
|
|
"subscriptions": [],
|
|
|
|
|
"expired_subscriptions": [],
|
|
|
|
|
"bmc_office_subscriptions": [],
|
|
|
|
|
}
|
|
|
|
|
return await get_customer_subscriptions(customer_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/contacts/{contact_id}/subscriptions/lock")
|
|
|
|
|
async def lock_contact_subscriptions(contact_id: int, lock_request: dict):
|
|
|
|
|
customer_id = get_primary_customer_id(contact_id)
|
|
|
|
|
if not customer_id:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde")
|
|
|
|
|
return await lock_customer_subscriptions(customer_id, lock_request)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/contacts/{contact_id}/subscription-comment")
|
|
|
|
|
async def save_contact_subscription_comment(contact_id: int, data: SubscriptionComment):
|
|
|
|
|
customer_id = get_primary_customer_id(contact_id)
|
|
|
|
|
if not customer_id:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde")
|
|
|
|
|
return await save_subscription_comment(customer_id, data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/contacts/{contact_id}/subscription-comment")
|
|
|
|
|
async def get_contact_subscription_comment(contact_id: int):
|
|
|
|
|
customer_id = get_primary_customer_id(contact_id)
|
|
|
|
|
if not customer_id:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde")
|
|
|
|
|
return await get_subscription_comment(customer_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/contacts/{contact_id}/subscriptions/billing-matrix")
|
|
|
|
|
async def get_contact_subscription_billing_matrix(
|
|
|
|
|
contact_id: int,
|
|
|
|
|
months: int = Query(default=12, ge=1, le=60, description="Number of months to show"),
|
|
|
|
|
):
|
|
|
|
|
customer_id = get_primary_customer_id(contact_id)
|
|
|
|
|
if not customer_id:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Kontakt har ingen tilknyttet kunde")
|
|
|
|
|
return await get_subscription_billing_matrix(customer_id, months)
|