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:
2026-02-03 21:11:29 +01:00
parent a76128e016
commit a8fae0fbc7
28 changed files with 3745 additions and 269 deletions

View File

@@ -44,6 +44,8 @@ if TYPE_CHECKING:
from fastapi import APIRouter
from pydantic import BaseModel
from app.modules.contracts.metrics import MetricsProviderProtocol
from app.modules.enums import FrontendType
@@ -391,6 +393,26 @@ class ModuleDefinition:
# )
context_providers: dict[FrontendType, Callable[..., dict[str, Any]]] = field(default_factory=dict)
# =========================================================================
# Metrics Provider (Module-Driven Statistics)
# =========================================================================
# Callable that returns a MetricsProviderProtocol implementation.
# Use a callable (factory function) to enable lazy loading and avoid
# circular imports. Each module can provide its own metrics for dashboards.
#
# Example:
# 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,
# )
#
# The provider will be discovered by core's StatsAggregator service.
metrics_provider: "Callable[[], MetricsProviderProtocol] | None" = None
# =========================================================================
# Menu Item Methods (Legacy - uses menu_items dict of IDs)
# =========================================================================
@@ -755,6 +777,28 @@ class ModuleDefinition:
"""Get list of frontend types this module provides context for."""
return list(self.context_providers.keys())
# =========================================================================
# Metrics Provider Methods
# =========================================================================
def has_metrics_provider(self) -> bool:
"""Check if this module has a metrics provider."""
return self.metrics_provider is not None
def get_metrics_provider_instance(self) -> "MetricsProviderProtocol | None":
"""
Get the metrics provider instance for this module.
Calls the metrics_provider factory function to get the provider.
Returns None if no provider is configured.
Returns:
MetricsProviderProtocol instance, or None
"""
if self.metrics_provider is None:
return None
return self.metrics_provider()
# =========================================================================
# Magic Methods
# =========================================================================