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>
238 lines
7.3 KiB
Python
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",
|
|
]
|