nexus-5/core/mcp/tools/customers.py
2026-01-26 11:09:40 -05:00

264 lines
8.3 KiB
Python

"""Customer and Account tools for MCP."""
from typing import Optional
from channels.db import database_sync_to_async
from core.mcp.auth import MCPContext, Role
from core.mcp.base import mcp, json_response, error_response
@mcp.tool()
async def list_customers(
limit: int = 25,
search: Optional[str] = None,
is_active: Optional[bool] = None
) -> str:
"""
List customers with optional filtering. Requires ADMIN or TEAM_LEADER role.
Args:
limit: Maximum customers to return (default 25)
search: Optional search term for customer name
is_active: Optional filter for active status
Returns:
JSON array of customer objects
"""
profile = MCPContext.get_profile()
if not profile:
return error_response("No active profile. Call set_active_profile first.")
if profile.role == Role.TEAM_MEMBER.value:
return error_response("Access denied. TEAM_LEADER or ADMIN role required.")
from core.models import Customer
@database_sync_to_async
def fetch():
qs = Customer.objects.prefetch_related('contacts')
if search:
qs = qs.filter(name__icontains=search)
if is_active is not None:
qs = qs.filter(status='ACTIVE' if is_active else 'INACTIVE')
results = []
for c in qs[:limit]:
# Get primary contact if available
primary_contact = c.contacts.filter(is_primary=True, is_active=True).first()
results.append({
"id": str(c.id),
"name": c.name,
"status": c.status,
"billing_email": c.billing_email,
"primary_contact": {
"name": f"{primary_contact.first_name} {primary_contact.last_name}".strip(),
"email": primary_contact.email,
"phone": primary_contact.phone
} if primary_contact else None
})
return results
customers = await fetch()
return json_response(customers)
@mcp.tool()
async def get_customer(customer_id: str) -> str:
"""
Get detailed customer information including accounts. Requires ADMIN or TEAM_LEADER role.
Args:
customer_id: UUID of the customer
Returns:
JSON object with customer details and accounts
"""
profile = MCPContext.get_profile()
if not profile:
return error_response("No active profile. Call set_active_profile first.")
if profile.role == Role.TEAM_MEMBER.value:
return error_response("Access denied. TEAM_LEADER or ADMIN role required.")
from core.models import Customer
@database_sync_to_async
def fetch():
try:
c = Customer.objects.prefetch_related(
'accounts__addresses', 'contacts', 'addresses'
).get(pk=customer_id)
return {
"id": str(c.id),
"name": c.name,
"status": c.status,
"billing_email": c.billing_email,
"contacts": [
{
"id": str(ct.id),
"first_name": ct.first_name,
"last_name": ct.last_name,
"email": ct.email,
"phone": ct.phone,
"is_primary": ct.is_primary
}
for ct in c.contacts.filter(is_active=True)
],
"accounts": [
{
"id": str(a.id),
"name": a.name,
"status": a.status,
"addresses": [
{
"id": str(addr.id),
"name": addr.name or "Primary",
"street_address": addr.street_address,
"city": addr.city,
"state": addr.state,
"zip_code": addr.zip_code
}
for addr in a.addresses.all()
]
}
for a in c.accounts.all()
]
}
except Customer.DoesNotExist:
return None
customer = await fetch()
if not customer:
return error_response(f"Customer {customer_id} not found")
return json_response(customer)
@mcp.tool()
async def list_accounts(
limit: int = 25,
customer_id: Optional[str] = None,
search: Optional[str] = None,
is_active: Optional[bool] = None
) -> str:
"""
List accounts with optional filtering. Requires ADMIN or TEAM_LEADER role.
Args:
limit: Maximum accounts to return (default 25)
customer_id: Optional customer UUID to filter by
search: Optional search term for account name
is_active: Optional filter for active status
Returns:
JSON array of account objects
"""
profile = MCPContext.get_profile()
if not profile:
return error_response("No active profile. Call set_active_profile first.")
if profile.role == Role.TEAM_MEMBER.value:
return error_response("Access denied. TEAM_LEADER or ADMIN role required.")
from core.models import Account
@database_sync_to_async
def fetch():
qs = Account.objects.select_related('customer').prefetch_related('addresses')
if customer_id:
qs = qs.filter(customer_id=customer_id)
if search:
qs = qs.filter(name__icontains=search)
if is_active is not None:
qs = qs.filter(status='ACTIVE' if is_active else 'INACTIVE')
return [
{
"id": str(a.id),
"name": a.name,
"status": a.status,
"customer": {"id": str(a.customer.id), "name": a.customer.name},
"addresses": [
{
"id": str(addr.id),
"name": addr.name or "Primary",
"city": addr.city,
"state": addr.state
}
for addr in a.addresses.all()
]
}
for a in qs[:limit]
]
accounts = await fetch()
return json_response(accounts)
@mcp.tool()
async def get_account(account_id: str) -> str:
"""
Get detailed account information. Requires ADMIN or TEAM_LEADER role.
Args:
account_id: UUID of the account
Returns:
JSON object with account details, addresses, and contacts
"""
profile = MCPContext.get_profile()
if not profile:
return error_response("No active profile. Call set_active_profile first.")
if profile.role == Role.TEAM_MEMBER.value:
return error_response("Access denied. TEAM_LEADER or ADMIN role required.")
from core.models import Account
@database_sync_to_async
def fetch():
try:
a = Account.objects.select_related('customer').prefetch_related(
'addresses', 'contacts'
).get(pk=account_id)
return {
"id": str(a.id),
"name": a.name,
"status": a.status,
"customer": {"id": str(a.customer.id), "name": a.customer.name},
"addresses": [
{
"id": str(addr.id),
"name": addr.name or "Primary",
"street_address": addr.street_address,
"city": addr.city,
"state": addr.state,
"zip_code": addr.zip_code
}
for addr in a.addresses.all()
],
"contacts": [
{
"id": str(ct.id),
"first_name": ct.first_name,
"last_name": ct.last_name,
"email": ct.email,
"phone": ct.phone
}
for ct in a.contacts.filter(is_active=True)
]
}
except Account.DoesNotExist:
return None
account = await fetch()
if not account:
return error_response(f"Account {account_id} not found")
return json_response(account)