Renamed schemas to schema as per naming conventions
This commit is contained in:
23
models/schema/__init__.py
Normal file
23
models/schema/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# models/schema/__init__.py
|
||||
"""API models package - Pydantic models for request/response validation."""
|
||||
|
||||
from . import auth
|
||||
# Import API model modules
|
||||
from . import base
|
||||
from . import marketplace_import_job
|
||||
from . import marketplace_product
|
||||
from . import stats
|
||||
from . import inventory
|
||||
from . import vendor
|
||||
# Common imports for convenience
|
||||
from .base import * # Base Pydantic models
|
||||
|
||||
__all__ = [
|
||||
"base",
|
||||
"auth",
|
||||
"marketplace_product",
|
||||
"inventory.py",
|
||||
"vendor",
|
||||
"marketplace_import_job",
|
||||
"stats",
|
||||
]
|
||||
57
models/schema/auth.py
Normal file
57
models/schema/auth.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# auth.py - Keep security-critical validation
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
|
||||
|
||||
|
||||
class UserRegister(BaseModel):
|
||||
email: EmailStr = Field(..., description="Valid email address")
|
||||
username: str = Field(..., description="Username")
|
||||
password: str = Field(..., description="Password")
|
||||
|
||||
# Keep security validation in Pydantic for auth
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
def validate_username(cls, v):
|
||||
if not re.match(r"^[a-zA-Z0-9_]+$", v):
|
||||
raise ValueError("Username must contain only letters, numbers, or underscores")
|
||||
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):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
email: str
|
||||
username: str
|
||||
role: str
|
||||
is_active: bool
|
||||
last_login: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int
|
||||
user: UserResponse
|
||||
21
models/schema/base.py
Normal file
21
models/schema/base.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from typing import Generic, List, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ListResponse(BaseModel, Generic[T]):
|
||||
"""Generic list response model"""
|
||||
|
||||
items: List[T]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
class StatusResponse(BaseModel):
|
||||
"""Generic status response"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
193
models/schema/customer.py
Normal file
193
models/schema/customer.py
Normal file
@@ -0,0 +1,193 @@
|
||||
# models/schema/customer.py
|
||||
"""
|
||||
Pydantic schema for customer-related operations.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Customer Registration & Authentication
|
||||
# ============================================================================
|
||||
|
||||
class CustomerRegister(BaseModel):
|
||||
"""Schema for customer registration."""
|
||||
|
||||
email: EmailStr = Field(..., description="Customer email address")
|
||||
password: str = Field(
|
||||
...,
|
||||
min_length=8,
|
||||
description="Password (minimum 8 characters)"
|
||||
)
|
||||
first_name: str = Field(..., min_length=1, max_length=100)
|
||||
last_name: str = Field(..., min_length=1, max_length=100)
|
||||
phone: Optional[str] = Field(None, max_length=50)
|
||||
marketing_consent: bool = Field(default=False)
|
||||
|
||||
@field_validator('email')
|
||||
@classmethod
|
||||
def email_lowercase(cls, v: str) -> str:
|
||||
"""Convert email to lowercase."""
|
||||
return v.lower()
|
||||
|
||||
@field_validator('password')
|
||||
@classmethod
|
||||
def password_strength(cls, v: str) -> str:
|
||||
"""Validate password strength."""
|
||||
if len(v) < 8:
|
||||
raise ValueError("Password must be at least 8 characters")
|
||||
if not any(char.isdigit() for char in v):
|
||||
raise ValueError("Password must contain at least one digit")
|
||||
if not any(char.isalpha() for char in v):
|
||||
raise ValueError("Password must contain at least one letter")
|
||||
return v
|
||||
|
||||
|
||||
class CustomerUpdate(BaseModel):
|
||||
"""Schema for updating customer profile."""
|
||||
|
||||
email: Optional[EmailStr] = None
|
||||
first_name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
last_name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
phone: Optional[str] = Field(None, max_length=50)
|
||||
marketing_consent: Optional[bool] = None
|
||||
|
||||
@field_validator('email')
|
||||
@classmethod
|
||||
def email_lowercase(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""Convert email to lowercase."""
|
||||
return v.lower() if v else None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Customer Response
|
||||
# ============================================================================
|
||||
|
||||
class CustomerResponse(BaseModel):
|
||||
"""Schema for customer response (excludes password)."""
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
email: str
|
||||
first_name: Optional[str]
|
||||
last_name: Optional[str]
|
||||
phone: Optional[str]
|
||||
customer_number: str
|
||||
marketing_consent: bool
|
||||
last_order_date: Optional[datetime]
|
||||
total_orders: int
|
||||
total_spent: Decimal
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {
|
||||
"from_attributes": True
|
||||
}
|
||||
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
"""Get customer full name."""
|
||||
if self.first_name and self.last_name:
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
return self.email
|
||||
|
||||
|
||||
class CustomerListResponse(BaseModel):
|
||||
"""Schema for paginated customer list."""
|
||||
|
||||
customers: List[CustomerResponse]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Customer Address
|
||||
# ============================================================================
|
||||
|
||||
class CustomerAddressCreate(BaseModel):
|
||||
"""Schema for creating customer address."""
|
||||
|
||||
address_type: str = Field(..., pattern="^(billing|shipping)$")
|
||||
first_name: str = Field(..., min_length=1, max_length=100)
|
||||
last_name: str = Field(..., min_length=1, max_length=100)
|
||||
company: Optional[str] = Field(None, max_length=200)
|
||||
address_line_1: str = Field(..., min_length=1, max_length=255)
|
||||
address_line_2: Optional[str] = Field(None, max_length=255)
|
||||
city: str = Field(..., min_length=1, max_length=100)
|
||||
postal_code: str = Field(..., min_length=1, max_length=20)
|
||||
country: str = Field(..., min_length=2, max_length=100)
|
||||
is_default: bool = Field(default=False)
|
||||
|
||||
|
||||
class CustomerAddressUpdate(BaseModel):
|
||||
"""Schema for updating customer address."""
|
||||
|
||||
address_type: Optional[str] = Field(None, pattern="^(billing|shipping)$")
|
||||
first_name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
last_name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
company: Optional[str] = Field(None, max_length=200)
|
||||
address_line_1: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
address_line_2: Optional[str] = Field(None, max_length=255)
|
||||
city: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
postal_code: Optional[str] = Field(None, min_length=1, max_length=20)
|
||||
country: Optional[str] = Field(None, min_length=2, max_length=100)
|
||||
is_default: Optional[bool] = None
|
||||
|
||||
|
||||
class CustomerAddressResponse(BaseModel):
|
||||
"""Schema for customer address response."""
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
customer_id: int
|
||||
address_type: str
|
||||
first_name: str
|
||||
last_name: str
|
||||
company: Optional[str]
|
||||
address_line_1: str
|
||||
address_line_2: Optional[str]
|
||||
city: str
|
||||
postal_code: str
|
||||
country: str
|
||||
is_default: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {
|
||||
"from_attributes": True
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Customer Statistics
|
||||
# ============================================================================
|
||||
|
||||
class CustomerStatsResponse(BaseModel):
|
||||
"""Schema for customer statistics."""
|
||||
|
||||
customer_id: int
|
||||
total_orders: int
|
||||
total_spent: Decimal
|
||||
average_order_value: Decimal
|
||||
last_order_date: Optional[datetime]
|
||||
first_order_date: Optional[datetime]
|
||||
lifetime_value: Decimal
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Customer Preferences
|
||||
# ============================================================================
|
||||
|
||||
class CustomerPreferencesUpdate(BaseModel):
|
||||
"""Schema for updating customer preferences."""
|
||||
|
||||
marketing_consent: Optional[bool] = None
|
||||
language: Optional[str] = Field(None, max_length=10)
|
||||
currency: Optional[str] = Field(None, max_length=3)
|
||||
notification_preferences: Optional[Dict[str, bool]] = None
|
||||
77
models/schema/inventory.py
Normal file
77
models/schema/inventory.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# models/schema/inventory.py
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class InventoryBase(BaseModel):
|
||||
product_id: int = Field(..., description="Product ID in vendor catalog")
|
||||
location: str = Field(..., description="Storage location")
|
||||
|
||||
|
||||
class InventoryCreate(InventoryBase):
|
||||
"""Set exact inventory quantity (replaces existing)."""
|
||||
quantity: int = Field(..., description="Exact inventory quantity", ge=0)
|
||||
|
||||
|
||||
class InventoryAdjust(InventoryBase):
|
||||
"""Add or remove inventory quantity."""
|
||||
quantity: int = Field(..., description="Quantity to add (positive) or remove (negative)")
|
||||
|
||||
|
||||
class InventoryUpdate(BaseModel):
|
||||
"""Update inventory fields."""
|
||||
quantity: Optional[int] = Field(None, ge=0)
|
||||
reserved_quantity: Optional[int] = Field(None, ge=0)
|
||||
location: Optional[str] = None
|
||||
|
||||
|
||||
class InventoryReserve(BaseModel):
|
||||
"""Reserve inventory for orders."""
|
||||
product_id: int
|
||||
location: str
|
||||
quantity: int = Field(..., gt=0)
|
||||
|
||||
|
||||
class InventoryResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
product_id: int
|
||||
vendor_id: int
|
||||
location: str
|
||||
quantity: int
|
||||
reserved_quantity: int
|
||||
gtin: Optional[str]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@property
|
||||
def available_quantity(self):
|
||||
return max(0, self.quantity - self.reserved_quantity)
|
||||
|
||||
|
||||
class InventoryLocationResponse(BaseModel):
|
||||
location: str
|
||||
quantity: int
|
||||
reserved_quantity: int
|
||||
available_quantity: int
|
||||
|
||||
|
||||
class ProductInventorySummary(BaseModel):
|
||||
"""Inventory summary for a product."""
|
||||
product_id: int
|
||||
vendor_id: int
|
||||
product_sku: Optional[str]
|
||||
product_title: str
|
||||
total_quantity: int
|
||||
total_reserved: int
|
||||
total_available: int
|
||||
locations: List[InventoryLocationResponse]
|
||||
|
||||
|
||||
class InventoryListResponse(BaseModel):
|
||||
inventories: List[InventoryResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
71
models/schema/marketplace_import_job.py
Normal file
71
models/schema/marketplace_import_job.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class MarketplaceImportJobRequest(BaseModel):
|
||||
"""Request schema for triggering marketplace import.
|
||||
|
||||
Note: vendor_id is injected by middleware, not from request body.
|
||||
"""
|
||||
source_url: str = Field(..., description="URL to CSV file from marketplace")
|
||||
marketplace: str = Field(default="Letzshop", description="Marketplace name")
|
||||
batch_size: Optional[int] = 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 MarketplaceImportJobResponse(BaseModel):
|
||||
"""Response schema for marketplace import job."""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
job_id: int
|
||||
vendor_id: int
|
||||
vendor_code: str # Populated from vendor relationship
|
||||
vendor_name: str # Populated from vendor relationship
|
||||
marketplace: str
|
||||
source_url: str
|
||||
status: str
|
||||
|
||||
# Counts
|
||||
imported: int = 0
|
||||
updated: int = 0
|
||||
total_processed: int = 0
|
||||
error_count: int = 0
|
||||
|
||||
# Error details
|
||||
error_message: Optional[str] = None
|
||||
|
||||
# Timestamps
|
||||
created_at: datetime
|
||||
started_at: Optional[datetime] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class MarketplaceImportJobListResponse(BaseModel):
|
||||
"""Response schema for list of import jobs."""
|
||||
jobs: list[MarketplaceImportJobResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
class MarketplaceImportJobStatusUpdate(BaseModel):
|
||||
"""Schema for updating import job status (internal use)."""
|
||||
status: str
|
||||
imported_count: Optional[int] = None
|
||||
updated_count: Optional[int] = None
|
||||
error_count: Optional[int] = None
|
||||
total_processed: Optional[int] = None
|
||||
error_message: Optional[str] = None
|
||||
71
models/schema/marketplace_product.py
Normal file
71
models/schema/marketplace_product.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# models/schema/marketplace_products.py - Simplified validation
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from models.schema.inventory import ProductInventorySummary
|
||||
|
||||
class MarketplaceProductBase(BaseModel):
|
||||
marketplace_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
|
||||
marketplace: Optional[str] = None
|
||||
vendor_name: Optional[str] = None
|
||||
|
||||
class MarketplaceProductCreate(MarketplaceProductBase):
|
||||
marketplace_product_id: str = Field(..., description="MarketplaceProduct identifier")
|
||||
title: str = Field(..., description="MarketplaceProduct title")
|
||||
# Removed: min_length constraints and custom validators
|
||||
# Service will handle empty string validation with proper domain exceptions
|
||||
|
||||
class MarketplaceProductUpdate(MarketplaceProductBase):
|
||||
pass
|
||||
|
||||
class MarketplaceProductResponse(MarketplaceProductBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class MarketplaceProductListResponse(BaseModel):
|
||||
products: List[MarketplaceProductResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
class MarketplaceProductDetailResponse(BaseModel):
|
||||
product: MarketplaceProductResponse
|
||||
inventory_info: Optional[ProductInventorySummary] = None
|
||||
170
models/schema/order.py
Normal file
170
models/schema/order.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# models/schema/order.py
|
||||
"""
|
||||
Pydantic schema for order operations.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Order Item Schemas
|
||||
# ============================================================================
|
||||
|
||||
class OrderItemCreate(BaseModel):
|
||||
"""Schema for creating an order item."""
|
||||
product_id: int
|
||||
quantity: int = Field(..., ge=1)
|
||||
|
||||
|
||||
class OrderItemResponse(BaseModel):
|
||||
"""Schema for order item response."""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
order_id: int
|
||||
product_id: int
|
||||
product_name: str
|
||||
product_sku: Optional[str]
|
||||
quantity: int
|
||||
unit_price: float
|
||||
total_price: float
|
||||
inventory_reserved: bool
|
||||
inventory_fulfilled: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Order Address Schemas
|
||||
# ============================================================================
|
||||
|
||||
class OrderAddressCreate(BaseModel):
|
||||
"""Schema for order address (shipping/billing)."""
|
||||
first_name: str = Field(..., min_length=1, max_length=100)
|
||||
last_name: str = Field(..., min_length=1, max_length=100)
|
||||
company: Optional[str] = Field(None, max_length=200)
|
||||
address_line_1: str = Field(..., min_length=1, max_length=255)
|
||||
address_line_2: Optional[str] = Field(None, max_length=255)
|
||||
city: str = Field(..., min_length=1, max_length=100)
|
||||
postal_code: str = Field(..., min_length=1, max_length=20)
|
||||
country: str = Field(..., min_length=2, max_length=100)
|
||||
|
||||
|
||||
class OrderAddressResponse(BaseModel):
|
||||
"""Schema for order address response."""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
address_type: str
|
||||
first_name: str
|
||||
last_name: str
|
||||
company: Optional[str]
|
||||
address_line_1: str
|
||||
address_line_2: Optional[str]
|
||||
city: str
|
||||
postal_code: str
|
||||
country: str
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Order Create/Update Schemas
|
||||
# ============================================================================
|
||||
|
||||
class OrderCreate(BaseModel):
|
||||
"""Schema for creating an order."""
|
||||
customer_id: Optional[int] = None # Optional for guest checkout
|
||||
items: List[OrderItemCreate] = Field(..., min_length=1)
|
||||
|
||||
# Addresses
|
||||
shipping_address: OrderAddressCreate
|
||||
billing_address: Optional[OrderAddressCreate] = None # Use shipping if not provided
|
||||
|
||||
# Optional fields
|
||||
shipping_method: Optional[str] = None
|
||||
customer_notes: Optional[str] = Field(None, max_length=1000)
|
||||
|
||||
# Cart/session info
|
||||
session_id: Optional[str] = None
|
||||
|
||||
|
||||
class OrderUpdate(BaseModel):
|
||||
"""Schema for updating order status."""
|
||||
status: Optional[str] = Field(
|
||||
None,
|
||||
pattern="^(pending|processing|shipped|delivered|cancelled|refunded)$"
|
||||
)
|
||||
tracking_number: Optional[str] = None
|
||||
internal_notes: Optional[str] = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Order Response Schemas
|
||||
# ============================================================================
|
||||
|
||||
class OrderResponse(BaseModel):
|
||||
"""Schema for order response."""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
customer_id: int
|
||||
order_number: str
|
||||
status: str
|
||||
|
||||
# Financial
|
||||
subtotal: float
|
||||
tax_amount: float
|
||||
shipping_amount: float
|
||||
discount_amount: float
|
||||
total_amount: float
|
||||
currency: str
|
||||
|
||||
# Shipping
|
||||
shipping_method: Optional[str]
|
||||
tracking_number: Optional[str]
|
||||
|
||||
# Notes
|
||||
customer_notes: Optional[str]
|
||||
internal_notes: Optional[str]
|
||||
|
||||
# Timestamps
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
paid_at: Optional[datetime]
|
||||
shipped_at: Optional[datetime]
|
||||
delivered_at: Optional[datetime]
|
||||
cancelled_at: Optional[datetime]
|
||||
|
||||
|
||||
class OrderDetailResponse(OrderResponse):
|
||||
"""Schema for detailed order response with items and addresses."""
|
||||
items: List[OrderItemResponse]
|
||||
shipping_address: OrderAddressResponse
|
||||
billing_address: OrderAddressResponse
|
||||
|
||||
|
||||
class OrderListResponse(BaseModel):
|
||||
"""Schema for paginated order list."""
|
||||
orders: List[OrderResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Order Statistics
|
||||
# ============================================================================
|
||||
|
||||
class OrderStatsResponse(BaseModel):
|
||||
"""Schema for order statistics."""
|
||||
total_orders: int
|
||||
pending_orders: int
|
||||
processing_orders: int
|
||||
shipped_orders: int
|
||||
delivered_orders: int
|
||||
cancelled_orders: int
|
||||
total_revenue: Decimal
|
||||
average_order_value: Decimal
|
||||
69
models/schema/product.py
Normal file
69
models/schema/product.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# models/schema/product.py
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from models.schema.marketplace_product import MarketplaceProductResponse
|
||||
from models.schema.inventory import InventoryLocationResponse
|
||||
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
marketplace_product_id: int = Field(..., description="MarketplaceProduct ID to add to vendor catalog")
|
||||
product_id: Optional[str] = Field(None, description="Vendor's internal SKU/product ID")
|
||||
price: Optional[float] = Field(None, ge=0)
|
||||
sale_price: Optional[float] = Field(None, ge=0)
|
||||
currency: Optional[str] = None
|
||||
availability: Optional[str] = None
|
||||
condition: Optional[str] = None
|
||||
is_featured: bool = False
|
||||
min_quantity: int = Field(1, ge=1)
|
||||
max_quantity: Optional[int] = Field(None, ge=1)
|
||||
|
||||
|
||||
class ProductUpdate(BaseModel):
|
||||
product_id: Optional[str] = None
|
||||
price: Optional[float] = Field(None, ge=0)
|
||||
sale_price: Optional[float] = Field(None, ge=0)
|
||||
currency: Optional[str] = None
|
||||
availability: Optional[str] = None
|
||||
condition: Optional[str] = None
|
||||
is_featured: Optional[bool] = None
|
||||
is_active: Optional[bool] = None
|
||||
min_quantity: Optional[int] = Field(None, ge=1)
|
||||
max_quantity: Optional[int] = Field(None, ge=1)
|
||||
|
||||
|
||||
class ProductResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
marketplace_product: MarketplaceProductResponse
|
||||
product_id: Optional[str]
|
||||
price: Optional[float]
|
||||
sale_price: Optional[float]
|
||||
currency: Optional[str]
|
||||
availability: Optional[str]
|
||||
condition: Optional[str]
|
||||
is_featured: bool
|
||||
is_active: bool
|
||||
display_order: int
|
||||
min_quantity: int
|
||||
max_quantity: Optional[int]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
# Include inventory summary
|
||||
total_inventory: Optional[int] = None
|
||||
available_inventory: Optional[int] = None
|
||||
|
||||
|
||||
class ProductDetailResponse(ProductResponse):
|
||||
"""Product with full inventory details."""
|
||||
inventory_locations: List[InventoryLocationResponse] = []
|
||||
|
||||
|
||||
class ProductListResponse(BaseModel):
|
||||
products: List[ProductResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
22
models/schema/stats.py
Normal file
22
models/schema/stats.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
|
||||
|
||||
|
||||
class StatsResponse(BaseModel):
|
||||
total_products: int
|
||||
unique_brands: int
|
||||
unique_categories: int
|
||||
unique_marketplaces: int = 0
|
||||
unique_vendors: int = 0
|
||||
total_inventory_entries: int = 0
|
||||
total_inventory_quantity: int = 0
|
||||
|
||||
|
||||
class MarketplaceStatsResponse(BaseModel):
|
||||
marketplace: str
|
||||
total_products: int
|
||||
unique_vendors: int
|
||||
unique_brands: int
|
||||
140
models/schema/vendor.py
Normal file
140
models/schema/vendor.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# models/schema/vendor.py
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class VendorCreate(BaseModel):
|
||||
"""Schema for creating a new vendor."""
|
||||
vendor_code: str = Field(..., description="Unique vendor identifier (e.g., TECHSTORE)")
|
||||
subdomain: str = Field(..., description="Unique subdomain for the vendor")
|
||||
name: str = Field(..., description="Display name of the vendor")
|
||||
description: Optional[str] = None
|
||||
|
||||
# Owner information - REQUIRED for admin creation
|
||||
owner_email: str = Field(..., description="Email for the vendor owner account")
|
||||
|
||||
# Contact information
|
||||
contact_phone: Optional[str] = None
|
||||
website: Optional[str] = None
|
||||
|
||||
# Business information
|
||||
business_address: Optional[str] = None
|
||||
tax_number: Optional[str] = None
|
||||
|
||||
# Letzshop CSV URLs (multi-language support)
|
||||
letzshop_csv_url_fr: Optional[str] = None
|
||||
letzshop_csv_url_en: Optional[str] = None
|
||||
letzshop_csv_url_de: Optional[str] = None
|
||||
|
||||
# Theme configuration
|
||||
theme_config: Optional[Dict] = Field(default_factory=dict)
|
||||
|
||||
@field_validator("owner_email")
|
||||
@classmethod
|
||||
def validate_owner_email(cls, v):
|
||||
if not v or "@" not in v or "." not in v:
|
||||
raise ValueError("Valid email address required for vendor owner")
|
||||
return v.lower()
|
||||
|
||||
@field_validator("subdomain")
|
||||
@classmethod
|
||||
def validate_subdomain(cls, v):
|
||||
# Basic subdomain validation: lowercase alphanumeric with hyphens
|
||||
if v and not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', v):
|
||||
raise ValueError("Subdomain must contain only lowercase letters, numbers, and hyphens")
|
||||
return v.lower() if v else v
|
||||
|
||||
@field_validator("vendor_code")
|
||||
@classmethod
|
||||
def validate_vendor_code(cls, v):
|
||||
# Ensure vendor code is uppercase for consistency
|
||||
return v.upper() if v else v
|
||||
|
||||
|
||||
class VendorUpdate(BaseModel):
|
||||
"""Schema for updating vendor information."""
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
contact_email: Optional[str] = None
|
||||
contact_phone: Optional[str] = None
|
||||
website: Optional[str] = None
|
||||
business_address: Optional[str] = None
|
||||
tax_number: Optional[str] = None
|
||||
letzshop_csv_url_fr: Optional[str] = None
|
||||
letzshop_csv_url_en: Optional[str] = None
|
||||
letzshop_csv_url_de: Optional[str] = None
|
||||
theme_config: Optional[Dict] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
@field_validator("contact_email")
|
||||
@classmethod
|
||||
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 VendorResponse(BaseModel):
|
||||
"""Schema for vendor response data."""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_code: str
|
||||
subdomain: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
owner_user_id: int
|
||||
|
||||
# Contact information
|
||||
contact_email: Optional[str]
|
||||
contact_phone: Optional[str]
|
||||
website: Optional[str]
|
||||
|
||||
# Business information
|
||||
business_address: Optional[str]
|
||||
tax_number: Optional[str]
|
||||
|
||||
# Letzshop URLs
|
||||
letzshop_csv_url_fr: Optional[str]
|
||||
letzshop_csv_url_en: Optional[str]
|
||||
letzshop_csv_url_de: Optional[str]
|
||||
|
||||
# Theme configuration
|
||||
theme_config: Dict
|
||||
|
||||
# Status flags
|
||||
is_active: bool
|
||||
is_verified: bool
|
||||
|
||||
# Timestamps
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class VendorListResponse(BaseModel):
|
||||
"""Schema for paginated vendor list."""
|
||||
vendors: List[VendorResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
|
||||
class VendorSummary(BaseModel):
|
||||
"""Lightweight vendor summary for dropdowns and quick references."""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
vendor_code: str
|
||||
subdomain: str
|
||||
name: str
|
||||
is_active: bool
|
||||
|
||||
|
||||
class VendorCreateResponse(VendorResponse):
|
||||
"""Extended response for vendor creation with owner credentials."""
|
||||
owner_email: str
|
||||
owner_username: str
|
||||
temporary_password: str
|
||||
login_url: Optional[str] = None
|
||||
Reference in New Issue
Block a user