major refactoring adding vendor and customer features

This commit is contained in:
2025-10-11 09:09:25 +02:00
parent f569995883
commit dd16198276
126 changed files with 15109 additions and 3747 deletions

View File

@@ -2,71 +2,281 @@
"""
Statistics service for generating system analytics and metrics.
This module provides classes and functions for:
- Comprehensive system statistics
- Marketplace-specific analytics
- Performance metrics and data insights
- Cached statistics for performance
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 ValidationException
from app.exceptions import (
VendorNotFoundException,
AdminOperationException,
)
from models.database.marketplace_product import MarketplaceProduct
from models.database.stock import Stock
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.customer import Customer
from models.database.marketplace_import_job import MarketplaceImportJob
logger = logging.getLogger(__name__)
class StatsService:
"""Service class for statistics operations following the application's service pattern."""
"""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 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)
)
# ========================================================================
# SYSTEM-WIDE STATISTICS (ADMIN)
# ========================================================================
def get_comprehensive_stats(self, db: Session) -> Dict[str, Any]:
"""
Get comprehensive statistics with marketplace data.
Get comprehensive system statistics for admin dashboard.
Args:
db: Database session
Returns:
Dictionary containing all statistics data
Dictionary with comprehensive statistics
Raises:
ValidationException: If statistics generation fails
AdminOperationException: If database query fails
"""
try:
# Use more efficient queries with proper indexes
total_products = self._get_product_count(db)
# 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)
unique_marketplaces = self._get_unique_marketplaces_count(db)
unique_vendors = self._get_unique_vendors_count(db)
# Stock statistics
stock_stats = self._get_stock_statistics(db)
# Marketplaces
unique_marketplaces = (
db.query(MarketplaceProduct.marketplace)
.filter(MarketplaceProduct.marketplace.isnot(None))
.distinct()
.count()
)
stats_data = {
"total_products": total_products,
# 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": unique_vendors,
"total_stock_entries": stock_stats["total_stock_entries"],
"total_inventory_quantity": stock_stats["total_inventory_quantity"],
"unique_vendors": total_vendors,
"total_inventory_entries": inventory_stats.get("total_entries", 0),
"total_inventory_quantity": inventory_stats.get("total_quantity", 0),
}
logger.info(
f"Generated comprehensive stats: {total_products} products, {unique_marketplaces} marketplaces"
)
return stats_data
except Exception as e:
logger.error(f"Error getting comprehensive stats: {str(e)}")
raise ValidationException("Failed to retrieve system statistics")
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]]:
"""
@@ -76,13 +286,12 @@ class StatsService:
db: Database session
Returns:
List of dictionaries containing marketplace statistics
List of marketplace statistics
Raises:
ValidationException: If marketplace statistics generation fails
AdminOperationException: If database query fails
"""
try:
# Query to get stats per marketplace
marketplace_stats = (
db.query(
MarketplaceProduct.marketplace,
@@ -95,7 +304,7 @@ class StatsService:
.all()
)
stats_list = [
return [
{
"marketplace": stat.marketplace,
"total_products": stat.total_products,
@@ -105,103 +314,35 @@ class StatsService:
for stat in marketplace_stats
]
logger.info(
f"Generated marketplace breakdown stats for {len(stats_list)} marketplaces"
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)}"
)
return stats_list
except Exception as e:
logger.error(f"Error getting marketplace breakdown stats: {str(e)}")
raise ValidationException("Failed to retrieve marketplace statistics")
# ========================================================================
# PRIVATE HELPER METHODS
# ========================================================================
def get_product_statistics(self, db: Session) -> Dict[str, Any]:
"""
Get detailed product statistics.
Args:
db: Database session
Returns:
Dictionary containing product statistics
"""
try:
stats = {
"total_products": self._get_product_count(db),
"unique_brands": self._get_unique_brands_count(db),
"unique_categories": self._get_unique_categories_count(db),
"unique_marketplaces": self._get_unique_marketplaces_count(db),
"unique_vendors": self._get_unique_vendors_count(db),
"products_with_gtin": self._get_products_with_gtin_count(db),
"products_with_images": self._get_products_with_images_count(db),
}
return stats
except Exception as e:
logger.error(f"Error getting product statistics: {str(e)}")
raise ValidationException("Failed to retrieve product statistics")
def get_stock_statistics(self, db: Session) -> Dict[str, Any]:
"""
Get stock-related statistics.
Args:
db: Database session
Returns:
Dictionary containing stock statistics
"""
try:
return self._get_stock_statistics(db)
except Exception as e:
logger.error(f"Error getting stock statistics: {str(e)}")
raise ValidationException("Failed to retrieve stock statistics")
def get_marketplace_details(self, db: Session, marketplace: str) -> Dict[str, Any]:
"""
Get detailed statistics for a specific marketplace.
Args:
db: Database session
marketplace: Marketplace name
Returns:
Dictionary containing marketplace details
"""
try:
if not marketplace or not marketplace.strip():
raise ValidationException("Marketplace name is required")
product_count = self._get_products_by_marketplace_count(db, marketplace)
brands = self._get_brands_by_marketplace(db, marketplace)
vendors =self._get_vendors_by_marketplace(db, marketplace)
return {
"marketplace": marketplace,
"total_products": product_count,
"unique_brands": len(brands),
"unique_vendors": len(vendors),
"brands": brands,
"vendors": vendors,
}
except ValidationException:
raise # Re-raise custom exceptions
except Exception as e:
logger.error(f"Error getting marketplace details for {marketplace}: {str(e)}")
raise ValidationException("Failed to retrieve marketplace details")
# Private helper methods
def _get_product_count(self, db: Session) -> int:
"""Get total product count."""
return db.query(MarketplaceProduct).count()
def _parse_period(self, period: str) -> int:
"""Parse period string to 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."""
return (
db.query(MarketplaceProduct.brand)
.filter(MarketplaceProduct.brand.isnot(None), MarketplaceProduct.brand != "")
.filter(
MarketplaceProduct.brand.isnot(None),
MarketplaceProduct.brand != ""
)
.distinct()
.count()
)
@@ -218,81 +359,19 @@ class StatsService:
.count()
)
def _get_unique_marketplaces_count(self, db: Session) -> int:
"""Get count of unique marketplaces."""
return (
db.query(MarketplaceProduct.marketplace)
.filter(MarketplaceProduct.marketplace.isnot(None), MarketplaceProduct.marketplace != "")
.distinct()
.count()
)
def _get_unique_vendors_count(self, db: Session) -> int:
"""Get count of unique vendors."""
return (
db.query(MarketplaceProduct.vendor_name)
.filter(MarketplaceProduct.vendor_name.isnot(None), MarketplaceProduct.vendor_name != "")
.distinct()
.count()
)
def _get_products_with_gtin_count(self, db: Session) -> int:
"""Get count of products with GTIN."""
return (
db.query(MarketplaceProduct)
.filter(MarketplaceProduct.gtin.isnot(None), MarketplaceProduct.gtin != "")
.count()
)
def _get_products_with_images_count(self, db: Session) -> int:
"""Get count of products with images."""
return (
db.query(MarketplaceProduct)
.filter(MarketplaceProduct.image_link.isnot(None), MarketplaceProduct.image_link != "")
.count()
)
def _get_stock_statistics(self, db: Session) -> Dict[str, int]:
"""Get stock-related statistics."""
total_stock_entries = db.query(Stock).count()
total_inventory = db.query(func.sum(Stock.quantity)).scalar() or 0
def _get_inventory_statistics(self, db: Session) -> Dict[str, int]:
"""Get inventory-related 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_stock_entries": total_stock_entries,
"total_inventory_quantity": total_inventory,
"total_entries": total_entries,
"total_quantity": int(total_quantity),
"total_reserved": int(total_reserved),
"total_available": int(total_quantity - total_reserved),
}
def _get_brands_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
"""Get unique brands for a specific marketplace."""
brands = (
db.query(MarketplaceProduct.brand)
.filter(
MarketplaceProduct.marketplace == marketplace,
MarketplaceProduct.brand.isnot(None),
MarketplaceProduct.brand != "",
)
.distinct()
.all()
)
return [brand[0] for brand in brands]
def _get_vendors_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
"""Get unique vendors for a specific marketplace."""
vendors =(
db.query(MarketplaceProduct.vendor_name)
.filter(
MarketplaceProduct.marketplace == marketplace,
MarketplaceProduct.vendor_name.isnot(None),
MarketplaceProduct.vendor_name != "",
)
.distinct()
.all()
)
return [vendor [0] for vendor in vendors]
def _get_products_by_marketplace_count(self, db: Session, marketplace: str) -> int:
"""Get product count for a specific marketplace."""
return db.query(MarketplaceProduct).filter(MarketplaceProduct.marketplace == marketplace).count()
# Create service instance following the same pattern as other services
# Create service instance
stats_service = StatsService()