shop product refactoring
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
34
app/exceptions/product.py
Normal 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",
|
||||||
|
)
|
||||||
@@ -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."""
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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__ = (
|
||||||
|
|||||||
48
models/database/product.py
Normal file
48
models/database/product.py
Normal 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"),
|
||||||
|
)
|
||||||
@@ -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
37
models/schemas/product.py
Normal 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
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
25
tests/fixtures/shop_fixtures.py
vendored
25
tests/fixtures/shop_fixtures.py
vendored
@@ -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
|
||||||
|
|||||||
4
tests/fixtures/testing_fixtures.py
vendored
4
tests/fixtures/testing_fixtures.py
vendored
@@ -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
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user