# app/services/vendor_service.py """ Vendor service for managing vendor operations and product catalog. This module provides classes and functions for: - Vendor creation and management - Vendor access control and validation - Vendor product catalog operations - Vendor filtering and search """ import logging from typing import List, Optional, Tuple from sqlalchemy import func from sqlalchemy.orm import Session from app.exceptions import (InvalidVendorDataException, MarketplaceProductNotFoundException, MaxVendorsReachedException, ProductAlreadyExistsException, UnauthorizedVendorAccessException, ValidationException, VendorAlreadyExistsException, VendorNotFoundException) from models.database.marketplace_product import MarketplaceProduct from models.database.product import Product from models.database.user import User from models.database.vendor import Vendor from models.schema.product import ProductCreate from models.schema.vendor import VendorCreate logger = logging.getLogger(__name__) class VendorService: """Service class for vendor operations following the application's service pattern.""" def create_vendor( self, db: Session, vendor_data: VendorCreate, current_user: User ) -> Vendor: """ Create a new vendor. Args: db: Database session vendor_data: Vendor creation data current_user: User creating the vendor Returns: Created vendor object Raises: VendorAlreadyExistsException: If vendor code already exists MaxVendorsReachedException: If user has reached maximum vendors InvalidVendorDataException: If vendor data is invalid """ try: # Validate vendor data self._validate_vendor_data(vendor_data) # Check user's vendor limit (if applicable) self._check_vendor_limit(db, current_user) # Normalize vendor code to uppercase normalized_vendor_code = vendor_data.vendor_code.upper() # Check if vendor code already exists (case-insensitive check) if self._vendor_code_exists(db, normalized_vendor_code): raise VendorAlreadyExistsException(normalized_vendor_code) # Create vendor with uppercase code vendor_dict = vendor_data.model_dump() vendor_dict["vendor_code"] = normalized_vendor_code # Store as uppercase new_vendor = Vendor( **vendor_dict, owner_user_id=current_user.id, is_active=True, is_verified=(current_user.role == "admin"), ) db.add(new_vendor) db.commit() db.refresh(new_vendor) logger.info( f"New vendor created: {new_vendor.vendor_code} by {current_user.username}" ) return new_vendor except ( VendorAlreadyExistsException, MaxVendorsReachedException, InvalidVendorDataException, ): db.rollback() raise # Re-raise custom exceptions except Exception as e: db.rollback() logger.error(f"Error creating vendor : {str(e)}") raise ValidationException("Failed to create vendor ") def get_vendors( self, db: Session, current_user: User, skip: int = 0, limit: int = 100, active_only: bool = True, verified_only: bool = False, ) -> Tuple[List[Vendor], int]: """ Get vendors with filtering. Args: db: Database session current_user: Current user requesting vendors skip: Number of records to skip limit: Maximum number of records to return active_only: Filter for active vendors only verified_only: Filter for verified vendors only Returns: Tuple of (vendors_list, total_count) """ try: query = db.query(Vendor) # Non-admin users can only see active and verified vendors, plus their own if current_user.role != "admin": query = query.filter( (Vendor.is_active == True) & ( (Vendor.is_verified == True) | (Vendor.owner_user_id == current_user.id) ) ) else: # Admin can apply filters if active_only: query = query.filter(Vendor.is_active == True) if verified_only: query = query.filter(Vendor.is_verified == True) total = query.count() vendors = query.offset(skip).limit(limit).all() return vendors, total except Exception as e: logger.error(f"Error getting vendors: {str(e)}") raise ValidationException("Failed to retrieve vendors") def get_vendor_by_code( self, db: Session, vendor_code: str, current_user: User ) -> Vendor: """ Get vendor by vendor code with access control. Args: db: Database session vendor_code: Vendor code to find current_user: Current user requesting the vendor Returns: Vendor object Raises: VendorNotFoundException: If vendor not found UnauthorizedVendorAccessException: If access denied """ try: vendor = ( db.query(Vendor) .filter(func.upper(Vendor.vendor_code) == vendor_code.upper()) .first() ) if not vendor: raise VendorNotFoundException(vendor_code) # Check access permissions if not self._can_access_vendor(vendor, current_user): raise UnauthorizedVendorAccessException(vendor_code, current_user.id) return vendor except (VendorNotFoundException, UnauthorizedVendorAccessException): raise # Re-raise custom exceptions except Exception as e: logger.error(f"Error getting vendor {vendor_code}: {str(e)}") raise ValidationException("Failed to retrieve vendor ") def add_product_to_catalog( self, db: Session, vendor: Vendor, product: ProductCreate ) -> Product: """ Add existing product to vendor catalog with vendor -specific settings. Args: db: Database session vendor : Vendor to add product to product: Vendor product data Returns: Created Product object Raises: MarketplaceProductNotFoundException: If product not found ProductAlreadyExistsException: If product already in vendor """ try: # Check if product exists marketplace_product = self._get_product_by_id_or_raise( db, product.marketplace_product_id ) # Check if product already in vendor if self._product_in_catalog(db, vendor.id, marketplace_product.id): raise ProductAlreadyExistsException( vendor.vendor_code, product.marketplace_product_id ) # Create vendor -product association new_product = Product( vendor_id=vendor.id, marketplace_product_id=marketplace_product.id, **product.model_dump(exclude={"marketplace_product_id"}), ) db.add(new_product) db.commit() db.refresh(new_product) # Load the product relationship db.refresh(new_product) logger.info( f"MarketplaceProduct {product.marketplace_product_id} added to vendor {vendor.vendor_code}" ) return new_product except (MarketplaceProductNotFoundException, ProductAlreadyExistsException): db.rollback() raise # Re-raise custom exceptions except Exception as e: db.rollback() logger.error(f"Error adding product to vendor : {str(e)}") raise ValidationException("Failed to add product to vendor ") def get_products( self, db: Session, vendor: Vendor, current_user: User, skip: int = 0, limit: int = 100, active_only: bool = True, featured_only: bool = False, ) -> Tuple[List[Product], int]: """ Get products in vendor catalog with filtering. Args: db: Database session vendor : Vendor to get products from current_user: Current user requesting products skip: Number of records to skip limit: Maximum number of records to return active_only: Filter for active products only featured_only: Filter for featured products only Returns: Tuple of (products_list, total_count) Raises: UnauthorizedVendorAccessException: If vendor access denied """ try: # Check access permissions if not self._can_access_vendor(vendor, current_user): raise UnauthorizedVendorAccessException( vendor.vendor_code, current_user.id ) # Query vendor products query = db.query(Product).filter(Product.vendor_id == vendor.id) if active_only: query = query.filter(Product.is_active == True) if featured_only: query = query.filter(Product.is_featured == True) total = query.count() products = query.offset(skip).limit(limit).all() return products, total except UnauthorizedVendorAccessException: raise # Re-raise custom exceptions except Exception as e: logger.error(f"Error getting vendor products: {str(e)}") raise ValidationException("Failed to retrieve vendor products") # Private helper methods def _validate_vendor_data(self, vendor_data: VendorCreate) -> None: """Validate vendor creation data.""" if not vendor_data.vendor_code or not vendor_data.vendor_code.strip(): raise InvalidVendorDataException( "Vendor code is required", field="vendor_code" ) if not vendor_data.vendor_name or not vendor_data.vendor_name.strip(): raise InvalidVendorDataException("Vendor name is required", field="name") # Validate vendor code format (alphanumeric, underscores, hyphens) import re if not re.match(r"^[A-Za-z0-9_-]+$", vendor_data.vendor_code): raise InvalidVendorDataException( "Vendor code can only contain letters, numbers, underscores, and hyphens", field="vendor_code", ) def _check_vendor_limit(self, db: Session, user: User) -> None: """Check if user has reached maximum vendor limit.""" if user.role == "admin": return # Admins have no limit user_vendor_count = ( db.query(Vendor).filter(Vendor.owner_user_id == user.id).count() ) max_vendors = 5 # Configure this as needed if user_vendor_count >= max_vendors: raise MaxVendorsReachedException(max_vendors, user.id) def _vendor_code_exists(self, db: Session, vendor_code: str) -> bool: """Check if vendor code already exists (case-insensitive).""" return ( db.query(Vendor) .filter(func.upper(Vendor.vendor_code) == vendor_code.upper()) .first() is not None ) def _get_product_by_id_or_raise( self, db: Session, marketplace_product_id: str ) -> MarketplaceProduct: """Get product by ID or raise exception.""" product = ( db.query(MarketplaceProduct) .filter(MarketplaceProduct.marketplace_product_id == marketplace_product_id) .first() ) if not product: raise MarketplaceProductNotFoundException(marketplace_product_id) return product def _product_in_catalog( self, db: Session, vendor_id: int, marketplace_product_id: int ) -> bool: """Check if product is already in vendor.""" return ( db.query(Product) .filter( Product.vendor_id == vendor_id, Product.marketplace_product_id == marketplace_product_id, ) .first() is not None ) def _can_access_vendor(self, vendor: Vendor, user: User) -> bool: """Check if user can access vendor.""" # Admins and owners can always access if user.role == "admin" or vendor.owner_user_id == user.id: return True # Others can only access active and verified vendors return vendor.is_active and vendor.is_verified def _is_vendor_owner(self, vendor: Vendor, user: User) -> bool: """Check if user is vendor owner.""" return vendor.owner_user_id == user.id # Create service instance following the same pattern as other services vendor_service = VendorService()