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:
2025-12-24 23:41:20 +01:00
parent 4ba911e263
commit 508e121a0e
10 changed files with 444 additions and 418 deletions

View File

@@ -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