Files
orion/app/services/product_service.py
Samir Boulahtit 8ee3f91467 feat: implement product search, media library, and vendor customers
- Add full-text product search in ProductService.search_products()
  searching titles, descriptions, SKUs, brands, and GTINs
- Implement complete vendor media library with file uploads,
  thumbnails, folders, and product associations
- Implement vendor customers API with listing, details, orders,
  statistics, and status management
- Add shop search results UI with pagination and add-to-cart
- Add vendor media library UI with drag-drop upload and grid view
- Add database migration for media_files and product_media tables
- Update TODO file with current launch status (~95% complete)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 21:32:59 +01:00

331 lines
10 KiB
Python

# app/services/product_service.py
"""
Product service for vendor catalog management.
This module provides:
- Product catalog CRUD operations
- Product publishing from marketplace staging
- Product search and filtering
"""
import logging
from datetime import UTC, datetime
from sqlalchemy.orm import Session
from app.exceptions import (
ProductAlreadyExistsException,
ProductNotFoundException,
ValidationException,
)
from models.database.marketplace_product import MarketplaceProduct
from models.database.product import Product
from models.schema.product import ProductCreate, ProductUpdate
logger = logging.getLogger(__name__)
class ProductService:
"""Service for vendor catalog product operations."""
def get_product(self, db: Session, vendor_id: int, product_id: int) -> Product:
"""
Get a product from vendor catalog.
Args:
db: Database session
vendor_id: Vendor ID
product_id: Product ID
Returns:
Product object
Raises:
ProductNotFoundException: If product not found
"""
try:
product = (
db.query(Product)
.filter(Product.id == product_id, Product.vendor_id == vendor_id)
.first()
)
if not product:
raise ProductNotFoundException(f"Product {product_id} not found")
return product
except ProductNotFoundException:
raise
except Exception as e:
logger.error(f"Error getting product: {str(e)}")
raise ValidationException("Failed to retrieve product")
def create_product(
self, db: Session, vendor_id: int, product_data: ProductCreate
) -> Product:
"""
Add a product from marketplace to vendor catalog.
Args:
db: Database session
vendor_id: Vendor ID
product_data: Product creation data
Returns:
Created Product object
Raises:
ProductAlreadyExistsException: If product already in catalog
ValidationException: If marketplace product not found
"""
try:
# Verify marketplace product exists
marketplace_product = (
db.query(MarketplaceProduct)
.filter(MarketplaceProduct.id == product_data.marketplace_product_id)
.first()
)
if not marketplace_product:
raise ValidationException(
f"Marketplace product {product_data.marketplace_product_id} not found"
)
# Check if already in catalog
existing = (
db.query(Product)
.filter(
Product.vendor_id == vendor_id,
Product.marketplace_product_id
== product_data.marketplace_product_id,
)
.first()
)
if existing:
raise ProductAlreadyExistsException("Product already exists in catalog")
# Create product
product = Product(
vendor_id=vendor_id,
marketplace_product_id=product_data.marketplace_product_id,
vendor_sku=product_data.vendor_sku,
price=product_data.price,
sale_price=product_data.sale_price,
currency=product_data.currency,
availability=product_data.availability,
condition=product_data.condition,
is_featured=product_data.is_featured,
is_active=True,
min_quantity=product_data.min_quantity,
max_quantity=product_data.max_quantity,
)
db.add(product)
db.flush()
db.refresh(product)
logger.info(f"Added product {product.id} to vendor {vendor_id} catalog")
return product
except (ProductAlreadyExistsException, ValidationException):
raise
except Exception as e:
logger.error(f"Error creating product: {str(e)}")
raise ValidationException("Failed to create product")
def update_product(
self,
db: Session,
vendor_id: int,
product_id: int,
product_update: ProductUpdate,
) -> Product:
"""
Update product in vendor catalog.
Args:
db: Database session
vendor_id: Vendor ID
product_id: Product ID
product_update: Update data
Returns:
Updated Product object
"""
try:
product = self.get_product(db, vendor_id, product_id)
# Update fields
update_data = product_update.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(product, key, value)
product.updated_at = datetime.now(UTC)
db.flush()
db.refresh(product)
logger.info(f"Updated product {product_id} in vendor {vendor_id} catalog")
return product
except ProductNotFoundException:
raise
except Exception as e:
logger.error(f"Error updating product: {str(e)}")
raise ValidationException("Failed to update product")
def delete_product(self, db: Session, vendor_id: int, product_id: int) -> bool:
"""
Remove product from vendor catalog.
Args:
db: Database session
vendor_id: Vendor ID
product_id: Product ID
Returns:
True if deleted
"""
try:
product = self.get_product(db, vendor_id, product_id)
db.delete(product)
logger.info(f"Deleted product {product_id} from vendor {vendor_id} catalog")
return True
except ProductNotFoundException:
raise
except Exception as e:
logger.error(f"Error deleting product: {str(e)}")
raise ValidationException("Failed to delete product")
def get_vendor_products(
self,
db: Session,
vendor_id: int,
skip: int = 0,
limit: int = 100,
is_active: bool | None = None,
is_featured: bool | None = None,
) -> tuple[list[Product], int]:
"""
Get products in vendor catalog with filtering.
Args:
db: Database session
vendor_id: Vendor ID
skip: Pagination offset
limit: Pagination limit
is_active: Filter by active status
is_featured: Filter by featured status
Returns:
Tuple of (products, total_count)
"""
try:
query = db.query(Product).filter(Product.vendor_id == vendor_id)
if is_active is not None:
query = query.filter(Product.is_active == is_active)
if is_featured is not None:
query = query.filter(Product.is_featured == is_featured)
total = query.count()
products = query.offset(skip).limit(limit).all()
return products, total
except Exception as e:
logger.error(f"Error getting vendor products: {str(e)}")
raise ValidationException("Failed to retrieve products")
def search_products(
self,
db: Session,
vendor_id: int,
query: str,
skip: int = 0,
limit: int = 50,
language: str = "en",
) -> tuple[list[Product], int]:
"""
Search products in vendor catalog.
Searches across:
- Product title and description (from translations)
- Product SKU, brand, and GTIN
Args:
db: Database session
vendor_id: Vendor ID
query: Search query string
skip: Pagination offset
limit: Pagination limit
language: Language for translation search (default: 'en')
Returns:
Tuple of (products, total_count)
"""
from sqlalchemy import or_
from sqlalchemy.orm import joinedload
from models.database.product_translation import ProductTranslation
try:
# Prepare search pattern for LIKE queries
search_pattern = f"%{query}%"
# Build base query with translation join
base_query = (
db.query(Product)
.outerjoin(
ProductTranslation,
(Product.id == ProductTranslation.product_id)
& (ProductTranslation.language == language),
)
.filter(
Product.vendor_id == vendor_id,
Product.is_active == True,
)
.filter(
or_(
# Search in translations
ProductTranslation.title.ilike(search_pattern),
ProductTranslation.description.ilike(search_pattern),
ProductTranslation.short_description.ilike(search_pattern),
# Search in product fields
Product.vendor_sku.ilike(search_pattern),
Product.brand.ilike(search_pattern),
Product.gtin.ilike(search_pattern),
)
)
.distinct()
)
# Get total count
total = base_query.count()
# Get paginated results with eager loading for performance
products = (
base_query.options(joinedload(Product.translations))
.offset(skip)
.limit(limit)
.all()
)
logger.debug(
f"Search '{query}' for vendor {vendor_id}: {total} results"
)
return products, total
except Exception as e:
logger.error(f"Error searching products: {str(e)}")
raise ValidationException("Failed to search products")
# Create service instance
product_service = ProductService()