This commit introduces a protocol-based metrics architecture that allows each module to provide its own statistics for dashboards without creating cross-module dependencies. Key changes: - Add MetricsProviderProtocol and MetricValue dataclass in contracts module - Add StatsAggregatorService in core module that discovers and aggregates metrics from all enabled modules - Implement metrics providers for all modules: - tenancy: vendor/user counts, team members, domains - customers: customer counts - cms: pages, media files - catalog: products - inventory: stock levels - orders: order counts, revenue - marketplace: import jobs, staging products - Update dashboard routes to use StatsAggregator instead of direct imports - Fix VendorPlatform junction table usage (Vendor.platform_id doesn't exist) - Add comprehensive documentation for the pattern This architecture ensures: - Dashboards always work (aggregator in core) - Each module owns its metrics (no cross-module coupling) - Optional modules are truly optional (can be removed without breaking app) - Multi-platform vendors are properly supported via VendorPlatform table Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
205 lines
6.6 KiB
Python
205 lines
6.6 KiB
Python
# app/modules/customers/services/customer_metrics.py
|
|
"""
|
|
Metrics provider for the customers module.
|
|
|
|
Provides metrics for:
|
|
- Customer counts
|
|
- New customers
|
|
- Customer addresses
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from typing import TYPE_CHECKING
|
|
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.modules.contracts.metrics import (
|
|
MetricValue,
|
|
MetricsContext,
|
|
MetricsProviderProtocol,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
pass
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CustomerMetricsProvider:
|
|
"""
|
|
Metrics provider for customers module.
|
|
|
|
Provides customer-related metrics for vendor and platform dashboards.
|
|
"""
|
|
|
|
@property
|
|
def metrics_category(self) -> str:
|
|
return "customers"
|
|
|
|
def get_vendor_metrics(
|
|
self,
|
|
db: Session,
|
|
vendor_id: int,
|
|
context: MetricsContext | None = None,
|
|
) -> list[MetricValue]:
|
|
"""
|
|
Get customer metrics for a specific vendor.
|
|
|
|
Provides:
|
|
- Total customers
|
|
- New customers (in period)
|
|
- Customers with addresses
|
|
"""
|
|
from app.modules.customers.models import Customer, CustomerAddress
|
|
|
|
try:
|
|
# Total customers
|
|
total_customers = (
|
|
db.query(Customer).filter(Customer.vendor_id == vendor_id).count()
|
|
)
|
|
|
|
# New customers (default to last 30 days)
|
|
date_from = context.date_from if context else None
|
|
if date_from is None:
|
|
date_from = datetime.utcnow() - timedelta(days=30)
|
|
|
|
new_customers_query = db.query(Customer).filter(
|
|
Customer.vendor_id == vendor_id,
|
|
Customer.created_at >= date_from,
|
|
)
|
|
if context and context.date_to:
|
|
new_customers_query = new_customers_query.filter(
|
|
Customer.created_at <= context.date_to
|
|
)
|
|
new_customers = new_customers_query.count()
|
|
|
|
# Customers with addresses
|
|
customers_with_addresses = (
|
|
db.query(func.count(func.distinct(CustomerAddress.customer_id)))
|
|
.join(Customer, Customer.id == CustomerAddress.customer_id)
|
|
.filter(Customer.vendor_id == vendor_id)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
return [
|
|
MetricValue(
|
|
key="customers.total",
|
|
value=total_customers,
|
|
label="Total Customers",
|
|
category="customers",
|
|
icon="users",
|
|
description="Total number of customers",
|
|
),
|
|
MetricValue(
|
|
key="customers.new",
|
|
value=new_customers,
|
|
label="New Customers",
|
|
category="customers",
|
|
icon="user-plus",
|
|
description="Customers acquired in the period",
|
|
),
|
|
MetricValue(
|
|
key="customers.with_addresses",
|
|
value=customers_with_addresses,
|
|
label="With Addresses",
|
|
category="customers",
|
|
icon="map-pin",
|
|
description="Customers who have saved addresses",
|
|
),
|
|
]
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get customer vendor metrics: {e}")
|
|
return []
|
|
|
|
def get_platform_metrics(
|
|
self,
|
|
db: Session,
|
|
platform_id: int,
|
|
context: MetricsContext | None = None,
|
|
) -> list[MetricValue]:
|
|
"""
|
|
Get customer metrics aggregated for a platform.
|
|
|
|
For platforms, aggregates customer data across all vendors.
|
|
"""
|
|
from app.modules.customers.models import Customer
|
|
from app.modules.tenancy.models import VendorPlatform
|
|
|
|
try:
|
|
# Get all vendor IDs for this platform using VendorPlatform junction table
|
|
vendor_ids = (
|
|
db.query(VendorPlatform.vendor_id)
|
|
.filter(
|
|
VendorPlatform.platform_id == platform_id,
|
|
VendorPlatform.is_active == True,
|
|
)
|
|
.subquery()
|
|
)
|
|
|
|
# Total customers across all vendors
|
|
total_customers = (
|
|
db.query(Customer).filter(Customer.vendor_id.in_(vendor_ids)).count()
|
|
)
|
|
|
|
# Unique customers (by email across platform)
|
|
unique_customer_emails = (
|
|
db.query(func.count(func.distinct(Customer.email)))
|
|
.filter(Customer.vendor_id.in_(vendor_ids))
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
# New customers (default to last 30 days)
|
|
date_from = context.date_from if context else None
|
|
if date_from is None:
|
|
date_from = datetime.utcnow() - timedelta(days=30)
|
|
|
|
new_customers_query = db.query(Customer).filter(
|
|
Customer.vendor_id.in_(vendor_ids),
|
|
Customer.created_at >= date_from,
|
|
)
|
|
if context and context.date_to:
|
|
new_customers_query = new_customers_query.filter(
|
|
Customer.created_at <= context.date_to
|
|
)
|
|
new_customers = new_customers_query.count()
|
|
|
|
return [
|
|
MetricValue(
|
|
key="customers.total",
|
|
value=total_customers,
|
|
label="Total Customers",
|
|
category="customers",
|
|
icon="users",
|
|
description="Total customer records across all vendors",
|
|
),
|
|
MetricValue(
|
|
key="customers.unique_emails",
|
|
value=unique_customer_emails,
|
|
label="Unique Customers",
|
|
category="customers",
|
|
icon="user",
|
|
description="Unique customer emails across platform",
|
|
),
|
|
MetricValue(
|
|
key="customers.new",
|
|
value=new_customers,
|
|
label="New Customers",
|
|
category="customers",
|
|
icon="user-plus",
|
|
description="Customers acquired in the period",
|
|
),
|
|
]
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get customer platform metrics: {e}")
|
|
return []
|
|
|
|
|
|
# Singleton instance
|
|
customer_metrics_provider = CustomerMetricsProvider()
|
|
|
|
__all__ = ["CustomerMetricsProvider", "customer_metrics_provider"]
|