# app/modules/contracts/metrics.py """ Metrics provider protocol for cross-module statistics aggregation. This module defines the protocol that modules implement to expose their own metrics. The core module's StatsAggregator discovers and aggregates all providers. Benefits: - Each module owns its metrics (no cross-module coupling) - Dashboards always work (aggregator is in core) - Optional modules are truly optional (can be removed without breaking app) - Easy to add new metrics (just implement protocol in your module) Usage: # 1. Implement the protocol in your module class OrderMetricsProvider: @property def metrics_category(self) -> str: return "orders" def get_store_metrics(self, db, store_id, **kwargs) -> list[MetricValue]: return [ MetricValue(key="orders.total", value=42, label="Total Orders", category="orders") ] # 2. Register in module definition def _get_metrics_provider(): from app.modules.orders.services.order_metrics import order_metrics_provider return order_metrics_provider orders_module = ModuleDefinition( code="orders", metrics_provider=_get_metrics_provider, # ... ) # 3. Metrics appear automatically in dashboards when module is enabled """ from dataclasses import dataclass from datetime import datetime from typing import TYPE_CHECKING, Protocol, runtime_checkable if TYPE_CHECKING: from sqlalchemy.orm import Session @dataclass class MetricValue: """ Standard metric value with metadata. This is the unit of data returned by metrics providers. It contains both the value and metadata for display. Attributes: key: Unique identifier for this metric (e.g., "orders.total_count") Format: "{category}.{metric_name}" for consistency value: The actual metric value (int, float, or str) label: Human-readable label for display (e.g., "Total Orders") category: Grouping category (should match metrics_category of provider) icon: Optional Lucide icon name for UI display (e.g., "shopping-cart") description: Optional longer description of what this metric represents unit: Optional unit suffix (e.g., "EUR", "%", "items") trend: Optional trend indicator ("up", "down", "stable") trend_value: Optional numeric trend value (e.g., percentage change) Example: MetricValue( key="orders.total_count", value=1234, label="Total Orders", category="orders", icon="shopping-cart", unit="orders", ) """ key: str value: int | float | str label: str category: str icon: str | None = None description: str | None = None unit: str | None = None trend: str | None = None # "up", "down", "stable" trend_value: float | None = None @dataclass class MetricsContext: """ Context for metrics collection. Provides filtering and scoping options for metrics providers. Attributes: date_from: Start of date range filter date_to: End of date range filter include_trends: Whether to calculate trends (may be expensive) period: Time period for analytics (e.g., "7d", "30d", "90d", "1y") """ date_from: datetime | None = None date_to: datetime | None = None include_trends: bool = False period: str = "30d" @runtime_checkable class MetricsProviderProtocol(Protocol): """ Protocol for modules that provide metrics/statistics. Each module implements this to expose its own metrics. The core module's StatsAggregator discovers and aggregates all providers. Implementation Notes: - Providers should be stateless (all data via db session) - Return empty list if no metrics available (don't raise) - Use consistent key format: "{category}.{metric_name}" - Include icon hints for UI rendering - Be mindful of query performance (use efficient aggregations) Example Implementation: class OrderMetricsProvider: @property def metrics_category(self) -> str: return "orders" def get_store_metrics( self, db: Session, store_id: int, context: MetricsContext | None = None ) -> list[MetricValue]: from app.modules.orders.models import Order total = db.query(Order).filter(Order.store_id == store_id).count() return [ MetricValue( key="orders.total", value=total, label="Total Orders", category="orders", icon="shopping-cart" ) ] def get_platform_metrics( self, db: Session, platform_id: int, context: MetricsContext | None = None ) -> list[MetricValue]: # Aggregate across all stores in platform ... """ @property def metrics_category(self) -> str: """ Category name for this provider's metrics. Should be a short, lowercase identifier matching the module's domain. Examples: "orders", "inventory", "customers", "billing" Returns: Category string used for grouping metrics """ ... def get_store_metrics( self, db: "Session", store_id: int, context: MetricsContext | None = None, ) -> list[MetricValue]: """ Get metrics for a specific store. Called by the store dashboard to display store-scoped statistics. Should only include data belonging to the specified store. Args: db: Database session for queries store_id: ID of the store to get metrics for context: Optional filtering/scoping context Returns: List of MetricValue objects for this store """ ... def get_platform_metrics( self, db: "Session", platform_id: int, context: MetricsContext | None = None, ) -> list[MetricValue]: """ Get metrics aggregated for a platform. Called by the admin dashboard to display platform-wide statistics. Should aggregate data across all stores in the platform. Args: db: Database session for queries platform_id: ID of the platform to get metrics for context: Optional filtering/scoping context Returns: List of MetricValue objects aggregated for the platform """ ... def get_merchant_metrics( self, db: "Session", merchant_id: int, context: MetricsContext | None = None, ) -> list[MetricValue]: """ Get metrics scoped to a specific merchant. Called by the merchant dashboard to display merchant-scoped statistics. Should only include data belonging to the specified merchant. Args: db: Database session for queries merchant_id: ID of the merchant to get metrics for context: Optional filtering/scoping context Returns: List of MetricValue objects for this merchant """ ... __all__ = [ "MetricValue", "MetricsContext", "MetricsProviderProtocol", ]