test: update tests for multi-language translation support
- Update marketplace_product_fixtures to create translations - Update test_marketplace_product.py for translation-based titles - Update test_product.py for effective property tests - Update test_order.py to use get_title() method - Add comprehensive CSV processor tests for translations - Update stats service tests for new flat response structure - Fix product schema tests with required marketplace_product_id field - Add helper function create_marketplace_product_with_translation() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
74
tests/fixtures/marketplace_product_fixtures.py
vendored
74
tests/fixtures/marketplace_product_fixtures.py
vendored
@@ -2,7 +2,10 @@
|
|||||||
"""
|
"""
|
||||||
Marketplace product test fixtures.
|
Marketplace product test fixtures.
|
||||||
|
|
||||||
Note: Fixtures should NOT use db.expunge() as it breaks lazy loading.
|
Note: Since title/description are now in translations table,
|
||||||
|
we create the translation alongside the marketplace product.
|
||||||
|
|
||||||
|
Fixtures should NOT use db.expunge() as it breaks lazy loading.
|
||||||
See tests/conftest.py for details on fixture best practices.
|
See tests/conftest.py for details on fixture best practices.
|
||||||
"""
|
"""
|
||||||
import uuid
|
import uuid
|
||||||
@@ -10,12 +13,42 @@ import uuid
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from models.database.marketplace_product import MarketplaceProduct
|
from models.database.marketplace_product import MarketplaceProduct
|
||||||
|
from models.database.marketplace_product_translation import MarketplaceProductTranslation
|
||||||
|
|
||||||
|
|
||||||
|
def _create_marketplace_product_with_translation(
|
||||||
|
db,
|
||||||
|
marketplace_product_id: str,
|
||||||
|
title: str,
|
||||||
|
description: str | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> MarketplaceProduct:
|
||||||
|
"""Helper to create a MarketplaceProduct with its English translation."""
|
||||||
|
marketplace_product = MarketplaceProduct(
|
||||||
|
marketplace_product_id=marketplace_product_id,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
db.add(marketplace_product)
|
||||||
|
db.flush() # Get the ID
|
||||||
|
|
||||||
|
# Create the English translation
|
||||||
|
translation = MarketplaceProductTranslation(
|
||||||
|
marketplace_product_id=marketplace_product.id,
|
||||||
|
language="en",
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
db.add(translation)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(marketplace_product)
|
||||||
|
return marketplace_product
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_marketplace_product(db):
|
def test_marketplace_product(db):
|
||||||
"""Create a test product."""
|
"""Create a test product with translation."""
|
||||||
marketplace_product = MarketplaceProduct(
|
return _create_marketplace_product_with_translation(
|
||||||
|
db,
|
||||||
marketplace_product_id="TEST001",
|
marketplace_product_id="TEST001",
|
||||||
title="Test MarketplaceProduct",
|
title="Test MarketplaceProduct",
|
||||||
description="A test product",
|
description="A test product",
|
||||||
@@ -27,17 +60,14 @@ def test_marketplace_product(db):
|
|||||||
marketplace="Letzshop",
|
marketplace="Letzshop",
|
||||||
vendor_name="TestVendor",
|
vendor_name="TestVendor",
|
||||||
)
|
)
|
||||||
db.add(marketplace_product)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(marketplace_product)
|
|
||||||
return marketplace_product
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def unique_product(db):
|
def unique_product(db):
|
||||||
"""Create a unique product for tests that need isolated product data."""
|
"""Create a unique product for tests that need isolated product data."""
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
marketplace_product = MarketplaceProduct(
|
return _create_marketplace_product_with_translation(
|
||||||
|
db,
|
||||||
marketplace_product_id=f"UNIQUE_{unique_id}",
|
marketplace_product_id=f"UNIQUE_{unique_id}",
|
||||||
title=f"Unique MarketplaceProduct {unique_id}",
|
title=f"Unique MarketplaceProduct {unique_id}",
|
||||||
description=f"A unique test product {unique_id}",
|
description=f"A unique test product {unique_id}",
|
||||||
@@ -50,10 +80,6 @@ def unique_product(db):
|
|||||||
vendor_name=f"UniqueVendor_{unique_id}",
|
vendor_name=f"UniqueVendor_{unique_id}",
|
||||||
google_product_category=f"UniqueCategory_{unique_id}",
|
google_product_category=f"UniqueCategory_{unique_id}",
|
||||||
)
|
)
|
||||||
db.add(marketplace_product)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(marketplace_product)
|
|
||||||
return marketplace_product
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -63,7 +89,8 @@ def multiple_products(db):
|
|||||||
marketplace_products = []
|
marketplace_products = []
|
||||||
|
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
marketplace_product = MarketplaceProduct(
|
marketplace_product = _create_marketplace_product_with_translation(
|
||||||
|
db,
|
||||||
marketplace_product_id=f"MULTI_{unique_id}_{i}",
|
marketplace_product_id=f"MULTI_{unique_id}_{i}",
|
||||||
title=f"Multi MarketplaceProduct {i} {unique_id}",
|
title=f"Multi MarketplaceProduct {i} {unique_id}",
|
||||||
description=f"Multi test product {i}",
|
description=f"Multi test product {i}",
|
||||||
@@ -77,33 +104,30 @@ def multiple_products(db):
|
|||||||
)
|
)
|
||||||
marketplace_products.append(marketplace_product)
|
marketplace_products.append(marketplace_product)
|
||||||
|
|
||||||
db.add_all(marketplace_products)
|
|
||||||
db.commit()
|
|
||||||
for product in marketplace_products:
|
|
||||||
db.refresh(product)
|
|
||||||
return marketplace_products
|
return marketplace_products
|
||||||
|
|
||||||
|
|
||||||
def create_unique_marketplace_product_factory():
|
def create_unique_marketplace_product_factory():
|
||||||
"""Factory function to create unique products in tests."""
|
"""Factory function to create unique products in tests."""
|
||||||
|
|
||||||
def _marketplace_create_product(db, **kwargs):
|
def _marketplace_create_product(db, title: str | None = None, **kwargs):
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
defaults = {
|
defaults = {
|
||||||
"marketplace_product_id": f"FACTORY_{unique_id}",
|
"marketplace_product_id": f"FACTORY_{unique_id}",
|
||||||
"title": f"Factory MarketplaceProduct {unique_id}",
|
|
||||||
"price": "15.99",
|
"price": "15.99",
|
||||||
"currency": "EUR",
|
"currency": "EUR",
|
||||||
"marketplace": "TestMarket",
|
"marketplace": "TestMarket",
|
||||||
"name": "TestVendor",
|
"vendor_name": "TestVendor",
|
||||||
}
|
}
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
|
|
||||||
marketplace_product = MarketplaceProduct(**defaults)
|
title = title or f"Factory MarketplaceProduct {unique_id}"
|
||||||
db.add(marketplace_product)
|
|
||||||
db.commit()
|
return _create_marketplace_product_with_translation(
|
||||||
db.refresh(marketplace_product)
|
db,
|
||||||
return marketplace_product
|
title=title,
|
||||||
|
**defaults,
|
||||||
|
)
|
||||||
|
|
||||||
return _marketplace_create_product
|
return _marketplace_create_product
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,24 @@ import pytest
|
|||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from models.database.marketplace_product import MarketplaceProduct
|
from models.database.marketplace_product import MarketplaceProduct
|
||||||
|
from models.database.marketplace_product_translation import MarketplaceProductTranslation
|
||||||
|
|
||||||
|
|
||||||
|
def _create_with_translation(db, marketplace_product_id, title, **kwargs):
|
||||||
|
"""Helper to create MarketplaceProduct with translation."""
|
||||||
|
mp = MarketplaceProduct(marketplace_product_id=marketplace_product_id, **kwargs)
|
||||||
|
db.add(mp)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
translation = MarketplaceProductTranslation(
|
||||||
|
marketplace_product_id=mp.id,
|
||||||
|
language="en",
|
||||||
|
title=title,
|
||||||
|
)
|
||||||
|
db.add(translation)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(mp)
|
||||||
|
return mp
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
@@ -13,10 +31,10 @@ class TestMarketplaceProductModel:
|
|||||||
|
|
||||||
def test_marketplace_product_creation(self, db):
|
def test_marketplace_product_creation(self, db):
|
||||||
"""Test MarketplaceProduct model creation."""
|
"""Test MarketplaceProduct model creation."""
|
||||||
marketplace_product = MarketplaceProduct(
|
marketplace_product = _create_with_translation(
|
||||||
|
db,
|
||||||
marketplace_product_id="DB_TEST_001",
|
marketplace_product_id="DB_TEST_001",
|
||||||
title="Database Test Product",
|
title="Database Test Product",
|
||||||
description="Testing product model",
|
|
||||||
price="25.99",
|
price="25.99",
|
||||||
currency="USD",
|
currency="USD",
|
||||||
brand="DBTest",
|
brand="DBTest",
|
||||||
@@ -26,42 +44,36 @@ class TestMarketplaceProductModel:
|
|||||||
vendor_name="Test Vendor",
|
vendor_name="Test Vendor",
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(marketplace_product)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(marketplace_product)
|
|
||||||
|
|
||||||
assert marketplace_product.id is not None
|
assert marketplace_product.id is not None
|
||||||
assert marketplace_product.marketplace_product_id == "DB_TEST_001"
|
assert marketplace_product.marketplace_product_id == "DB_TEST_001"
|
||||||
assert marketplace_product.title == "Database Test Product"
|
assert marketplace_product.get_title("en") == "Database Test Product"
|
||||||
assert marketplace_product.marketplace == "Letzshop"
|
assert marketplace_product.marketplace == "Letzshop"
|
||||||
assert marketplace_product.created_at is not None
|
assert marketplace_product.created_at is not None
|
||||||
|
|
||||||
def test_marketplace_product_id_uniqueness(self, db):
|
def test_marketplace_product_id_uniqueness(self, db):
|
||||||
"""Test unique marketplace_product_id constraint."""
|
"""Test unique marketplace_product_id constraint."""
|
||||||
product1 = MarketplaceProduct(
|
_create_with_translation(
|
||||||
|
db,
|
||||||
marketplace_product_id="UNIQUE_001",
|
marketplace_product_id="UNIQUE_001",
|
||||||
title="Product 1",
|
title="Product 1",
|
||||||
marketplace="Letzshop",
|
marketplace="Letzshop",
|
||||||
)
|
)
|
||||||
db.add(product1)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# Duplicate marketplace_product_id should raise error
|
# Duplicate marketplace_product_id should raise error
|
||||||
with pytest.raises(IntegrityError):
|
with pytest.raises(IntegrityError):
|
||||||
product2 = MarketplaceProduct(
|
_create_with_translation(
|
||||||
|
db,
|
||||||
marketplace_product_id="UNIQUE_001",
|
marketplace_product_id="UNIQUE_001",
|
||||||
title="Product 2",
|
title="Product 2",
|
||||||
marketplace="Letzshop",
|
marketplace="Letzshop",
|
||||||
)
|
)
|
||||||
db.add(product2)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
def test_marketplace_product_all_fields(self, db):
|
def test_marketplace_product_all_fields(self, db):
|
||||||
"""Test MarketplaceProduct with all optional fields."""
|
"""Test MarketplaceProduct with all optional fields."""
|
||||||
marketplace_product = MarketplaceProduct(
|
marketplace_product = _create_with_translation(
|
||||||
|
db,
|
||||||
marketplace_product_id="FULL_001",
|
marketplace_product_id="FULL_001",
|
||||||
title="Full Product",
|
title="Full Product",
|
||||||
description="Complete product description",
|
|
||||||
link="https://example.com/product",
|
link="https://example.com/product",
|
||||||
image_link="https://example.com/image.jpg",
|
image_link="https://example.com/image.jpg",
|
||||||
availability="in stock",
|
availability="in stock",
|
||||||
@@ -78,16 +90,12 @@ class TestMarketplaceProductModel:
|
|||||||
pattern="solid",
|
pattern="solid",
|
||||||
size="M",
|
size="M",
|
||||||
google_product_category="Apparel & Accessories",
|
google_product_category="Apparel & Accessories",
|
||||||
product_type="Clothing",
|
product_type_raw="Clothing",
|
||||||
currency="EUR",
|
currency="EUR",
|
||||||
marketplace="Letzshop",
|
marketplace="Letzshop",
|
||||||
vendor_name="Full Vendor",
|
vendor_name="Full Vendor",
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(marketplace_product)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(marketplace_product)
|
|
||||||
|
|
||||||
assert marketplace_product.brand == "TestBrand"
|
assert marketplace_product.brand == "TestBrand"
|
||||||
assert marketplace_product.gtin == "9876543210123"
|
assert marketplace_product.gtin == "9876543210123"
|
||||||
assert marketplace_product.color == "blue"
|
assert marketplace_product.color == "blue"
|
||||||
@@ -95,7 +103,8 @@ class TestMarketplaceProductModel:
|
|||||||
|
|
||||||
def test_marketplace_product_custom_labels(self, db):
|
def test_marketplace_product_custom_labels(self, db):
|
||||||
"""Test MarketplaceProduct with custom labels."""
|
"""Test MarketplaceProduct with custom labels."""
|
||||||
marketplace_product = MarketplaceProduct(
|
marketplace_product = _create_with_translation(
|
||||||
|
db,
|
||||||
marketplace_product_id="LABELS_001",
|
marketplace_product_id="LABELS_001",
|
||||||
title="Labeled Product",
|
title="Labeled Product",
|
||||||
marketplace="Letzshop",
|
marketplace="Letzshop",
|
||||||
@@ -106,26 +115,127 @@ class TestMarketplaceProductModel:
|
|||||||
custom_label_4="Label4",
|
custom_label_4="Label4",
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(marketplace_product)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(marketplace_product)
|
|
||||||
|
|
||||||
assert marketplace_product.custom_label_0 == "Label0"
|
assert marketplace_product.custom_label_0 == "Label0"
|
||||||
assert marketplace_product.custom_label_4 == "Label4"
|
assert marketplace_product.custom_label_4 == "Label4"
|
||||||
|
|
||||||
def test_marketplace_product_minimal_fields(self, db):
|
def test_marketplace_product_minimal_fields(self, db):
|
||||||
"""Test MarketplaceProduct with only required fields."""
|
"""Test MarketplaceProduct with only required fields."""
|
||||||
marketplace_product = MarketplaceProduct(
|
marketplace_product = _create_with_translation(
|
||||||
|
db,
|
||||||
marketplace_product_id="MINIMAL_001",
|
marketplace_product_id="MINIMAL_001",
|
||||||
title="Minimal Product",
|
title="Minimal Product",
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(marketplace_product)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(marketplace_product)
|
|
||||||
|
|
||||||
assert marketplace_product.id is not None
|
assert marketplace_product.id is not None
|
||||||
assert marketplace_product.marketplace_product_id == "MINIMAL_001"
|
assert marketplace_product.marketplace_product_id == "MINIMAL_001"
|
||||||
assert marketplace_product.title == "Minimal Product"
|
assert marketplace_product.get_title("en") == "Minimal Product"
|
||||||
assert marketplace_product.description is None
|
assert marketplace_product.get_description("en") is None
|
||||||
assert marketplace_product.price is None
|
assert marketplace_product.price is None
|
||||||
|
|
||||||
|
def test_marketplace_product_digital_fields(self, db):
|
||||||
|
"""Test MarketplaceProduct with digital product fields."""
|
||||||
|
marketplace_product = _create_with_translation(
|
||||||
|
db,
|
||||||
|
marketplace_product_id="DIGITAL_001",
|
||||||
|
title="Digital Product",
|
||||||
|
product_type_enum="digital",
|
||||||
|
is_digital=True,
|
||||||
|
digital_delivery_method="license_key",
|
||||||
|
platform="steam",
|
||||||
|
region_restrictions=["EU", "US"],
|
||||||
|
license_type="single_use",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert marketplace_product.product_type_enum == "digital"
|
||||||
|
assert marketplace_product.is_digital is True
|
||||||
|
assert marketplace_product.digital_delivery_method == "license_key"
|
||||||
|
assert marketplace_product.platform == "steam"
|
||||||
|
assert marketplace_product.region_restrictions == ["EU", "US"]
|
||||||
|
assert marketplace_product.license_type == "single_use"
|
||||||
|
|
||||||
|
def test_marketplace_product_translation_methods(self, db):
|
||||||
|
"""Test translation helper methods."""
|
||||||
|
mp = MarketplaceProduct(marketplace_product_id="TRANS_001")
|
||||||
|
db.add(mp)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
# Add English translation
|
||||||
|
en_trans = MarketplaceProductTranslation(
|
||||||
|
marketplace_product_id=mp.id,
|
||||||
|
language="en",
|
||||||
|
title="English Title",
|
||||||
|
description="English Description",
|
||||||
|
)
|
||||||
|
db.add(en_trans)
|
||||||
|
|
||||||
|
# Add French translation
|
||||||
|
fr_trans = MarketplaceProductTranslation(
|
||||||
|
marketplace_product_id=mp.id,
|
||||||
|
language="fr",
|
||||||
|
title="Titre Français",
|
||||||
|
description="Description Française",
|
||||||
|
)
|
||||||
|
db.add(fr_trans)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(mp)
|
||||||
|
|
||||||
|
assert mp.get_title("en") == "English Title"
|
||||||
|
assert mp.get_title("fr") == "Titre Français"
|
||||||
|
assert mp.get_description("en") == "English Description"
|
||||||
|
assert mp.get_description("fr") == "Description Française"
|
||||||
|
|
||||||
|
# Test fallback to English for unknown language
|
||||||
|
assert mp.get_title("de") == "English Title" # Falls back to 'en'
|
||||||
|
assert mp.get_description("de") == "English Description"
|
||||||
|
|
||||||
|
def test_marketplace_product_numeric_prices(self, db):
|
||||||
|
"""Test numeric price fields."""
|
||||||
|
marketplace_product = _create_with_translation(
|
||||||
|
db,
|
||||||
|
marketplace_product_id="PRICES_001",
|
||||||
|
title="Priced Product",
|
||||||
|
price="99.99 EUR",
|
||||||
|
price_numeric=99.99,
|
||||||
|
sale_price="79.99 EUR",
|
||||||
|
sale_price_numeric=79.99,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert marketplace_product.price == "99.99 EUR"
|
||||||
|
assert marketplace_product.price_numeric == 99.99
|
||||||
|
assert marketplace_product.sale_price_numeric == 79.99
|
||||||
|
assert marketplace_product.effective_price == 99.99
|
||||||
|
assert marketplace_product.effective_sale_price == 79.99
|
||||||
|
|
||||||
|
def test_marketplace_product_attributes_json(self, db):
|
||||||
|
"""Test flexible attributes JSON field."""
|
||||||
|
marketplace_product = _create_with_translation(
|
||||||
|
db,
|
||||||
|
marketplace_product_id="ATTRS_001",
|
||||||
|
title="Product with Attributes",
|
||||||
|
attributes={
|
||||||
|
"custom_field": "custom_value",
|
||||||
|
"nested": {"key": "value"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert marketplace_product.attributes["custom_field"] == "custom_value"
|
||||||
|
assert marketplace_product.attributes["nested"]["key"] == "value"
|
||||||
|
|
||||||
|
def test_marketplace_product_all_images_property(self, db):
|
||||||
|
"""Test all_images property."""
|
||||||
|
marketplace_product = _create_with_translation(
|
||||||
|
db,
|
||||||
|
marketplace_product_id="IMAGES_001",
|
||||||
|
title="Product with Images",
|
||||||
|
image_link="https://example.com/main.jpg",
|
||||||
|
additional_images=[
|
||||||
|
"https://example.com/img1.jpg",
|
||||||
|
"https://example.com/img2.jpg",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
images = marketplace_product.all_images
|
||||||
|
assert len(images) == 3
|
||||||
|
assert images[0] == "https://example.com/main.jpg"
|
||||||
|
assert "https://example.com/img1.jpg" in images
|
||||||
|
assert "https://example.com/img2.jpg" in images
|
||||||
|
|||||||
@@ -152,11 +152,14 @@ class TestOrderItemModel:
|
|||||||
|
|
||||||
def test_order_item_creation(self, db, test_order, test_product):
|
def test_order_item_creation(self, db, test_order, test_product):
|
||||||
"""Test OrderItem model."""
|
"""Test OrderItem model."""
|
||||||
|
# Get title from translation
|
||||||
|
product_title = test_product.marketplace_product.get_title("en")
|
||||||
|
|
||||||
order_item = OrderItem(
|
order_item = OrderItem(
|
||||||
order_id=test_order.id,
|
order_id=test_order.id,
|
||||||
product_id=test_product.id,
|
product_id=test_product.id,
|
||||||
product_name=test_product.marketplace_product.title,
|
product_name=product_title,
|
||||||
product_sku=test_product.product_id or "SKU001",
|
product_sku=test_product.vendor_sku or "SKU001",
|
||||||
quantity=2,
|
quantity=2,
|
||||||
unit_price=49.99,
|
unit_price=49.99,
|
||||||
total_price=99.98,
|
total_price=99.98,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class TestProductModel:
|
|||||||
product = Product(
|
product = Product(
|
||||||
vendor_id=test_vendor.id,
|
vendor_id=test_vendor.id,
|
||||||
marketplace_product_id=test_marketplace_product.id,
|
marketplace_product_id=test_marketplace_product.id,
|
||||||
product_id="VENDOR_PROD_001",
|
vendor_sku="VENDOR_PROD_001",
|
||||||
price=89.99,
|
price=89.99,
|
||||||
currency="EUR",
|
currency="EUR",
|
||||||
availability="in stock",
|
availability="in stock",
|
||||||
@@ -34,7 +34,11 @@ class TestProductModel:
|
|||||||
assert product.price == 89.99
|
assert product.price == 89.99
|
||||||
assert product.is_featured is True
|
assert product.is_featured is True
|
||||||
assert product.vendor.vendor_code == test_vendor.vendor_code
|
assert product.vendor.vendor_code == test_vendor.vendor_code
|
||||||
assert product.marketplace_product.title == test_marketplace_product.title
|
# Use get_title() method instead of .title attribute
|
||||||
|
assert (
|
||||||
|
product.marketplace_product.get_title("en")
|
||||||
|
== test_marketplace_product.get_title("en")
|
||||||
|
)
|
||||||
|
|
||||||
def test_product_unique_per_vendor(self, db, test_vendor, test_marketplace_product):
|
def test_product_unique_per_vendor(self, db, test_vendor, test_marketplace_product):
|
||||||
"""Test that same marketplace product can't be added twice to vendor catalog."""
|
"""Test that same marketplace product can't be added twice to vendor catalog."""
|
||||||
@@ -76,7 +80,7 @@ class TestProductModel:
|
|||||||
product = Product(
|
product = Product(
|
||||||
vendor_id=test_vendor.id,
|
vendor_id=test_vendor.id,
|
||||||
marketplace_product_id=test_marketplace_product.id,
|
marketplace_product_id=test_marketplace_product.id,
|
||||||
product_id="CUSTOM_SKU_001",
|
vendor_sku="CUSTOM_SKU_001",
|
||||||
price=49.99,
|
price=49.99,
|
||||||
sale_price=39.99,
|
sale_price=39.99,
|
||||||
currency="USD",
|
currency="USD",
|
||||||
@@ -87,7 +91,7 @@ class TestProductModel:
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(product)
|
db.refresh(product)
|
||||||
|
|
||||||
assert product.product_id == "CUSTOM_SKU_001"
|
assert product.vendor_sku == "CUSTOM_SKU_001"
|
||||||
assert product.price == 49.99
|
assert product.price == 49.99
|
||||||
assert product.sale_price == 39.99
|
assert product.sale_price == 39.99
|
||||||
assert product.currency == "USD"
|
assert product.currency == "USD"
|
||||||
@@ -121,3 +125,99 @@ class TestProductModel:
|
|||||||
assert product.vendor is not None
|
assert product.vendor is not None
|
||||||
assert product.marketplace_product is not None
|
assert product.marketplace_product is not None
|
||||||
assert product.inventory_entries == [] # No inventory yet
|
assert product.inventory_entries == [] # No inventory yet
|
||||||
|
|
||||||
|
def test_product_effective_properties(self, db, test_vendor, test_marketplace_product):
|
||||||
|
"""Test Product effective properties with override pattern."""
|
||||||
|
# First, set some values on the marketplace product
|
||||||
|
test_marketplace_product.price_numeric = 100.00
|
||||||
|
test_marketplace_product.brand = "SourceBrand"
|
||||||
|
db.commit()
|
||||||
|
db.refresh(test_marketplace_product)
|
||||||
|
|
||||||
|
# Create product without overrides
|
||||||
|
product = Product(
|
||||||
|
vendor_id=test_vendor.id,
|
||||||
|
marketplace_product_id=test_marketplace_product.id,
|
||||||
|
)
|
||||||
|
db.add(product)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(product)
|
||||||
|
|
||||||
|
# Should inherit from marketplace product
|
||||||
|
assert product.effective_price == 100.00
|
||||||
|
assert product.effective_brand == "SourceBrand"
|
||||||
|
|
||||||
|
# Now override the price
|
||||||
|
product.price = 89.99
|
||||||
|
db.commit()
|
||||||
|
db.refresh(product)
|
||||||
|
|
||||||
|
# Should use the override
|
||||||
|
assert product.effective_price == 89.99
|
||||||
|
# Brand still inherited
|
||||||
|
assert product.effective_brand == "SourceBrand"
|
||||||
|
|
||||||
|
def test_product_reset_to_source(self, db, test_vendor, test_marketplace_product):
|
||||||
|
"""Test reset_to_source methods."""
|
||||||
|
# Set up marketplace product values
|
||||||
|
test_marketplace_product.price_numeric = 100.00
|
||||||
|
test_marketplace_product.brand = "SourceBrand"
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Create product with overrides
|
||||||
|
product = Product(
|
||||||
|
vendor_id=test_vendor.id,
|
||||||
|
marketplace_product_id=test_marketplace_product.id,
|
||||||
|
price=89.99,
|
||||||
|
brand="OverrideBrand",
|
||||||
|
)
|
||||||
|
db.add(product)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(product)
|
||||||
|
|
||||||
|
assert product.effective_price == 89.99
|
||||||
|
assert product.effective_brand == "OverrideBrand"
|
||||||
|
|
||||||
|
# Reset price to source
|
||||||
|
product.reset_field_to_source("price")
|
||||||
|
db.commit()
|
||||||
|
db.refresh(product)
|
||||||
|
|
||||||
|
assert product.price is None
|
||||||
|
assert product.effective_price == 100.00 # Now inherits
|
||||||
|
|
||||||
|
# Reset all fields
|
||||||
|
product.reset_all_to_source()
|
||||||
|
db.commit()
|
||||||
|
db.refresh(product)
|
||||||
|
|
||||||
|
assert product.brand is None
|
||||||
|
assert product.effective_brand == "SourceBrand" # Now inherits
|
||||||
|
|
||||||
|
def test_product_get_override_info(self, db, test_vendor, test_marketplace_product):
|
||||||
|
"""Test get_override_info method."""
|
||||||
|
test_marketplace_product.price_numeric = 100.00
|
||||||
|
test_marketplace_product.brand = "SourceBrand"
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
product = Product(
|
||||||
|
vendor_id=test_vendor.id,
|
||||||
|
marketplace_product_id=test_marketplace_product.id,
|
||||||
|
price=89.99, # Override
|
||||||
|
# brand not set - will inherit
|
||||||
|
)
|
||||||
|
db.add(product)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(product)
|
||||||
|
|
||||||
|
info = product.get_override_info()
|
||||||
|
|
||||||
|
# Price is overridden
|
||||||
|
assert info["price"] == 89.99
|
||||||
|
assert info["price_overridden"] is True
|
||||||
|
assert info["price_source"] == 100.00
|
||||||
|
|
||||||
|
# Brand is inherited
|
||||||
|
assert info["brand"] == "SourceBrand"
|
||||||
|
assert info["brand_overridden"] is False
|
||||||
|
assert info["brand_source"] == "SourceBrand"
|
||||||
|
|||||||
@@ -157,12 +157,13 @@ class TestProductResponseSchema:
|
|||||||
"vendor_id": 1,
|
"vendor_id": 1,
|
||||||
"marketplace_product": {
|
"marketplace_product": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"marketplace_product_id": "TEST001", # Required field
|
||||||
"gtin": "1234567890123",
|
"gtin": "1234567890123",
|
||||||
"title": "Test Product",
|
"title": "Test Product",
|
||||||
"description": "A test product",
|
"description": "A test product",
|
||||||
"brand": "Test Brand",
|
"brand": "Test Brand",
|
||||||
"category": "Electronics",
|
"google_product_category": "Electronics",
|
||||||
"image_url": "https://example.com/image.jpg",
|
"image_link": "https://example.com/image.jpg",
|
||||||
"created_at": datetime.now(),
|
"created_at": datetime.now(),
|
||||||
"updated_at": datetime.now(),
|
"updated_at": datetime.now(),
|
||||||
},
|
},
|
||||||
@@ -194,12 +195,13 @@ class TestProductResponseSchema:
|
|||||||
"vendor_id": 1,
|
"vendor_id": 1,
|
||||||
"marketplace_product": {
|
"marketplace_product": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
"marketplace_product_id": "TEST002", # Required field
|
||||||
"gtin": "1234567890123",
|
"gtin": "1234567890123",
|
||||||
"title": "Test Product",
|
"title": "Test Product",
|
||||||
"description": None,
|
"description": None,
|
||||||
"brand": None,
|
"brand": None,
|
||||||
"category": None,
|
"google_product_category": None,
|
||||||
"image_url": None,
|
"image_link": None,
|
||||||
"created_at": datetime.now(),
|
"created_at": datetime.now(),
|
||||||
"updated_at": datetime.now(),
|
"updated_at": datetime.now(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,10 +11,31 @@ from app.services.stats_service import StatsService
|
|||||||
from models.database.inventory import Inventory
|
from models.database.inventory import Inventory
|
||||||
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.marketplace_product_translation import MarketplaceProductTranslation
|
||||||
from models.database.product import Product
|
from models.database.product import Product
|
||||||
from models.database.vendor import Vendor
|
from models.database.vendor import Vendor
|
||||||
|
|
||||||
|
|
||||||
|
def create_marketplace_product_with_translation(
|
||||||
|
db, marketplace_product_id, title, language="en", **kwargs
|
||||||
|
):
|
||||||
|
"""Helper to create a MarketplaceProduct with its translation."""
|
||||||
|
product = MarketplaceProduct(
|
||||||
|
marketplace_product_id=marketplace_product_id,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
db.add(product)
|
||||||
|
db.flush() # Get the product ID
|
||||||
|
|
||||||
|
translation = MarketplaceProductTranslation(
|
||||||
|
marketplace_product_id=product.id,
|
||||||
|
language=language,
|
||||||
|
title=title,
|
||||||
|
)
|
||||||
|
db.add(translation)
|
||||||
|
return product
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
@pytest.mark.stats
|
@pytest.mark.stats
|
||||||
class TestStatsService:
|
class TestStatsService:
|
||||||
@@ -58,29 +79,28 @@ class TestStatsService:
|
|||||||
):
|
):
|
||||||
"""Test comprehensive stats with multiple marketplaces."""
|
"""Test comprehensive stats with multiple marketplaces."""
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
additional_products = [
|
create_marketplace_product_with_translation(
|
||||||
MarketplaceProduct(
|
db,
|
||||||
marketplace_product_id=f"PROD002_{unique_id}",
|
marketplace_product_id=f"PROD002_{unique_id}",
|
||||||
title="MarketplaceProduct 2",
|
title="MarketplaceProduct 2",
|
||||||
brand="DifferentBrand",
|
brand="DifferentBrand",
|
||||||
google_product_category="Different Category",
|
google_product_category="Different Category",
|
||||||
marketplace="Amazon",
|
marketplace="Amazon",
|
||||||
vendor_name="AmazonVendor",
|
vendor_name="AmazonVendor",
|
||||||
price="15.99",
|
price="15.99",
|
||||||
currency="EUR",
|
currency="EUR",
|
||||||
),
|
)
|
||||||
MarketplaceProduct(
|
create_marketplace_product_with_translation(
|
||||||
marketplace_product_id=f"PROD003_{unique_id}",
|
db,
|
||||||
title="MarketplaceProduct 3",
|
marketplace_product_id=f"PROD003_{unique_id}",
|
||||||
brand="ThirdBrand",
|
title="MarketplaceProduct 3",
|
||||||
google_product_category="Third Category",
|
brand="ThirdBrand",
|
||||||
marketplace="eBay",
|
google_product_category="Third Category",
|
||||||
vendor_name="eBayVendor",
|
marketplace="eBay",
|
||||||
price="25.99",
|
vendor_name="eBayVendor",
|
||||||
currency="USD",
|
price="25.99",
|
||||||
),
|
currency="USD",
|
||||||
]
|
)
|
||||||
db.add_all(additional_products)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
stats = self.service.get_comprehensive_stats(db)
|
stats = self.service.get_comprehensive_stats(db)
|
||||||
@@ -91,29 +111,28 @@ class TestStatsService:
|
|||||||
def test_get_comprehensive_stats_handles_nulls(self, db):
|
def test_get_comprehensive_stats_handles_nulls(self, db):
|
||||||
"""Test comprehensive stats handles null/empty values correctly."""
|
"""Test comprehensive stats handles null/empty values correctly."""
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
products_with_nulls = [
|
create_marketplace_product_with_translation(
|
||||||
MarketplaceProduct(
|
db,
|
||||||
marketplace_product_id=f"NULL001_{unique_id}",
|
marketplace_product_id=f"NULL001_{unique_id}",
|
||||||
title="MarketplaceProduct with Nulls",
|
title="MarketplaceProduct with Nulls",
|
||||||
brand=None,
|
brand=None,
|
||||||
google_product_category=None,
|
google_product_category=None,
|
||||||
marketplace=None,
|
marketplace=None,
|
||||||
vendor_name=None,
|
vendor_name=None,
|
||||||
price="10.00",
|
price="10.00",
|
||||||
currency="EUR",
|
currency="EUR",
|
||||||
),
|
)
|
||||||
MarketplaceProduct(
|
create_marketplace_product_with_translation(
|
||||||
marketplace_product_id=f"EMPTY001_{unique_id}",
|
db,
|
||||||
title="MarketplaceProduct with Empty Values",
|
marketplace_product_id=f"EMPTY001_{unique_id}",
|
||||||
brand="",
|
title="MarketplaceProduct with Empty Values",
|
||||||
google_product_category="",
|
brand="",
|
||||||
marketplace="",
|
google_product_category="",
|
||||||
vendor_name="",
|
marketplace="",
|
||||||
price="15.00",
|
vendor_name="",
|
||||||
currency="EUR",
|
price="15.00",
|
||||||
),
|
currency="EUR",
|
||||||
]
|
)
|
||||||
db.add_all(products_with_nulls)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
stats = self.service.get_comprehensive_stats(db)
|
stats = self.service.get_comprehensive_stats(db)
|
||||||
@@ -170,36 +189,36 @@ class TestStatsService:
|
|||||||
):
|
):
|
||||||
"""Test marketplace breakdown with multiple marketplaces."""
|
"""Test marketplace breakdown with multiple marketplaces."""
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
marketplace_products = [
|
create_marketplace_product_with_translation(
|
||||||
MarketplaceProduct(
|
db,
|
||||||
marketplace_product_id=f"AMAZON001_{unique_id}",
|
marketplace_product_id=f"AMAZON001_{unique_id}",
|
||||||
title="Amazon MarketplaceProduct 1",
|
title="Amazon MarketplaceProduct 1",
|
||||||
brand="AmazonBrand1",
|
brand="AmazonBrand1",
|
||||||
marketplace="Amazon",
|
marketplace="Amazon",
|
||||||
vendor_name="AmazonVendor1",
|
vendor_name="AmazonVendor1",
|
||||||
price="20.00",
|
price="20.00",
|
||||||
currency="EUR",
|
currency="EUR",
|
||||||
),
|
)
|
||||||
MarketplaceProduct(
|
create_marketplace_product_with_translation(
|
||||||
marketplace_product_id=f"AMAZON002_{unique_id}",
|
db,
|
||||||
title="Amazon MarketplaceProduct 2",
|
marketplace_product_id=f"AMAZON002_{unique_id}",
|
||||||
brand="AmazonBrand2",
|
title="Amazon MarketplaceProduct 2",
|
||||||
marketplace="Amazon",
|
brand="AmazonBrand2",
|
||||||
vendor_name="AmazonVendor2",
|
marketplace="Amazon",
|
||||||
price="25.00",
|
vendor_name="AmazonVendor2",
|
||||||
currency="EUR",
|
price="25.00",
|
||||||
),
|
currency="EUR",
|
||||||
MarketplaceProduct(
|
)
|
||||||
marketplace_product_id=f"EBAY001_{unique_id}",
|
create_marketplace_product_with_translation(
|
||||||
title="eBay MarketplaceProduct",
|
db,
|
||||||
brand="eBayBrand",
|
marketplace_product_id=f"EBAY001_{unique_id}",
|
||||||
marketplace="eBay",
|
title="eBay MarketplaceProduct",
|
||||||
vendor_name="eBayVendor",
|
brand="eBayBrand",
|
||||||
price="30.00",
|
marketplace="eBay",
|
||||||
currency="USD",
|
vendor_name="eBayVendor",
|
||||||
),
|
price="30.00",
|
||||||
]
|
currency="USD",
|
||||||
db.add_all(marketplace_products)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
stats = self.service.get_marketplace_breakdown_stats(db)
|
stats = self.service.get_marketplace_breakdown_stats(db)
|
||||||
@@ -223,7 +242,8 @@ class TestStatsService:
|
|||||||
def test_get_marketplace_breakdown_stats_excludes_nulls(self, db):
|
def test_get_marketplace_breakdown_stats_excludes_nulls(self, db):
|
||||||
"""Test marketplace breakdown excludes products with null marketplaces."""
|
"""Test marketplace breakdown excludes products with null marketplaces."""
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
null_marketplace_product = MarketplaceProduct(
|
create_marketplace_product_with_translation(
|
||||||
|
db,
|
||||||
marketplace_product_id=f"NULLMARKET001_{unique_id}",
|
marketplace_product_id=f"NULLMARKET001_{unique_id}",
|
||||||
title="MarketplaceProduct without marketplace",
|
title="MarketplaceProduct without marketplace",
|
||||||
marketplace=None,
|
marketplace=None,
|
||||||
@@ -232,7 +252,6 @@ class TestStatsService:
|
|||||||
price="10.00",
|
price="10.00",
|
||||||
currency="EUR",
|
currency="EUR",
|
||||||
)
|
)
|
||||||
db.add(null_marketplace_product)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
stats = self.service.get_marketplace_breakdown_stats(db)
|
stats = self.service.get_marketplace_breakdown_stats(db)
|
||||||
@@ -264,15 +283,16 @@ class TestStatsService:
|
|||||||
"""Test getting vendor statistics successfully."""
|
"""Test getting vendor statistics successfully."""
|
||||||
stats = self.service.get_vendor_stats(db, test_vendor.id)
|
stats = self.service.get_vendor_stats(db, test_vendor.id)
|
||||||
|
|
||||||
assert "catalog" in stats
|
# New flat structure compatible with VendorDashboardStatsResponse
|
||||||
assert "staging" in stats
|
assert "total_products" in stats
|
||||||
assert "inventory" in stats
|
assert "active_products" in stats
|
||||||
assert "imports" in stats
|
assert "total_orders" in stats
|
||||||
assert "orders" in stats
|
assert "total_customers" in stats
|
||||||
assert "customers" in stats
|
assert "total_revenue" in stats
|
||||||
|
assert "total_inventory_quantity" in stats
|
||||||
|
|
||||||
assert stats["catalog"]["total_products"] >= 0
|
assert stats["total_products"] >= 0
|
||||||
assert stats["inventory"]["total_quantity"] >= 0
|
assert stats["total_inventory_quantity"] >= 0
|
||||||
|
|
||||||
def test_get_vendor_stats_vendor_not_found(self, db):
|
def test_get_vendor_stats_vendor_not_found(self, db):
|
||||||
"""Test vendor stats with non-existent vendor."""
|
"""Test vendor stats with non-existent vendor."""
|
||||||
@@ -285,8 +305,8 @@ class TestStatsService:
|
|||||||
"""Test vendor stats includes inventory data."""
|
"""Test vendor stats includes inventory data."""
|
||||||
stats = self.service.get_vendor_stats(db, test_vendor.id)
|
stats = self.service.get_vendor_stats(db, test_vendor.id)
|
||||||
|
|
||||||
assert stats["inventory"]["total_quantity"] >= test_inventory.quantity
|
assert stats["total_inventory_quantity"] >= test_inventory.quantity
|
||||||
assert stats["inventory"]["reserved_quantity"] >= 0
|
assert stats["reserved_inventory_quantity"] >= 0
|
||||||
|
|
||||||
def test_get_vendor_stats_database_error(self, db, test_vendor):
|
def test_get_vendor_stats_database_error(self, db, test_vendor):
|
||||||
"""Test vendor stats handles database errors after vendor check."""
|
"""Test vendor stats handles database errors after vendor check."""
|
||||||
@@ -453,27 +473,26 @@ class TestStatsService:
|
|||||||
def test_get_unique_brands_count(self, db, test_marketplace_product):
|
def test_get_unique_brands_count(self, db, test_marketplace_product):
|
||||||
"""Test getting unique brands count."""
|
"""Test getting unique brands count."""
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
brand_products = [
|
create_marketplace_product_with_translation(
|
||||||
MarketplaceProduct(
|
db,
|
||||||
marketplace_product_id=f"BRAND001_{unique_id}",
|
marketplace_product_id=f"BRAND001_{unique_id}",
|
||||||
title="Brand MarketplaceProduct 1",
|
title="Brand MarketplaceProduct 1",
|
||||||
brand="BrandA",
|
brand="BrandA",
|
||||||
marketplace="Test",
|
marketplace="Test",
|
||||||
vendor_name="TestVendor",
|
vendor_name="TestVendor",
|
||||||
price="10.00",
|
price="10.00",
|
||||||
currency="EUR",
|
currency="EUR",
|
||||||
),
|
)
|
||||||
MarketplaceProduct(
|
create_marketplace_product_with_translation(
|
||||||
marketplace_product_id=f"BRAND002_{unique_id}",
|
db,
|
||||||
title="Brand MarketplaceProduct 2",
|
marketplace_product_id=f"BRAND002_{unique_id}",
|
||||||
brand="BrandB",
|
title="Brand MarketplaceProduct 2",
|
||||||
marketplace="Test",
|
brand="BrandB",
|
||||||
vendor_name="TestVendor",
|
marketplace="Test",
|
||||||
price="15.00",
|
vendor_name="TestVendor",
|
||||||
currency="EUR",
|
price="15.00",
|
||||||
),
|
currency="EUR",
|
||||||
]
|
)
|
||||||
db.add_all(brand_products)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
count = self.service._get_unique_brands_count(db)
|
count = self.service._get_unique_brands_count(db)
|
||||||
@@ -484,27 +503,26 @@ class TestStatsService:
|
|||||||
def test_get_unique_categories_count(self, db, test_marketplace_product):
|
def test_get_unique_categories_count(self, db, test_marketplace_product):
|
||||||
"""Test getting unique categories count."""
|
"""Test getting unique categories count."""
|
||||||
unique_id = str(uuid.uuid4())[:8]
|
unique_id = str(uuid.uuid4())[:8]
|
||||||
category_products = [
|
create_marketplace_product_with_translation(
|
||||||
MarketplaceProduct(
|
db,
|
||||||
marketplace_product_id=f"CAT001_{unique_id}",
|
marketplace_product_id=f"CAT001_{unique_id}",
|
||||||
title="Category MarketplaceProduct 1",
|
title="Category MarketplaceProduct 1",
|
||||||
google_product_category="Electronics",
|
google_product_category="Electronics",
|
||||||
marketplace="Test",
|
marketplace="Test",
|
||||||
vendor_name="TestVendor",
|
vendor_name="TestVendor",
|
||||||
price="10.00",
|
price="10.00",
|
||||||
currency="EUR",
|
currency="EUR",
|
||||||
),
|
)
|
||||||
MarketplaceProduct(
|
create_marketplace_product_with_translation(
|
||||||
marketplace_product_id=f"CAT002_{unique_id}",
|
db,
|
||||||
title="Category MarketplaceProduct 2",
|
marketplace_product_id=f"CAT002_{unique_id}",
|
||||||
google_product_category="Books",
|
title="Category MarketplaceProduct 2",
|
||||||
marketplace="Test",
|
google_product_category="Books",
|
||||||
vendor_name="TestVendor",
|
marketplace="Test",
|
||||||
price="15.00",
|
vendor_name="TestVendor",
|
||||||
currency="EUR",
|
price="15.00",
|
||||||
),
|
currency="EUR",
|
||||||
]
|
)
|
||||||
db.add_all(category_products)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
count = self.service._get_unique_categories_count(db)
|
count = self.service._get_unique_categories_count(db)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# tests/test_csv_processor.py
|
# tests/unit/utils/test_csv_processor.py
|
||||||
|
"""Unit tests for CSV processor with translation support."""
|
||||||
|
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -7,6 +9,8 @@ import requests
|
|||||||
import requests.exceptions
|
import requests.exceptions
|
||||||
|
|
||||||
from app.utils.csv_processor import CSVProcessor
|
from app.utils.csv_processor import CSVProcessor
|
||||||
|
from models.database.marketplace_product import MarketplaceProduct
|
||||||
|
from models.database.marketplace_product_translation import MarketplaceProductTranslation
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
@@ -102,6 +106,74 @@ TEST002,Test MarketplaceProduct 2,15.99,TestMarket"""
|
|||||||
assert df.iloc[0]["marketplace_product_id"] == "TEST001"
|
assert df.iloc[0]["marketplace_product_id"] == "TEST001"
|
||||||
assert df.iloc[1]["price"] == 15.99
|
assert df.iloc[1]["price"] == 15.99
|
||||||
|
|
||||||
|
def test_normalize_columns_google_shopping(self):
|
||||||
|
"""Test column normalization for Google Shopping feed format"""
|
||||||
|
csv_content = """g:id,g:title,g:description,g:price,g:brand,g:product_type
|
||||||
|
TEST001,Product 1,Description 1,19.99 EUR,Brand1,Category1"""
|
||||||
|
|
||||||
|
df = self.processor.parse_csv(csv_content)
|
||||||
|
df = self.processor.normalize_columns(df)
|
||||||
|
|
||||||
|
assert "marketplace_product_id" in df.columns
|
||||||
|
assert "title" in df.columns
|
||||||
|
assert "description" in df.columns
|
||||||
|
assert "product_type_raw" in df.columns # Renamed from product_type
|
||||||
|
assert df.iloc[0]["marketplace_product_id"] == "TEST001"
|
||||||
|
assert df.iloc[0]["title"] == "Product 1"
|
||||||
|
|
||||||
|
def test_extract_translation_data(self):
|
||||||
|
"""Test extraction of translation fields from product data"""
|
||||||
|
product_data = {
|
||||||
|
"marketplace_product_id": "TEST001",
|
||||||
|
"title": "Test Product",
|
||||||
|
"description": "Test Description",
|
||||||
|
"short_description": "Short desc",
|
||||||
|
"price": "19.99",
|
||||||
|
"brand": "TestBrand",
|
||||||
|
}
|
||||||
|
|
||||||
|
translation_data = self.processor._extract_translation_data(product_data)
|
||||||
|
|
||||||
|
# Translation fields should be extracted
|
||||||
|
assert translation_data["title"] == "Test Product"
|
||||||
|
assert translation_data["description"] == "Test Description"
|
||||||
|
assert translation_data["short_description"] == "Short desc"
|
||||||
|
|
||||||
|
# Product data should no longer have translation fields
|
||||||
|
assert "title" not in product_data
|
||||||
|
assert "description" not in product_data
|
||||||
|
assert "short_description" not in product_data
|
||||||
|
|
||||||
|
# Non-translation fields should remain
|
||||||
|
assert product_data["marketplace_product_id"] == "TEST001"
|
||||||
|
assert product_data["price"] == "19.99"
|
||||||
|
assert product_data["brand"] == "TestBrand"
|
||||||
|
|
||||||
|
def test_parse_price_to_numeric(self):
|
||||||
|
"""Test price string to numeric conversion"""
|
||||||
|
assert self.processor._parse_price_to_numeric("19.99 EUR") == 19.99
|
||||||
|
assert self.processor._parse_price_to_numeric("19,99 EUR") == 19.99
|
||||||
|
assert self.processor._parse_price_to_numeric("$29.99") == 29.99
|
||||||
|
assert self.processor._parse_price_to_numeric("100") == 100.0
|
||||||
|
assert self.processor._parse_price_to_numeric(None) is None
|
||||||
|
assert self.processor._parse_price_to_numeric("") is None
|
||||||
|
|
||||||
|
def test_clean_row_data_with_prices(self):
|
||||||
|
"""Test row data cleaning with price parsing"""
|
||||||
|
row_data = {
|
||||||
|
"marketplace_product_id": "TEST001",
|
||||||
|
"title": "Test Product",
|
||||||
|
"price": "19.99 EUR",
|
||||||
|
"sale_price": "14.99 EUR",
|
||||||
|
"gtin": "1234567890123",
|
||||||
|
}
|
||||||
|
|
||||||
|
cleaned = self.processor._clean_row_data(row_data)
|
||||||
|
|
||||||
|
assert cleaned["price_numeric"] == 19.99
|
||||||
|
assert cleaned["sale_price_numeric"] == 14.99
|
||||||
|
assert cleaned["currency"] == "EUR"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_process_marketplace_csv_from_url(self, db):
|
async def test_process_marketplace_csv_from_url(self, db):
|
||||||
"""Test complete marketplace CSV processing"""
|
"""Test complete marketplace CSV processing"""
|
||||||
@@ -117,15 +189,275 @@ TEST002,Test MarketplaceProduct 2,15.99,TestMarket"""
|
|||||||
"title": ["MarketplaceProduct 1", "MarketplaceProduct 2"],
|
"title": ["MarketplaceProduct 1", "MarketplaceProduct 2"],
|
||||||
"price": ["10.99", "15.99"],
|
"price": ["10.99", "15.99"],
|
||||||
"marketplace": ["TestMarket", "TestMarket"],
|
"marketplace": ["TestMarket", "TestMarket"],
|
||||||
"name": ["TestVendor", "TestVendor"],
|
"vendor_name": ["TestVendor", "TestVendor"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
mock_parse.return_value = mock_df
|
mock_parse.return_value = mock_df
|
||||||
|
|
||||||
result = await self.processor.process_marketplace_csv_from_url(
|
result = await self.processor.process_marketplace_csv_from_url(
|
||||||
"http://example.com/test.csv", "TestMarket", "TestVendor", 1000, db
|
"http://example.com/test.csv",
|
||||||
|
"TestMarket",
|
||||||
|
"TestVendor",
|
||||||
|
1000,
|
||||||
|
db,
|
||||||
|
language="en",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "imported" in result
|
assert "imported" in result
|
||||||
assert "updated" in result
|
assert "updated" in result
|
||||||
assert "total_processed" in result
|
assert "total_processed" in result
|
||||||
|
|
||||||
|
assert "language" in result
|
||||||
|
assert result["language"] == "en"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_process_batch_creates_translations(self, db):
|
||||||
|
"""Test that batch processing creates translation records"""
|
||||||
|
# Clean up any existing test data
|
||||||
|
existing = (
|
||||||
|
db.query(MarketplaceProduct)
|
||||||
|
.filter(
|
||||||
|
MarketplaceProduct.marketplace_product_id.in_(
|
||||||
|
["TRANS_TEST_001", "TRANS_TEST_002"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for p in existing:
|
||||||
|
db.delete(p)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Create test DataFrame
|
||||||
|
batch_df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"marketplace_product_id": ["TRANS_TEST_001", "TRANS_TEST_002"],
|
||||||
|
"title": ["Product One", "Product Two"],
|
||||||
|
"description": ["Description One", "Description Two"],
|
||||||
|
"price": ["19.99 EUR", "29.99 EUR"],
|
||||||
|
"brand": ["Brand1", "Brand2"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.processor._process_marketplace_batch(
|
||||||
|
batch_df,
|
||||||
|
"TestMarket",
|
||||||
|
"TestVendor",
|
||||||
|
db,
|
||||||
|
batch_num=1,
|
||||||
|
language="en",
|
||||||
|
source_file="test.csv",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["imported"] == 2
|
||||||
|
assert result["errors"] == 0
|
||||||
|
|
||||||
|
# Verify products were created
|
||||||
|
products = (
|
||||||
|
db.query(MarketplaceProduct)
|
||||||
|
.filter(
|
||||||
|
MarketplaceProduct.marketplace_product_id.in_(
|
||||||
|
["TRANS_TEST_001", "TRANS_TEST_002"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
assert len(products) == 2
|
||||||
|
|
||||||
|
# Verify translations were created
|
||||||
|
for product in products:
|
||||||
|
assert len(product.translations) == 1
|
||||||
|
translation = product.translations[0]
|
||||||
|
assert translation.language == "en"
|
||||||
|
assert translation.title is not None
|
||||||
|
assert translation.source_file == "test.csv"
|
||||||
|
|
||||||
|
# Verify get_title method works
|
||||||
|
product1 = next(
|
||||||
|
p for p in products if p.marketplace_product_id == "TRANS_TEST_001"
|
||||||
|
)
|
||||||
|
assert product1.get_title("en") == "Product One"
|
||||||
|
assert product1.get_description("en") == "Description One"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
for p in products:
|
||||||
|
db.delete(p)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_process_batch_updates_existing_translations(self, db):
|
||||||
|
"""Test that batch processing updates existing translation records"""
|
||||||
|
# Clean up any existing test data
|
||||||
|
existing = (
|
||||||
|
db.query(MarketplaceProduct)
|
||||||
|
.filter(MarketplaceProduct.marketplace_product_id == "UPDATE_TEST_001")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
db.delete(existing)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Create initial product with translation
|
||||||
|
batch_df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"marketplace_product_id": ["UPDATE_TEST_001"],
|
||||||
|
"title": ["Original Title"],
|
||||||
|
"description": ["Original Description"],
|
||||||
|
"price": ["19.99 EUR"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.processor._process_marketplace_batch(
|
||||||
|
batch_df, "TestMarket", "TestVendor", db, 1, language="en"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update with new data
|
||||||
|
update_df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"marketplace_product_id": ["UPDATE_TEST_001"],
|
||||||
|
"title": ["Updated Title"],
|
||||||
|
"description": ["Updated Description"],
|
||||||
|
"price": ["24.99 EUR"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.processor._process_marketplace_batch(
|
||||||
|
update_df, "TestMarket", "TestVendor", db, 1, language="en"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["updated"] == 1
|
||||||
|
assert result["imported"] == 0
|
||||||
|
|
||||||
|
# Verify translation was updated
|
||||||
|
product = (
|
||||||
|
db.query(MarketplaceProduct)
|
||||||
|
.filter(MarketplaceProduct.marketplace_product_id == "UPDATE_TEST_001")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
assert product.get_title("en") == "Updated Title"
|
||||||
|
assert product.get_description("en") == "Updated Description"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
db.delete(product)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_process_batch_multi_language(self, db):
|
||||||
|
"""Test importing same product in multiple languages"""
|
||||||
|
# Clean up
|
||||||
|
existing = (
|
||||||
|
db.query(MarketplaceProduct)
|
||||||
|
.filter(MarketplaceProduct.marketplace_product_id == "MULTI_LANG_001")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
db.delete(existing)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Import English version
|
||||||
|
en_df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"marketplace_product_id": ["MULTI_LANG_001"],
|
||||||
|
"title": ["English Title"],
|
||||||
|
"description": ["English Description"],
|
||||||
|
"price": ["19.99 EUR"],
|
||||||
|
"brand": ["TestBrand"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.processor._process_marketplace_batch(
|
||||||
|
en_df, "TestMarket", "TestVendor", db, 1, language="en"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Import French version (same product, different language)
|
||||||
|
fr_df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"marketplace_product_id": ["MULTI_LANG_001"],
|
||||||
|
"title": ["Titre Français"],
|
||||||
|
"description": ["Description Française"],
|
||||||
|
"price": ["19.99 EUR"],
|
||||||
|
"brand": ["TestBrand"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.processor._process_marketplace_batch(
|
||||||
|
fr_df, "TestMarket", "TestVendor", db, 1, language="fr"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["updated"] == 1 # Product existed, so it's an update
|
||||||
|
|
||||||
|
# Verify both translations exist
|
||||||
|
product = (
|
||||||
|
db.query(MarketplaceProduct)
|
||||||
|
.filter(MarketplaceProduct.marketplace_product_id == "MULTI_LANG_001")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
assert len(product.translations) == 2
|
||||||
|
|
||||||
|
# Verify each language
|
||||||
|
assert product.get_title("en") == "English Title"
|
||||||
|
assert product.get_title("fr") == "Titre Français"
|
||||||
|
assert product.get_description("en") == "English Description"
|
||||||
|
assert product.get_description("fr") == "Description Française"
|
||||||
|
|
||||||
|
# Test fallback to English for unknown language
|
||||||
|
assert product.get_title("de") == "English Title"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
db.delete(product)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_process_batch_skips_missing_title(self, db):
|
||||||
|
"""Test that rows without title are skipped"""
|
||||||
|
batch_df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"marketplace_product_id": ["NO_TITLE_001", "HAS_TITLE_001"],
|
||||||
|
"title": [None, "Has Title"],
|
||||||
|
"price": ["19.99", "29.99"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.processor._process_marketplace_batch(
|
||||||
|
batch_df, "TestMarket", "TestVendor", db, 1, language="en"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["imported"] == 1
|
||||||
|
assert result["errors"] == 1 # Missing title is an error
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
product = (
|
||||||
|
db.query(MarketplaceProduct)
|
||||||
|
.filter(MarketplaceProduct.marketplace_product_id == "HAS_TITLE_001")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if product:
|
||||||
|
db.delete(product)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_process_batch_skips_missing_product_id(self, db):
|
||||||
|
"""Test that rows without marketplace_product_id are skipped"""
|
||||||
|
batch_df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"marketplace_product_id": [None, "HAS_ID_001"],
|
||||||
|
"title": ["No ID Product", "Has ID Product"],
|
||||||
|
"price": ["19.99", "29.99"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.processor._process_marketplace_batch(
|
||||||
|
batch_df, "TestMarket", "TestVendor", db, 1, language="en"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["imported"] == 1
|
||||||
|
assert result["errors"] == 1 # Missing ID is an error
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
product = (
|
||||||
|
db.query(MarketplaceProduct)
|
||||||
|
.filter(MarketplaceProduct.marketplace_product_id == "HAS_ID_001")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if product:
|
||||||
|
db.delete(product)
|
||||||
|
db.commit()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# tests/test_data_validation.py
|
# tests/unit/utils/test_data_validation.py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.utils.data_processing import GTINProcessor, PriceProcessor
|
from app.utils.data_processing import GTINProcessor, PriceProcessor
|
||||||
|
|||||||
Reference in New Issue
Block a user