feat: update CSV import to support multi-language translations

- Add language parameter to import endpoints and background tasks
- Extract translation fields (title, description, short_description)
- Create/update MarketplaceProductTranslation records during import
- Add MarketplaceProductTranslationSchema for API responses
- Map product_type column to product_type_raw to avoid enum conflict
- Parse prices to numeric format (price_numeric, sale_price_numeric)
- Update marketplace product service for translation-based lookups
- Update CSV export to retrieve titles from translations

🤖 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:29:13 +01:00
parent 92a1c0249f
commit f2af3aae29
7 changed files with 535 additions and 103 deletions

View File

@@ -14,32 +14,9 @@ class MarketplaceImportJobRequest(BaseModel):
batch_size: int | None = Field(
1000, description="Processing batch size", ge=100, le=10000
)
@field_validator("source_url")
@classmethod
def validate_url(cls, v):
# Basic URL security validation
if not v.startswith(("http://", "https://")):
raise ValueError("URL must start with http:// or https://")
return v.strip()
@field_validator("marketplace")
@classmethod
def validate_marketplace(cls, v):
return v.strip()
class AdminMarketplaceImportJobRequest(BaseModel):
"""Request schema for admin-triggered marketplace import.
Includes vendor_id since admin can import for any vendor.
"""
vendor_id: int = Field(..., description="Vendor ID to import products for")
source_url: str = Field(..., description="URL to CSV file from marketplace")
marketplace: str = Field(default="Letzshop", description="Marketplace name")
batch_size: int | None = Field(
1000, description="Processing batch size", ge=100, le=10000
language: str = Field(
default="en",
description="Language code for product translations (e.g., 'en', 'fr', 'de')",
)
@field_validator("source_url")
@@ -55,6 +32,54 @@ class AdminMarketplaceImportJobRequest(BaseModel):
def validate_marketplace(cls, v):
return v.strip()
@field_validator("language")
@classmethod
def validate_language(cls, v):
# Basic language code validation (2-5 chars)
v = v.strip().lower()
if not 2 <= len(v) <= 5:
raise ValueError("Language code must be 2-5 characters (e.g., 'en', 'fr')")
return v
class AdminMarketplaceImportJobRequest(BaseModel):
"""Request schema for admin-triggered marketplace import.
Includes vendor_id since admin can import for any vendor.
"""
vendor_id: int = Field(..., description="Vendor ID to import products for")
source_url: str = Field(..., description="URL to CSV file from marketplace")
marketplace: str = Field(default="Letzshop", description="Marketplace name")
batch_size: int | None = Field(
1000, description="Processing batch size", ge=100, le=10000
)
language: str = Field(
default="en",
description="Language code for product translations (e.g., 'en', 'fr', 'de')",
)
@field_validator("source_url")
@classmethod
def validate_url(cls, v):
# Basic URL security validation
if not v.startswith(("http://", "https://")):
raise ValueError("URL must start with http:// or https://")
return v.strip()
@field_validator("marketplace")
@classmethod
def validate_marketplace(cls, v):
return v.strip()
@field_validator("language")
@classmethod
def validate_language(cls, v):
v = v.strip().lower()
if not 2 <= len(v) <= 5:
raise ValueError("Language code must be 2-5 characters (e.g., 'en', 'fr')")
return v
class MarketplaceImportJobResponse(BaseModel):
"""Response schema for marketplace import job."""
@@ -68,6 +93,7 @@ class MarketplaceImportJobResponse(BaseModel):
marketplace: str
source_url: str
status: str
language: str | None = None # Language used for translations
# Counts
imported: int = 0

View File

@@ -1,4 +1,11 @@
# models/schema/marketplace_products.py - Simplified validation
# models/schema/marketplace_product.py
"""Pydantic schemas for MarketplaceProduct API validation.
Note: title and description are stored in MarketplaceProductTranslation table,
but we keep them in the API schemas for convenience. The service layer
handles creating/updating translations separately.
"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
@@ -6,17 +13,50 @@ from pydantic import BaseModel, ConfigDict, Field
from models.schema.inventory import ProductInventorySummary
class MarketplaceProductTranslationSchema(BaseModel):
"""Schema for product translation."""
model_config = ConfigDict(from_attributes=True)
language: str
title: str
description: str | None = None
short_description: str | None = None
meta_title: str | None = None
meta_description: str | None = None
url_slug: str | None = None
class MarketplaceProductBase(BaseModel):
"""Base schema for marketplace products."""
marketplace_product_id: str | None = None
# Localized fields (passed to translations)
title: str | None = None
description: str | None = None
# Links and media
link: str | None = None
image_link: str | None = None
additional_image_link: str | None = None
# Status
availability: str | None = None
is_active: bool | None = None
# Pricing
price: str | None = None
sale_price: str | None = None
currency: str | None = None
# Product identifiers
brand: str | None = None
gtin: str | None = None
mpn: str | None = None
sku: str | None = None
# Product attributes
condition: str | None = None
adult: str | None = None
multipack: int | None = None
@@ -30,45 +70,127 @@ class MarketplaceProductBase(BaseModel):
size_type: str | None = None
size_system: str | None = None
item_group_id: str | None = None
# Categories
google_product_category: str | None = None
product_type: str | None = None
product_type_raw: str | None = None # Original feed value (renamed from product_type)
category_path: str | None = None
# Custom labels
custom_label_0: str | None = None
custom_label_1: str | None = None
custom_label_2: str | None = None
custom_label_3: str | None = None
custom_label_4: str | None = None
additional_image_link: str | None = None
sale_price: str | None = None
# Unit pricing
unit_pricing_measure: str | None = None
unit_pricing_base_measure: str | None = None
identifier_exists: str | None = None
shipping: str | None = None
currency: str | None = None
# Source tracking
marketplace: str | None = None
vendor_name: str | None = None
source_url: str | None = None
# Product type classification
product_type_enum: str | None = None # 'physical', 'digital', 'service', 'subscription'
is_digital: bool | None = None
# Digital product fields
digital_delivery_method: str | None = None
platform: str | None = None
license_type: str | None = None
# Physical product fields
weight: float | None = None
weight_unit: str | None = None
class MarketplaceProductCreate(MarketplaceProductBase):
"""Schema for creating a marketplace product."""
marketplace_product_id: str = Field(
..., description="MarketplaceProduct identifier"
..., description="Unique product identifier from marketplace"
)
title: str = Field(..., description="MarketplaceProduct title")
# Removed: min_length constraints and custom validators
# Service will handle empty string validation with proper domain exceptions
# Title is required for API creation (will be stored in translations)
title: str = Field(..., description="Product title")
class MarketplaceProductUpdate(MarketplaceProductBase):
"""Schema for updating a marketplace product.
All fields are optional - only provided fields will be updated.
"""
pass
class MarketplaceProductResponse(MarketplaceProductBase):
class MarketplaceProductResponse(BaseModel):
"""Schema for marketplace product API response."""
model_config = ConfigDict(from_attributes=True)
id: int
marketplace_product_id: str
# These will be populated from translations
title: str | None = None
description: str | None = None
# Links and media
link: str | None = None
image_link: str | None = None
additional_image_link: str | None = None
# Status
availability: str | None = None
is_active: bool | None = None
# Pricing
price: str | None = None
price_numeric: float | None = None
sale_price: str | None = None
sale_price_numeric: float | None = None
currency: str | None = None
# Product identifiers
brand: str | None = None
gtin: str | None = None
mpn: str | None = None
sku: str | None = None
# Product attributes
condition: str | None = None
color: str | None = None
size: str | None = None
# Categories
google_product_category: str | None = None
product_type_raw: str | None = None
category_path: str | None = None
# Source tracking
marketplace: str | None = None
vendor_name: str | None = None
# Product type
product_type_enum: str | None = None
is_digital: bool | None = None
platform: str | None = None
# Timestamps
created_at: datetime
updated_at: datetime
# Translations (optional - included when requested)
translations: list[MarketplaceProductTranslationSchema] | None = None
class MarketplaceProductListResponse(BaseModel):
"""Schema for paginated product list response."""
products: list[MarketplaceProductResponse]
total: int
skip: int
@@ -76,5 +198,26 @@ class MarketplaceProductListResponse(BaseModel):
class MarketplaceProductDetailResponse(BaseModel):
"""Schema for detailed product response with inventory."""
product: MarketplaceProductResponse
inventory_info: ProductInventorySummary | None = None
translations: list[MarketplaceProductTranslationSchema] | None = None
class MarketplaceImportRequest(BaseModel):
"""Schema for marketplace import request."""
url: str = Field(..., description="URL to CSV file")
marketplace: str = Field(default="Letzshop", description="Marketplace name")
vendor_name: str | None = Field(default=None, description="Vendor name")
language: str = Field(default="en", description="Language code for translations")
batch_size: int = Field(default=100, ge=1, le=1000, description="Batch size")
class MarketplaceImportResponse(BaseModel):
"""Schema for marketplace import response."""
job_id: int
status: str
message: str