diff --git a/tests/fixtures/marketplace_product_fixtures.py b/tests/fixtures/marketplace_product_fixtures.py index e83d0fd3..163cd3a4 100644 --- a/tests/fixtures/marketplace_product_fixtures.py +++ b/tests/fixtures/marketplace_product_fixtures.py @@ -2,7 +2,10 @@ """ 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. """ import uuid @@ -10,12 +13,42 @@ import uuid import pytest 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 def test_marketplace_product(db): - """Create a test product.""" - marketplace_product = MarketplaceProduct( + """Create a test product with translation.""" + return _create_marketplace_product_with_translation( + db, marketplace_product_id="TEST001", title="Test MarketplaceProduct", description="A test product", @@ -27,17 +60,14 @@ def test_marketplace_product(db): marketplace="Letzshop", vendor_name="TestVendor", ) - db.add(marketplace_product) - db.commit() - db.refresh(marketplace_product) - return marketplace_product @pytest.fixture def unique_product(db): """Create a unique product for tests that need isolated product data.""" unique_id = str(uuid.uuid4())[:8] - marketplace_product = MarketplaceProduct( + return _create_marketplace_product_with_translation( + db, marketplace_product_id=f"UNIQUE_{unique_id}", title=f"Unique MarketplaceProduct {unique_id}", description=f"A unique test product {unique_id}", @@ -50,10 +80,6 @@ def unique_product(db): vendor_name=f"UniqueVendor_{unique_id}", google_product_category=f"UniqueCategory_{unique_id}", ) - db.add(marketplace_product) - db.commit() - db.refresh(marketplace_product) - return marketplace_product @pytest.fixture @@ -63,7 +89,8 @@ def multiple_products(db): marketplace_products = [] for i in range(5): - marketplace_product = MarketplaceProduct( + marketplace_product = _create_marketplace_product_with_translation( + db, marketplace_product_id=f"MULTI_{unique_id}_{i}", title=f"Multi MarketplaceProduct {i} {unique_id}", description=f"Multi test product {i}", @@ -77,33 +104,30 @@ def multiple_products(db): ) marketplace_products.append(marketplace_product) - db.add_all(marketplace_products) - db.commit() - for product in marketplace_products: - db.refresh(product) return marketplace_products def create_unique_marketplace_product_factory(): """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] defaults = { "marketplace_product_id": f"FACTORY_{unique_id}", - "title": f"Factory MarketplaceProduct {unique_id}", "price": "15.99", "currency": "EUR", "marketplace": "TestMarket", - "name": "TestVendor", + "vendor_name": "TestVendor", } defaults.update(kwargs) - marketplace_product = MarketplaceProduct(**defaults) - db.add(marketplace_product) - db.commit() - db.refresh(marketplace_product) - return marketplace_product + title = title or f"Factory MarketplaceProduct {unique_id}" + + return _create_marketplace_product_with_translation( + db, + title=title, + **defaults, + ) return _marketplace_create_product diff --git a/tests/unit/models/database/test_marketplace_product.py b/tests/unit/models/database/test_marketplace_product.py index 42613715..2026fdb0 100644 --- a/tests/unit/models/database/test_marketplace_product.py +++ b/tests/unit/models/database/test_marketplace_product.py @@ -4,6 +4,24 @@ import pytest from sqlalchemy.exc import IntegrityError 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 @@ -13,10 +31,10 @@ class TestMarketplaceProductModel: def test_marketplace_product_creation(self, db): """Test MarketplaceProduct model creation.""" - marketplace_product = MarketplaceProduct( + marketplace_product = _create_with_translation( + db, marketplace_product_id="DB_TEST_001", title="Database Test Product", - description="Testing product model", price="25.99", currency="USD", brand="DBTest", @@ -26,42 +44,36 @@ class TestMarketplaceProductModel: vendor_name="Test Vendor", ) - db.add(marketplace_product) - db.commit() - db.refresh(marketplace_product) - assert marketplace_product.id is not None 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.created_at is not None def test_marketplace_product_id_uniqueness(self, db): """Test unique marketplace_product_id constraint.""" - product1 = MarketplaceProduct( + _create_with_translation( + db, marketplace_product_id="UNIQUE_001", title="Product 1", marketplace="Letzshop", ) - db.add(product1) - db.commit() # Duplicate marketplace_product_id should raise error with pytest.raises(IntegrityError): - product2 = MarketplaceProduct( + _create_with_translation( + db, marketplace_product_id="UNIQUE_001", title="Product 2", marketplace="Letzshop", ) - db.add(product2) - db.commit() def test_marketplace_product_all_fields(self, db): """Test MarketplaceProduct with all optional fields.""" - marketplace_product = MarketplaceProduct( + marketplace_product = _create_with_translation( + db, marketplace_product_id="FULL_001", title="Full Product", - description="Complete product description", link="https://example.com/product", image_link="https://example.com/image.jpg", availability="in stock", @@ -78,16 +90,12 @@ class TestMarketplaceProductModel: pattern="solid", size="M", google_product_category="Apparel & Accessories", - product_type="Clothing", + product_type_raw="Clothing", currency="EUR", marketplace="Letzshop", vendor_name="Full Vendor", ) - db.add(marketplace_product) - db.commit() - db.refresh(marketplace_product) - assert marketplace_product.brand == "TestBrand" assert marketplace_product.gtin == "9876543210123" assert marketplace_product.color == "blue" @@ -95,7 +103,8 @@ class TestMarketplaceProductModel: def test_marketplace_product_custom_labels(self, db): """Test MarketplaceProduct with custom labels.""" - marketplace_product = MarketplaceProduct( + marketplace_product = _create_with_translation( + db, marketplace_product_id="LABELS_001", title="Labeled Product", marketplace="Letzshop", @@ -106,26 +115,127 @@ class TestMarketplaceProductModel: 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_4 == "Label4" def test_marketplace_product_minimal_fields(self, db): """Test MarketplaceProduct with only required fields.""" - marketplace_product = MarketplaceProduct( + marketplace_product = _create_with_translation( + db, marketplace_product_id="MINIMAL_001", title="Minimal Product", ) - db.add(marketplace_product) - db.commit() - db.refresh(marketplace_product) - assert marketplace_product.id is not None assert marketplace_product.marketplace_product_id == "MINIMAL_001" - assert marketplace_product.title == "Minimal Product" - assert marketplace_product.description is None + assert marketplace_product.get_title("en") == "Minimal Product" + assert marketplace_product.get_description("en") 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 diff --git a/tests/unit/models/database/test_order.py b/tests/unit/models/database/test_order.py index d00d297a..5974e5f3 100644 --- a/tests/unit/models/database/test_order.py +++ b/tests/unit/models/database/test_order.py @@ -152,11 +152,14 @@ class TestOrderItemModel: def test_order_item_creation(self, db, test_order, test_product): """Test OrderItem model.""" + # Get title from translation + product_title = test_product.marketplace_product.get_title("en") + order_item = OrderItem( order_id=test_order.id, product_id=test_product.id, - product_name=test_product.marketplace_product.title, - product_sku=test_product.product_id or "SKU001", + product_name=product_title, + product_sku=test_product.vendor_sku or "SKU001", quantity=2, unit_price=49.99, total_price=99.98, diff --git a/tests/unit/models/database/test_product.py b/tests/unit/models/database/test_product.py index ed832da2..6961fe9e 100644 --- a/tests/unit/models/database/test_product.py +++ b/tests/unit/models/database/test_product.py @@ -16,7 +16,7 @@ class TestProductModel: product = Product( vendor_id=test_vendor.id, marketplace_product_id=test_marketplace_product.id, - product_id="VENDOR_PROD_001", + vendor_sku="VENDOR_PROD_001", price=89.99, currency="EUR", availability="in stock", @@ -34,7 +34,11 @@ class TestProductModel: assert product.price == 89.99 assert product.is_featured is True 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): """Test that same marketplace product can't be added twice to vendor catalog.""" @@ -76,7 +80,7 @@ class TestProductModel: product = Product( vendor_id=test_vendor.id, marketplace_product_id=test_marketplace_product.id, - product_id="CUSTOM_SKU_001", + vendor_sku="CUSTOM_SKU_001", price=49.99, sale_price=39.99, currency="USD", @@ -87,7 +91,7 @@ class TestProductModel: db.commit() db.refresh(product) - assert product.product_id == "CUSTOM_SKU_001" + assert product.vendor_sku == "CUSTOM_SKU_001" assert product.price == 49.99 assert product.sale_price == 39.99 assert product.currency == "USD" @@ -121,3 +125,99 @@ class TestProductModel: assert product.vendor is not None assert product.marketplace_product is not None 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" diff --git a/tests/unit/models/schema/test_product.py b/tests/unit/models/schema/test_product.py index 71bd6085..2505cc0d 100644 --- a/tests/unit/models/schema/test_product.py +++ b/tests/unit/models/schema/test_product.py @@ -157,12 +157,13 @@ class TestProductResponseSchema: "vendor_id": 1, "marketplace_product": { "id": 1, + "marketplace_product_id": "TEST001", # Required field "gtin": "1234567890123", "title": "Test Product", "description": "A test product", "brand": "Test Brand", - "category": "Electronics", - "image_url": "https://example.com/image.jpg", + "google_product_category": "Electronics", + "image_link": "https://example.com/image.jpg", "created_at": datetime.now(), "updated_at": datetime.now(), }, @@ -194,12 +195,13 @@ class TestProductResponseSchema: "vendor_id": 1, "marketplace_product": { "id": 1, + "marketplace_product_id": "TEST002", # Required field "gtin": "1234567890123", "title": "Test Product", "description": None, "brand": None, - "category": None, - "image_url": None, + "google_product_category": None, + "image_link": None, "created_at": datetime.now(), "updated_at": datetime.now(), }, diff --git a/tests/unit/services/test_stats_service.py b/tests/unit/services/test_stats_service.py index 3134f1f7..46df3c1d 100644 --- a/tests/unit/services/test_stats_service.py +++ b/tests/unit/services/test_stats_service.py @@ -11,10 +11,31 @@ from app.services.stats_service import StatsService from models.database.inventory import Inventory from models.database.marketplace_import_job import MarketplaceImportJob from models.database.marketplace_product import MarketplaceProduct +from models.database.marketplace_product_translation import MarketplaceProductTranslation from models.database.product import Product 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.stats class TestStatsService: @@ -58,29 +79,28 @@ class TestStatsService: ): """Test comprehensive stats with multiple marketplaces.""" unique_id = str(uuid.uuid4())[:8] - additional_products = [ - MarketplaceProduct( - marketplace_product_id=f"PROD002_{unique_id}", - title="MarketplaceProduct 2", - brand="DifferentBrand", - google_product_category="Different Category", - marketplace="Amazon", - vendor_name="AmazonVendor", - price="15.99", - currency="EUR", - ), - MarketplaceProduct( - marketplace_product_id=f"PROD003_{unique_id}", - title="MarketplaceProduct 3", - brand="ThirdBrand", - google_product_category="Third Category", - marketplace="eBay", - vendor_name="eBayVendor", - price="25.99", - currency="USD", - ), - ] - db.add_all(additional_products) + create_marketplace_product_with_translation( + db, + marketplace_product_id=f"PROD002_{unique_id}", + title="MarketplaceProduct 2", + brand="DifferentBrand", + google_product_category="Different Category", + marketplace="Amazon", + vendor_name="AmazonVendor", + price="15.99", + currency="EUR", + ) + create_marketplace_product_with_translation( + db, + marketplace_product_id=f"PROD003_{unique_id}", + title="MarketplaceProduct 3", + brand="ThirdBrand", + google_product_category="Third Category", + marketplace="eBay", + vendor_name="eBayVendor", + price="25.99", + currency="USD", + ) db.commit() stats = self.service.get_comprehensive_stats(db) @@ -91,29 +111,28 @@ class TestStatsService: def test_get_comprehensive_stats_handles_nulls(self, db): """Test comprehensive stats handles null/empty values correctly.""" unique_id = str(uuid.uuid4())[:8] - products_with_nulls = [ - MarketplaceProduct( - marketplace_product_id=f"NULL001_{unique_id}", - title="MarketplaceProduct with Nulls", - brand=None, - google_product_category=None, - marketplace=None, - vendor_name=None, - price="10.00", - currency="EUR", - ), - MarketplaceProduct( - marketplace_product_id=f"EMPTY001_{unique_id}", - title="MarketplaceProduct with Empty Values", - brand="", - google_product_category="", - marketplace="", - vendor_name="", - price="15.00", - currency="EUR", - ), - ] - db.add_all(products_with_nulls) + create_marketplace_product_with_translation( + db, + marketplace_product_id=f"NULL001_{unique_id}", + title="MarketplaceProduct with Nulls", + brand=None, + google_product_category=None, + marketplace=None, + vendor_name=None, + price="10.00", + currency="EUR", + ) + create_marketplace_product_with_translation( + db, + marketplace_product_id=f"EMPTY001_{unique_id}", + title="MarketplaceProduct with Empty Values", + brand="", + google_product_category="", + marketplace="", + vendor_name="", + price="15.00", + currency="EUR", + ) db.commit() stats = self.service.get_comprehensive_stats(db) @@ -170,36 +189,36 @@ class TestStatsService: ): """Test marketplace breakdown with multiple marketplaces.""" unique_id = str(uuid.uuid4())[:8] - marketplace_products = [ - MarketplaceProduct( - marketplace_product_id=f"AMAZON001_{unique_id}", - title="Amazon MarketplaceProduct 1", - brand="AmazonBrand1", - marketplace="Amazon", - vendor_name="AmazonVendor1", - price="20.00", - currency="EUR", - ), - MarketplaceProduct( - marketplace_product_id=f"AMAZON002_{unique_id}", - title="Amazon MarketplaceProduct 2", - brand="AmazonBrand2", - marketplace="Amazon", - vendor_name="AmazonVendor2", - price="25.00", - currency="EUR", - ), - MarketplaceProduct( - marketplace_product_id=f"EBAY001_{unique_id}", - title="eBay MarketplaceProduct", - brand="eBayBrand", - marketplace="eBay", - vendor_name="eBayVendor", - price="30.00", - currency="USD", - ), - ] - db.add_all(marketplace_products) + create_marketplace_product_with_translation( + db, + marketplace_product_id=f"AMAZON001_{unique_id}", + title="Amazon MarketplaceProduct 1", + brand="AmazonBrand1", + marketplace="Amazon", + vendor_name="AmazonVendor1", + price="20.00", + currency="EUR", + ) + create_marketplace_product_with_translation( + db, + marketplace_product_id=f"AMAZON002_{unique_id}", + title="Amazon MarketplaceProduct 2", + brand="AmazonBrand2", + marketplace="Amazon", + vendor_name="AmazonVendor2", + price="25.00", + currency="EUR", + ) + create_marketplace_product_with_translation( + db, + marketplace_product_id=f"EBAY001_{unique_id}", + title="eBay MarketplaceProduct", + brand="eBayBrand", + marketplace="eBay", + vendor_name="eBayVendor", + price="30.00", + currency="USD", + ) db.commit() stats = self.service.get_marketplace_breakdown_stats(db) @@ -223,7 +242,8 @@ class TestStatsService: def test_get_marketplace_breakdown_stats_excludes_nulls(self, db): """Test marketplace breakdown excludes products with null marketplaces.""" unique_id = str(uuid.uuid4())[:8] - null_marketplace_product = MarketplaceProduct( + create_marketplace_product_with_translation( + db, marketplace_product_id=f"NULLMARKET001_{unique_id}", title="MarketplaceProduct without marketplace", marketplace=None, @@ -232,7 +252,6 @@ class TestStatsService: price="10.00", currency="EUR", ) - db.add(null_marketplace_product) db.commit() stats = self.service.get_marketplace_breakdown_stats(db) @@ -264,15 +283,16 @@ class TestStatsService: """Test getting vendor statistics successfully.""" stats = self.service.get_vendor_stats(db, test_vendor.id) - assert "catalog" in stats - assert "staging" in stats - assert "inventory" in stats - assert "imports" in stats - assert "orders" in stats - assert "customers" in stats + # New flat structure compatible with VendorDashboardStatsResponse + assert "total_products" in stats + assert "active_products" in stats + assert "total_orders" in stats + assert "total_customers" in stats + assert "total_revenue" in stats + assert "total_inventory_quantity" in stats - assert stats["catalog"]["total_products"] >= 0 - assert stats["inventory"]["total_quantity"] >= 0 + assert stats["total_products"] >= 0 + assert stats["total_inventory_quantity"] >= 0 def test_get_vendor_stats_vendor_not_found(self, db): """Test vendor stats with non-existent vendor.""" @@ -285,8 +305,8 @@ class TestStatsService: """Test vendor stats includes inventory data.""" stats = self.service.get_vendor_stats(db, test_vendor.id) - assert stats["inventory"]["total_quantity"] >= test_inventory.quantity - assert stats["inventory"]["reserved_quantity"] >= 0 + assert stats["total_inventory_quantity"] >= test_inventory.quantity + assert stats["reserved_inventory_quantity"] >= 0 def test_get_vendor_stats_database_error(self, db, test_vendor): """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): """Test getting unique brands count.""" unique_id = str(uuid.uuid4())[:8] - brand_products = [ - MarketplaceProduct( - marketplace_product_id=f"BRAND001_{unique_id}", - title="Brand MarketplaceProduct 1", - brand="BrandA", - marketplace="Test", - vendor_name="TestVendor", - price="10.00", - currency="EUR", - ), - MarketplaceProduct( - marketplace_product_id=f"BRAND002_{unique_id}", - title="Brand MarketplaceProduct 2", - brand="BrandB", - marketplace="Test", - vendor_name="TestVendor", - price="15.00", - currency="EUR", - ), - ] - db.add_all(brand_products) + create_marketplace_product_with_translation( + db, + marketplace_product_id=f"BRAND001_{unique_id}", + title="Brand MarketplaceProduct 1", + brand="BrandA", + marketplace="Test", + vendor_name="TestVendor", + price="10.00", + currency="EUR", + ) + create_marketplace_product_with_translation( + db, + marketplace_product_id=f"BRAND002_{unique_id}", + title="Brand MarketplaceProduct 2", + brand="BrandB", + marketplace="Test", + vendor_name="TestVendor", + price="15.00", + currency="EUR", + ) db.commit() 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): """Test getting unique categories count.""" unique_id = str(uuid.uuid4())[:8] - category_products = [ - MarketplaceProduct( - marketplace_product_id=f"CAT001_{unique_id}", - title="Category MarketplaceProduct 1", - google_product_category="Electronics", - marketplace="Test", - vendor_name="TestVendor", - price="10.00", - currency="EUR", - ), - MarketplaceProduct( - marketplace_product_id=f"CAT002_{unique_id}", - title="Category MarketplaceProduct 2", - google_product_category="Books", - marketplace="Test", - vendor_name="TestVendor", - price="15.00", - currency="EUR", - ), - ] - db.add_all(category_products) + create_marketplace_product_with_translation( + db, + marketplace_product_id=f"CAT001_{unique_id}", + title="Category MarketplaceProduct 1", + google_product_category="Electronics", + marketplace="Test", + vendor_name="TestVendor", + price="10.00", + currency="EUR", + ) + create_marketplace_product_with_translation( + db, + marketplace_product_id=f"CAT002_{unique_id}", + title="Category MarketplaceProduct 2", + google_product_category="Books", + marketplace="Test", + vendor_name="TestVendor", + price="15.00", + currency="EUR", + ) db.commit() count = self.service._get_unique_categories_count(db) diff --git a/tests/unit/utils/test_csv_processor.py b/tests/unit/utils/test_csv_processor.py index a24e2ee0..13218cd4 100644 --- a/tests/unit/utils/test_csv_processor.py +++ b/tests/unit/utils/test_csv_processor.py @@ -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 import pandas as pd @@ -7,6 +9,8 @@ import requests import requests.exceptions 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 @@ -102,6 +106,74 @@ TEST002,Test MarketplaceProduct 2,15.99,TestMarket""" assert df.iloc[0]["marketplace_product_id"] == "TEST001" 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 async def test_process_marketplace_csv_from_url(self, db): """Test complete marketplace CSV processing""" @@ -117,15 +189,275 @@ TEST002,Test MarketplaceProduct 2,15.99,TestMarket""" "title": ["MarketplaceProduct 1", "MarketplaceProduct 2"], "price": ["10.99", "15.99"], "marketplace": ["TestMarket", "TestMarket"], - "name": ["TestVendor", "TestVendor"], + "vendor_name": ["TestVendor", "TestVendor"], } ) mock_parse.return_value = mock_df 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 "updated" 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() diff --git a/tests/unit/utils/test_data_validation.py b/tests/unit/utils/test_data_validation.py index 08ad5d97..4d5d1008 100644 --- a/tests/unit/utils/test_data_validation.py +++ b/tests/unit/utils/test_data_validation.py @@ -1,4 +1,4 @@ -# tests/test_data_validation.py +# tests/unit/utils/test_data_validation.py import pytest from app.utils.data_processing import GTINProcessor, PriceProcessor