# 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"]