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:
@@ -29,6 +29,13 @@ def _get_vendor_router():
|
||||
return vendor_router
|
||||
|
||||
|
||||
def _get_metrics_provider():
|
||||
"""Lazy import of metrics provider to avoid circular imports."""
|
||||
from app.modules.inventory.services.inventory_metrics import inventory_metrics_provider
|
||||
|
||||
return inventory_metrics_provider
|
||||
|
||||
|
||||
# Inventory module definition
|
||||
inventory_module = ModuleDefinition(
|
||||
code="inventory",
|
||||
@@ -131,6 +138,8 @@ inventory_module = ModuleDefinition(
|
||||
models_path="app.modules.inventory.models",
|
||||
schemas_path="app.modules.inventory.schemas",
|
||||
exceptions_path="app.modules.inventory.exceptions",
|
||||
# Metrics provider for dashboard statistics
|
||||
metrics_provider=_get_metrics_provider,
|
||||
)
|
||||
|
||||
|
||||
|
||||
318
app/modules/inventory/services/inventory_metrics.py
Normal file
318
app/modules/inventory/services/inventory_metrics.py
Normal file
@@ -0,0 +1,318 @@
|
||||
# app/modules/inventory/services/inventory_metrics.py
|
||||
"""
|
||||
Metrics provider for the inventory module.
|
||||
|
||||
Provides metrics for:
|
||||
- Inventory quantities
|
||||
- Stock levels
|
||||
- Low stock alerts
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.modules.contracts.metrics import (
|
||||
MetricValue,
|
||||
MetricsContext,
|
||||
MetricsProviderProtocol,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InventoryMetricsProvider:
|
||||
"""
|
||||
Metrics provider for inventory module.
|
||||
|
||||
Provides stock and inventory metrics for vendor and platform dashboards.
|
||||
"""
|
||||
|
||||
@property
|
||||
def metrics_category(self) -> str:
|
||||
return "inventory"
|
||||
|
||||
def get_vendor_metrics(
|
||||
self,
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
context: MetricsContext | None = None,
|
||||
) -> list[MetricValue]:
|
||||
"""
|
||||
Get inventory metrics for a specific vendor.
|
||||
|
||||
Provides:
|
||||
- Total inventory quantity
|
||||
- Reserved quantity
|
||||
- Available quantity
|
||||
- Inventory locations
|
||||
- Low stock items
|
||||
"""
|
||||
from app.modules.inventory.models import Inventory
|
||||
|
||||
try:
|
||||
# Total inventory
|
||||
total_quantity = (
|
||||
db.query(func.sum(Inventory.quantity))
|
||||
.filter(Inventory.vendor_id == vendor_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Reserved inventory
|
||||
reserved_quantity = (
|
||||
db.query(func.sum(Inventory.reserved_quantity))
|
||||
.filter(Inventory.vendor_id == vendor_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Available inventory
|
||||
available_quantity = int(total_quantity) - int(reserved_quantity)
|
||||
|
||||
# Inventory entries (SKU/location combinations)
|
||||
inventory_entries = (
|
||||
db.query(Inventory).filter(Inventory.vendor_id == vendor_id).count()
|
||||
)
|
||||
|
||||
# Unique locations
|
||||
unique_locations = (
|
||||
db.query(func.count(func.distinct(Inventory.location)))
|
||||
.filter(Inventory.vendor_id == vendor_id)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Low stock items (quantity < 10 and > 0)
|
||||
low_stock_items = (
|
||||
db.query(Inventory)
|
||||
.filter(
|
||||
Inventory.vendor_id == vendor_id,
|
||||
Inventory.quantity > 0,
|
||||
Inventory.quantity < 10,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Out of stock items (quantity = 0)
|
||||
out_of_stock_items = (
|
||||
db.query(Inventory)
|
||||
.filter(Inventory.vendor_id == vendor_id, Inventory.quantity == 0)
|
||||
.count()
|
||||
)
|
||||
|
||||
return [
|
||||
MetricValue(
|
||||
key="inventory.total_quantity",
|
||||
value=int(total_quantity),
|
||||
label="Total Stock",
|
||||
category="inventory",
|
||||
icon="package",
|
||||
unit="items",
|
||||
description="Total inventory quantity",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.reserved_quantity",
|
||||
value=int(reserved_quantity),
|
||||
label="Reserved",
|
||||
category="inventory",
|
||||
icon="lock",
|
||||
unit="items",
|
||||
description="Inventory reserved for orders",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.available_quantity",
|
||||
value=available_quantity,
|
||||
label="Available",
|
||||
category="inventory",
|
||||
icon="check",
|
||||
unit="items",
|
||||
description="Inventory available for sale",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.entries",
|
||||
value=inventory_entries,
|
||||
label="SKU/Location Entries",
|
||||
category="inventory",
|
||||
icon="list",
|
||||
description="Total inventory entries",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.locations",
|
||||
value=unique_locations,
|
||||
label="Locations",
|
||||
category="inventory",
|
||||
icon="map-pin",
|
||||
description="Unique storage locations",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.low_stock",
|
||||
value=low_stock_items,
|
||||
label="Low Stock",
|
||||
category="inventory",
|
||||
icon="alert-triangle",
|
||||
description="Items with quantity < 10",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.out_of_stock",
|
||||
value=out_of_stock_items,
|
||||
label="Out of Stock",
|
||||
category="inventory",
|
||||
icon="x-circle",
|
||||
description="Items with zero quantity",
|
||||
),
|
||||
]
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get inventory vendor metrics: {e}")
|
||||
return []
|
||||
|
||||
def get_platform_metrics(
|
||||
self,
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
context: MetricsContext | None = None,
|
||||
) -> list[MetricValue]:
|
||||
"""
|
||||
Get inventory metrics aggregated for a platform.
|
||||
|
||||
Aggregates stock data across all vendors.
|
||||
"""
|
||||
from app.modules.inventory.models import Inventory
|
||||
from app.modules.tenancy.models import VendorPlatform
|
||||
|
||||
try:
|
||||
# Get all vendor IDs for this platform using VendorPlatform junction table
|
||||
vendor_ids = (
|
||||
db.query(VendorPlatform.vendor_id)
|
||||
.filter(
|
||||
VendorPlatform.platform_id == platform_id,
|
||||
VendorPlatform.is_active == True,
|
||||
)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
# Total inventory
|
||||
total_quantity = (
|
||||
db.query(func.sum(Inventory.quantity))
|
||||
.filter(Inventory.vendor_id.in_(vendor_ids))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Reserved inventory
|
||||
reserved_quantity = (
|
||||
db.query(func.sum(Inventory.reserved_quantity))
|
||||
.filter(Inventory.vendor_id.in_(vendor_ids))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Available inventory
|
||||
available_quantity = int(total_quantity) - int(reserved_quantity)
|
||||
|
||||
# Total inventory entries
|
||||
inventory_entries = (
|
||||
db.query(Inventory).filter(Inventory.vendor_id.in_(vendor_ids)).count()
|
||||
)
|
||||
|
||||
# Vendors with inventory
|
||||
vendors_with_inventory = (
|
||||
db.query(func.count(func.distinct(Inventory.vendor_id)))
|
||||
.filter(Inventory.vendor_id.in_(vendor_ids))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Low stock items across platform
|
||||
low_stock_items = (
|
||||
db.query(Inventory)
|
||||
.filter(
|
||||
Inventory.vendor_id.in_(vendor_ids),
|
||||
Inventory.quantity > 0,
|
||||
Inventory.quantity < 10,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
# Out of stock items
|
||||
out_of_stock_items = (
|
||||
db.query(Inventory)
|
||||
.filter(Inventory.vendor_id.in_(vendor_ids), Inventory.quantity == 0)
|
||||
.count()
|
||||
)
|
||||
|
||||
return [
|
||||
MetricValue(
|
||||
key="inventory.total_quantity",
|
||||
value=int(total_quantity),
|
||||
label="Total Stock",
|
||||
category="inventory",
|
||||
icon="package",
|
||||
unit="items",
|
||||
description="Total inventory across all vendors",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.reserved_quantity",
|
||||
value=int(reserved_quantity),
|
||||
label="Reserved",
|
||||
category="inventory",
|
||||
icon="lock",
|
||||
unit="items",
|
||||
description="Inventory reserved for orders",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.available_quantity",
|
||||
value=available_quantity,
|
||||
label="Available",
|
||||
category="inventory",
|
||||
icon="check",
|
||||
unit="items",
|
||||
description="Inventory available for sale",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.entries",
|
||||
value=inventory_entries,
|
||||
label="Total Entries",
|
||||
category="inventory",
|
||||
icon="list",
|
||||
description="Total inventory entries across vendors",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.vendors_with_inventory",
|
||||
value=vendors_with_inventory,
|
||||
label="Vendors with Stock",
|
||||
category="inventory",
|
||||
icon="store",
|
||||
description="Vendors managing inventory",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.low_stock",
|
||||
value=low_stock_items,
|
||||
label="Low Stock Items",
|
||||
category="inventory",
|
||||
icon="alert-triangle",
|
||||
description="Items with quantity < 10",
|
||||
),
|
||||
MetricValue(
|
||||
key="inventory.out_of_stock",
|
||||
value=out_of_stock_items,
|
||||
label="Out of Stock",
|
||||
category="inventory",
|
||||
icon="x-circle",
|
||||
description="Items with zero quantity",
|
||||
),
|
||||
]
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get inventory platform metrics: {e}")
|
||||
return []
|
||||
|
||||
|
||||
# Singleton instance
|
||||
inventory_metrics_provider = InventoryMetricsProvider()
|
||||
|
||||
__all__ = ["InventoryMetricsProvider", "inventory_metrics_provider"]
|
||||
Reference in New Issue
Block a user