# 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 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()