Files
orion/app/services/vendor_service.py
Samir Boulahtit ec2885dab5 refactor: update services for company-centric ownership
- Update admin_service: remove vendor owner methods, fix get_all_vendors query
- Update company_service: add transfer_ownership method
- Update vendor_service: check ownership via company relationship
- Remove backwards compatibility code for Vendor.owner_user_id

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 19:39:23 +01:00

397 lines
14 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 sqlalchemy import func
from sqlalchemy.orm import Session
from app.exceptions import (
InvalidVendorDataException,
MarketplaceProductNotFoundException,
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 under a company.
DEPRECATED: This method is for self-service vendor creation by company owners.
For admin operations, use admin_service.create_vendor() instead.
The new architecture:
- Companies are the business entities with owners and contact info
- Vendors are storefronts/brands under companies
- The company_id is required in vendor_data
Args:
db: Database session
vendor_data: Vendor creation data (must include company_id)
current_user: User creating the vendor (must be company owner or admin)
Returns:
Created vendor object
Raises:
VendorAlreadyExistsException: If vendor code already exists
UnauthorizedVendorAccessException: If user is not company owner
InvalidVendorDataException: If vendor data is invalid
"""
from models.database.company import Company
try:
# Validate company_id is provided
if not hasattr(vendor_data, 'company_id') or not vendor_data.company_id:
raise InvalidVendorDataException(
"company_id is required to create a vendor", field="company_id"
)
# Get company and verify ownership
company = db.query(Company).filter(Company.id == vendor_data.company_id).first()
if not company:
raise InvalidVendorDataException(
f"Company with ID {vendor_data.company_id} not found", field="company_id"
)
# Check if user is company owner or admin
if current_user.role != "admin" and company.owner_user_id != current_user.id:
raise UnauthorizedVendorAccessException(
f"company-{vendor_data.company_id}", current_user.id
)
# 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 linked to company
new_vendor = Vendor(
company_id=company.id,
vendor_code=normalized_vendor_code,
subdomain=vendor_data.subdomain.lower(),
name=vendor_data.name,
description=vendor_data.description,
letzshop_csv_url_fr=vendor_data.letzshop_csv_url_fr,
letzshop_csv_url_en=vendor_data.letzshop_csv_url_en,
letzshop_csv_url_de=vendor_data.letzshop_csv_url_de,
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} under company {company.name} by {current_user.username}"
)
return new_vendor
except (
VendorAlreadyExistsException,
UnauthorizedVendorAccessException,
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":
# Get vendor IDs the user owns through companies
from models.database.company import Company
owned_vendor_ids = (
db.query(Vendor.id)
.join(Company)
.filter(Company.owner_user_id == current_user.id)
.subquery()
)
query = query.filter(
(Vendor.is_active == True)
& (
(Vendor.is_verified == True) | (Vendor.id.in_(owned_vendor_ids))
)
)
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 _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 can always access
if user.role == "admin":
return True
# Company owners can access their vendors
if vendor.company and vendor.company.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 (via company ownership)."""
return vendor.company and vendor.company.owner_user_id == user.id
# Create service instance following the same pattern as other services
vendor_service = VendorService()