diff --git a/alembic/env.py b/alembic/env.py index b2cf6cce..a35d43ef 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -35,10 +35,16 @@ except ImportError as e: print(f" ✗ Stock model failed: {e}") try: - from models.database.shop import Shop, ShopProduct - print(" ✓ Shop models imported") + from models.database.shop import Shop + print(" ✓ Shop model imported") 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: from models.database.marketplace_import_job import MarketplaceImportJob diff --git a/app/api/v1/shop.py b/app/api/v1/shop.py index 6ad94a40..ae904505 100644 --- a/app/api/v1/shop.py +++ b/app/api/v1/shop.py @@ -16,8 +16,8 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_user, get_user_shop from app.core.database import get_db from app.services.shop_service import shop_service -from models.schemas.shop import (ShopCreate, ShopListResponse, ShopProductCreate, - ShopProductResponse, ShopResponse) +from models.schemas.shop import (ShopCreate, ShopListResponse,ShopResponse) +from models.schemas.product import (ProductCreate,ProductResponse) from models.database.user import User router = APIRouter() @@ -72,10 +72,10 @@ def get_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( shop_code: str, - shop_product: ShopProductCreate, + product: ProductCreate, db: Session = Depends(get_db), 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) # Add product to shop - new_shop_product = shop_service.add_product_to_shop( - db=db, shop=shop, shop_product=shop_product + new_product = shop_service.add_product_to_shop( + db=db, shop=shop, product=product ) # Return with product details - response = ShopProductResponse.model_validate(new_shop_product) - response.product = new_shop_product.product + response = ProductResponse.model_validate(new_product) + response.marketplace_product = new_product.marketplace_product return response @router.get("/shop/{shop_code}/products") -def get_shop_products( +def get_products( shop_code: str, skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), @@ -111,7 +111,7 @@ def get_shop_products( ) # Get shop products - shop_products, total = shop_service.get_shop_products( + vendor_products, total = shop_service.get_products( db=db, shop=shop, current_user=current_user, @@ -123,9 +123,9 @@ def get_shop_products( # Format response products = [] - for sp in shop_products: - product_response = ShopProductResponse.model_validate(sp) - product_response.product = sp.product + for vp in vendor_products: + product_response = ProductResponse.model_validate(vp) + product_response.marketplace_product = vp.marketplace_product products.append(product_response) return { diff --git a/app/exceptions/__init__.py b/app/exceptions/__init__.py index 9f6b715b..ea25d9d8 100644 --- a/app/exceptions/__init__.py +++ b/app/exceptions/__init__.py @@ -55,12 +55,15 @@ from .shop import ( ShopNotVerifiedException, UnauthorizedShopAccessException, InvalidShopDataException, - ShopProductAlreadyExistsException, - ShopProductNotFoundException, MaxShopsReachedException, ShopValidationException, ) +from .product import ( + ProductNotFoundException, + ProductAlreadyExistsException, +) + from .marketplace_import_job import ( MarketplaceImportException, ImportJobNotFoundException, @@ -131,11 +134,13 @@ __all__ = [ "ShopNotVerifiedException", "UnauthorizedShopAccessException", "InvalidShopDataException", - "ShopProductAlreadyExistsException", - "ShopProductNotFoundException", "MaxShopsReachedException", "ShopValidationException", + # Product exceptions + "ProductAlreadyExistsException", + "ProductNotFoundException", + # Marketplace import exceptions "MarketplaceImportException", "ImportJobNotFoundException", diff --git a/app/exceptions/product.py b/app/exceptions/product.py new file mode 100644 index 00000000..8086f428 --- /dev/null +++ b/app/exceptions/product.py @@ -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", + ) diff --git a/app/exceptions/shop.py b/app/exceptions/shop.py index 8373fe05..d2015e9b 100644 --- a/app/exceptions/shop.py +++ b/app/exceptions/shop.py @@ -95,32 +95,6 @@ class InvalidShopDataException(ValidationException): 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): """Raised when user tries to create more shops than allowed.""" diff --git a/app/services/shop_service.py b/app/services/shop_service.py index fd60fd1f..8636e303 100644 --- a/app/services/shop_service.py +++ b/app/services/shop_service.py @@ -21,13 +21,15 @@ from app.exceptions import ( UnauthorizedShopAccessException, InvalidShopDataException, MarketplaceProductNotFoundException, - ShopProductAlreadyExistsException, + ProductAlreadyExistsException, MaxShopsReachedException, 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.shop import Shop, ShopProduct +from models.database.shop import Shop +from models.database.product import Product from models.database.user import User logger = logging.getLogger(__name__) @@ -184,49 +186,49 @@ class ShopService: raise ValidationException("Failed to retrieve shop") def add_product_to_shop( - self, db: Session, shop: Shop, shop_product: ShopProductCreate - ) -> ShopProduct: + self, db: Session, shop: Shop, product: ProductCreate + ) -> Product: """ Add existing product to shop catalog with shop-specific settings. Args: db: Database session shop: Shop to add product to - shop_product: Shop product data + product: Shop product data Returns: Created ShopProduct object Raises: MarketplaceProductNotFoundException: If product not found - ShopProductAlreadyExistsException: If product already in shop + ProductAlreadyExistsException: If product already in shop """ try: # 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 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 - new_shop_product = ShopProduct( + new_product = Product( shop_id=shop.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.refresh(new_shop_product) + db.refresh(new_product) # 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}") - return new_shop_product + logger.info(f"MarketplaceProduct {product.marketplace_product_id} added to shop {shop.shop_code}") + return new_product - except (MarketplaceProductNotFoundException, ShopProductAlreadyExistsException): + except (MarketplaceProductNotFoundException, ProductAlreadyExistsException): db.rollback() raise # Re-raise custom exceptions except Exception as e: @@ -234,7 +236,7 @@ class ShopService: logger.error(f"Error adding product to shop: {str(e)}") raise ValidationException("Failed to add product to shop") - def get_shop_products( + def get_products( self, db: Session, shop: Shop, @@ -243,7 +245,7 @@ class ShopService: limit: int = 100, active_only: bool = True, featured_only: bool = False, - ) -> Tuple[List[ShopProduct], int]: + ) -> Tuple[List[Product], int]: """ Get products in shop catalog with filtering. @@ -257,7 +259,7 @@ class ShopService: featured_only: Filter for featured products only Returns: - Tuple of (shop_products_list, total_count) + Tuple of (products_list, total_count) Raises: UnauthorizedShopAccessException: If shop access denied @@ -268,17 +270,17 @@ class ShopService: raise UnauthorizedShopAccessException(shop.shop_code, current_user.id) # 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: - query = query.filter(ShopProduct.is_active == True) + query = query.filter(Product.is_active == True) if featured_only: - query = query.filter(ShopProduct.is_featured == True) + query = query.filter(Product.is_featured == True) 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: 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: """Check if product is already in shop.""" return ( - db.query(ShopProduct) + db.query(Product) .filter( - ShopProduct.shop_id == shop_id, - ShopProduct.marketplace_product_id == marketplace_product_id + Product.shop_id == shop_id, + Product.marketplace_product_id == marketplace_product_id ) .first() is not None ) diff --git a/models/__init__.py b/models/__init__.py index 811951f0..c4688d89 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -6,7 +6,8 @@ from .database.base import Base from .database.user import User from .database.marketplace_product import MarketplaceProduct 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 # API models (Pydantic) - import the modules, not all classes @@ -19,7 +20,7 @@ __all__ = [ "MarketplaceProduct", "Stock", "Shop", - "ShopProduct", + "Product", "MarketplaceImportJob", "api", # API models namespace ] diff --git a/models/database/__init__.py b/models/database/__init__.py index 5d3c55e8..bb506941 100644 --- a/models/database/__init__.py +++ b/models/database/__init__.py @@ -5,7 +5,8 @@ from .base import Base from .user import User from .marketplace_product import MarketplaceProduct from .stock import Stock -from .shop import Shop, ShopProduct +from .shop import Shop +from .product import Product from .marketplace_import_job import MarketplaceImportJob __all__ = [ @@ -14,6 +15,6 @@ __all__ = [ "MarketplaceProduct", "Stock", "Shop", - "ShopProduct", + "Product", "MarketplaceImportJob", ] diff --git a/models/database/marketplace_product.py b/models/database/marketplace_product.py index db1f51c1..0b7e24ad 100644 --- a/models/database/marketplace_product.py +++ b/models/database/marketplace_product.py @@ -64,7 +64,7 @@ class MarketplaceProduct(Base, TimestampMixin): primaryjoin="MarketplaceProduct.gtin == Stock.gtin", viewonly=True, ) - shop_products = relationship("ShopProduct", back_populates="product") + product = relationship("Product", back_populates="marketplace_product") # Additional indexes for marketplace queries __table_args__ = ( diff --git a/models/database/product.py b/models/database/product.py new file mode 100644 index 00000000..a2bf8ab1 --- /dev/null +++ b/models/database/product.py @@ -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"), + ) diff --git a/models/database/shop.py b/models/database/shop.py index 5e85ec0d..c75629aa 100644 --- a/models/database/shop.py +++ b/models/database/shop.py @@ -35,47 +35,7 @@ class Shop(Base, TimestampMixin): # Relationships owner = relationship("User", back_populates="owned_shops") - shop_products = relationship("ShopProduct", back_populates="shop") + product = relationship("Product", back_populates="shop") marketplace_import_jobs = relationship( "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"), - ) diff --git a/models/schemas/product.py b/models/schemas/product.py new file mode 100644 index 00000000..1e6b52ee --- /dev/null +++ b/models/schemas/product.py @@ -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 diff --git a/models/schemas/shop.py b/models/schemas/shop.py index c0c7c91d..c5651eb4 100644 --- a/models/schemas/shop.py +++ b/models/schemas/shop.py @@ -3,7 +3,6 @@ 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 ShopCreate(BaseModel): shop_code: str = Field(..., description="Unique shop identifier") @@ -62,34 +61,3 @@ class ShopListResponse(BaseModel): total: int skip: 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 diff --git a/scripts/verify_setup.py b/scripts/verify_setup.py index c0b9aefd..b9c12fbd 100644 --- a/scripts/verify_setup.py +++ b/scripts/verify_setup.py @@ -65,7 +65,7 @@ def verify_database_setup(): # Expected tables from your models expected_tables = [ - 'users', 'products', 'stock', 'shops', 'shop_products', + 'users', 'products', 'stock', 'shops', 'products', 'marketplace_import_jobs', 'alembic_version' ] @@ -133,7 +133,8 @@ def verify_model_structure(): from models.database.user import User from models.database.marketplace_product import MarketplaceProduct 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 print("[OK] All database models imported successfully") diff --git a/tests/conftest.py b/tests/conftest.py index 95c7c912..8381cf8d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,8 @@ from main import app # Import all models to ensure they're registered with Base metadata from models.database.marketplace_import_job import MarketplaceImportJob 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.user import User diff --git a/tests/fixtures/shop_fixtures.py b/tests/fixtures/shop_fixtures.py index 866976fa..ab7ba911 100644 --- a/tests/fixtures/shop_fixtures.py +++ b/tests/fixtures/shop_fixtures.py @@ -3,7 +3,8 @@ import uuid 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 @@ -77,23 +78,23 @@ def verified_shop(db, other_user): @pytest.fixture -def shop_product(db, test_shop, unique_product): +def test_product(db, test_shop, unique_product): """Create a shop product relationship""" - shop_product = ShopProduct( + product = Product( shop_id=test_shop.id, marketplace_product_id=unique_product.id, is_active=True ) # Add optional fields if they exist in your model - if hasattr(ShopProduct, "shop_price"): - shop_product.shop_price = 24.99 - if hasattr(ShopProduct, "is_featured"): - shop_product.is_featured = False - if hasattr(ShopProduct, "min_quantity"): - shop_product.min_quantity = 1 + if hasattr(Product, "shop_price"): + product.price = 24.99 + if hasattr(Product, "is_featured"): + product.is_featured = False + if hasattr(Product, "min_quantity"): + product.min_quantity = 1 - db.add(shop_product) + db.add(product) db.commit() - db.refresh(shop_product) - return shop_product + db.refresh(product) + return product @pytest.fixture diff --git a/tests/fixtures/testing_fixtures.py b/tests/fixtures/testing_fixtures.py index 3f730790..ee1524ae 100644 --- a/tests/fixtures/testing_fixtures.py +++ b/tests/fixtures/testing_fixtures.py @@ -22,9 +22,9 @@ def empty_db(db): # In order to respect foreign key constraints tables_to_clear = [ "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 - "products", # Referenced by shop_products + "products", # Referenced by products "shops", # Has foreign key to users "users" # Base table ] diff --git a/tests/integration/api/v1/test_shop_endpoints.py b/tests/integration/api/v1/test_shop_endpoints.py index a2b0fffb..83db1d3c 100644 --- a/tests/integration/api/v1/test_shop_endpoints.py +++ b/tests/integration/api/v1/test_shop_endpoints.py @@ -183,9 +183,9 @@ class TestShopsAPI: def test_add_product_to_shop_success(self, client, auth_headers, test_shop, unique_product): """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 - "shop_price": 29.99, + "price": 29.99, "is_active": True, "is_featured": False, } @@ -193,7 +193,7 @@ class TestShopsAPI: response = client.post( f"/api/v1/shop/{test_shop.shop_code}/products", headers=auth_headers, - json=shop_product_data + json=product_data ) assert response.status_code == 200 @@ -201,21 +201,21 @@ class TestShopsAPI: # The response structure contains nested product data 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_featured"] is False - # MarketplaceProduct details are nested in the 'product' field - assert "product" in data - assert data["product"]["marketplace_product_id"] == unique_product.marketplace_product_id - assert data["product"]["id"] == unique_product.id + # MarketplaceProduct details are nested in the 'marketplace_product' field + assert "marketplace_product" in data + assert data["marketplace_product"]["marketplace_product_id"] == unique_product.marketplace_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): - """Test adding product that already exists in shop returns ShopProductAlreadyExistsException""" - # shop_product fixture already creates a relationship, get the marketplace_product_id string - existing_product = shop_product.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 ProductAlreadyExistsException""" + # test_product fixture already creates a relationship, get the marketplace_product_id string + existing_product = test_product.marketplace_product - shop_product_data = { + product_data = { "marketplace_product_id": existing_product.marketplace_product_id, # Use string marketplace_product_id "shop_price": 29.99, } @@ -223,19 +223,19 @@ class TestShopsAPI: response = client.post( f"/api/v1/shop/{test_shop.shop_code}/products", headers=auth_headers, - json=shop_product_data + json=product_data ) assert response.status_code == 409 data = response.json() - assert data["error_code"] == "SHOP_PRODUCT_ALREADY_EXISTS" + assert data["error_code"] == "PRODUCT_ALREADY_EXISTS" assert data["status_code"] == 409 assert test_shop.shop_code 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): """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 "shop_price": 29.99, } @@ -243,7 +243,7 @@ class TestShopsAPI: response = client.post( f"/api/v1/shop/{test_shop.shop_code}/products", headers=auth_headers, - json=shop_product_data + json=product_data ) assert response.status_code == 404 @@ -252,7 +252,7 @@ class TestShopsAPI: assert data["status_code"] == 404 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""" response = client.get( f"/api/v1/shop/{test_shop.shop_code}/products", @@ -266,7 +266,7 @@ class TestShopsAPI: assert "shop" in data 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 active_only filter response = client.get( diff --git a/tests/integration/workflows/test_integration.py b/tests/integration/workflows/test_integration.py index bef1b630..a8206726 100644 --- a/tests/integration/workflows/test_integration.py +++ b/tests/integration/workflows/test_integration.py @@ -60,7 +60,7 @@ class TestIntegrationFlows: assert response.status_code == 200 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""" # 1. Create a shop shop_data = { diff --git a/tests/unit/services/test_shop_service.py b/tests/unit/services/test_shop_service.py index 11c7f2fc..9df0bf8d 100644 --- a/tests/unit/services/test_shop_service.py +++ b/tests/unit/services/test_shop_service.py @@ -8,11 +8,12 @@ from app.exceptions import ( UnauthorizedShopAccessException, InvalidShopDataException, MarketplaceProductNotFoundException, - ShopProductAlreadyExistsException, + ProductAlreadyExistsException, MaxShopsReachedException, ValidationException, ) -from models.schemas.shop import ShopCreate, ShopProductCreate +from models.schemas.shop import ShopCreate +from models.schemas.product import ProductCreate @pytest.mark.unit @@ -178,27 +179,26 @@ class TestShopService: def test_add_product_to_shop_success(self, db, test_shop, unique_product): """Test successfully adding product to shop""" - shop_product_data = ShopProductCreate( + product_data = ProductCreate( marketplace_product_id=unique_product.marketplace_product_id, price="15.99", is_featured=True, - stock_quantity=5, ) - shop_product = self.service.add_product_to_shop( - db, test_shop, shop_product_data + product = self.service.add_product_to_shop( + db, test_shop, product_data ) - assert shop_product is not None - assert shop_product.shop_id == test_shop.id - assert shop_product.marketplace_product_id == unique_product.id + assert product is not None + assert product.shop_id == test_shop.id + assert product.marketplace_product_id == unique_product.id def test_add_product_to_shop_product_not_found(self, db, test_shop): """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: - 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 assert exception.status_code == 404 @@ -206,36 +206,36 @@ class TestShopService: assert exception.details["resource_type"] == "MarketplaceProduct" 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""" - shop_product_data = ShopProductCreate( - marketplace_product_id=shop_product.product.marketplace_product_id, price="15.99" + product_data = ProductCreate( + marketplace_product_id=test_product.marketplace_product.marketplace_product_id, price="15.99" ) - with pytest.raises(ShopProductAlreadyExistsException) as exc_info: - self.service.add_product_to_shop(db, test_shop, shop_product_data) + with pytest.raises(ProductAlreadyExistsException) as exc_info: + self.service.add_product_to_shop(db, test_shop, product_data) exception = exc_info.value 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["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( - self, db, test_user, test_shop, shop_product + def test_get_products_owner_access( + self, db, test_user, test_shop, test_product ): """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 len(products) >= 1 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""" 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 assert exception.status_code == 403 @@ -243,16 +243,16 @@ class TestShopService: assert exception.details["shop_code"] == inactive_shop.shop_code 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 active only filter - products, total = self.service.get_shop_products( + products, total = self.service.get_products( db, test_shop, test_user, active_only=True ) assert all(p.is_active for p in products) # 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 ) assert all(p.is_featured for p in products) @@ -299,12 +299,12 @@ class TestShopService: monkeypatch.setattr(db, "commit", mock_commit) - shop_product_data = ShopProductCreate( + product_data = ProductCreate( marketplace_product_id=unique_product.marketplace_product_id, price="15.99" ) 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 assert exception.error_code == "VALIDATION_ERROR" diff --git a/tests/unit/services/test_stats_service.py b/tests/unit/services/test_stats_service.py index 9be98b8e..230eab3e 100644 --- a/tests/unit/services/test_stats_service.py +++ b/tests/unit/services/test_stats_service.py @@ -315,7 +315,7 @@ class TestStatsService: def test_get_unique_shops_count(self, db, test_marketplace_product): """Test getting unique shops count""" # Add products with different shop names - shop_products = [ + products = [ MarketplaceProduct( marketplace_product_id="SHOP001", title="Shop MarketplaceProduct 1", @@ -333,7 +333,7 @@ class TestStatsService: currency="EUR", ), ] - db.add_all(shop_products) + db.add_all(products) db.commit() count = self.service._get_unique_shops_count(db)