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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user