feat: make Product fully independent from MarketplaceProduct
- Add is_digital and product_type columns to Product model - Remove is_digital/product_type properties that derived from MarketplaceProduct - Update Create form with translation tabs, GTIN type, sale price, VAT rate, image - Update Edit form to allow editing is_digital (remove disabled state) - Add Availability field to Edit form - Fix Detail page for directly created products (no marketplace source) - Update vendor_product_service to handle new fields in create/update - Add VendorProductCreate/Update schema fields for translations and is_digital - Add unit tests for is_digital column and direct product creation - Add integration tests for create/update API with new fields - Create product-architecture.md documenting the independent copy pattern - Add migration y3d4e5f6g7h8 for is_digital and product_type columns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,8 @@ class TestProductModel:
|
||||
|
||||
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
|
||||
|
||||
@@ -202,6 +204,82 @@ class TestProductModel:
|
||||
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
|
||||
@@ -209,17 +287,13 @@ class TestProductModel:
|
||||
class TestProductInventoryProperties:
|
||||
"""Test Product inventory properties including digital product handling."""
|
||||
|
||||
def test_physical_product_no_inventory_returns_zero(
|
||||
self, db, test_vendor, test_marketplace_product
|
||||
):
|
||||
def test_physical_product_no_inventory_returns_zero(self, db, test_vendor):
|
||||
"""Test physical product with no inventory entries returns 0."""
|
||||
# Ensure product is physical
|
||||
test_marketplace_product.is_digital = False
|
||||
db.commit()
|
||||
|
||||
product = Product(
|
||||
vendor_id=test_vendor.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
vendor_sku="PHYS_INV_001",
|
||||
is_digital=False,
|
||||
product_type="physical",
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
@@ -230,18 +304,15 @@ class TestProductInventoryProperties:
|
||||
assert product.total_inventory == 0
|
||||
assert product.available_inventory == 0
|
||||
|
||||
def test_physical_product_with_inventory(
|
||||
self, db, test_vendor, test_marketplace_product
|
||||
):
|
||||
def test_physical_product_with_inventory(self, db, test_vendor):
|
||||
"""Test physical product calculates inventory from entries."""
|
||||
from models.database.inventory import Inventory
|
||||
|
||||
test_marketplace_product.is_digital = False
|
||||
db.commit()
|
||||
|
||||
product = Product(
|
||||
vendor_id=test_vendor.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
vendor_sku="PHYS_INV_002",
|
||||
is_digital=False,
|
||||
product_type="physical",
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
@@ -274,16 +345,13 @@ class TestProductInventoryProperties:
|
||||
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_marketplace_product
|
||||
):
|
||||
def test_digital_product_has_unlimited_inventory(self, db, test_vendor):
|
||||
"""Test digital product returns unlimited inventory."""
|
||||
test_marketplace_product.is_digital = True
|
||||
db.commit()
|
||||
|
||||
product = Product(
|
||||
vendor_id=test_vendor.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
vendor_sku="DIG_INV_001",
|
||||
is_digital=True,
|
||||
product_type="digital",
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
@@ -294,18 +362,15 @@ class TestProductInventoryProperties:
|
||||
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_marketplace_product
|
||||
):
|
||||
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
|
||||
|
||||
test_marketplace_product.is_digital = True
|
||||
db.commit()
|
||||
|
||||
product = Product(
|
||||
vendor_id=test_vendor.id,
|
||||
marketplace_product_id=test_marketplace_product.id,
|
||||
vendor_sku="DIG_INV_002",
|
||||
is_digital=True,
|
||||
product_type="digital",
|
||||
)
|
||||
db.add(product)
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user