Problem: - Ruff removed 'from app.core.database import Base' from models/database/base.py - Import appeared "unused" (F401) but was actually a critical re-export - Caused ImportError: cannot import name 'Base' at runtime - Re-export pattern: import in one file to export from package Solution: 1. Added F401 ignore for models/database/base.py in pyproject.toml 2. Created scripts/verify_critical_imports.py verification script 3. Integrated verification into make check and CI pipeline 4. Updated documentation with explanation New Verification Script: - Checks all critical re-export imports exist - Detects import variations (parentheses, 'as' clauses) - Handles SQLAlchemy declarative_base alternatives - Runs as part of make check automatically Protected Files: - models/database/base.py - Re-exports Base for all models - models/__init__.py - Exports Base for Alembic - models/database/__init__.py - Exports Base from package - All __init__.py files (already protected) Makefile Changes: - make verify-imports - Run import verification - make check - Now includes verify-imports - make ci - Includes verify-imports in pipeline Documentation Updated: - Code quality guide explains re-export protection - Pre-commit workflow includes verification - Examples of why re-exports matter This prevents future issues where linters remove seemingly "unused" imports that are actually critical for application structure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
257 lines
7.6 KiB
Python
257 lines
7.6 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 and belongs to vendor
|
|
marketplace_product = (
|
|
db.query(MarketplaceProduct)
|
|
.filter(
|
|
MarketplaceProduct.id == product_data.marketplace_product_id,
|
|
MarketplaceProduct.vendor_id == vendor_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,
|
|
product_id=product_data.product_id,
|
|
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.commit()
|
|
db.refresh(product)
|
|
|
|
logger.info(f"Added product {product.id} to vendor {vendor_id} catalog")
|
|
return product
|
|
|
|
except (ProductAlreadyExistsException, ValidationException):
|
|
db.rollback()
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
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.commit()
|
|
db.refresh(product)
|
|
|
|
logger.info(f"Updated product {product_id} in vendor {vendor_id} catalog")
|
|
return product
|
|
|
|
except ProductNotFoundException:
|
|
db.rollback()
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
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)
|
|
db.commit()
|
|
|
|
logger.info(f"Deleted product {product_id} from vendor {vendor_id} catalog")
|
|
return True
|
|
|
|
except ProductNotFoundException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
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")
|
|
|
|
|
|
# Create service instance
|
|
product_service = ProductService()
|