shop product refactoring

This commit is contained in:
2025-10-04 21:27:48 +02:00
parent c971674ec2
commit 4d2866af5e
21 changed files with 259 additions and 220 deletions

View File

@@ -35,10 +35,16 @@ except ImportError as e:
print(f" ✗ Stock model failed: {e}") print(f" ✗ Stock model failed: {e}")
try: try:
from models.database.shop import Shop, ShopProduct from models.database.shop import Shop
print(" ✓ Shop models imported") print(" ✓ Shop model imported")
except ImportError as e: except ImportError as e:
print(f" ✗ Shop models failed: {e}") print(f" ✗ Shop model failed: {e}")
try:
from models.database.product import Product
print(" ✓ Product model imported")
except ImportError as e:
print(f" ✗ Product model failed: {e}")
try: try:
from models.database.marketplace_import_job import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob

View File

@@ -16,8 +16,8 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_user, get_user_shop from app.api.deps import get_current_user, get_user_shop
from app.core.database import get_db from app.core.database import get_db
from app.services.shop_service import shop_service from app.services.shop_service import shop_service
from models.schemas.shop import (ShopCreate, ShopListResponse, ShopProductCreate, from models.schemas.shop import (ShopCreate, ShopListResponse,ShopResponse)
ShopProductResponse, ShopResponse) from models.schemas.product import (ProductCreate,ProductResponse)
from models.database.user import User from models.database.user import User
router = APIRouter() router = APIRouter()
@@ -72,10 +72,10 @@ def get_shop(
return ShopResponse.model_validate(shop) return ShopResponse.model_validate(shop)
@router.post("/shop/{shop_code}/products", response_model=ShopProductResponse) @router.post("/shop/{shop_code}/products", response_model=ProductResponse)
def add_product_to_shop( def add_product_to_shop(
shop_code: str, shop_code: str,
shop_product: ShopProductCreate, product: ProductCreate,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
@@ -84,18 +84,18 @@ def add_product_to_shop(
shop = get_user_shop(shop_code, current_user, db) shop = get_user_shop(shop_code, current_user, db)
# Add product to shop # Add product to shop
new_shop_product = shop_service.add_product_to_shop( new_product = shop_service.add_product_to_shop(
db=db, shop=shop, shop_product=shop_product db=db, shop=shop, product=product
) )
# Return with product details # Return with product details
response = ShopProductResponse.model_validate(new_shop_product) response = ProductResponse.model_validate(new_product)
response.product = new_shop_product.product response.marketplace_product = new_product.marketplace_product
return response return response
@router.get("/shop/{shop_code}/products") @router.get("/shop/{shop_code}/products")
def get_shop_products( def get_products(
shop_code: str, shop_code: str,
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000), limit: int = Query(100, ge=1, le=1000),
@@ -111,7 +111,7 @@ def get_shop_products(
) )
# Get shop products # Get shop products
shop_products, total = shop_service.get_shop_products( vendor_products, total = shop_service.get_products(
db=db, db=db,
shop=shop, shop=shop,
current_user=current_user, current_user=current_user,
@@ -123,9 +123,9 @@ def get_shop_products(
# Format response # Format response
products = [] products = []
for sp in shop_products: for vp in vendor_products:
product_response = ShopProductResponse.model_validate(sp) product_response = ProductResponse.model_validate(vp)
product_response.product = sp.product product_response.marketplace_product = vp.marketplace_product
products.append(product_response) products.append(product_response)
return { return {

View File

@@ -55,12 +55,15 @@ from .shop import (
ShopNotVerifiedException, ShopNotVerifiedException,
UnauthorizedShopAccessException, UnauthorizedShopAccessException,
InvalidShopDataException, InvalidShopDataException,
ShopProductAlreadyExistsException,
ShopProductNotFoundException,
MaxShopsReachedException, MaxShopsReachedException,
ShopValidationException, ShopValidationException,
) )
from .product import (
ProductNotFoundException,
ProductAlreadyExistsException,
)
from .marketplace_import_job import ( from .marketplace_import_job import (
MarketplaceImportException, MarketplaceImportException,
ImportJobNotFoundException, ImportJobNotFoundException,
@@ -131,11 +134,13 @@ __all__ = [
"ShopNotVerifiedException", "ShopNotVerifiedException",
"UnauthorizedShopAccessException", "UnauthorizedShopAccessException",
"InvalidShopDataException", "InvalidShopDataException",
"ShopProductAlreadyExistsException",
"ShopProductNotFoundException",
"MaxShopsReachedException", "MaxShopsReachedException",
"ShopValidationException", "ShopValidationException",
# Product exceptions
"ProductAlreadyExistsException",
"ProductNotFoundException",
# Marketplace import exceptions # Marketplace import exceptions
"MarketplaceImportException", "MarketplaceImportException",
"ImportJobNotFoundException", "ImportJobNotFoundException",

34
app/exceptions/product.py Normal file
View File

@@ -0,0 +1,34 @@
# app/exceptions/shop.py
"""
Shop management specific exceptions.
"""
from .base import (
ResourceNotFoundException,
ConflictException
)
class ProductAlreadyExistsException(ConflictException):
"""Raised when trying to add a product that already exists in shop."""
def __init__(self, shop_code: str, marketplace_product_id: str):
super().__init__(
message=f"MarketplaceProduct '{marketplace_product_id}' already exists in shop '{shop_code}'",
error_code="PRODUCT_ALREADY_EXISTS",
details={
"shop_code": shop_code,
"marketplace_product_id": marketplace_product_id,
},
)
class ProductNotFoundException(ResourceNotFoundException):
"""Raised when a shop product relationship is not found."""
def __init__(self, shop_code: str, marketplace_product_id: str):
super().__init__(
resource_type="ShopProduct",
identifier=f"{shop_code}/{marketplace_product_id}",
message=f"MarketplaceProduct '{marketplace_product_id}' not found in shop '{shop_code}'",
error_code="PRODUCT_NOT_FOUND",
)

View File

@@ -95,32 +95,6 @@ class InvalidShopDataException(ValidationException):
self.error_code = "INVALID_SHOP_DATA" self.error_code = "INVALID_SHOP_DATA"
class ShopProductAlreadyExistsException(ConflictException):
"""Raised when trying to add a product that already exists in shop."""
def __init__(self, shop_code: str, marketplace_product_id: str):
super().__init__(
message=f"MarketplaceProduct '{marketplace_product_id}' already exists in shop '{shop_code}'",
error_code="SHOP_PRODUCT_ALREADY_EXISTS",
details={
"shop_code": shop_code,
"marketplace_product_id": marketplace_product_id,
},
)
class ShopProductNotFoundException(ResourceNotFoundException):
"""Raised when a shop product relationship is not found."""
def __init__(self, shop_code: str, marketplace_product_id: str):
super().__init__(
resource_type="ShopProduct",
identifier=f"{shop_code}/{marketplace_product_id}",
message=f"MarketplaceProduct '{marketplace_product_id}' not found in shop '{shop_code}'",
error_code="SHOP_PRODUCT_NOT_FOUND",
)
class MaxShopsReachedException(BusinessLogicException): class MaxShopsReachedException(BusinessLogicException):
"""Raised when user tries to create more shops than allowed.""" """Raised when user tries to create more shops than allowed."""

View File

@@ -21,13 +21,15 @@ from app.exceptions import (
UnauthorizedShopAccessException, UnauthorizedShopAccessException,
InvalidShopDataException, InvalidShopDataException,
MarketplaceProductNotFoundException, MarketplaceProductNotFoundException,
ShopProductAlreadyExistsException, ProductAlreadyExistsException,
MaxShopsReachedException, MaxShopsReachedException,
ValidationException, ValidationException,
) )
from models.schemas.shop import ShopCreate, ShopProductCreate from models.schemas.shop import ShopCreate
from models.schemas.product import ProductCreate
from models.database.marketplace_product import MarketplaceProduct from models.database.marketplace_product import MarketplaceProduct
from models.database.shop import Shop, ShopProduct from models.database.shop import Shop
from models.database.product import Product
from models.database.user import User from models.database.user import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -184,49 +186,49 @@ class ShopService:
raise ValidationException("Failed to retrieve shop") raise ValidationException("Failed to retrieve shop")
def add_product_to_shop( def add_product_to_shop(
self, db: Session, shop: Shop, shop_product: ShopProductCreate self, db: Session, shop: Shop, product: ProductCreate
) -> ShopProduct: ) -> Product:
""" """
Add existing product to shop catalog with shop-specific settings. Add existing product to shop catalog with shop-specific settings.
Args: Args:
db: Database session db: Database session
shop: Shop to add product to shop: Shop to add product to
shop_product: Shop product data product: Shop product data
Returns: Returns:
Created ShopProduct object Created ShopProduct object
Raises: Raises:
MarketplaceProductNotFoundException: If product not found MarketplaceProductNotFoundException: If product not found
ShopProductAlreadyExistsException: If product already in shop ProductAlreadyExistsException: If product already in shop
""" """
try: try:
# Check if product exists # Check if product exists
marketplace_product = self._get_product_by_id_or_raise(db, shop_product.marketplace_product_id) marketplace_product = self._get_product_by_id_or_raise(db, product.marketplace_product_id)
# Check if product already in shop # Check if product already in shop
if self._product_in_shop(db, shop.id, marketplace_product.id): if self._product_in_shop(db, shop.id, marketplace_product.id):
raise ShopProductAlreadyExistsException(shop.shop_code, shop_product.marketplace_product_id) raise ProductAlreadyExistsException(shop.shop_code, product.marketplace_product_id)
# Create shop-product association # Create shop-product association
new_shop_product = ShopProduct( new_product = Product(
shop_id=shop.id, shop_id=shop.id,
marketplace_product_id=marketplace_product.id, marketplace_product_id=marketplace_product.id,
**shop_product.model_dump(exclude={"marketplace_product_id"}), **product.model_dump(exclude={"marketplace_product_id"}),
) )
db.add(new_shop_product) db.add(new_product)
db.commit() db.commit()
db.refresh(new_shop_product) db.refresh(new_product)
# Load the product relationship # Load the product relationship
db.refresh(new_shop_product) db.refresh(new_product)
logger.info(f"MarketplaceProduct {shop_product.marketplace_product_id} added to shop {shop.shop_code}") logger.info(f"MarketplaceProduct {product.marketplace_product_id} added to shop {shop.shop_code}")
return new_shop_product return new_product
except (MarketplaceProductNotFoundException, ShopProductAlreadyExistsException): except (MarketplaceProductNotFoundException, ProductAlreadyExistsException):
db.rollback() db.rollback()
raise # Re-raise custom exceptions raise # Re-raise custom exceptions
except Exception as e: except Exception as e:
@@ -234,7 +236,7 @@ class ShopService:
logger.error(f"Error adding product to shop: {str(e)}") logger.error(f"Error adding product to shop: {str(e)}")
raise ValidationException("Failed to add product to shop") raise ValidationException("Failed to add product to shop")
def get_shop_products( def get_products(
self, self,
db: Session, db: Session,
shop: Shop, shop: Shop,
@@ -243,7 +245,7 @@ class ShopService:
limit: int = 100, limit: int = 100,
active_only: bool = True, active_only: bool = True,
featured_only: bool = False, featured_only: bool = False,
) -> Tuple[List[ShopProduct], int]: ) -> Tuple[List[Product], int]:
""" """
Get products in shop catalog with filtering. Get products in shop catalog with filtering.
@@ -257,7 +259,7 @@ class ShopService:
featured_only: Filter for featured products only featured_only: Filter for featured products only
Returns: Returns:
Tuple of (shop_products_list, total_count) Tuple of (products_list, total_count)
Raises: Raises:
UnauthorizedShopAccessException: If shop access denied UnauthorizedShopAccessException: If shop access denied
@@ -268,17 +270,17 @@ class ShopService:
raise UnauthorizedShopAccessException(shop.shop_code, current_user.id) raise UnauthorizedShopAccessException(shop.shop_code, current_user.id)
# Query shop products # Query shop products
query = db.query(ShopProduct).filter(ShopProduct.shop_id == shop.id) query = db.query(Product).filter(Product.shop_id == shop.id)
if active_only: if active_only:
query = query.filter(ShopProduct.is_active == True) query = query.filter(Product.is_active == True)
if featured_only: if featured_only:
query = query.filter(ShopProduct.is_featured == True) query = query.filter(Product.is_featured == True)
total = query.count() total = query.count()
shop_products = query.offset(skip).limit(limit).all() products = query.offset(skip).limit(limit).all()
return shop_products, total return products, total
except UnauthorizedShopAccessException: except UnauthorizedShopAccessException:
raise # Re-raise custom exceptions raise # Re-raise custom exceptions
@@ -332,10 +334,10 @@ class ShopService:
def _product_in_shop(self, db: Session, shop_id: int, marketplace_product_id: int) -> bool: def _product_in_shop(self, db: Session, shop_id: int, marketplace_product_id: int) -> bool:
"""Check if product is already in shop.""" """Check if product is already in shop."""
return ( return (
db.query(ShopProduct) db.query(Product)
.filter( .filter(
ShopProduct.shop_id == shop_id, Product.shop_id == shop_id,
ShopProduct.marketplace_product_id == marketplace_product_id Product.marketplace_product_id == marketplace_product_id
) )
.first() is not None .first() is not None
) )

View File

@@ -6,7 +6,8 @@ from .database.base import Base
from .database.user import User from .database.user import User
from .database.marketplace_product import MarketplaceProduct from .database.marketplace_product import MarketplaceProduct
from .database.stock import Stock from .database.stock import Stock
from .database.shop import Shop, ShopProduct from .database.shop import Shop
from .database.product import Product
from .database.marketplace_import_job import MarketplaceImportJob from .database.marketplace_import_job import MarketplaceImportJob
# API models (Pydantic) - import the modules, not all classes # API models (Pydantic) - import the modules, not all classes
@@ -19,7 +20,7 @@ __all__ = [
"MarketplaceProduct", "MarketplaceProduct",
"Stock", "Stock",
"Shop", "Shop",
"ShopProduct", "Product",
"MarketplaceImportJob", "MarketplaceImportJob",
"api", # API models namespace "api", # API models namespace
] ]

View File

@@ -5,7 +5,8 @@ from .base import Base
from .user import User from .user import User
from .marketplace_product import MarketplaceProduct from .marketplace_product import MarketplaceProduct
from .stock import Stock from .stock import Stock
from .shop import Shop, ShopProduct from .shop import Shop
from .product import Product
from .marketplace_import_job import MarketplaceImportJob from .marketplace_import_job import MarketplaceImportJob
__all__ = [ __all__ = [
@@ -14,6 +15,6 @@ __all__ = [
"MarketplaceProduct", "MarketplaceProduct",
"Stock", "Stock",
"Shop", "Shop",
"ShopProduct", "Product",
"MarketplaceImportJob", "MarketplaceImportJob",
] ]

View File

@@ -64,7 +64,7 @@ class MarketplaceProduct(Base, TimestampMixin):
primaryjoin="MarketplaceProduct.gtin == Stock.gtin", primaryjoin="MarketplaceProduct.gtin == Stock.gtin",
viewonly=True, viewonly=True,
) )
shop_products = relationship("ShopProduct", back_populates="product") product = relationship("Product", back_populates="marketplace_product")
# Additional indexes for marketplace queries # Additional indexes for marketplace queries
__table_args__ = ( __table_args__ = (

View File

@@ -0,0 +1,48 @@
from datetime import datetime
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Index,
Integer, String, Text, UniqueConstraint)
from sqlalchemy.orm import relationship
# Import Base from the central database module instead of creating a new one
from app.core.database import Base
from models.database.base import TimestampMixin
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
shop_id = Column(Integer, ForeignKey("shops.id"), nullable=False)
marketplace_product_id = Column(Integer, ForeignKey("marketplace_products.id"), nullable=False)
# Shop-specific overrides (can override the main product data)
product_id = Column(String) # Shop's internal product ID
price = Column(Float) # Override main product price
sale_price = Column(Float)
currency = Column(String)
availability = Column(String) # Override availability
condition = Column(String)
# Shop-specific metadata
is_featured = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
display_order = Column(Integer, default=0)
# Inventory management
min_quantity = Column(Integer, default=1)
max_quantity = Column(Integer)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
shop = relationship("Shop", back_populates="product")
marketplace_product = relationship("MarketplaceProduct", back_populates="product")
# Constraints
__table_args__ = (
UniqueConstraint("shop_id", "marketplace_product_id", name="uq_product"),
Index("idx_product_active", "shop_id", "is_active"),
Index("idx_product_featured", "shop_id", "is_featured"),
)

View File

@@ -35,47 +35,7 @@ class Shop(Base, TimestampMixin):
# Relationships # Relationships
owner = relationship("User", back_populates="owned_shops") owner = relationship("User", back_populates="owned_shops")
shop_products = relationship("ShopProduct", back_populates="shop") product = relationship("Product", back_populates="shop")
marketplace_import_jobs = relationship( marketplace_import_jobs = relationship(
"MarketplaceImportJob", back_populates="shop" "MarketplaceImportJob", back_populates="shop"
) )
class ShopProduct(Base):
__tablename__ = "shop_products"
id = Column(Integer, primary_key=True, index=True)
shop_id = Column(Integer, ForeignKey("shops.id"), nullable=False)
marketplace_product_id = Column(Integer, ForeignKey("marketplace_products.id"), nullable=False)
# Shop-specific overrides (can override the main product data)
shop_product_id = Column(String) # Shop's internal product ID
shop_price = Column(Float) # Override main product price
shop_sale_price = Column(Float)
shop_currency = Column(String)
shop_availability = Column(String) # Override availability
shop_condition = Column(String)
# Shop-specific metadata
is_featured = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
display_order = Column(Integer, default=0)
# Inventory management
min_quantity = Column(Integer, default=1)
max_quantity = Column(Integer)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
shop = relationship("Shop", back_populates="shop_products")
product = relationship("MarketplaceProduct", back_populates="shop_products")
# Constraints
__table_args__ = (
UniqueConstraint("shop_id", "marketplace_product_id", name="uq_shop_product"),
Index("idx_shop_product_active", "shop_id", "is_active"),
Index("idx_shop_product_featured", "shop_id", "is_featured"),
)

37
models/schemas/product.py Normal file
View File

@@ -0,0 +1,37 @@
# product.py - Keep basic format validation, remove business logic
import re
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
from models.schemas.marketplace_product import MarketplaceProductResponse
class ProductCreate(BaseModel):
marketplace_product_id: str = Field(..., description="MarketplaceProduct ID to add to shop")
product_id: Optional[str] = None
price: Optional[float] = None # Removed: ge=0 constraint
sale_price: Optional[float] = None # Removed: ge=0 constraint
currency: Optional[str] = None
availability: Optional[str] = None
condition: Optional[str] = None
is_featured: bool = Field(False, description="Featured product flag")
min_quantity: int = Field(1, description="Minimum order quantity")
max_quantity: Optional[int] = None # Removed: ge=1 constraint
# Service will validate price ranges and quantity logic
class ProductResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
shop_id: int
marketplace_product: MarketplaceProductResponse
product_id: Optional[str]
price: Optional[float]
sale_price: Optional[float]
currency: Optional[str]
availability: Optional[str]
condition: Optional[str]
is_featured: bool
is_active: bool
min_quantity: int
max_quantity: Optional[int]
created_at: datetime
updated_at: datetime

View File

@@ -3,7 +3,6 @@ import re
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic import BaseModel, ConfigDict, Field, field_validator
from models.schemas.marketplace_product import MarketplaceProductResponse
class ShopCreate(BaseModel): class ShopCreate(BaseModel):
shop_code: str = Field(..., description="Unique shop identifier") shop_code: str = Field(..., description="Unique shop identifier")
@@ -62,34 +61,3 @@ class ShopListResponse(BaseModel):
total: int total: int
skip: int skip: int
limit: int limit: int
class ShopProductCreate(BaseModel):
marketplace_product_id: str = Field(..., description="MarketplaceProduct ID to add to shop")
shop_product_id: Optional[str] = None
shop_price: Optional[float] = None # Removed: ge=0 constraint
shop_sale_price: Optional[float] = None # Removed: ge=0 constraint
shop_currency: Optional[str] = None
shop_availability: Optional[str] = None
shop_condition: Optional[str] = None
is_featured: bool = Field(False, description="Featured product flag")
min_quantity: int = Field(1, description="Minimum order quantity")
max_quantity: Optional[int] = None # Removed: ge=1 constraint
# Service will validate price ranges and quantity logic
class ShopProductResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
shop_id: int
product: MarketplaceProductResponse
shop_product_id: Optional[str]
shop_price: Optional[float]
shop_sale_price: Optional[float]
shop_currency: Optional[str]
shop_availability: Optional[str]
shop_condition: Optional[str]
is_featured: bool
is_active: bool
min_quantity: int
max_quantity: Optional[int]
created_at: datetime
updated_at: datetime

View File

@@ -65,7 +65,7 @@ def verify_database_setup():
# Expected tables from your models # Expected tables from your models
expected_tables = [ expected_tables = [
'users', 'products', 'stock', 'shops', 'shop_products', 'users', 'products', 'stock', 'shops', 'products',
'marketplace_import_jobs', 'alembic_version' 'marketplace_import_jobs', 'alembic_version'
] ]
@@ -133,7 +133,8 @@ def verify_model_structure():
from models.database.user import User from models.database.user import User
from models.database.marketplace_product import MarketplaceProduct from models.database.marketplace_product import MarketplaceProduct
from models.database.stock import Stock from models.database.stock import Stock
from models.database.shop import Shop, ShopProduct from models.database.shop import Shop
from models.database.product import Product
from models.database.marketplace_import_job import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
print("[OK] All database models imported successfully") print("[OK] All database models imported successfully")

View File

@@ -10,7 +10,8 @@ from main import app
# Import all models to ensure they're registered with Base metadata # Import all models to ensure they're registered with Base metadata
from models.database.marketplace_import_job import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.marketplace_product import MarketplaceProduct from models.database.marketplace_product import MarketplaceProduct
from models.database.shop import Shop, ShopProduct from models.database.shop import Shop
from models.database.product import Product
from models.database.stock import Stock from models.database.stock import Stock
from models.database.user import User from models.database.user import User

View File

@@ -3,7 +3,8 @@ import uuid
import pytest import pytest
from models.database.shop import Shop, ShopProduct from models.database.shop import Shop
from models.database.product import Product
from models.database.stock import Stock from models.database.stock import Stock
@@ -77,23 +78,23 @@ def verified_shop(db, other_user):
@pytest.fixture @pytest.fixture
def shop_product(db, test_shop, unique_product): def test_product(db, test_shop, unique_product):
"""Create a shop product relationship""" """Create a shop product relationship"""
shop_product = ShopProduct( product = Product(
shop_id=test_shop.id, marketplace_product_id=unique_product.id, is_active=True shop_id=test_shop.id, marketplace_product_id=unique_product.id, is_active=True
) )
# Add optional fields if they exist in your model # Add optional fields if they exist in your model
if hasattr(ShopProduct, "shop_price"): if hasattr(Product, "shop_price"):
shop_product.shop_price = 24.99 product.price = 24.99
if hasattr(ShopProduct, "is_featured"): if hasattr(Product, "is_featured"):
shop_product.is_featured = False product.is_featured = False
if hasattr(ShopProduct, "min_quantity"): if hasattr(Product, "min_quantity"):
shop_product.min_quantity = 1 product.min_quantity = 1
db.add(shop_product) db.add(product)
db.commit() db.commit()
db.refresh(shop_product) db.refresh(product)
return shop_product return product
@pytest.fixture @pytest.fixture

View File

@@ -22,9 +22,9 @@ def empty_db(db):
# In order to respect foreign key constraints # In order to respect foreign key constraints
tables_to_clear = [ tables_to_clear = [
"marketplace_import_jobs", # Has foreign keys to shops and users "marketplace_import_jobs", # Has foreign keys to shops and users
"shop_products", # Has foreign keys to shops and products "products", # Has foreign keys to shops and products
"stock", # Fixed: singular not plural "stock", # Fixed: singular not plural
"products", # Referenced by shop_products "products", # Referenced by products
"shops", # Has foreign key to users "shops", # Has foreign key to users
"users" # Base table "users" # Base table
] ]

View File

@@ -183,9 +183,9 @@ class TestShopsAPI:
def test_add_product_to_shop_success(self, client, auth_headers, test_shop, unique_product): def test_add_product_to_shop_success(self, client, auth_headers, test_shop, unique_product):
"""Test adding product to shop successfully""" """Test adding product to shop successfully"""
shop_product_data = { product_data = {
"marketplace_product_id": unique_product.marketplace_product_id, # Use string marketplace_product_id, not database id "marketplace_product_id": unique_product.marketplace_product_id, # Use string marketplace_product_id, not database id
"shop_price": 29.99, "price": 29.99,
"is_active": True, "is_active": True,
"is_featured": False, "is_featured": False,
} }
@@ -193,7 +193,7 @@ class TestShopsAPI:
response = client.post( response = client.post(
f"/api/v1/shop/{test_shop.shop_code}/products", f"/api/v1/shop/{test_shop.shop_code}/products",
headers=auth_headers, headers=auth_headers,
json=shop_product_data json=product_data
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -201,21 +201,21 @@ class TestShopsAPI:
# The response structure contains nested product data # The response structure contains nested product data
assert data["shop_id"] == test_shop.id assert data["shop_id"] == test_shop.id
assert data["shop_price"] == 29.99 assert data["price"] == 29.99
assert data["is_active"] is True assert data["is_active"] is True
assert data["is_featured"] is False assert data["is_featured"] is False
# MarketplaceProduct details are nested in the 'product' field # MarketplaceProduct details are nested in the 'marketplace_product' field
assert "product" in data assert "marketplace_product" in data
assert data["product"]["marketplace_product_id"] == unique_product.marketplace_product_id assert data["marketplace_product"]["marketplace_product_id"] == unique_product.marketplace_product_id
assert data["product"]["id"] == unique_product.id assert data["marketplace_product"]["id"] == unique_product.id
def test_add_product_to_shop_already_exists_conflict(self, client, auth_headers, test_shop, shop_product): def test_add_product_to_shop_already_exists_conflict(self, client, auth_headers, test_shop, test_product):
"""Test adding product that already exists in shop returns ShopProductAlreadyExistsException""" """Test adding product that already exists in shop returns ProductAlreadyExistsException"""
# shop_product fixture already creates a relationship, get the marketplace_product_id string # test_product fixture already creates a relationship, get the marketplace_product_id string
existing_product = shop_product.product existing_product = test_product.marketplace_product
shop_product_data = { product_data = {
"marketplace_product_id": existing_product.marketplace_product_id, # Use string marketplace_product_id "marketplace_product_id": existing_product.marketplace_product_id, # Use string marketplace_product_id
"shop_price": 29.99, "shop_price": 29.99,
} }
@@ -223,19 +223,19 @@ class TestShopsAPI:
response = client.post( response = client.post(
f"/api/v1/shop/{test_shop.shop_code}/products", f"/api/v1/shop/{test_shop.shop_code}/products",
headers=auth_headers, headers=auth_headers,
json=shop_product_data json=product_data
) )
assert response.status_code == 409 assert response.status_code == 409
data = response.json() data = response.json()
assert data["error_code"] == "SHOP_PRODUCT_ALREADY_EXISTS" assert data["error_code"] == "PRODUCT_ALREADY_EXISTS"
assert data["status_code"] == 409 assert data["status_code"] == 409
assert test_shop.shop_code in data["message"] assert test_shop.shop_code in data["message"]
assert existing_product.marketplace_product_id in data["message"] assert existing_product.marketplace_product_id in data["message"]
def test_add_nonexistent_product_to_shop_not_found(self, client, auth_headers, test_shop): def test_add_nonexistent_product_to_shop_not_found(self, client, auth_headers, test_shop):
"""Test adding nonexistent product to shop returns MarketplaceProductNotFoundException""" """Test adding nonexistent product to shop returns MarketplaceProductNotFoundException"""
shop_product_data = { product_data = {
"marketplace_product_id": "NONEXISTENT_PRODUCT", # Use string marketplace_product_id that doesn't exist "marketplace_product_id": "NONEXISTENT_PRODUCT", # Use string marketplace_product_id that doesn't exist
"shop_price": 29.99, "shop_price": 29.99,
} }
@@ -243,7 +243,7 @@ class TestShopsAPI:
response = client.post( response = client.post(
f"/api/v1/shop/{test_shop.shop_code}/products", f"/api/v1/shop/{test_shop.shop_code}/products",
headers=auth_headers, headers=auth_headers,
json=shop_product_data json=product_data
) )
assert response.status_code == 404 assert response.status_code == 404
@@ -252,7 +252,7 @@ class TestShopsAPI:
assert data["status_code"] == 404 assert data["status_code"] == 404
assert "NONEXISTENT_PRODUCT" in data["message"] assert "NONEXISTENT_PRODUCT" in data["message"]
def test_get_shop_products_success(self, client, auth_headers, test_shop, shop_product): def test_get_products_success(self, client, auth_headers, test_shop, test_product):
"""Test getting shop products successfully""" """Test getting shop products successfully"""
response = client.get( response = client.get(
f"/api/v1/shop/{test_shop.shop_code}/products", f"/api/v1/shop/{test_shop.shop_code}/products",
@@ -266,7 +266,7 @@ class TestShopsAPI:
assert "shop" in data assert "shop" in data
assert data["shop"]["shop_code"] == test_shop.shop_code assert data["shop"]["shop_code"] == test_shop.shop_code
def test_get_shop_products_with_filters(self, client, auth_headers, test_shop): def test_get_products_with_filters(self, client, auth_headers, test_shop):
"""Test getting shop products with filtering""" """Test getting shop products with filtering"""
# Test active_only filter # Test active_only filter
response = client.get( response = client.get(

View File

@@ -60,7 +60,7 @@ class TestIntegrationFlows:
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["total"] == 1 assert response.json()["total"] == 1
def test_shop_product_workflow(self, client, auth_headers): def test_product_workflow(self, client, auth_headers):
"""Test shop creation and product management workflow""" """Test shop creation and product management workflow"""
# 1. Create a shop # 1. Create a shop
shop_data = { shop_data = {

View File

@@ -8,11 +8,12 @@ from app.exceptions import (
UnauthorizedShopAccessException, UnauthorizedShopAccessException,
InvalidShopDataException, InvalidShopDataException,
MarketplaceProductNotFoundException, MarketplaceProductNotFoundException,
ShopProductAlreadyExistsException, ProductAlreadyExistsException,
MaxShopsReachedException, MaxShopsReachedException,
ValidationException, ValidationException,
) )
from models.schemas.shop import ShopCreate, ShopProductCreate from models.schemas.shop import ShopCreate
from models.schemas.product import ProductCreate
@pytest.mark.unit @pytest.mark.unit
@@ -178,27 +179,26 @@ class TestShopService:
def test_add_product_to_shop_success(self, db, test_shop, unique_product): def test_add_product_to_shop_success(self, db, test_shop, unique_product):
"""Test successfully adding product to shop""" """Test successfully adding product to shop"""
shop_product_data = ShopProductCreate( product_data = ProductCreate(
marketplace_product_id=unique_product.marketplace_product_id, marketplace_product_id=unique_product.marketplace_product_id,
price="15.99", price="15.99",
is_featured=True, is_featured=True,
stock_quantity=5,
) )
shop_product = self.service.add_product_to_shop( product = self.service.add_product_to_shop(
db, test_shop, shop_product_data db, test_shop, product_data
) )
assert shop_product is not None assert product is not None
assert shop_product.shop_id == test_shop.id assert product.shop_id == test_shop.id
assert shop_product.marketplace_product_id == unique_product.id assert product.marketplace_product_id == unique_product.id
def test_add_product_to_shop_product_not_found(self, db, test_shop): def test_add_product_to_shop_product_not_found(self, db, test_shop):
"""Test adding non-existent product to shop fails""" """Test adding non-existent product to shop fails"""
shop_product_data = ShopProductCreate(marketplace_product_id="NONEXISTENT", price="15.99") product_data = ProductCreate(marketplace_product_id="NONEXISTENT", price="15.99")
with pytest.raises(MarketplaceProductNotFoundException) as exc_info: with pytest.raises(MarketplaceProductNotFoundException) as exc_info:
self.service.add_product_to_shop(db, test_shop, shop_product_data) self.service.add_product_to_shop(db, test_shop, product_data)
exception = exc_info.value exception = exc_info.value
assert exception.status_code == 404 assert exception.status_code == 404
@@ -206,36 +206,36 @@ class TestShopService:
assert exception.details["resource_type"] == "MarketplaceProduct" assert exception.details["resource_type"] == "MarketplaceProduct"
assert exception.details["identifier"] == "NONEXISTENT" assert exception.details["identifier"] == "NONEXISTENT"
def test_add_product_to_shop_already_exists(self, db, test_shop, shop_product): def test_add_product_to_shop_already_exists(self, db, test_shop, test_product):
"""Test adding product that's already in shop fails""" """Test adding product that's already in shop fails"""
shop_product_data = ShopProductCreate( product_data = ProductCreate(
marketplace_product_id=shop_product.product.marketplace_product_id, price="15.99" marketplace_product_id=test_product.marketplace_product.marketplace_product_id, price="15.99"
) )
with pytest.raises(ShopProductAlreadyExistsException) as exc_info: with pytest.raises(ProductAlreadyExistsException) as exc_info:
self.service.add_product_to_shop(db, test_shop, shop_product_data) self.service.add_product_to_shop(db, test_shop, product_data)
exception = exc_info.value exception = exc_info.value
assert exception.status_code == 409 assert exception.status_code == 409
assert exception.error_code == "SHOP_PRODUCT_ALREADY_EXISTS" assert exception.error_code == "PRODUCT_ALREADY_EXISTS"
assert exception.details["shop_code"] == test_shop.shop_code assert exception.details["shop_code"] == test_shop.shop_code
assert exception.details["marketplace_product_id"] == shop_product.product.marketplace_product_id assert exception.details["marketplace_product_id"] == test_product.marketplace_product.marketplace_product_id
def test_get_shop_products_owner_access( def test_get_products_owner_access(
self, db, test_user, test_shop, shop_product self, db, test_user, test_shop, test_product
): ):
"""Test shop owner can get shop products""" """Test shop owner can get shop products"""
products, total = self.service.get_shop_products(db, test_shop, test_user) products, total = self.service.get_products(db, test_shop, test_user)
assert total >= 1 assert total >= 1
assert len(products) >= 1 assert len(products) >= 1
product_ids = [p.marketplace_product_id for p in products] product_ids = [p.marketplace_product_id for p in products]
assert shop_product.marketplace_product_id in product_ids assert test_product.marketplace_product_id in product_ids
def test_get_shop_products_access_denied(self, db, test_user, inactive_shop): def test_get_products_access_denied(self, db, test_user, inactive_shop):
"""Test non-owner cannot access unverified shop products""" """Test non-owner cannot access unverified shop products"""
with pytest.raises(UnauthorizedShopAccessException) as exc_info: with pytest.raises(UnauthorizedShopAccessException) as exc_info:
self.service.get_shop_products(db, inactive_shop, test_user) self.service.get_products(db, inactive_shop, test_user)
exception = exc_info.value exception = exc_info.value
assert exception.status_code == 403 assert exception.status_code == 403
@@ -243,16 +243,16 @@ class TestShopService:
assert exception.details["shop_code"] == inactive_shop.shop_code assert exception.details["shop_code"] == inactive_shop.shop_code
assert exception.details["user_id"] == test_user.id assert exception.details["user_id"] == test_user.id
def test_get_shop_products_with_filters(self, db, test_user, test_shop, shop_product): def test_get_products_with_filters(self, db, test_user, test_shop, test_product):
"""Test getting shop products with various filters""" """Test getting shop products with various filters"""
# Test active only filter # Test active only filter
products, total = self.service.get_shop_products( products, total = self.service.get_products(
db, test_shop, test_user, active_only=True db, test_shop, test_user, active_only=True
) )
assert all(p.is_active for p in products) assert all(p.is_active for p in products)
# Test featured only filter # Test featured only filter
products, total = self.service.get_shop_products( products, total = self.service.get_products(
db, test_shop, test_user, featured_only=True db, test_shop, test_user, featured_only=True
) )
assert all(p.is_featured for p in products) assert all(p.is_featured for p in products)
@@ -299,12 +299,12 @@ class TestShopService:
monkeypatch.setattr(db, "commit", mock_commit) monkeypatch.setattr(db, "commit", mock_commit)
shop_product_data = ShopProductCreate( product_data = ProductCreate(
marketplace_product_id=unique_product.marketplace_product_id, price="15.99" marketplace_product_id=unique_product.marketplace_product_id, price="15.99"
) )
with pytest.raises(ValidationException) as exc_info: with pytest.raises(ValidationException) as exc_info:
self.service.add_product_to_shop(db, test_shop, shop_product_data) self.service.add_product_to_shop(db, test_shop, product_data)
exception = exc_info.value exception = exc_info.value
assert exception.error_code == "VALIDATION_ERROR" assert exception.error_code == "VALIDATION_ERROR"

View File

@@ -315,7 +315,7 @@ class TestStatsService:
def test_get_unique_shops_count(self, db, test_marketplace_product): def test_get_unique_shops_count(self, db, test_marketplace_product):
"""Test getting unique shops count""" """Test getting unique shops count"""
# Add products with different shop names # Add products with different shop names
shop_products = [ products = [
MarketplaceProduct( MarketplaceProduct(
marketplace_product_id="SHOP001", marketplace_product_id="SHOP001",
title="Shop MarketplaceProduct 1", title="Shop MarketplaceProduct 1",
@@ -333,7 +333,7 @@ class TestStatsService:
currency="EUR", currency="EUR",
), ),
] ]
db.add_all(shop_products) db.add_all(products)
db.commit() db.commit()
count = self.service._get_unique_shops_count(db) count = self.service._get_unique_shops_count(db)