Files
orion/app/modules/contracts/metrics.py
Samir Boulahtit ff852f1ab3 fix: use metrics provider pattern for merchant dashboard stats
The merchant dashboard was showing subscription count as "Total Stores".
Add get_merchant_metrics() to MetricsProviderProtocol and implement it
in tenancy, billing, and customer providers. Dashboard now fetches real
stats from a new /merchants/core/dashboard/stats endpoint and displays
4 cards: active subscriptions, total stores, customers, team members.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:28:59 +01:00

238 lines
7.3 KiB
Python

# 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",
]