major refactoring adding vendor and customer features
This commit is contained in:
@@ -7,7 +7,7 @@ from . import base
|
||||
from . import marketplace_import_job
|
||||
from . import marketplace_product
|
||||
from . import stats
|
||||
from . import stock
|
||||
from . import inventory
|
||||
from . import vendor
|
||||
# Common imports for convenience
|
||||
from .base import * # Base Pydantic models
|
||||
@@ -16,7 +16,7 @@ __all__ = [
|
||||
"base",
|
||||
"auth",
|
||||
"marketplace_product",
|
||||
"stock",
|
||||
"inventory.py",
|
||||
"vendor",
|
||||
"marketplace_import_job",
|
||||
"stats",
|
||||
|
||||
193
models/schemas/customer.py
Normal file
193
models/schemas/customer.py
Normal file
@@ -0,0 +1,193 @@
|
||||
# models/schemas/customer.py
|
||||
"""
|
||||
Pydantic schemas 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/schemas/inventory.py
Normal file
77
models/schemas/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
|
||||
@@ -1,41 +1,71 @@
|
||||
# models/schemas/marketplace_import_job.py - Keep URL validation, remove business constraints
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class MarketplaceImportJobRequest(BaseModel):
|
||||
url: str = Field(..., description="URL to CSV file from marketplace")
|
||||
marketplace: str = Field(default="Letzshop", description="Marketplace name")
|
||||
vendor_code: str = Field(..., description="Vendor code to associate products with")
|
||||
batch_size: Optional[int] = Field(1000, description="Processing batch size")
|
||||
# Removed: gt=0, le=10000 constraints - let service handle
|
||||
"""Request schema for triggering marketplace import.
|
||||
|
||||
@field_validator("url")
|
||||
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):
|
||||
# Keep URL format validation for security
|
||||
# Basic URL security validation
|
||||
if not v.startswith(("http://", "https://")):
|
||||
raise ValueError("URL must start with http:// or https://")
|
||||
return v
|
||||
|
||||
@field_validator("marketplace", "vendor_code")
|
||||
@classmethod
|
||||
def validate_strings(cls, v):
|
||||
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
|
||||
status: str
|
||||
marketplace: str
|
||||
vendor_id: int
|
||||
vendor_code: Optional[str] = None
|
||||
vendor_name: str
|
||||
message: Optional[str] = None
|
||||
imported: Optional[int] = 0
|
||||
updated: Optional[int] = 0
|
||||
total_processed: Optional[int] = 0
|
||||
error_count: Optional[int] = 0
|
||||
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
|
||||
created_at: Optional[datetime] = 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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from models.schemas.stock import StockSummaryResponse
|
||||
from models.schemas.inventory import ProductInventorySummary
|
||||
|
||||
class MarketplaceProductBase(BaseModel):
|
||||
marketplace_product_id: Optional[str] = None
|
||||
@@ -68,4 +68,4 @@ class MarketplaceProductListResponse(BaseModel):
|
||||
|
||||
class MarketplaceProductDetailResponse(BaseModel):
|
||||
product: MarketplaceProductResponse
|
||||
stock_info: Optional[StockSummaryResponse] = None
|
||||
inventory_info: Optional[ProductInventorySummary] = None
|
||||
|
||||
170
models/schemas/order.py
Normal file
170
models/schemas/order.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# models/schemas/order.py
|
||||
"""
|
||||
Pydantic schemas 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
|
||||
@@ -1,25 +1,40 @@
|
||||
# product.py - Keep basic format validation, remove business logic
|
||||
import re
|
||||
# models/schema/product.py
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from models.schemas.marketplace_product import MarketplaceProductResponse
|
||||
from models.schemas.inventory import InventoryLocationResponse
|
||||
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
marketplace_product_id: str = Field(..., description="MarketplaceProduct ID to add to vendor ")
|
||||
product_id: Optional[str] = None
|
||||
price: Optional[float] = None # Removed: ge=0 constraint
|
||||
sale_price: Optional[float] = None # Removed: ge=0 constraint
|
||||
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 = Field(False, description="Featured product flag")
|
||||
min_quantity: int = Field(1, description="Minimum order quantity")
|
||||
max_quantity: Optional[int] = None # Removed: ge=1 constraint
|
||||
# Service will validate price ranges and quantity logic
|
||||
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
|
||||
@@ -31,7 +46,24 @@ class ProductResponse(BaseModel):
|
||||
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
|
||||
|
||||
@@ -11,7 +11,7 @@ class StatsResponse(BaseModel):
|
||||
unique_categories: int
|
||||
unique_marketplaces: int = 0
|
||||
unique_vendors: int = 0
|
||||
total_stock_entries: int = 0
|
||||
total_inventory_entries: int = 0
|
||||
total_inventory_quantity: int = 0
|
||||
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# stock.py - Remove business logic validation constraints
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
class StockBase(BaseModel):
|
||||
gtin: str = Field(..., description="GTIN identifier")
|
||||
location: str = Field(..., description="Storage location")
|
||||
|
||||
class StockCreate(StockBase):
|
||||
quantity: int = Field(..., description="Initial stock quantity")
|
||||
# Removed: ge=0 constraint - let service handle negative validation
|
||||
|
||||
class StockAdd(StockBase):
|
||||
quantity: int = Field(..., description="Quantity to add/remove")
|
||||
# Removed: gt=0 constraint - let service handle zero/negative validation
|
||||
|
||||
class StockUpdate(BaseModel):
|
||||
quantity: int = Field(..., description="New stock quantity")
|
||||
# Removed: ge=0 constraint - let service handle negative validation
|
||||
|
||||
class StockResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
gtin: str
|
||||
location: str
|
||||
quantity: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class StockLocationResponse(BaseModel):
|
||||
location: str
|
||||
quantity: int
|
||||
|
||||
class StockSummaryResponse(BaseModel):
|
||||
gtin: str
|
||||
total_quantity: int
|
||||
locations: List[StockLocationResponse]
|
||||
product_title: Optional[str] = None
|
||||
@@ -1,36 +1,72 @@
|
||||
# vendor.py - Keep basic format validation, remove business logic
|
||||
# models/schemas/vendor.py
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class VendorCreate(BaseModel):
|
||||
vendor_code: str = Field(..., description="Unique vendor identifier")
|
||||
vendor_name: str = Field(..., description="Display name of the vendor ")
|
||||
description: Optional[str] = Field(None, description="Vendor description")
|
||||
contact_email: Optional[str] = None
|
||||
"""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
|
||||
# Removed: min_length, max_length constraints - let service handle
|
||||
|
||||
@field_validator("contact_email")
|
||||
# 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_contact_email(cls, v):
|
||||
# Keep basic format validation for data integrity
|
||||
if v and ("@" not in v or "." not in v):
|
||||
raise ValueError("Invalid email format")
|
||||
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):
|
||||
vendor_name: Optional[str] = None
|
||||
"""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
|
||||
@@ -39,25 +75,66 @@ class VendorUpdate(BaseModel):
|
||||
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
|
||||
vendor_name: str
|
||||
subdomain: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
owner_id: int
|
||||
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