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:
@@ -0,0 +1,262 @@
|
||||
"""Populate product fields from marketplace for independence refactor
|
||||
|
||||
Revision ID: j8e9f0a1b2c3
|
||||
Revises: i7d8e9f0a1b2
|
||||
Create Date: 2025-12-24
|
||||
|
||||
This migration populates NULL fields on products and product_translations
|
||||
with values from their linked marketplace products. This is part of the
|
||||
"product independence" refactor where products become standalone entities
|
||||
instead of inheriting from marketplace products via NULL fallback.
|
||||
|
||||
After this migration:
|
||||
- All Product fields will have actual values (no NULL inheritance)
|
||||
- All ProductTranslation records will exist with actual values
|
||||
- The marketplace_product_id FK is kept for "view original source" feature
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "j8e9f0a1b2c3"
|
||||
down_revision: Union[str, None] = "i7d8e9f0a1b2"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Populate NULL product fields with marketplace product values."""
|
||||
|
||||
# Get database connection for raw SQL
|
||||
connection = op.get_bind()
|
||||
|
||||
# =========================================================================
|
||||
# STEP 1: Populate Product fields from MarketplaceProduct
|
||||
# =========================================================================
|
||||
|
||||
# Price cents
|
||||
connection.execute(text("""
|
||||
UPDATE products
|
||||
SET price_cents = (
|
||||
SELECT mp.price_cents
|
||||
FROM marketplace_products mp
|
||||
WHERE mp.id = products.marketplace_product_id
|
||||
)
|
||||
WHERE price_cents IS NULL
|
||||
AND marketplace_product_id IS NOT NULL
|
||||
"""))
|
||||
|
||||
# Sale price cents
|
||||
connection.execute(text("""
|
||||
UPDATE products
|
||||
SET sale_price_cents = (
|
||||
SELECT mp.sale_price_cents
|
||||
FROM marketplace_products mp
|
||||
WHERE mp.id = products.marketplace_product_id
|
||||
)
|
||||
WHERE sale_price_cents IS NULL
|
||||
AND marketplace_product_id IS NOT NULL
|
||||
"""))
|
||||
|
||||
# Currency (default to EUR if marketplace has NULL)
|
||||
connection.execute(text("""
|
||||
UPDATE products
|
||||
SET currency = COALESCE(
|
||||
(SELECT mp.currency FROM marketplace_products mp WHERE mp.id = products.marketplace_product_id),
|
||||
'EUR'
|
||||
)
|
||||
WHERE currency IS NULL
|
||||
AND marketplace_product_id IS NOT NULL
|
||||
"""))
|
||||
|
||||
# Brand
|
||||
connection.execute(text("""
|
||||
UPDATE products
|
||||
SET brand = (
|
||||
SELECT mp.brand
|
||||
FROM marketplace_products mp
|
||||
WHERE mp.id = products.marketplace_product_id
|
||||
)
|
||||
WHERE brand IS NULL
|
||||
AND marketplace_product_id IS NOT NULL
|
||||
"""))
|
||||
|
||||
# Condition
|
||||
connection.execute(text("""
|
||||
UPDATE products
|
||||
SET condition = (
|
||||
SELECT mp.condition
|
||||
FROM marketplace_products mp
|
||||
WHERE mp.id = products.marketplace_product_id
|
||||
)
|
||||
WHERE condition IS NULL
|
||||
AND marketplace_product_id IS NOT NULL
|
||||
"""))
|
||||
|
||||
# Availability
|
||||
connection.execute(text("""
|
||||
UPDATE products
|
||||
SET availability = (
|
||||
SELECT mp.availability
|
||||
FROM marketplace_products mp
|
||||
WHERE mp.id = products.marketplace_product_id
|
||||
)
|
||||
WHERE availability IS NULL
|
||||
AND marketplace_product_id IS NOT NULL
|
||||
"""))
|
||||
|
||||
# Primary image URL (marketplace uses 'image_link')
|
||||
connection.execute(text("""
|
||||
UPDATE products
|
||||
SET primary_image_url = (
|
||||
SELECT mp.image_link
|
||||
FROM marketplace_products mp
|
||||
WHERE mp.id = products.marketplace_product_id
|
||||
)
|
||||
WHERE primary_image_url IS NULL
|
||||
AND marketplace_product_id IS NOT NULL
|
||||
"""))
|
||||
|
||||
# Additional images
|
||||
connection.execute(text("""
|
||||
UPDATE products
|
||||
SET additional_images = (
|
||||
SELECT mp.additional_images
|
||||
FROM marketplace_products mp
|
||||
WHERE mp.id = products.marketplace_product_id
|
||||
)
|
||||
WHERE additional_images IS NULL
|
||||
AND marketplace_product_id IS NOT NULL
|
||||
"""))
|
||||
|
||||
# =========================================================================
|
||||
# STEP 2: Create missing ProductTranslation records from MarketplaceProductTranslation
|
||||
# =========================================================================
|
||||
|
||||
# Insert missing translations (where product doesn't have translation for a language
|
||||
# that the marketplace product has)
|
||||
connection.execute(text("""
|
||||
INSERT INTO product_translations (product_id, language, title, description, short_description,
|
||||
meta_title, meta_description, url_slug, created_at, updated_at)
|
||||
SELECT
|
||||
p.id,
|
||||
mpt.language,
|
||||
mpt.title,
|
||||
mpt.description,
|
||||
mpt.short_description,
|
||||
mpt.meta_title,
|
||||
mpt.meta_description,
|
||||
mpt.url_slug,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM products p
|
||||
JOIN marketplace_products mp ON mp.id = p.marketplace_product_id
|
||||
JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM product_translations pt
|
||||
WHERE pt.product_id = p.id AND pt.language = mpt.language
|
||||
)
|
||||
"""))
|
||||
|
||||
# =========================================================================
|
||||
# STEP 3: Update existing ProductTranslation NULL fields with marketplace values
|
||||
# =========================================================================
|
||||
|
||||
# Update title where NULL
|
||||
connection.execute(text("""
|
||||
UPDATE product_translations
|
||||
SET title = (
|
||||
SELECT mpt.title
|
||||
FROM products p
|
||||
JOIN marketplace_products mp ON mp.id = p.marketplace_product_id
|
||||
JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id
|
||||
AND mpt.language = product_translations.language
|
||||
WHERE p.id = product_translations.product_id
|
||||
)
|
||||
WHERE title IS NULL
|
||||
"""))
|
||||
|
||||
# Update description where NULL
|
||||
connection.execute(text("""
|
||||
UPDATE product_translations
|
||||
SET description = (
|
||||
SELECT mpt.description
|
||||
FROM products p
|
||||
JOIN marketplace_products mp ON mp.id = p.marketplace_product_id
|
||||
JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id
|
||||
AND mpt.language = product_translations.language
|
||||
WHERE p.id = product_translations.product_id
|
||||
)
|
||||
WHERE description IS NULL
|
||||
"""))
|
||||
|
||||
# Update short_description where NULL
|
||||
connection.execute(text("""
|
||||
UPDATE product_translations
|
||||
SET short_description = (
|
||||
SELECT mpt.short_description
|
||||
FROM products p
|
||||
JOIN marketplace_products mp ON mp.id = p.marketplace_product_id
|
||||
JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id
|
||||
AND mpt.language = product_translations.language
|
||||
WHERE p.id = product_translations.product_id
|
||||
)
|
||||
WHERE short_description IS NULL
|
||||
"""))
|
||||
|
||||
# Update meta_title where NULL
|
||||
connection.execute(text("""
|
||||
UPDATE product_translations
|
||||
SET meta_title = (
|
||||
SELECT mpt.meta_title
|
||||
FROM products p
|
||||
JOIN marketplace_products mp ON mp.id = p.marketplace_product_id
|
||||
JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id
|
||||
AND mpt.language = product_translations.language
|
||||
WHERE p.id = product_translations.product_id
|
||||
)
|
||||
WHERE meta_title IS NULL
|
||||
"""))
|
||||
|
||||
# Update meta_description where NULL
|
||||
connection.execute(text("""
|
||||
UPDATE product_translations
|
||||
SET meta_description = (
|
||||
SELECT mpt.meta_description
|
||||
FROM products p
|
||||
JOIN marketplace_products mp ON mp.id = p.marketplace_product_id
|
||||
JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id
|
||||
AND mpt.language = product_translations.language
|
||||
WHERE p.id = product_translations.product_id
|
||||
)
|
||||
WHERE meta_description IS NULL
|
||||
"""))
|
||||
|
||||
# Update url_slug where NULL
|
||||
connection.execute(text("""
|
||||
UPDATE product_translations
|
||||
SET url_slug = (
|
||||
SELECT mpt.url_slug
|
||||
FROM products p
|
||||
JOIN marketplace_products mp ON mp.id = p.marketplace_product_id
|
||||
JOIN marketplace_product_translations mpt ON mpt.marketplace_product_id = mp.id
|
||||
AND mpt.language = product_translations.language
|
||||
WHERE p.id = product_translations.product_id
|
||||
)
|
||||
WHERE url_slug IS NULL
|
||||
"""))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""
|
||||
Downgrade is a no-op for data population.
|
||||
|
||||
The data was copied, not moved. The original marketplace product data
|
||||
is still intact. We don't reset fields to NULL because:
|
||||
1. It would lose any vendor customizations made after migration
|
||||
2. The model code may still work with populated fields
|
||||
"""
|
||||
pass
|
||||
Reference in New Issue
Block a user