# tests/unit/models/database/test_product.py """Unit tests for Product (vendor catalog) database model.""" import pytest from sqlalchemy.exc import IntegrityError from models.database.product import Product @pytest.mark.unit @pytest.mark.database class TestProductModel: """Test Product (vendor catalog) model.""" def test_product_creation(self, db, test_vendor, test_marketplace_product): """Test Product model linking vendor catalog to marketplace product.""" product = Product( vendor_id=test_vendor.id, marketplace_product_id=test_marketplace_product.id, vendor_sku="VENDOR_PROD_001", price=89.99, currency="EUR", availability="in stock", is_featured=True, is_active=True, ) db.add(product) db.commit() db.refresh(product) assert product.id is not None assert product.vendor_id == test_vendor.id assert product.marketplace_product_id == test_marketplace_product.id assert product.price == 89.99 assert product.is_featured is True assert product.vendor.vendor_code == test_vendor.vendor_code # 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.""" product1 = Product( vendor_id=test_vendor.id, marketplace_product_id=test_marketplace_product.id, is_active=True, ) db.add(product1) db.commit() # Same marketplace product to same vendor should fail with pytest.raises(IntegrityError): product2 = Product( vendor_id=test_vendor.id, marketplace_product_id=test_marketplace_product.id, is_active=True, ) db.add(product2) db.commit() def test_product_default_values(self, db, test_vendor, test_marketplace_product): """Test Product model default values.""" product = Product( vendor_id=test_vendor.id, marketplace_product_id=test_marketplace_product.id, ) db.add(product) db.commit() db.refresh(product) assert product.is_active is True # Default assert product.is_featured is False # Default assert product.is_digital is False # Default assert product.product_type == "physical" # Default assert product.min_quantity == 1 # Default assert product.display_order == 0 # Default def test_product_vendor_override_fields( self, db, test_vendor, test_marketplace_product ): """Test Product model vendor-specific override fields.""" product = Product( vendor_id=test_vendor.id, marketplace_product_id=test_marketplace_product.id, vendor_sku="CUSTOM_SKU_001", price=49.99, sale_price=39.99, currency="USD", availability="limited", condition="new", ) db.add(product) db.commit() db.refresh(product) assert product.vendor_sku == "CUSTOM_SKU_001" assert product.price == 49.99 assert product.sale_price == 39.99 assert product.currency == "USD" assert product.availability == "limited" def test_product_inventory_settings( self, db, test_vendor, test_marketplace_product ): """Test Product model inventory settings.""" product = Product( vendor_id=test_vendor.id, marketplace_product_id=test_marketplace_product.id, min_quantity=2, max_quantity=10, ) db.add(product) db.commit() db.refresh(product) assert product.min_quantity == 2 assert product.max_quantity == 10 def test_product_relationships(self, db, test_vendor, test_marketplace_product): """Test Product relationships.""" product = Product( vendor_id=test_vendor.id, marketplace_product_id=test_marketplace_product.id, ) db.add(product) db.commit() db.refresh(product) assert product.vendor is not None assert product.marketplace_product is not None assert product.inventory_entries == [] # No inventory yet def test_product_get_source_comparison_info( self, db, test_vendor, test_marketplace_product ): """Test get_source_comparison_info method for 'view original source' feature. Products are independent entities with all fields populated at creation. Source values are kept for comparison only, not inheritance. """ # Set up marketplace product values test_marketplace_product.price_cents = 10000 # €100.00 test_marketplace_product.brand = "SourceBrand" db.commit() # Create product with its own values (independent copy pattern) product = Product( vendor_id=test_vendor.id, marketplace_product_id=test_marketplace_product.id, price_cents=8999, # €89.99 - vendor's price brand="VendorBrand", # Vendor's brand ) db.add(product) db.commit() db.refresh(product) info = product.get_source_comparison_info() # Product has its own price assert info["price"] == 89.99 assert info["price_cents"] == 8999 assert info["price_source"] == 100.00 # Original marketplace price # Product has its own brand assert info["brand"] == "VendorBrand" assert info["brand_source"] == "SourceBrand" # Original marketplace brand # No more *_overridden keys in the pattern assert "price_overridden" not in info assert "brand_overridden" not in info def test_product_fields_are_independent( self, db, test_vendor, test_marketplace_product ): """Test that product fields don't inherit from marketplace product. Products are independent entities - NULL fields stay NULL, no inheritance/fallback logic. """ # Set up marketplace product values test_marketplace_product.price_cents = 10000 test_marketplace_product.brand = "SourceBrand" db.commit() # Create product without copying values product = Product( vendor_id=test_vendor.id, marketplace_product_id=test_marketplace_product.id, # Not copying price_cents or brand ) db.add(product) db.commit() db.refresh(product) # Fields should be NULL (not inherited) assert product.price_cents is None assert product.price is None assert product.brand is None # But we can still see the source values for comparison info = product.get_source_comparison_info() assert info["price_source"] == 100.00 assert info["brand_source"] == "SourceBrand" def test_product_direct_creation_without_marketplace(self, db, test_vendor): """Test creating a product directly without a marketplace source. Products can be created directly without a marketplace_product_id, making them fully independent vendor products. """ product = Product( vendor_id=test_vendor.id, marketplace_product_id=None, # No marketplace source vendor_sku="DIRECT_001", brand="DirectBrand", price=59.99, currency="EUR", is_digital=True, product_type="digital", is_active=True, ) db.add(product) db.commit() db.refresh(product) assert product.id is not None assert product.marketplace_product_id is None assert product.marketplace_product is None assert product.vendor_sku == "DIRECT_001" assert product.brand == "DirectBrand" assert product.is_digital is True assert product.product_type == "digital" def test_product_is_digital_column(self, db, test_vendor): """Test is_digital is an independent column, not derived from marketplace.""" # Create digital product without marketplace source digital_product = Product( vendor_id=test_vendor.id, vendor_sku="DIGITAL_001", is_digital=True, product_type="digital", ) db.add(digital_product) db.commit() db.refresh(digital_product) assert digital_product.is_digital is True assert digital_product.product_type == "digital" # Create physical product without marketplace source physical_product = Product( vendor_id=test_vendor.id, vendor_sku="PHYSICAL_001", is_digital=False, product_type="physical", ) db.add(physical_product) db.commit() db.refresh(physical_product) assert physical_product.is_digital is False assert physical_product.product_type == "physical" def test_product_type_values(self, db, test_vendor): """Test product_type can be set to various values.""" product_types = ["physical", "digital", "service", "subscription"] for ptype in product_types: product = Product( vendor_id=test_vendor.id, vendor_sku=f"TYPE_{ptype.upper()}", product_type=ptype, is_digital=(ptype == "digital"), ) db.add(product) db.commit() db.refresh(product) assert product.product_type == ptype @pytest.mark.unit @pytest.mark.database @pytest.mark.inventory class TestProductInventoryProperties: """Test Product inventory properties including digital product handling.""" def test_physical_product_no_inventory_returns_zero(self, db, test_vendor): """Test physical product with no inventory entries returns 0.""" product = Product( vendor_id=test_vendor.id, vendor_sku="PHYS_INV_001", is_digital=False, product_type="physical", ) db.add(product) db.commit() db.refresh(product) assert product.is_digital is False assert product.has_unlimited_inventory is False assert product.total_inventory == 0 assert product.available_inventory == 0 def test_physical_product_with_inventory(self, db, test_vendor): """Test physical product calculates inventory from entries.""" from models.database.inventory import Inventory product = Product( vendor_id=test_vendor.id, vendor_sku="PHYS_INV_002", is_digital=False, product_type="physical", ) db.add(product) db.commit() db.refresh(product) # Add inventory entries inv1 = Inventory( product_id=product.id, vendor_id=test_vendor.id, warehouse="strassen", bin_location="SA-01-01", location="WAREHOUSE_A", quantity=100, reserved_quantity=10, ) inv2 = Inventory( product_id=product.id, vendor_id=test_vendor.id, warehouse="strassen", bin_location="SA-01-02", location="WAREHOUSE_B", quantity=50, reserved_quantity=5, ) db.add_all([inv1, inv2]) db.commit() db.refresh(product) assert product.has_unlimited_inventory is False assert product.total_inventory == 150 # 100 + 50 assert product.available_inventory == 135 # (100-10) + (50-5) def test_digital_product_has_unlimited_inventory(self, db, test_vendor): """Test digital product returns unlimited inventory.""" product = Product( vendor_id=test_vendor.id, vendor_sku="DIG_INV_001", is_digital=True, product_type="digital", ) db.add(product) db.commit() db.refresh(product) assert product.is_digital is True assert product.has_unlimited_inventory is True assert product.total_inventory == Product.UNLIMITED_INVENTORY assert product.available_inventory == Product.UNLIMITED_INVENTORY def test_digital_product_ignores_inventory_entries(self, db, test_vendor): """Test digital product returns unlimited even with inventory entries.""" from models.database.inventory import Inventory product = Product( vendor_id=test_vendor.id, vendor_sku="DIG_INV_002", is_digital=True, product_type="digital", ) db.add(product) db.commit() db.refresh(product) # Add inventory entries (e.g., for license keys) inv = Inventory( product_id=product.id, vendor_id=test_vendor.id, warehouse="strassen", bin_location="DIG-01-01", location="DIGITAL_LICENSES", quantity=10, reserved_quantity=2, ) db.add(inv) db.commit() db.refresh(product) # Digital product should still return unlimited assert product.has_unlimited_inventory is True assert product.total_inventory == Product.UNLIMITED_INVENTORY assert product.available_inventory == Product.UNLIMITED_INVENTORY def test_unlimited_inventory_constant(self): """Test UNLIMITED_INVENTORY constant value.""" assert Product.UNLIMITED_INVENTORY == 999999 # Should be large enough to never cause "insufficient inventory" assert Product.UNLIMITED_INVENTORY > 100000