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

@@ -1,13 +1,11 @@
"""Product Translation model for vendor-specific localized content.
This model stores vendor-specific translations that can override the
marketplace product translations. The override pattern works as follows:
- NULL value = inherit from marketplace_product_translations
- Non-NULL value = vendor-specific override
This model stores vendor-specific translations. Translations are independent
entities with all fields populated at creation time from the source
marketplace product translation.
This allows vendors to customize titles, descriptions, and SEO content
per language while still being able to "reset to source" by setting
values back to NULL.
The marketplace product translation can be accessed via the product's
marketplace_product relationship for "view original source" comparison.
"""
from sqlalchemy import (
@@ -26,11 +24,11 @@ from models.database.base import TimestampMixin
class ProductTranslation(Base, TimestampMixin):
"""Vendor-specific localized content with override capability.
"""Vendor-specific localized content - independent copy.
Each vendor can have their own translations that override the
marketplace product translations. Fields set to NULL inherit
their value from the linked marketplace_product_translation.
Each vendor has their own translations with all fields populated
at creation time. The source marketplace translation can be accessed
for comparison via the product's marketplace_product relationship.
"""
__tablename__ = "product_translations"
@@ -43,12 +41,12 @@ class ProductTranslation(Base, TimestampMixin):
)
language = Column(String(5), nullable=False) # 'en', 'fr', 'de', 'lb'
# === OVERRIDABLE LOCALIZED FIELDS (NULL = inherit) ===
# === LOCALIZED FIELDS (copied from marketplace at creation) ===
title = Column(String)
description = Column(Text)
short_description = Column(String(500))
# SEO Overrides
# SEO Fields
meta_title = Column(String(70))
meta_description = Column(String(160))
url_slug = Column(String(255))
@@ -62,16 +60,6 @@ class ProductTranslation(Base, TimestampMixin):
Index("idx_pt_product_language", "product_id", "language"),
)
# === OVERRIDABLE FIELDS LIST ===
OVERRIDABLE_FIELDS = [
"title",
"description",
"short_description",
"meta_title",
"meta_description",
"url_slug",
]
def __repr__(self):
return (
f"<ProductTranslation(id={self.id}, "
@@ -80,10 +68,10 @@ class ProductTranslation(Base, TimestampMixin):
f"title='{self.title[:30] if self.title else None}...')>"
)
# === HELPER METHODS ===
# === SOURCE COMPARISON METHOD ===
def _get_marketplace_translation(self):
"""Get the corresponding marketplace translation for fallback."""
def _find_marketplace_translation(self):
"""Get the corresponding marketplace translation for comparison."""
product = self.product
if product and product.marketplace_product:
mp = product.marketplace_product
@@ -92,125 +80,35 @@ class ProductTranslation(Base, TimestampMixin):
return t
return None
def _get_marketplace_translation_field(self, field: str):
"""Helper to get a field from marketplace translation."""
mp_translation = self._get_marketplace_translation()
if mp_translation:
return getattr(mp_translation, field, None)
return None
def get_source_comparison_info(self) -> dict:
"""Get current values with source values for comparison.
# === EFFECTIVE PROPERTIES (Override Pattern) ===
def get_effective_title(self) -> str | None:
"""Get title with fallback to marketplace translation."""
if self.title is not None:
return self.title
return self._get_marketplace_translation_field("title")
def get_effective_description(self) -> str | None:
"""Get description with fallback to marketplace translation."""
if self.description is not None:
return self.description
return self._get_marketplace_translation_field("description")
def get_effective_short_description(self) -> str | None:
"""Get short description with fallback to marketplace translation."""
if self.short_description is not None:
return self.short_description
return self._get_marketplace_translation_field("short_description")
def get_effective_meta_title(self) -> str | None:
"""Get meta title with fallback to marketplace translation."""
if self.meta_title is not None:
return self.meta_title
return self._get_marketplace_translation_field("meta_title")
def get_effective_meta_description(self) -> str | None:
"""Get meta description with fallback to marketplace translation."""
if self.meta_description is not None:
return self.meta_description
return self._get_marketplace_translation_field("meta_description")
def get_effective_url_slug(self) -> str | None:
"""Get URL slug with fallback to marketplace translation."""
if self.url_slug is not None:
return self.url_slug
return self._get_marketplace_translation_field("url_slug")
# === OVERRIDE INFO METHOD ===
def get_override_info(self) -> dict:
"""Get all fields with inheritance flags.
Returns a dict with effective values and override flags.
Returns a dict with current field values and original source values
from the marketplace product translation. Used for "view original source" feature.
"""
mp_translation = self._get_marketplace_translation()
mp_translation = self._find_marketplace_translation()
return {
# Title
"title": self.get_effective_title(),
"title_overridden": self.title is not None,
"title": self.title,
"title_source": mp_translation.title if mp_translation else None,
# Description
"description": self.get_effective_description(),
"description_overridden": self.description is not None,
"description_source": mp_translation.description
if mp_translation
else None,
"description": self.description,
"description_source": mp_translation.description if mp_translation else None,
# Short Description
"short_description": self.get_effective_short_description(),
"short_description_overridden": self.short_description is not None,
"short_description": self.short_description,
"short_description_source": (
mp_translation.short_description if mp_translation else None
),
# Meta Title
"meta_title": self.get_effective_meta_title(),
"meta_title_overridden": self.meta_title is not None,
"meta_title": self.meta_title,
"meta_title_source": mp_translation.meta_title if mp_translation else None,
# Meta Description
"meta_description": self.get_effective_meta_description(),
"meta_description_overridden": self.meta_description is not None,
"meta_description": self.meta_description,
"meta_description_source": (
mp_translation.meta_description if mp_translation else None
),
# URL Slug
"url_slug": self.get_effective_url_slug(),
"url_slug_overridden": self.url_slug is not None,
"url_slug": self.url_slug,
"url_slug_source": mp_translation.url_slug if mp_translation else None,
}
# === RESET METHODS ===
def reset_field_to_source(self, field_name: str) -> bool:
"""Reset a single field to inherit from marketplace translation.
Args:
field_name: Name of the field to reset
Returns:
True if field was reset, False if field is not overridable
"""
if field_name in self.OVERRIDABLE_FIELDS:
setattr(self, field_name, None)
return True
return False
def reset_to_source(self) -> None:
"""Reset all fields to inherit from marketplace translation."""
for field in self.OVERRIDABLE_FIELDS:
setattr(self, field, None)
def reset_fields_to_source(self, field_names: list[str]) -> list[str]:
"""Reset multiple fields to inherit from marketplace translation.
Args:
field_names: List of field names to reset
Returns:
List of fields that were successfully reset
"""
reset_fields = []
for field in field_names:
if self.reset_field_to_source(field):
reset_fields.append(field)
return reset_fields