refactor: product independence - remove inheritance pattern
Change Product/ProductTranslation from "override/inheritance" pattern (NULL = inherit from marketplace) to "independent copy" pattern (all fields populated at creation). Key changes: - Remove OVERRIDABLE_FIELDS, effective_* properties, reset_* methods - Rename get_override_info() → get_source_comparison_info() - Update copy_to_vendor_catalog() to copy ALL fields + translations - Replace effective_* with direct field access in services - Remove *_overridden fields from schema, keep *_source for comparison - Add migration to populate NULL fields from marketplace products The marketplace_product_id FK is kept for "view original source" feature. Rollback tag: v1.0.0-pre-product-independence 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -176,8 +176,8 @@ class CartService:
|
||||
|
||||
# Get current price in cents (use sale_price if available, otherwise regular price)
|
||||
current_price_cents = (
|
||||
product.effective_sale_price_cents
|
||||
or product.effective_price_cents
|
||||
product.sale_price_cents
|
||||
or product.price_cents
|
||||
or 0
|
||||
)
|
||||
|
||||
|
||||
@@ -835,10 +835,18 @@ class MarketplaceProductService:
|
||||
"""
|
||||
Copy marketplace products to a vendor's catalog.
|
||||
|
||||
Creates independent vendor products with ALL fields copied from the
|
||||
marketplace product. Each vendor product is a standalone entity - no
|
||||
field inheritance or fallback logic. The marketplace_product_id FK is
|
||||
kept for "view original source" feature.
|
||||
|
||||
Also copies ALL translations from the marketplace product.
|
||||
|
||||
Returns:
|
||||
Dict with copied, skipped, failed counts and details
|
||||
"""
|
||||
from models.database.product import Product
|
||||
from models.database.product_translation import ProductTranslation
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first()
|
||||
@@ -849,6 +857,7 @@ class MarketplaceProductService:
|
||||
|
||||
marketplace_products = (
|
||||
db.query(MarketplaceProduct)
|
||||
.options(joinedload(MarketplaceProduct.translations))
|
||||
.filter(MarketplaceProduct.id.in_(marketplace_product_ids))
|
||||
.all()
|
||||
)
|
||||
@@ -883,19 +892,58 @@ class MarketplaceProductService:
|
||||
)
|
||||
continue
|
||||
|
||||
# Create vendor product with ALL fields copied from marketplace
|
||||
product = Product(
|
||||
vendor_id=vendor_id,
|
||||
marketplace_product_id=mp.id,
|
||||
# === Vendor settings (defaults) ===
|
||||
is_active=True,
|
||||
is_featured=False,
|
||||
# Copy GTIN for order matching
|
||||
# === Product identifiers ===
|
||||
gtin=mp.gtin,
|
||||
gtin_type=mp.gtin_type if hasattr(mp, "gtin_type") else None,
|
||||
# === Pricing (copy from marketplace) ===
|
||||
price_cents=mp.price_cents,
|
||||
sale_price_cents=mp.sale_price_cents,
|
||||
currency=mp.currency or "EUR",
|
||||
# === Product info ===
|
||||
brand=mp.brand,
|
||||
condition=mp.condition,
|
||||
availability=mp.availability,
|
||||
# === Media ===
|
||||
primary_image_url=mp.image_link,
|
||||
additional_images=mp.additional_images,
|
||||
# === Digital product fields ===
|
||||
download_url=mp.download_url if hasattr(mp, "download_url") else None,
|
||||
license_type=mp.license_type if hasattr(mp, "license_type") else None,
|
||||
)
|
||||
|
||||
db.add(product)
|
||||
db.flush() # Get product.id for translations
|
||||
|
||||
# Copy ALL translations from marketplace product
|
||||
translations_copied = 0
|
||||
for mpt in mp.translations:
|
||||
product_translation = ProductTranslation(
|
||||
product_id=product.id,
|
||||
language=mpt.language,
|
||||
title=mpt.title,
|
||||
description=mpt.description,
|
||||
short_description=mpt.short_description,
|
||||
meta_title=mpt.meta_title,
|
||||
meta_description=mpt.meta_description,
|
||||
url_slug=mpt.url_slug,
|
||||
)
|
||||
db.add(product_translation)
|
||||
translations_copied += 1
|
||||
|
||||
copied += 1
|
||||
details.append({"id": mp.id, "status": "copied", "gtin": mp.gtin})
|
||||
details.append({
|
||||
"id": mp.id,
|
||||
"status": "copied",
|
||||
"gtin": mp.gtin,
|
||||
"translations_copied": translations_copied,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to copy product {mp.id}: {str(e)}")
|
||||
|
||||
@@ -340,8 +340,8 @@ class OrderService:
|
||||
|
||||
# Get price in cents (prefer sale price, then regular price)
|
||||
unit_price_cents = (
|
||||
product.effective_sale_price_cents
|
||||
or product.effective_price_cents
|
||||
product.sale_price_cents
|
||||
or product.price_cents
|
||||
)
|
||||
if not unit_price_cents:
|
||||
raise ValidationException(f"Product {product.id} has no price")
|
||||
|
||||
@@ -171,9 +171,9 @@ class VendorProductService:
|
||||
raise ProductNotFoundException(product_id)
|
||||
|
||||
mp = product.marketplace_product
|
||||
override_info = product.get_override_info()
|
||||
source_comparison_info = product.get_source_comparison_info()
|
||||
|
||||
# Get marketplace product translations
|
||||
# Get marketplace product translations (for "view original source")
|
||||
mp_translations = {}
|
||||
if mp:
|
||||
for t in mp.translations:
|
||||
@@ -183,7 +183,7 @@ class VendorProductService:
|
||||
"short_description": t.short_description,
|
||||
}
|
||||
|
||||
# Get vendor translations (overrides)
|
||||
# Get vendor translations
|
||||
vendor_translations = {}
|
||||
for t in product.translations:
|
||||
vendor_translations[t.language] = {
|
||||
@@ -198,8 +198,8 @@ class VendorProductService:
|
||||
"vendor_code": product.vendor.vendor_code if product.vendor else None,
|
||||
"marketplace_product_id": product.marketplace_product_id,
|
||||
"vendor_sku": product.vendor_sku,
|
||||
# Override info
|
||||
**override_info,
|
||||
# Product fields with source comparison info
|
||||
**source_comparison_info,
|
||||
# Vendor-specific fields
|
||||
"is_featured": product.is_featured,
|
||||
"is_active": product.is_active,
|
||||
@@ -270,13 +270,13 @@ class VendorProductService:
|
||||
"marketplace_product_id": product.marketplace_product_id,
|
||||
"vendor_sku": product.vendor_sku,
|
||||
"title": title,
|
||||
"brand": product.effective_brand,
|
||||
"effective_price": product.effective_price,
|
||||
"effective_currency": product.effective_currency,
|
||||
"brand": product.brand,
|
||||
"price": product.price,
|
||||
"currency": product.currency,
|
||||
"is_active": product.is_active,
|
||||
"is_featured": product.is_featured,
|
||||
"is_digital": product.is_digital,
|
||||
"image_url": product.effective_primary_image_url,
|
||||
"image_url": product.primary_image_url,
|
||||
"source_marketplace": mp.marketplace if mp else None,
|
||||
"source_vendor": mp.vendor_name if mp else None,
|
||||
"created_at": product.created_at.isoformat()
|
||||
|
||||
Reference in New Issue
Block a user