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:
@@ -47,6 +47,7 @@ class VendorProductService:
|
||||
.options(
|
||||
joinedload(Product.vendor),
|
||||
joinedload(Product.marketplace_product),
|
||||
joinedload(Product.translations),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -246,26 +247,67 @@ class VendorProductService:
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
data: Product data dict
|
||||
data: Product data dict (includes translations dict for multiple languages)
|
||||
|
||||
Returns:
|
||||
Created Product instance
|
||||
"""
|
||||
from models.database.product_translation import ProductTranslation
|
||||
|
||||
# Determine product_type from is_digital flag
|
||||
is_digital = data.get("is_digital", False)
|
||||
product_type = "digital" if is_digital else data.get("product_type", "physical")
|
||||
|
||||
product = Product(
|
||||
vendor_id=data["vendor_id"],
|
||||
vendor_sku=data.get("vendor_sku"),
|
||||
brand=data.get("brand"),
|
||||
gtin=data.get("gtin"),
|
||||
price=data.get("price"),
|
||||
gtin_type=data.get("gtin_type"),
|
||||
currency=data.get("currency", "EUR"),
|
||||
tax_rate_percent=data.get("tax_rate_percent", 17),
|
||||
availability=data.get("availability"),
|
||||
primary_image_url=data.get("primary_image_url"),
|
||||
is_active=data.get("is_active", True),
|
||||
is_featured=data.get("is_featured", False),
|
||||
is_digital=data.get("is_digital", False),
|
||||
description=data.get("description"),
|
||||
is_digital=is_digital,
|
||||
product_type=product_type,
|
||||
)
|
||||
|
||||
# Handle price fields via setters (convert to cents)
|
||||
if data.get("price") is not None:
|
||||
product.price = data["price"]
|
||||
if data.get("sale_price") is not None:
|
||||
product.sale_price = data["sale_price"]
|
||||
|
||||
db.add(product)
|
||||
db.flush() # Get the product ID
|
||||
|
||||
# Handle translations dict (new format with multiple languages)
|
||||
translations = data.get("translations")
|
||||
if translations:
|
||||
for lang, trans_data in translations.items():
|
||||
if trans_data and (trans_data.get("title") or trans_data.get("description")):
|
||||
translation = ProductTranslation(
|
||||
product_id=product.id,
|
||||
language=lang,
|
||||
title=trans_data.get("title"),
|
||||
description=trans_data.get("description"),
|
||||
)
|
||||
db.add(translation)
|
||||
else:
|
||||
# Fallback for old format with single title/description
|
||||
title = data.get("title")
|
||||
description = data.get("description")
|
||||
if title or description:
|
||||
translation = ProductTranslation(
|
||||
product_id=product.id,
|
||||
language="en",
|
||||
title=title,
|
||||
description=description,
|
||||
)
|
||||
db.add(translation)
|
||||
|
||||
db.flush()
|
||||
|
||||
logger.info(f"Created vendor product {product.id} for vendor {data['vendor_id']}")
|
||||
@@ -327,7 +369,6 @@ class VendorProductService:
|
||||
product.cost = data["cost"] # Uses property setter
|
||||
|
||||
# Update other allowed fields
|
||||
# Note: is_digital is derived from marketplace_product, not directly updatable
|
||||
updatable_fields = [
|
||||
"vendor_sku",
|
||||
"brand",
|
||||
@@ -335,6 +376,8 @@ class VendorProductService:
|
||||
"gtin_type",
|
||||
"currency",
|
||||
"tax_rate_percent",
|
||||
"availability",
|
||||
"is_digital",
|
||||
"is_active",
|
||||
"is_featured",
|
||||
"primary_image_url",
|
||||
@@ -370,9 +413,22 @@ class VendorProductService:
|
||||
"""Build a product list item dict."""
|
||||
mp = product.marketplace_product
|
||||
|
||||
# Get title from marketplace product translations
|
||||
# Get title: prefer vendor translations, fallback to marketplace translations
|
||||
title = None
|
||||
if mp:
|
||||
# First try vendor's own translations
|
||||
if product.translations:
|
||||
for trans in product.translations:
|
||||
if trans.language == language and trans.title:
|
||||
title = trans.title
|
||||
break
|
||||
# Fallback to English if requested language not found
|
||||
if not title:
|
||||
for trans in product.translations:
|
||||
if trans.language == "en" and trans.title:
|
||||
title = trans.title
|
||||
break
|
||||
# Fallback to marketplace translations
|
||||
if not title and mp:
|
||||
title = mp.get_title(language)
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user