# app/services/stats_service.py """ Statistics service for generating system analytics and metrics. This module provides: - System-wide statistics (admin) - Vendor-specific statistics - Marketplace analytics - Performance metrics """ import logging from typing import Any, Dict, List from datetime import datetime, timedelta from sqlalchemy import func from sqlalchemy.orm import Session from app.exceptions import ( VendorNotFoundException, AdminOperationException, ) from models.database.marketplace_product import MarketplaceProduct from models.database.product import Product from models.database.inventory import Inventory from models.database.vendor import Vendor from models.database.order import Order from models.database.user import User from models.database.customer import Customer from models.database.marketplace_import_job import MarketplaceImportJob logger = logging.getLogger(__name__) class StatsService: """Service for statistics operations.""" # ======================================================================== # VENDOR-SPECIFIC STATISTICS # ======================================================================== def get_vendor_stats(self, db: Session, vendor_id: int) -> Dict[str, Any]: """ Get statistics for a specific vendor. Args: db: Database session vendor_id: Vendor ID Returns: Dictionary with vendor statistics Raises: VendorNotFoundException: If vendor doesn't exist AdminOperationException: If database query fails """ # Verify vendor exists vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first() if not vendor: raise VendorNotFoundException(str(vendor_id), identifier_type="id") try: # Catalog statistics total_catalog_products = db.query(Product).filter( Product.vendor_id == vendor_id, Product.is_active == True ).count() featured_products = db.query(Product).filter( Product.vendor_id == vendor_id, Product.is_featured == True, Product.is_active == True ).count() # Staging statistics staging_products = db.query(MarketplaceProduct).filter( MarketplaceProduct.vendor_id == vendor_id ).count() # Inventory statistics total_inventory = db.query( func.sum(Inventory.quantity) ).filter( Inventory.vendor_id == vendor_id ).scalar() or 0 reserved_inventory = db.query( func.sum(Inventory.reserved_quantity) ).filter( Inventory.vendor_id == vendor_id ).scalar() or 0 inventory_locations = db.query( func.count(func.distinct(Inventory.location)) ).filter( Inventory.vendor_id == vendor_id ).scalar() or 0 # Import statistics total_imports = db.query(MarketplaceImportJob).filter( MarketplaceImportJob.vendor_id == vendor_id ).count() successful_imports = db.query(MarketplaceImportJob).filter( MarketplaceImportJob.vendor_id == vendor_id, MarketplaceImportJob.status == "completed" ).count() # Orders total_orders = db.query(Order).filter( Order.vendor_id == vendor_id ).count() # Customers total_customers = db.query(Customer).filter( Customer.vendor_id == vendor_id ).count() return { "catalog": { "total_products": total_catalog_products, "featured_products": featured_products, "active_products": total_catalog_products, }, "staging": { "imported_products": staging_products, }, "inventory": { "total_quantity": int(total_inventory), "reserved_quantity": int(reserved_inventory), "available_quantity": int(total_inventory - reserved_inventory), "locations_count": inventory_locations, }, "imports": { "total_imports": total_imports, "successful_imports": successful_imports, "success_rate": (successful_imports / total_imports * 100) if total_imports > 0 else 0, }, "orders": { "total_orders": total_orders, }, "customers": { "total_customers": total_customers, }, } except VendorNotFoundException: raise except Exception as e: logger.error(f"Failed to retrieve vendor statistics for vendor {vendor_id}: {str(e)}") raise AdminOperationException( operation="get_vendor_stats", reason=f"Database query failed: {str(e)}", target_type="vendor", target_id=str(vendor_id) ) def get_vendor_analytics( self, db: Session, vendor_id: int, period: str = "30d" ) -> Dict[str, Any]: """ Get a specific vendor analytics for a time period. Args: db: Database session vendor_id: Vendor ID period: Time period (7d, 30d, 90d, 1y) Returns: Analytics data Raises: VendorNotFoundException: If vendor doesn't exist AdminOperationException: If database query fails """ # Verify vendor exists vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first() if not vendor: raise VendorNotFoundException(str(vendor_id), identifier_type="id") try: # Parse period days = self._parse_period(period) start_date = datetime.utcnow() - timedelta(days=days) # Import activity recent_imports = db.query(MarketplaceImportJob).filter( MarketplaceImportJob.vendor_id == vendor_id, MarketplaceImportJob.created_at >= start_date ).count() # Products added to catalog products_added = db.query(Product).filter( Product.vendor_id == vendor_id, Product.created_at >= start_date ).count() # Inventory changes inventory_entries = db.query(Inventory).filter( Inventory.vendor_id == vendor_id ).count() return { "period": period, "start_date": start_date.isoformat(), "imports": { "count": recent_imports, }, "catalog": { "products_added": products_added, }, "inventory": { "total_locations": inventory_entries, }, } except VendorNotFoundException: raise except Exception as e: logger.error(f"Failed to retrieve vendor analytics for vendor {vendor_id}: {str(e)}") raise AdminOperationException( operation="get_vendor_analytics", reason=f"Database query failed: {str(e)}", target_type="vendor", target_id=str(vendor_id) ) def get_vendor_statistics(self, db: Session) -> dict: """Get vendor statistics for admin dashboard.""" try: total_vendors = db.query(Vendor).count() active_vendors = db.query(Vendor).filter(Vendor.is_active == True).count() verified_vendors = db.query(Vendor).filter(Vendor.is_verified == True).count() inactive_vendors = total_vendors - active_vendors return { "total_vendors": total_vendors, "active_vendors": active_vendors, "inactive_vendors": inactive_vendors, "verified_vendors": verified_vendors, "verification_rate": (verified_vendors / total_vendors * 100) if total_vendors > 0 else 0 } except Exception as e: logger.error(f"Failed to get vendor statistics: {str(e)}") raise AdminOperationException( operation="get_vendor_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: # Vendors total_vendors = db.query(Vendor).filter(Vendor.is_active == True).count() # Products total_catalog_products = db.query(Product).count() unique_brands = self._get_unique_brands_count(db) unique_categories = self._get_unique_categories_count(db) # Marketplaces unique_marketplaces = ( db.query(MarketplaceProduct.marketplace) .filter(MarketplaceProduct.marketplace.isnot(None)) .distinct() .count() ) # 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_vendors": total_vendors, "total_inventory_entries": inventory_stats.get("total_entries", 0), "total_inventory_quantity": inventory_stats.get("total_quantity", 0), } except Exception 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: marketplace_stats = ( db.query( MarketplaceProduct.marketplace, func.count(MarketplaceProduct.id).label("total_products"), func.count(func.distinct(MarketplaceProduct.vendor_name)).label("unique_vendors"), func.count(func.distinct(MarketplaceProduct.brand)).label("unique_brands"), ) .filter(MarketplaceProduct.marketplace.isnot(None)) .group_by(MarketplaceProduct.marketplace) .all() ) return [ { "marketplace": stat.marketplace, "total_products": stat.total_products, "unique_vendors": stat.unique_vendors, "unique_brands": stat.unique_brands, } for stat in marketplace_stats ] except Exception 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: total_users = db.query(User).count() active_users = db.query(User).filter(User.is_active == True).count() inactive_users = total_users - active_users admin_users = db.query(User).filter(User.role == "admin").count() return { "total_users": total_users, "active_users": active_users, "inactive_users": inactive_users, "admin_users": admin_users, "activation_rate": (active_users / total_users * 100) if total_users > 0 else 0 } except Exception 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: total = db.query(MarketplaceImportJob).count() completed = db.query(MarketplaceImportJob).filter( MarketplaceImportJob.status == "completed" ).count() failed = db.query(MarketplaceImportJob).filter( MarketplaceImportJob.status == "failed" ).count() return { "total_imports": total, "completed_imports": completed, "failed_imports": failed, "success_rate": (completed / total * 100) if total > 0 else 0 } except Exception as e: logger.error(f"Failed to get import statistics: {str(e)}") return { "total_imports": 0, "completed_imports": 0, "failed_imports": 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_unique_brands_count(self, db: Session) -> int: """ Get count of unique brands. Args: db: Database session Returns: Count of unique brands """ return ( db.query(MarketplaceProduct.brand) .filter( MarketplaceProduct.brand.isnot(None), MarketplaceProduct.brand != "" ) .distinct() .count() ) def _get_unique_categories_count(self, db: Session) -> int: """ Get count of unique categories. Args: db: Database session Returns: Count of unique categories """ return ( db.query(MarketplaceProduct.google_product_category) .filter( MarketplaceProduct.google_product_category.isnot(None), MarketplaceProduct.google_product_category != "", ) .distinct() .count() ) def _get_inventory_statistics(self, db: Session) -> Dict[str, int]: """ Get inventory-related statistics. Args: db: Database session Returns: Dictionary with inventory statistics """ total_entries = db.query(Inventory).count() total_quantity = db.query(func.sum(Inventory.quantity)).scalar() or 0 total_reserved = db.query(func.sum(Inventory.reserved_quantity)).scalar() or 0 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()