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

@@ -2,7 +2,9 @@
"""
Analytics module schemas for statistics and reporting.
This is the canonical location for stats schemas.
Base dashboard schemas are defined in core.schemas.dashboard.
This module re-exports them for backward compatibility and adds
analytics-specific schemas (trends, reports, etc.).
"""
from datetime import datetime
@@ -11,206 +13,29 @@ from typing import Any
from pydantic import BaseModel, Field
class StatsResponse(BaseModel):
"""Comprehensive platform statistics response schema."""
total_products: int
unique_brands: int
unique_categories: int
unique_marketplaces: int = 0
unique_vendors: int = 0
total_inventory_entries: int = 0
total_inventory_quantity: int = 0
class MarketplaceStatsResponse(BaseModel):
"""Statistics per marketplace response schema."""
marketplace: str
total_products: int
unique_vendors: int
unique_brands: int
# Re-export base dashboard schemas from core for backward compatibility
# These are the canonical definitions in core module
from app.modules.core.schemas.dashboard import (
AdminDashboardResponse,
ImportStatsResponse,
MarketplaceStatsResponse,
OrderStatsBasicResponse,
PlatformStatsResponse,
ProductStatsResponse,
StatsResponse,
UserStatsResponse,
VendorCustomerStats,
VendorDashboardStatsResponse,
VendorInfo,
VendorOrderStats,
VendorProductStats,
VendorRevenueStats,
VendorStatsResponse,
)
# ============================================================================
# Import Statistics
# ============================================================================
class ImportStatsResponse(BaseModel):
"""Import job statistics response schema.
Used by: GET /api/v1/admin/marketplace-import-jobs/stats
"""
total: int = Field(..., description="Total number of import jobs")
pending: int = Field(..., description="Jobs waiting to start")
processing: int = Field(..., description="Jobs currently running")
completed: int = Field(..., description="Successfully completed jobs")
failed: int = Field(..., description="Failed jobs")
success_rate: float = Field(..., description="Percentage of successful imports")
# ============================================================================
# User Statistics
# ============================================================================
class UserStatsResponse(BaseModel):
"""User statistics response schema.
Used by: Platform statistics endpoints
"""
total_users: int = Field(..., description="Total number of users")
active_users: int = Field(..., description="Number of active users")
inactive_users: int = Field(..., description="Number of inactive users")
admin_users: int = Field(..., description="Number of admin users")
activation_rate: float = Field(..., description="Percentage of active users")
# ============================================================================
# Vendor Statistics (Admin)
# ============================================================================
class VendorStatsResponse(BaseModel):
"""Vendor statistics response schema for admin dashboard.
Used by: GET /api/v1/admin/vendors/stats
"""
total: int = Field(..., description="Total number of vendors")
verified: int = Field(..., description="Number of verified vendors")
pending: int = Field(..., description="Number of pending verification vendors")
inactive: int = Field(..., description="Number of inactive vendors")
# ============================================================================
# Product Statistics
# ============================================================================
class ProductStatsResponse(BaseModel):
"""Product statistics response schema.
Used by: Platform statistics endpoints
"""
total_products: int = Field(0, description="Total number of products")
active_products: int = Field(0, description="Number of active products")
out_of_stock: int = Field(0, description="Number of out-of-stock products")
# ============================================================================
# Platform Statistics (Combined)
# ============================================================================
class OrderStatsBasicResponse(BaseModel):
"""Basic order statistics (stub until Order model is fully implemented).
Used by: Platform statistics endpoints
"""
total_orders: int = Field(0, description="Total number of orders")
pending_orders: int = Field(0, description="Number of pending orders")
completed_orders: int = Field(0, description="Number of completed orders")
class PlatformStatsResponse(BaseModel):
"""Combined platform statistics response schema.
Used by: GET /api/v1/admin/dashboard/stats/platform
"""
users: UserStatsResponse
vendors: VendorStatsResponse
products: ProductStatsResponse
orders: OrderStatsBasicResponse
imports: ImportStatsResponse
# ============================================================================
# Admin Dashboard Response
# ============================================================================
class AdminDashboardResponse(BaseModel):
"""Admin dashboard response schema.
Used by: GET /api/v1/admin/dashboard
"""
platform: dict[str, Any] = Field(..., description="Platform information")
users: UserStatsResponse
vendors: VendorStatsResponse
recent_vendors: list[dict[str, Any]] = Field(
default_factory=list, description="Recent vendors"
)
recent_imports: list[dict[str, Any]] = Field(
default_factory=list, description="Recent import jobs"
)
# ============================================================================
# Vendor Dashboard Statistics
# ============================================================================
class VendorProductStats(BaseModel):
"""Vendor product statistics."""
total: int = Field(0, description="Total products in catalog")
active: int = Field(0, description="Active products")
class VendorOrderStats(BaseModel):
"""Vendor order statistics."""
total: int = Field(0, description="Total orders")
pending: int = Field(0, description="Pending orders")
completed: int = Field(0, description="Completed orders")
class VendorCustomerStats(BaseModel):
"""Vendor customer statistics."""
total: int = Field(0, description="Total customers")
active: int = Field(0, description="Active customers")
class VendorRevenueStats(BaseModel):
"""Vendor revenue statistics."""
total: float = Field(0, description="Total revenue")
this_month: float = Field(0, description="Revenue this month")
class VendorInfo(BaseModel):
"""Vendor basic info for dashboard."""
id: int
name: str
vendor_code: str
class VendorDashboardStatsResponse(BaseModel):
"""Vendor dashboard statistics response schema.
Used by: GET /api/v1/vendor/dashboard/stats
"""
vendor: VendorInfo
products: VendorProductStats
orders: VendorOrderStats
customers: VendorCustomerStats
revenue: VendorRevenueStats
# ============================================================================
# Vendor Analytics
# Vendor Analytics (Analytics-specific, not in core)
# ============================================================================
@@ -327,6 +152,7 @@ class OrderStatsResponse(BaseModel):
__all__ = [
# Re-exported from core.schemas.dashboard (for backward compatibility)
"StatsResponse",
"MarketplaceStatsResponse",
"ImportStatsResponse",
@@ -342,6 +168,7 @@ __all__ = [
"VendorRevenueStats",
"VendorInfo",
"VendorDashboardStatsResponse",
# Analytics-specific schemas
"VendorAnalyticsImports",
"VendorAnalyticsCatalog",
"VendorAnalyticsInventory",