Some checks failed
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>
477 lines
17 KiB
Python
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"]
|