Files
orion/app/modules/analytics/services/stats_service.py
Samir Boulahtit 86e85a98b8
Some checks failed
CI / ruff (push) Successful in 9s
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / pytest (push) Has been cancelled
refactor(arch): eliminate all cross-module model imports in service layer
Enforce MOD-025/MOD-026 rules: zero top-level cross-module model imports
remain in any service file. All 66 files migrated using deferred import
patterns (method-body, _get_model() helpers, instance-cached self._Model)
and new cross-module service methods in tenancy. Documentation updated
with Pattern 6 (deferred imports), migration plan marked complete, and
violations status reflects 84→0 service-layer violations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 06:13:15 +01:00

477 lines
17 KiB
Python

# app/modules/analytics/services/stats_service.py
"""
Statistics service for generating system analytics and metrics.
This is the canonical location for the stats service.
This module provides:
- System-wide statistics (admin)
- Store-specific statistics
- Marketplace analytics
- Performance metrics
"""
import logging
from datetime import datetime, timedelta
from typing import Any
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from app.modules.tenancy.exceptions import (
AdminOperationException,
StoreNotFoundException,
)
logger = logging.getLogger(__name__)
class StatsService:
"""Service for statistics operations."""
# ========================================================================
# STORE-SPECIFIC STATISTICS
# ========================================================================
def get_store_stats(self, db: Session, store_id: int) -> dict[str, Any]:
"""
Get statistics for a specific store.
Args:
db: Database session
store_id: Store ID
Returns:
Dictionary with store statistics
Raises:
StoreNotFoundException: If store doesn't exist
AdminOperationException: If database query fails
"""
from app.modules.catalog.services.product_service import product_service
from app.modules.customers.services.customer_service import customer_service
from app.modules.inventory.services.inventory_service import inventory_service
from app.modules.marketplace.services.marketplace_import_job_service import (
marketplace_import_job_service,
)
from app.modules.marketplace.services.marketplace_product_service import (
marketplace_product_service,
)
from app.modules.orders.services.order_service import order_service
from app.modules.tenancy.services.store_service import store_service
# Verify store exists
store = store_service.get_store_by_id_optional(db, store_id)
if not store:
raise StoreNotFoundException(str(store_id), identifier_type="id")
try:
# Catalog statistics
total_catalog_products = product_service.get_store_product_count(
db, store_id, active_only=True,
)
featured_products = product_service.get_store_product_count(
db, store_id, active_only=True, featured_only=True,
)
# Staging statistics
staging_products = marketplace_product_service.get_staging_product_count(
db, store_name=store.name,
)
# Inventory statistics
inv_stats = inventory_service.get_store_inventory_stats(db, store_id)
total_inventory = inv_stats["total"]
reserved_inventory = inv_stats["reserved"]
inventory_locations = inv_stats["locations"]
# Import statistics
import_stats = marketplace_import_job_service.get_import_job_stats(
db, store_id=store_id,
)
total_imports = import_stats["total"]
successful_imports = import_stats["completed"]
# Orders
total_orders = order_service.get_store_order_count(db, store_id)
# Customers
total_customers = customer_service.get_store_customer_count(db, store_id)
# Return flat structure compatible with StoreDashboardStatsResponse schema
# The endpoint will restructure this into nested format
return {
# Product stats
"total_products": total_catalog_products,
"active_products": total_catalog_products,
"featured_products": featured_products,
# Order stats (TODO: implement when Order model has status field)
"total_orders": total_orders,
"pending_orders": 0, # TODO: filter by status
"completed_orders": 0, # TODO: filter by status
# Customer stats
"total_customers": total_customers,
"active_customers": 0, # TODO: implement active customer logic
# Revenue stats (TODO: implement when Order model has amount field)
"total_revenue": 0,
"revenue_this_month": 0,
# Import stats
"total_imports": total_imports,
"successful_imports": successful_imports,
"import_success_rate": (
(successful_imports / total_imports * 100)
if total_imports > 0
else 0
),
# Staging stats
"imported_products": staging_products,
# Inventory stats
"total_inventory_quantity": int(total_inventory),
"reserved_inventory_quantity": int(reserved_inventory),
"available_inventory_quantity": int(
total_inventory - reserved_inventory
),
"inventory_locations_count": inventory_locations,
}
except StoreNotFoundException:
raise
except SQLAlchemyError as e:
logger.error(
f"Failed to retrieve store statistics for store {store_id}: {str(e)}"
)
raise AdminOperationException(
operation="get_store_stats",
reason=f"Database query failed: {str(e)}",
target_type="store",
target_id=str(store_id),
)
def get_store_analytics(
self, db: Session, store_id: int, period: str = "30d"
) -> dict[str, Any]:
"""
Get a specific store analytics for a time period.
Args:
db: Database session
store_id: Store ID
period: Time period (7d, 30d, 90d, 1y)
Returns:
Analytics data
Raises:
StoreNotFoundException: If store doesn't exist
AdminOperationException: If database query fails
"""
from app.modules.catalog.services.product_service import product_service
from app.modules.inventory.services.inventory_service import inventory_service
from app.modules.marketplace.services.marketplace_import_job_service import (
marketplace_import_job_service,
)
from app.modules.tenancy.services.store_service import store_service
# Verify store exists
store = store_service.get_store_by_id_optional(db, store_id)
if not store:
raise StoreNotFoundException(str(store_id), identifier_type="id")
try:
# Parse period
days = self._parse_period(period)
start_date = datetime.utcnow() - timedelta(days=days)
# Import activity
import_stats = marketplace_import_job_service.get_import_job_stats(
db, store_id=store_id,
)
recent_imports = import_stats["total"]
# Products added to catalog
products_added = product_service.get_store_product_count(db, store_id)
# Inventory changes
inv_stats = inventory_service.get_store_inventory_stats(db, store_id)
inventory_entries = inv_stats.get("locations", 0)
return {
"period": period,
"start_date": start_date.isoformat(),
"imports": {
"count": recent_imports,
},
"catalog": {
"products_added": products_added,
},
"inventory": {
"total_locations": inventory_entries,
},
}
except StoreNotFoundException:
raise
except SQLAlchemyError as e:
logger.error(
f"Failed to retrieve store analytics for store {store_id}: {str(e)}"
)
raise AdminOperationException(
operation="get_store_analytics",
reason=f"Database query failed: {str(e)}",
target_type="store",
target_id=str(store_id),
)
def get_store_statistics(self, db: Session) -> dict:
"""Get store statistics for admin dashboard.
Returns dict compatible with StoreStatsResponse schema.
Keys: total, verified, pending, inactive (mapped from internal names)
"""
from app.modules.tenancy.services.store_service import store_service
try:
total_stores = store_service.get_total_store_count(db)
active_stores = store_service.get_total_store_count(db, active_only=True)
inactive_stores = total_stores - active_stores
# Use store_service for verified/pending counts
verified_stores = store_service.get_store_count_by_status(db, verified=True)
pending_stores = store_service.get_store_count_by_status(db, active=True, verified=False)
return {
"total": total_stores,
"verified": verified_stores,
"pending": pending_stores,
"inactive": inactive_stores,
"verification_rate": (
(verified_stores / total_stores * 100) if total_stores > 0 else 0
),
}
except SQLAlchemyError as e:
logger.error(f"Failed to get store statistics: {str(e)}")
raise AdminOperationException(
operation="get_store_statistics", reason="Database query failed"
)
# ========================================================================
# SYSTEM-WIDE STATISTICS (ADMIN)
# ========================================================================
def get_comprehensive_stats(self, db: Session) -> dict[str, Any]:
"""
Get comprehensive system statistics for admin dashboard.
Args:
db: Database session
Returns:
Dictionary with comprehensive statistics
Raises:
AdminOperationException: If database query fails
"""
try:
from app.modules.catalog.services.product_service import product_service
from app.modules.marketplace.services.marketplace_product_service import (
marketplace_product_service,
)
from app.modules.tenancy.services.store_service import store_service
# Stores
total_stores = store_service.get_total_store_count(db, active_only=True)
# Products
total_catalog_products = product_service.get_total_product_count(db)
unique_brands = marketplace_product_service.get_distinct_brand_count(db)
unique_categories = marketplace_product_service.get_distinct_category_count(db)
# Marketplaces
unique_marketplaces = marketplace_product_service.get_distinct_marketplace_count(db)
# Inventory
inventory_stats = self._get_inventory_statistics(db)
return {
"total_products": total_catalog_products,
"unique_brands": unique_brands,
"unique_categories": unique_categories,
"unique_marketplaces": unique_marketplaces,
"unique_stores": total_stores,
"total_inventory_entries": inventory_stats.get("total_entries", 0),
"total_inventory_quantity": inventory_stats.get("total_quantity", 0),
}
except SQLAlchemyError as e:
logger.error(f"Failed to retrieve comprehensive statistics: {str(e)}")
raise AdminOperationException(
operation="get_comprehensive_stats",
reason=f"Database query failed: {str(e)}",
)
def get_marketplace_breakdown_stats(self, db: Session) -> list[dict[str, Any]]:
"""
Get statistics broken down by marketplace.
Args:
db: Database session
Returns:
List of marketplace statistics
Raises:
AdminOperationException: If database query fails
"""
try:
from app.modules.marketplace.services.marketplace_product_service import (
marketplace_product_service,
)
return marketplace_product_service.get_marketplace_breakdown(db)
except SQLAlchemyError as e:
logger.error(
f"Failed to retrieve marketplace breakdown statistics: {str(e)}"
)
raise AdminOperationException(
operation="get_marketplace_breakdown_stats",
reason=f"Database query failed: {str(e)}",
)
def get_user_statistics(self, db: Session) -> dict[str, Any]:
"""
Get user statistics for admin dashboard.
Args:
db: Database session
Returns:
Dictionary with user statistics
Raises:
AdminOperationException: If database query fails
"""
try:
from app.modules.tenancy.services.admin_service import admin_service
user_stats = admin_service.get_user_statistics(db)
return user_stats
except SQLAlchemyError as e:
logger.error(f"Failed to get user statistics: {str(e)}")
raise AdminOperationException(
operation="get_user_statistics", reason="Database query failed"
)
def get_import_statistics(self, db: Session) -> dict[str, Any]:
"""
Get import job statistics.
Args:
db: Database session
Returns:
Dictionary with import statistics
Raises:
AdminOperationException: If database query fails
"""
try:
from app.modules.marketplace.services.marketplace_import_job_service import (
marketplace_import_job_service,
)
stats = marketplace_import_job_service.get_import_job_stats(db)
total = stats["total"]
completed = stats["completed"]
return {
"total": total,
"pending": stats["pending"],
"processing": stats.get("processing", 0),
"completed": completed,
"failed": stats["failed"],
"success_rate": (completed / total * 100) if total > 0 else 0,
}
except SQLAlchemyError as e:
logger.error(f"Failed to get import statistics: {str(e)}")
return {
"total": 0,
"pending": 0,
"processing": 0,
"completed": 0,
"failed": 0,
"success_rate": 0,
}
def get_order_statistics(self, db: Session) -> dict[str, Any]:
"""
Get order statistics.
Args:
db: Database session
Returns:
Dictionary with order statistics
Note:
TODO: Implement when Order model is fully available
"""
return {"total_orders": 0, "pending_orders": 0, "completed_orders": 0}
def get_product_statistics(self, db: Session) -> dict[str, Any]:
"""
Get product statistics.
Args:
db: Database session
Returns:
Dictionary with product statistics
Note:
TODO: Implement when Product model is fully available
"""
return {"total_products": 0, "active_products": 0, "out_of_stock": 0}
# ========================================================================
# PRIVATE HELPER METHODS
# ========================================================================
def _parse_period(self, period: str) -> int:
"""
Parse period string to days.
Args:
period: Period string (7d, 30d, 90d, 1y)
Returns:
Number of days
"""
period_map = {
"7d": 7,
"30d": 30,
"90d": 90,
"1y": 365,
}
return period_map.get(period, 30)
def _get_inventory_statistics(self, db: Session) -> dict[str, int]:
"""Get inventory-related statistics via inventory service."""
from app.modules.inventory.services.inventory_service import inventory_service
total_entries = inventory_service.get_total_inventory_count(db)
total_quantity = inventory_service.get_total_inventory_quantity(db)
total_reserved = inventory_service.get_total_reserved_quantity(db)
return {
"total_entries": total_entries,
"total_quantity": int(total_quantity),
"total_reserved": int(total_reserved),
"total_available": int(total_quantity - total_reserved),
}
# Create service instance
stats_service = StatsService()
__all__ = ["stats_service", "StatsService"]