# models/api_models.py - Updated with Marketplace Support from pydantic import BaseModel, Field, field_validator, EmailStr from typing import Optional, List from datetime import datetime # User Authentication Models class UserRegister(BaseModel): email: EmailStr = Field(..., description="Valid email address") username: str = Field(..., min_length=3, max_length=50, description="Username (3-50 characters)") password: str = Field(..., min_length=6, description="Password (minimum 6 characters)") @field_validator('username') @classmethod def validate_username(cls, v): if not v.isalnum(): raise ValueError('Username must contain only alphanumeric characters') return v.lower().strip() @field_validator('password') @classmethod def validate_password(cls, v): if len(v) < 6: raise ValueError('Password must be at least 6 characters long') return v class UserLogin(BaseModel): username: str = Field(..., description="Username") password: str = Field(..., description="Password") @field_validator('username') @classmethod def validate_username(cls, v): return v.strip() class UserResponse(BaseModel): id: int email: str username: str role: str is_active: bool last_login: Optional[datetime] = None created_at: datetime updated_at: datetime model_config = {"from_attributes": True} class LoginResponse(BaseModel): access_token: str token_type: str = "bearer" expires_in: int user: UserResponse # NEW: Shop models class ShopCreate(BaseModel): shop_code: str = Field(..., min_length=3, max_length=50, description="Unique shop code (e.g., TECHSTORE)") shop_name: str = Field(..., min_length=1, max_length=200, description="Display name of the shop") description: Optional[str] = Field(None, max_length=2000, description="Shop description") contact_email: Optional[str] = None contact_phone: Optional[str] = None website: Optional[str] = None business_address: Optional[str] = None tax_number: Optional[str] = None @field_validator('shop_code') def validate_shop_code(cls, v): # Convert to uppercase and check format v = v.upper().strip() if not v.replace('_', '').replace('-', '').isalnum(): raise ValueError('Shop code must be alphanumeric (underscores and hyphens allowed)') return v @field_validator('contact_email') def validate_contact_email(cls, v): if v and ('@' not in v or '.' not in v): raise ValueError('Invalid email format') return v.lower() if v else v class ShopUpdate(BaseModel): shop_name: Optional[str] = Field(None, min_length=1, max_length=200) description: Optional[str] = Field(None, max_length=2000) contact_email: Optional[str] = None contact_phone: Optional[str] = None website: Optional[str] = None business_address: Optional[str] = None tax_number: Optional[str] = None @field_validator('contact_email') def validate_contact_email(cls, v): if v and ('@' not in v or '.' not in v): raise ValueError('Invalid email format') return v.lower() if v else v class ShopResponse(BaseModel): id: int shop_code: str shop_name: str description: Optional[str] owner_id: int contact_email: Optional[str] contact_phone: Optional[str] website: Optional[str] business_address: Optional[str] tax_number: Optional[str] is_active: bool is_verified: bool created_at: datetime updated_at: datetime class Config: from_attributes = True class ShopListResponse(BaseModel): shops: List[ShopResponse] total: int skip: int limit: int # Base Product Models with Marketplace Support class ProductBase(BaseModel): product_id: Optional[str] = None title: Optional[str] = None description: Optional[str] = None link: Optional[str] = None image_link: Optional[str] = None availability: Optional[str] = None price: Optional[str] = None brand: Optional[str] = None gtin: Optional[str] = None mpn: Optional[str] = None condition: Optional[str] = None adult: Optional[str] = None multipack: Optional[int] = None is_bundle: Optional[str] = None age_group: Optional[str] = None color: Optional[str] = None gender: Optional[str] = None material: Optional[str] = None pattern: Optional[str] = None size: Optional[str] = None size_type: Optional[str] = None size_system: Optional[str] = None item_group_id: Optional[str] = None google_product_category: Optional[str] = None product_type: Optional[str] = None custom_label_0: Optional[str] = None custom_label_1: Optional[str] = None custom_label_2: Optional[str] = None custom_label_3: Optional[str] = None custom_label_4: Optional[str] = None additional_image_link: Optional[str] = None sale_price: Optional[str] = None unit_pricing_measure: Optional[str] = None unit_pricing_base_measure: Optional[str] = None identifier_exists: Optional[str] = None shipping: Optional[str] = None currency: Optional[str] = None # New marketplace fields marketplace: Optional[str] = None shop_name: Optional[str] = None class ProductCreate(ProductBase): product_id: str = Field(..., min_length=1, description="Product ID is required") title: str = Field(..., min_length=1, description="Title is required") @field_validator('product_id', 'title') @classmethod def validate_required_fields(cls, v): if not v or not v.strip(): raise ValueError('Field cannot be empty') return v.strip() class ProductUpdate(ProductBase): pass class ProductResponse(ProductBase): id: int created_at: datetime updated_at: datetime model_config = {"from_attributes": True} # NEW: Shop Product models class ShopProductCreate(BaseModel): product_id: str = Field(..., description="Product ID to add to shop") shop_product_id: Optional[str] = Field(None, description="Shop's internal product ID") shop_price: Optional[float] = Field(None, ge=0, description="Shop-specific price override") shop_sale_price: Optional[float] = Field(None, ge=0, description="Shop-specific sale price") shop_currency: Optional[str] = Field(None, description="Shop-specific currency") shop_availability: Optional[str] = Field(None, description="Shop-specific availability") shop_condition: Optional[str] = Field(None, description="Shop-specific condition") is_featured: bool = Field(False, description="Featured product flag") min_quantity: int = Field(1, ge=1, description="Minimum order quantity") max_quantity: Optional[int] = Field(None, ge=1, description="Maximum order quantity") class ShopProductResponse(BaseModel): id: int shop_id: int product: ProductResponse shop_product_id: Optional[str] shop_price: Optional[float] shop_sale_price: Optional[float] shop_currency: Optional[str] shop_availability: Optional[str] shop_condition: Optional[str] is_featured: bool is_active: bool min_quantity: int max_quantity: Optional[int] created_at: datetime updated_at: datetime class Config: from_attributes = True # Stock Models class StockBase(BaseModel): gtin: str = Field(..., min_length=1, description="GTIN is required") location: str = Field(..., min_length=1, description="Location is required") class StockCreate(StockBase): quantity: int = Field(ge=0, description="Quantity must be non-negative") class StockAdd(StockBase): quantity: int = Field(gt=0, description="Quantity to add must be positive") class StockUpdate(BaseModel): quantity: int = Field(ge=0, description="Quantity must be non-negative") class StockResponse(BaseModel): id: int gtin: str location: str quantity: int created_at: datetime updated_at: datetime model_config = {"from_attributes": True} class StockLocationResponse(BaseModel): location: str quantity: int class StockSummaryResponse(BaseModel): gtin: str total_quantity: int locations: List[StockLocationResponse] product_title: Optional[str] = None # Marketplace Import Models class MarketplaceImportRequest(BaseModel): url: str = Field(..., description="URL to CSV file from marketplace") marketplace: str = Field(default="Letzshop", description="Name of the marketplace (e.g., Letzshop, Amazon, eBay)") shop_code: str = Field(..., description="Shop code to associate products with") batch_size: Optional[int] = Field(1000, gt=0, le=10000, description="Batch size for processing") @field_validator('url') @classmethod def validate_url(cls, v): if not v.startswith(('http://', 'https://')): raise ValueError('URL must start with http:// or https://') return v @field_validator('marketplace') @classmethod def validate_marketplace(cls, v): # You can add validation for supported marketplaces here supported_marketplaces = ['Letzshop', 'Amazon', 'eBay', 'Etsy', 'Shopify', 'Other'] if v not in supported_marketplaces: # For now, allow any marketplace but log it pass return v.strip() @field_validator('shop_code') @classmethod def validate_shop_code(cls, v): return v.upper().strip() class MarketplaceImportJobResponse(BaseModel): job_id: int status: str marketplace: str shop_id: int shop_code: Optional[str] = None # Will be populated from shop relationship shop_name: str message: Optional[str] = None imported: Optional[int] = 0 updated: Optional[int] = 0 total_processed: Optional[int] = 0 error_count: Optional[int] = 0 error_message: Optional[str] = None created_at: Optional[datetime] = None started_at: Optional[datetime] = None completed_at: Optional[datetime] = None # Response Models class ProductListResponse(BaseModel): products: List[ProductResponse] total: int skip: int limit: int class ProductDetailResponse(BaseModel): product: ProductResponse stock_info: Optional[StockSummaryResponse] = None class StatsResponse(BaseModel): total_products: int unique_brands: int unique_categories: int unique_marketplaces: int = 0 unique_shops: int = 0 total_stock_entries: int = 0 total_inventory_quantity: int = 0 class MarketplaceStatsResponse(BaseModel): marketplace: str total_products: int unique_shops: int unique_brands: int