feat: implement metrics provider pattern for modular dashboard statistics
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>
This commit is contained in:
261
app/modules/catalog/services/catalog_metrics.py
Normal file
261
app/modules/catalog/services/catalog_metrics.py
Normal file
@@ -0,0 +1,261 @@
|
||||
# app/modules/catalog/services/catalog_metrics.py
|
||||
"""
|
||||
Metrics provider for the catalog module.
|
||||
|
||||
Provides metrics for:
|
||||
- Product counts
|
||||
- Active/inactive products
|
||||
- Featured products
|
||||
"""
|
||||
|
||||
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 CatalogMetricsProvider:
|
||||
"""
|
||||
Metrics provider for catalog module.
|
||||
|
||||
Provides product-related metrics for vendor and platform dashboards.
|
||||
"""
|
||||
|
||||
@property
|
||||
def metrics_category(self) -> str:
|
||||
return "catalog"
|
||||
|
||||
def get_vendor_metrics(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
context: MetricsContext | None = None,
|
||||
) -> list[MetricValue]:
|
||||
"""
|
||||
Get product metrics for a specific vendor.
|
||||
|
||||
Provides:
|
||||
- Total products
|
||||
- Active products
|
||||
- Featured products
|
||||
- New products (in period)
|
||||
"""
|
||||
from app.modules.catalog.models import Product
|
||||
|
||||
try:
|
||||
# Total products
|
||||
total_products = (
|
||||
db.query(Product).filter(Product.vendor_id == vendor_id).count()
|
||||
)
|
||||
|
||||
# Active products
|
||||
active_products = (
|
||||
db.query(Product)
|
||||
.filter(Product.vendor_id == vendor_id, Product.is_active == True)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Featured products
|
||||
featured_products = (
|
||||
db.query(Product)
|
||||
.filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.is_featured == True,
|
||||
Product.is_active == True,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
# New products (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_products_query = db.query(Product).filter(
|
||||
Product.vendor_id == vendor_id,
|
||||
Product.created_at >= date_from,
|
||||
)
|
||||
if context and context.date_to:
|
||||
new_products_query = new_products_query.filter(
|
||||
Product.created_at <= context.date_to
|
||||
)
|
||||
new_products = new_products_query.count()
|
||||
|
||||
# Products with translations
|
||||
products_with_translations = (
|
||||
db.query(func.count(func.distinct(Product.id)))
|
||||
.filter(Product.vendor_id == vendor_id)
|
||||
.join(Product.translations)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
return [
|
||||
MetricValue(
|
||||
key="catalog.total_products",
|
||||
value=total_products,
|
||||
label="Total Products",
|
||||
category="catalog",
|
||||
icon="box",
|
||||
description="Total products in catalog",
|
||||
),
|
||||
MetricValue(
|
||||
key="catalog.active_products",
|
||||
value=active_products,
|
||||
label="Active Products",
|
||||
category="catalog",
|
||||
icon="check-circle",
|
||||
description="Products that are active and visible",
|
||||
),
|
||||
MetricValue(
|
||||
key="catalog.featured_products",
|
||||
value=featured_products,
|
||||
label="Featured Products",
|
||||
category="catalog",
|
||||
icon="star",
|
||||
description="Products marked as featured",
|
||||
),
|
||||
MetricValue(
|
||||
key="catalog.new_products",
|
||||
value=new_products,
|
||||
label="New Products",
|
||||
category="catalog",
|
||||
icon="plus-circle",
|
||||
description="Products added in the period",
|
||||
),
|
||||
]
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get catalog vendor metrics: {e}")
|
||||
return []
|
||||
|
||||
def get_platform_metrics(
|
||||
self,
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
context: MetricsContext | None = None,
|
||||
) -> list[MetricValue]:
|
||||
"""
|
||||
Get product metrics aggregated for a platform.
|
||||
|
||||
Aggregates catalog data across all vendors.
|
||||
"""
|
||||
from app.modules.catalog.models import Product
|
||||
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 products
|
||||
total_products = (
|
||||
db.query(Product).filter(Product.vendor_id.in_(vendor_ids)).count()
|
||||
)
|
||||
|
||||
# Active products
|
||||
active_products = (
|
||||
db.query(Product)
|
||||
.filter(Product.vendor_id.in_(vendor_ids), Product.is_active == True)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Featured products
|
||||
featured_products = (
|
||||
db.query(Product)
|
||||
.filter(
|
||||
Product.vendor_id.in_(vendor_ids),
|
||||
Product.is_featured == True,
|
||||
Product.is_active == True,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Vendors with products
|
||||
vendors_with_products = (
|
||||
db.query(func.count(func.distinct(Product.vendor_id)))
|
||||
.filter(Product.vendor_id.in_(vendor_ids))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Average products per vendor
|
||||
total_vendors = (
|
||||
db.query(VendorPlatform)
|
||||
.filter(
|
||||
VendorPlatform.platform_id == platform_id,
|
||||
VendorPlatform.is_active == True,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
avg_products = round(total_products / total_vendors, 1) if total_vendors > 0 else 0
|
||||
|
||||
return [
|
||||
MetricValue(
|
||||
key="catalog.total_products",
|
||||
value=total_products,
|
||||
label="Total Products",
|
||||
category="catalog",
|
||||
icon="box",
|
||||
description="Total products across all vendors",
|
||||
),
|
||||
MetricValue(
|
||||
key="catalog.active_products",
|
||||
value=active_products,
|
||||
label="Active Products",
|
||||
category="catalog",
|
||||
icon="check-circle",
|
||||
description="Products that are active and visible",
|
||||
),
|
||||
MetricValue(
|
||||
key="catalog.featured_products",
|
||||
value=featured_products,
|
||||
label="Featured Products",
|
||||
category="catalog",
|
||||
icon="star",
|
||||
description="Products marked as featured",
|
||||
),
|
||||
MetricValue(
|
||||
key="catalog.vendors_with_products",
|
||||
value=vendors_with_products,
|
||||
label="Vendors with Products",
|
||||
category="catalog",
|
||||
icon="store",
|
||||
description="Vendors that have created products",
|
||||
),
|
||||
MetricValue(
|
||||
key="catalog.avg_products_per_vendor",
|
||||
value=avg_products,
|
||||
label="Avg Products/Vendor",
|
||||
category="catalog",
|
||||
icon="calculator",
|
||||
description="Average products per vendor",
|
||||
),
|
||||
]
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get catalog platform metrics: {e}")
|
||||
return []
|
||||
|
||||
|
||||
# Singleton instance
|
||||
catalog_metrics_provider = CatalogMetricsProvider()
|
||||
|
||||
__all__ = ["CatalogMetricsProvider", "catalog_metrics_provider"]
|
||||
Reference in New Issue
Block a user