feat: implement multi-language translation support for products

- Add database migrations for translation tables and digital product fields
- Create MarketplaceProductTranslation model for localized content
- Create ProductTranslation model for vendor translation overrides
- Add product type enum (physical, digital, service, subscription)
- Add digital delivery method and platform fields
- Add numeric price fields (price_numeric, sale_price_numeric)
- Implement language fallback pattern (requested -> 'en' -> None)
- Add helper methods: get_title(), get_description(), get_translation()
- Add vendor override pattern with effective_* properties
- Move title/description from products to translations table

🤖 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-11 17:28:54 +01:00
parent 2d6410164e
commit 92a1c0249f
9 changed files with 1416 additions and 58 deletions

View File

@@ -20,9 +20,15 @@ from .content_page import ContentPage
from .customer import Customer, CustomerAddress
from .inventory import Inventory
from .marketplace_import_job import MarketplaceImportJob
from .marketplace_product import MarketplaceProduct
from .marketplace_product import (
DigitalDeliveryMethod,
MarketplaceProduct,
ProductType,
)
from .marketplace_product_translation import MarketplaceProductTranslation
from .order import Order, OrderItem
from .product import Product
from .product_translation import ProductTranslation
from .user import User
from .vendor import Role, Vendor, VendorUser
from .vendor_domain import VendorDomain
@@ -40,21 +46,35 @@ __all__ = [
"ArchitectureViolation",
"ViolationAssignment",
"ViolationComment",
# Base
"Base",
# User & Auth
"User",
# Company & Vendor
"Company",
"ContentPage",
"Inventory",
"Customer",
"CustomerAddress",
"Order",
"OrderItem",
"Vendor",
"VendorUser",
"Role",
"Product",
"MarketplaceImportJob",
"MarketplaceProduct",
"VendorDomain",
"VendorTheme",
# Content
"ContentPage",
# Customer
"Customer",
"CustomerAddress",
# Product - Enums
"ProductType",
"DigitalDeliveryMethod",
# Product - Models
"MarketplaceProduct",
"MarketplaceProductTranslation",
"Product",
"ProductTranslation",
# Import
"MarketplaceImportJob",
# Inventory
"Inventory",
# Orders
"Order",
"OrderItem",
]

View File

@@ -1,31 +1,115 @@
"""Marketplace Product model for multi-marketplace product integration.
This model stores canonical product data from various marketplaces (Letzshop,
Amazon, eBay, CodesWholesale, etc.) in a universal format. It supports:
- Physical and digital products
- Multi-language translations (via MarketplaceProductTranslation)
- Flexible attributes for marketplace-specific data
- Google Shopping fields for Letzshop compatibility
"""
from enum import Enum
from sqlalchemy import (
Boolean,
Column,
Float,
Index,
Integer,
String,
Text,
)
from sqlalchemy.dialects.sqlite import JSON
from sqlalchemy.orm import relationship
# Import Base from the central database module instead of creating a new one
from app.core.database import Base
from models.database.base import TimestampMixin
class ProductType(str, Enum):
"""Product type classification."""
PHYSICAL = "physical"
DIGITAL = "digital"
SERVICE = "service"
SUBSCRIPTION = "subscription"
class DigitalDeliveryMethod(str, Enum):
"""Digital product delivery methods."""
DOWNLOAD = "download"
EMAIL = "email"
IN_APP = "in_app"
STREAMING = "streaming"
LICENSE_KEY = "license_key"
class MarketplaceProduct(Base, TimestampMixin):
"""Canonical product data from marketplace sources.
This table stores normalized product information from all marketplace sources.
Localized content (title, description) is stored in MarketplaceProductTranslation.
"""
__tablename__ = "marketplace_products"
id = Column(Integer, primary_key=True, index=True)
# === UNIVERSAL IDENTIFIERS ===
marketplace_product_id = Column(String, unique=True, index=True, nullable=False)
title = Column(String, nullable=False)
description = Column(String)
link = Column(String)
image_link = Column(String)
availability = Column(String, index=True) # Index for filtering
price = Column(String)
brand = Column(String, index=True) # Index for filtering
gtin = Column(String, index=True) # Index for inventory lookups
mpn = Column(String)
gtin = Column(String, index=True) # EAN/UPC - primary cross-marketplace matching
mpn = Column(String, index=True) # Manufacturer Part Number
sku = Column(String, index=True) # Internal SKU if assigned
# === SOURCE TRACKING ===
marketplace = Column(
String, index=True, nullable=True, default="letzshop"
) # 'letzshop', 'amazon', 'ebay', 'codeswholesale'
source_url = Column(String) # Original product URL
vendor_name = Column(String, index=True) # Seller/vendor in marketplace
# === PRODUCT TYPE ===
product_type_enum = Column(
String(20), nullable=False, default=ProductType.PHYSICAL.value
)
is_digital = Column(Boolean, default=False, index=True)
# === DIGITAL PRODUCT FIELDS ===
digital_delivery_method = Column(String(20)) # DigitalDeliveryMethod values
platform = Column(String(50), index=True) # 'steam', 'playstation', 'xbox', etc.
region_restrictions = Column(JSON) # ["EU", "US"] or null for global
license_type = Column(String(50)) # 'single_use', 'subscription', 'lifetime'
# === NON-LOCALIZED FIELDS ===
brand = Column(String, index=True)
google_product_category = Column(String, index=True)
category_path = Column(String) # Normalized category hierarchy
condition = Column(String)
# === PRICING ===
price = Column(String) # Raw price string "19.99 EUR" (kept for reference)
price_numeric = Column(Float) # Parsed numeric price
sale_price = Column(String) # Raw sale price string
sale_price_numeric = Column(Float) # Parsed numeric sale price
currency = Column(String(3), default="EUR")
# === MEDIA ===
image_link = Column(String)
additional_image_link = Column(String) # Legacy single string
additional_images = Column(JSON) # Array of image URLs
# === PRODUCT ATTRIBUTES (Flexible) ===
attributes = Column(JSON) # {color, size, material, etc.}
# === PHYSICAL PRODUCT FIELDS ===
weight = Column(Float) # In kg
weight_unit = Column(String(10), default="kg")
dimensions = Column(JSON) # {length, width, height, unit}
# === GOOGLE SHOPPING FIELDS (Preserved for Letzshop) ===
link = Column(String)
availability = Column(String, index=True)
adult = Column(String)
multipack = Column(Integer)
is_bundle = Column(String)
@@ -38,43 +122,130 @@ class MarketplaceProduct(Base, TimestampMixin):
size_type = Column(String)
size_system = Column(String)
item_group_id = Column(String)
google_product_category = Column(String, index=True) # Index for filtering
product_type = Column(String)
product_type_raw = Column(String) # Original feed value (renamed from product_type)
custom_label_0 = Column(String)
custom_label_1 = Column(String)
custom_label_2 = Column(String)
custom_label_3 = Column(String)
custom_label_4 = Column(String)
additional_image_link = Column(String)
sale_price = Column(String)
unit_pricing_measure = Column(String)
unit_pricing_base_measure = Column(String)
identifier_exists = Column(String)
shipping = Column(String)
currency = Column(String)
# New marketplace fields
marketplace = Column(
String, index=True, nullable=True, default="Letzshop"
) # Index for marketplace filtering
vendor_name = Column(
String, index=True, nullable=True
) # Index for vendor filtering
# === STATUS ===
is_active = Column(Boolean, default=True, index=True)
product = relationship("Product", back_populates="marketplace_product")
# === RELATIONSHIPS ===
translations = relationship(
"MarketplaceProductTranslation",
back_populates="marketplace_product",
cascade="all, delete-orphan",
)
vendor_products = relationship("Product", back_populates="marketplace_product")
# Additional indexes for marketplace queries
# === INDEXES ===
__table_args__ = (
Index(
"idx_marketplace_vendor", "marketplace", "vendor_name"
), # Composite index for marketplace+vendor queries
Index(
"idx_marketplace_brand", "marketplace", "brand"
), # Composite index for marketplace+brand queries
Index("idx_marketplace_vendor", "marketplace", "vendor_name"),
Index("idx_marketplace_brand", "marketplace", "brand"),
Index("idx_mp_gtin_marketplace", "gtin", "marketplace"),
Index("idx_mp_product_type", "product_type_enum", "is_digital"),
)
def __repr__(self):
return (
f"<MarketplaceProduct(marketplace_product_id='{self.marketplace_product_id}', title='{self.title}', marketplace='{self.marketplace}', "
f"<MarketplaceProduct(id={self.id}, "
f"marketplace_product_id='{self.marketplace_product_id}', "
f"marketplace='{self.marketplace}', "
f"vendor='{self.vendor_name}')>"
)
# === HELPER PROPERTIES ===
@property
def product_type(self) -> ProductType:
"""Get product type as enum."""
return ProductType(self.product_type_enum)
@product_type.setter
def product_type(self, value: ProductType | str):
"""Set product type from enum or string."""
if isinstance(value, ProductType):
self.product_type_enum = value.value
else:
self.product_type_enum = value
@property
def delivery_method(self) -> DigitalDeliveryMethod | None:
"""Get digital delivery method as enum."""
if self.digital_delivery_method:
return DigitalDeliveryMethod(self.digital_delivery_method)
return None
@delivery_method.setter
def delivery_method(self, value: DigitalDeliveryMethod | str | None):
"""Set delivery method from enum or string."""
if value is None:
self.digital_delivery_method = None
elif isinstance(value, DigitalDeliveryMethod):
self.digital_delivery_method = value.value
else:
self.digital_delivery_method = value
def get_translation(self, language: str) -> "MarketplaceProductTranslation | None":
"""Get translation for a specific language."""
for t in self.translations:
if t.language == language:
return t
return None
def get_title(self, language: str = "en") -> str | None:
"""Get title for a specific language with fallback to 'en'."""
translation = self.get_translation(language)
if translation:
return translation.title
# Fallback to English
if language != "en":
en_translation = self.get_translation("en")
if en_translation:
return en_translation.title
return None
def get_description(self, language: str = "en") -> str | None:
"""Get description for a specific language with fallback to 'en'."""
translation = self.get_translation(language)
if translation:
return translation.description
# Fallback to English
if language != "en":
en_translation = self.get_translation("en")
if en_translation:
return en_translation.description
return None
@property
def effective_price(self) -> float | None:
"""Get the effective numeric price."""
return self.price_numeric
@property
def effective_sale_price(self) -> float | None:
"""Get the effective numeric sale price."""
return self.sale_price_numeric
@property
def all_images(self) -> list[str]:
"""Get all product images as a list."""
images = []
if self.image_link:
images.append(self.image_link)
if self.additional_images:
images.extend(self.additional_images)
elif self.additional_image_link:
# Legacy single string format
images.append(self.additional_image_link)
return images

View File

@@ -0,0 +1,76 @@
"""Marketplace Product Translation model for multi-language support.
This model stores localized content (title, description, SEO fields) for
marketplace products. Each marketplace product can have multiple translations
for different languages.
"""
from sqlalchemy import (
Column,
ForeignKey,
Index,
Integer,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
class MarketplaceProductTranslation(Base, TimestampMixin):
"""Localized content for marketplace products.
Stores translations for product titles, descriptions, and SEO fields.
Each marketplace_product can have one translation per language.
"""
__tablename__ = "marketplace_product_translations"
id = Column(Integer, primary_key=True, index=True)
marketplace_product_id = Column(
Integer,
ForeignKey("marketplace_products.id", ondelete="CASCADE"),
nullable=False,
)
language = Column(String(5), nullable=False) # 'en', 'fr', 'de', 'lb'
# === LOCALIZED CONTENT ===
title = Column(String, nullable=False)
description = Column(Text)
short_description = Column(String(500))
# === SEO FIELDS ===
meta_title = Column(String(70))
meta_description = Column(String(160))
url_slug = Column(String(255))
# === SOURCE TRACKING ===
source_import_id = Column(Integer) # Which import job provided this
source_file = Column(String) # e.g., "letzshop_fr.csv"
# === RELATIONSHIPS ===
marketplace_product = relationship(
"MarketplaceProduct",
back_populates="translations",
)
__table_args__ = (
UniqueConstraint(
"marketplace_product_id",
"language",
name="uq_marketplace_product_translation",
),
Index("idx_mpt_marketplace_product_id", "marketplace_product_id"),
Index("idx_mpt_language", "language"),
)
def __repr__(self):
return (
f"<MarketplaceProductTranslation(id={self.id}, "
f"marketplace_product_id={self.marketplace_product_id}, "
f"language='{self.language}', "
f"title='{self.title[:30] if self.title else None}...')>"
)

View File

@@ -1,4 +1,13 @@
# models/database/product.py
"""Vendor Product model with override pattern.
This model represents a vendor's copy of a marketplace product with the ability
to override any field. The override pattern works as follows:
- NULL value = inherit from marketplace_product
- Non-NULL value = vendor-specific override
This allows vendors to customize pricing, images, descriptions etc. while
still being able to "reset to source" by setting values back to NULL.
"""
from sqlalchemy import (
Boolean,
@@ -10,6 +19,7 @@ from sqlalchemy import (
String,
UniqueConstraint,
)
from sqlalchemy.dialects.sqlite import JSON
from sqlalchemy.orm import relationship
from app.core.database import Base
@@ -17,6 +27,13 @@ from models.database.base import TimestampMixin
class Product(Base, TimestampMixin):
"""Vendor-specific product with override capability.
Each vendor can have their own version of a marketplace product with
custom pricing, images, and other overrides. Fields set to NULL
inherit their value from the linked marketplace_product.
"""
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
@@ -25,46 +42,295 @@ class Product(Base, TimestampMixin):
Integer, ForeignKey("marketplace_products.id"), nullable=False
)
# Vendor-specific overrides
product_id = Column(String) # Vendor's internal SKU
# === VENDOR REFERENCE ===
vendor_sku = Column(String, index=True) # Vendor's internal SKU
# === OVERRIDABLE FIELDS (NULL = inherit from marketplace_product) ===
# Pricing
price = Column(Float)
sale_price = Column(Float)
currency = Column(String)
availability = Column(String)
condition = Column(String)
currency = Column(String(3))
# Vendor-specific metadata
# Product Info
brand = Column(String)
condition = Column(String)
availability = Column(String)
# Media
primary_image_url = Column(String)
additional_images = Column(JSON)
# Digital Product Overrides
download_url = Column(String)
license_type = Column(String(50))
# === SUPPLIER TRACKING ===
supplier = Column(String(50)) # 'codeswholesale', 'internal', etc.
supplier_product_id = Column(String) # Supplier's product reference
supplier_cost = Column(Float) # What we pay the supplier
margin_percent = Column(Float) # Markup percentage
# === VENDOR-SPECIFIC (No inheritance) ===
is_featured = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
display_order = Column(Integer, default=0)
# Inventory settings
# Inventory Settings
min_quantity = Column(Integer, default=1)
max_quantity = Column(Integer)
# Relationships
# Digital Fulfillment
fulfillment_email_template = Column(String) # Template name for digital delivery
# === RELATIONSHIPS ===
vendor = relationship("Vendor", back_populates="products")
marketplace_product = relationship("MarketplaceProduct", back_populates="product")
marketplace_product = relationship(
"MarketplaceProduct", back_populates="vendor_products"
)
translations = relationship(
"ProductTranslation",
back_populates="product",
cascade="all, delete-orphan",
)
inventory_entries = relationship(
"Inventory", back_populates="product", cascade="all, delete-orphan"
)
# Constraints
# === CONSTRAINTS & INDEXES ===
__table_args__ = (
UniqueConstraint("vendor_id", "marketplace_product_id", name="uq_product"),
Index("idx_product_active", "vendor_id", "is_active"),
Index("idx_product_featured", "vendor_id", "is_featured"),
UniqueConstraint(
"vendor_id", "marketplace_product_id", name="uq_vendor_marketplace_product"
),
Index("idx_product_vendor_active", "vendor_id", "is_active"),
Index("idx_product_vendor_featured", "vendor_id", "is_featured"),
Index("idx_product_vendor_sku", "vendor_id", "vendor_sku"),
Index("idx_product_supplier", "supplier", "supplier_product_id"),
)
# === OVERRIDABLE FIELDS LIST ===
OVERRIDABLE_FIELDS = [
"price",
"sale_price",
"currency",
"brand",
"condition",
"availability",
"primary_image_url",
"additional_images",
"download_url",
"license_type",
]
def __repr__(self):
return f"<Product(id={self.id}, vendor_id={self.vendor_id}, product_id='{self.product_id}')>"
return (
f"<Product(id={self.id}, vendor_id={self.vendor_id}, "
f"vendor_sku='{self.vendor_sku}')>"
)
# === EFFECTIVE PROPERTIES (Override Pattern) ===
@property
def total_inventory(self):
def effective_price(self) -> float | None:
"""Get price (vendor override or marketplace fallback)."""
if self.price is not None:
return self.price
mp = self.marketplace_product
return mp.price_numeric if mp else None
@property
def effective_sale_price(self) -> float | None:
"""Get sale price (vendor override or marketplace fallback)."""
if self.sale_price is not None:
return self.sale_price
mp = self.marketplace_product
return mp.sale_price_numeric if mp else None
@property
def effective_currency(self) -> str:
"""Get currency (vendor override or marketplace fallback)."""
if self.currency is not None:
return self.currency
mp = self.marketplace_product
return mp.currency if mp else "EUR"
@property
def effective_brand(self) -> str | None:
"""Get brand (vendor override or marketplace fallback)."""
if self.brand is not None:
return self.brand
mp = self.marketplace_product
return mp.brand if mp else None
@property
def effective_condition(self) -> str | None:
"""Get condition (vendor override or marketplace fallback)."""
if self.condition is not None:
return self.condition
mp = self.marketplace_product
return mp.condition if mp else None
@property
def effective_availability(self) -> str | None:
"""Get availability (vendor override or marketplace fallback)."""
if self.availability is not None:
return self.availability
mp = self.marketplace_product
return mp.availability if mp else None
@property
def effective_primary_image_url(self) -> str | None:
"""Get primary image (vendor override or marketplace fallback)."""
if self.primary_image_url is not None:
return self.primary_image_url
mp = self.marketplace_product
return mp.image_link if mp else None
@property
def effective_additional_images(self) -> list | None:
"""Get additional images (vendor override or marketplace fallback)."""
if self.additional_images is not None:
return self.additional_images
mp = self.marketplace_product
if mp and mp.additional_images:
return mp.additional_images
return None
@property
def is_digital(self) -> bool:
"""Check if this is a digital product."""
mp = self.marketplace_product
return mp.is_digital if mp else False
@property
def product_type(self) -> str:
"""Get product type from marketplace product."""
mp = self.marketplace_product
return mp.product_type_enum if mp else "physical"
# === INVENTORY PROPERTIES ===
@property
def total_inventory(self) -> int:
"""Calculate total inventory across all locations."""
return sum(inv.quantity for inv in self.inventory_entries)
@property
def available_inventory(self):
def available_inventory(self) -> int:
"""Calculate available inventory (total - reserved)."""
return sum(inv.available_quantity for inv in self.inventory_entries)
# === OVERRIDE INFO METHOD ===
def get_override_info(self) -> dict:
"""Get all fields with inheritance flags.
Returns a dict with effective values, override flags, and source values.
Similar to Vendor.get_contact_info_with_inheritance().
"""
mp = self.marketplace_product
return {
# Price
"price": self.effective_price,
"price_overridden": self.price is not None,
"price_source": mp.price_numeric if mp else None,
# Sale Price
"sale_price": self.effective_sale_price,
"sale_price_overridden": self.sale_price is not None,
"sale_price_source": mp.sale_price_numeric if mp else None,
# Currency
"currency": self.effective_currency,
"currency_overridden": self.currency is not None,
"currency_source": mp.currency if mp else None,
# Brand
"brand": self.effective_brand,
"brand_overridden": self.brand is not None,
"brand_source": mp.brand if mp else None,
# Condition
"condition": self.effective_condition,
"condition_overridden": self.condition is not None,
"condition_source": mp.condition if mp else None,
# Availability
"availability": self.effective_availability,
"availability_overridden": self.availability is not None,
"availability_source": mp.availability if mp else None,
# Images
"primary_image_url": self.effective_primary_image_url,
"primary_image_url_overridden": self.primary_image_url is not None,
"primary_image_url_source": mp.image_link if mp else None,
# Product type info
"is_digital": self.is_digital,
"product_type": self.product_type,
}
# === RESET METHODS ===
def reset_field_to_source(self, field_name: str) -> bool:
"""Reset a single field to inherit from marketplace product.
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_all_to_source(self) -> None:
"""Reset all overridable fields to inherit from marketplace product."""
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 product.
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
# === TRANSLATION HELPERS ===
def get_translation(self, language: str) -> "ProductTranslation | None":
"""Get vendor translation for a specific language."""
for t in self.translations:
if t.language == language:
return t
return None
def get_effective_title(self, language: str = "en") -> str | None:
"""Get title with vendor override or marketplace fallback."""
# Check vendor translation first
translation = self.get_translation(language)
if translation and translation.title:
return translation.title
# Fall back to marketplace translation
mp = self.marketplace_product
if mp:
return mp.get_title(language)
return None
def get_effective_description(self, language: str = "en") -> str | None:
"""Get description with vendor override or marketplace fallback."""
# Check vendor translation first
translation = self.get_translation(language)
if translation and translation.description:
return translation.description
# Fall back to marketplace translation
mp = self.marketplace_product
if mp:
return mp.get_description(language)
return None

View File

@@ -0,0 +1,214 @@
"""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 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.
"""
from sqlalchemy import (
Column,
ForeignKey,
Index,
Integer,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
class ProductTranslation(Base, TimestampMixin):
"""Vendor-specific localized content with override capability.
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.
"""
__tablename__ = "product_translations"
id = Column(Integer, primary_key=True, index=True)
product_id = Column(
Integer,
ForeignKey("products.id", ondelete="CASCADE"),
nullable=False,
)
language = Column(String(5), nullable=False) # 'en', 'fr', 'de', 'lb'
# === OVERRIDABLE LOCALIZED FIELDS (NULL = inherit) ===
title = Column(String)
description = Column(Text)
short_description = Column(String(500))
# SEO Overrides
meta_title = Column(String(70))
meta_description = Column(String(160))
url_slug = Column(String(255))
# === RELATIONSHIPS ===
product = relationship("Product", back_populates="translations")
__table_args__ = (
UniqueConstraint("product_id", "language", name="uq_product_translation"),
Index("idx_pt_product_id", "product_id"),
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}, "
f"product_id={self.product_id}, "
f"language='{self.language}', "
f"title='{self.title[:30] if self.title else None}...')>"
)
# === HELPER METHODS ===
def _get_marketplace_translation(self):
"""Get the corresponding marketplace translation for fallback."""
product = self.product
if product and product.marketplace_product:
mp = product.marketplace_product
for t in mp.translations:
if t.language == self.language:
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
# === 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.
"""
mp_translation = self._get_marketplace_translation()
return {
# Title
"title": self.get_effective_title(),
"title_overridden": self.title is not None,
"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,
# Short Description
"short_description": self.get_effective_short_description(),
"short_description_overridden": self.short_description is not None,
"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_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_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_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