Move actual code implementations into module directories: - orders: 5 services, 4 models, order/invoice schemas - inventory: 3 services, 2 models, 30+ schemas - customers: 3 services, 2 models, customer schemas - messaging: 3 services, 2 models, message/notification schemas - monitoring: background_tasks_service - marketplace: 5+ services including letzshop submodule - dev_tools: code_quality_service, test_runner_service - billing: billing_service - contracts: definition.py Legacy files in app/services/, models/database/, models/schema/ now re-export from canonical module locations for backwards compatibility. Architecture validator passes with 0 errors. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
243 lines
7.6 KiB
Python
243 lines
7.6 KiB
Python
# app/modules/customers/services/admin_customer_service.py
|
|
"""
|
|
Admin customer management service.
|
|
|
|
Handles customer operations for admin users across all vendors.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.exceptions.customer import CustomerNotFoundException
|
|
from app.modules.customers.models import Customer
|
|
from models.database.vendor import Vendor
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AdminCustomerService:
|
|
"""Service for admin-level customer management across vendors."""
|
|
|
|
def list_customers(
|
|
self,
|
|
db: Session,
|
|
vendor_id: int | None = None,
|
|
search: str | None = None,
|
|
is_active: bool | None = None,
|
|
skip: int = 0,
|
|
limit: int = 20,
|
|
) -> tuple[list[dict[str, Any]], int]:
|
|
"""
|
|
Get paginated list of customers across all vendors.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_id: Optional vendor ID filter
|
|
search: Search by email, name, or customer number
|
|
is_active: Filter by active status
|
|
skip: Number of records to skip
|
|
limit: Maximum records to return
|
|
|
|
Returns:
|
|
Tuple of (customers list, total count)
|
|
"""
|
|
# Build query
|
|
query = db.query(Customer).join(Vendor, Customer.vendor_id == Vendor.id)
|
|
|
|
# Apply filters
|
|
if vendor_id:
|
|
query = query.filter(Customer.vendor_id == vendor_id)
|
|
|
|
if search:
|
|
search_term = f"%{search}%"
|
|
query = query.filter(
|
|
(Customer.email.ilike(search_term))
|
|
| (Customer.first_name.ilike(search_term))
|
|
| (Customer.last_name.ilike(search_term))
|
|
| (Customer.customer_number.ilike(search_term))
|
|
)
|
|
|
|
if is_active is not None:
|
|
query = query.filter(Customer.is_active == is_active)
|
|
|
|
# Get total count
|
|
total = query.count()
|
|
|
|
# Get paginated results with vendor info
|
|
customers = (
|
|
query.add_columns(Vendor.name.label("vendor_name"), Vendor.vendor_code)
|
|
.order_by(Customer.created_at.desc())
|
|
.offset(skip)
|
|
.limit(limit)
|
|
.all()
|
|
)
|
|
|
|
# Format response
|
|
result = []
|
|
for row in customers:
|
|
customer = row[0]
|
|
vendor_name = row[1]
|
|
vendor_code = row[2]
|
|
|
|
customer_dict = {
|
|
"id": customer.id,
|
|
"vendor_id": customer.vendor_id,
|
|
"email": customer.email,
|
|
"first_name": customer.first_name,
|
|
"last_name": customer.last_name,
|
|
"phone": customer.phone,
|
|
"customer_number": customer.customer_number,
|
|
"marketing_consent": customer.marketing_consent,
|
|
"preferred_language": customer.preferred_language,
|
|
"last_order_date": customer.last_order_date,
|
|
"total_orders": customer.total_orders,
|
|
"total_spent": float(customer.total_spent) if customer.total_spent else 0,
|
|
"is_active": customer.is_active,
|
|
"created_at": customer.created_at,
|
|
"updated_at": customer.updated_at,
|
|
"vendor_name": vendor_name,
|
|
"vendor_code": vendor_code,
|
|
}
|
|
result.append(customer_dict)
|
|
|
|
return result, total
|
|
|
|
def get_customer_stats(
|
|
self,
|
|
db: Session,
|
|
vendor_id: int | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Get customer statistics.
|
|
|
|
Args:
|
|
db: Database session
|
|
vendor_id: Optional vendor ID filter
|
|
|
|
Returns:
|
|
Dict with customer statistics
|
|
"""
|
|
query = db.query(Customer)
|
|
|
|
if vendor_id:
|
|
query = query.filter(Customer.vendor_id == vendor_id)
|
|
|
|
total = query.count()
|
|
active = query.filter(Customer.is_active == True).count() # noqa: E712
|
|
inactive = query.filter(Customer.is_active == False).count() # noqa: E712
|
|
with_orders = query.filter(Customer.total_orders > 0).count()
|
|
|
|
# Total spent across all customers
|
|
total_spent_result = query.with_entities(func.sum(Customer.total_spent)).scalar()
|
|
total_spent = float(total_spent_result) if total_spent_result else 0
|
|
|
|
# Average order value
|
|
total_orders_result = query.with_entities(func.sum(Customer.total_orders)).scalar()
|
|
total_orders = int(total_orders_result) if total_orders_result else 0
|
|
avg_order_value = total_spent / total_orders if total_orders > 0 else 0
|
|
|
|
return {
|
|
"total": total,
|
|
"active": active,
|
|
"inactive": inactive,
|
|
"with_orders": with_orders,
|
|
"total_spent": total_spent,
|
|
"total_orders": total_orders,
|
|
"avg_order_value": round(avg_order_value, 2),
|
|
}
|
|
|
|
def get_customer(
|
|
self,
|
|
db: Session,
|
|
customer_id: int,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Get customer details by ID.
|
|
|
|
Args:
|
|
db: Database session
|
|
customer_id: Customer ID
|
|
|
|
Returns:
|
|
Customer dict with vendor info
|
|
|
|
Raises:
|
|
CustomerNotFoundException: If customer not found
|
|
"""
|
|
result = (
|
|
db.query(Customer)
|
|
.join(Vendor, Customer.vendor_id == Vendor.id)
|
|
.add_columns(Vendor.name.label("vendor_name"), Vendor.vendor_code)
|
|
.filter(Customer.id == customer_id)
|
|
.first()
|
|
)
|
|
|
|
if not result:
|
|
raise CustomerNotFoundException(str(customer_id))
|
|
|
|
customer = result[0]
|
|
return {
|
|
"id": customer.id,
|
|
"vendor_id": customer.vendor_id,
|
|
"email": customer.email,
|
|
"first_name": customer.first_name,
|
|
"last_name": customer.last_name,
|
|
"phone": customer.phone,
|
|
"customer_number": customer.customer_number,
|
|
"marketing_consent": customer.marketing_consent,
|
|
"preferred_language": customer.preferred_language,
|
|
"last_order_date": customer.last_order_date,
|
|
"total_orders": customer.total_orders,
|
|
"total_spent": float(customer.total_spent) if customer.total_spent else 0,
|
|
"is_active": customer.is_active,
|
|
"created_at": customer.created_at,
|
|
"updated_at": customer.updated_at,
|
|
"vendor_name": result[1],
|
|
"vendor_code": result[2],
|
|
}
|
|
|
|
def toggle_customer_status(
|
|
self,
|
|
db: Session,
|
|
customer_id: int,
|
|
admin_email: str,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Toggle customer active status.
|
|
|
|
Args:
|
|
db: Database session
|
|
customer_id: Customer ID
|
|
admin_email: Admin user email for logging
|
|
|
|
Returns:
|
|
Dict with customer ID, new status, and message
|
|
|
|
Raises:
|
|
CustomerNotFoundException: If customer not found
|
|
"""
|
|
customer = db.query(Customer).filter(Customer.id == customer_id).first()
|
|
|
|
if not customer:
|
|
raise CustomerNotFoundException(str(customer_id))
|
|
|
|
customer.is_active = not customer.is_active
|
|
db.flush()
|
|
db.refresh(customer)
|
|
|
|
status = "activated" if customer.is_active else "deactivated"
|
|
logger.info(f"Customer {customer.email} {status} by admin {admin_email}")
|
|
|
|
return {
|
|
"id": customer.id,
|
|
"is_active": customer.is_active,
|
|
"message": f"Customer {status} successfully",
|
|
}
|
|
|
|
|
|
# Singleton instance
|
|
admin_customer_service = AdminCustomerService()
|