360 lines
13 KiB
Python
360 lines
13 KiB
Python
# 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 (
|
|
VendorNotFoundException,
|
|
VendorAlreadyExistsException,
|
|
UnauthorizedVendorAccessException,
|
|
InvalidVendorDataException,
|
|
MarketplaceProductNotFoundException,
|
|
ProductAlreadyExistsException,
|
|
MaxVendorsReachedException,
|
|
ValidationException,
|
|
)
|
|
from models.schema.vendor import VendorCreate
|
|
from models.schema.product import ProductCreate
|
|
from models.database.marketplace_product import MarketplaceProduct
|
|
from models.database.vendor import Vendor
|
|
from models.database.product import Product
|
|
from models.database.user import User
|
|
|
|
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()
|