ifixkart-backend/Backend/app/schemas/Catalog.py

584 lines
16 KiB
Python

from pydantic import BaseModel, Field, model_validator
from typing import Optional, List, Dict, Any
from decimal import Decimal
from datetime import datetime, date
# --- Brand ---
class BrandCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
logo_url: Optional[str] = Field(None, max_length=500)
device_types: Optional[List[str]] = None
class BrandUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
logo_url: Optional[str] = Field(None, max_length=500)
is_active: Optional[bool] = None
device_types: Optional[List[str]] = None
class BrandResponse(BaseModel):
brand_id: str
name: str
slug: str
logo_url: Optional[str]
is_active: bool
device_types: List[str] = []
created_at: Optional[datetime] = None
class Config:
from_attributes = True
# --- Category ---
class CategoryCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
parent_category_id: Optional[str] = Field(None, min_length=26, max_length=26)
description: Optional[str] = Field(None, max_length=500)
image_url: Optional[str] = Field(None, max_length=500)
sort_order: Optional[str] = "0"
is_parent_feature: bool = False
class CategoryUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
parent_category_id: Optional[str] = Field(None, min_length=26, max_length=26)
description: Optional[str] = Field(None, max_length=500)
image_url: Optional[str] = Field(None, max_length=500)
sort_order: Optional[str] = None
is_active: Optional[bool] = None
is_parent_feature: Optional[bool] = None
class CategoryResponse(BaseModel):
category_id: str
parent_category_id: Optional[str]
name: str
slug: str
description: Optional[str]
image_url: Optional[str]
sort_order: str
is_parent_feature: bool = False
is_active: bool
created_at: Optional[datetime] = None
class Config:
from_attributes = True
# --- Category Parent Hierarchy (nested response) ---
class HierarchyModelItem(BaseModel):
model_id: str
name: str
slug: str
image_url: Optional[str] = None
class HierarchySeriesItem(BaseModel):
series_id: str
name: str
slug: str
models: List[HierarchyModelItem] = []
class HierarchyBrandItem(BaseModel):
brand_id: str
name: str
slug: str
logo_url: Optional[str] = None
series: List[HierarchySeriesItem] = []
class CategoryParentHierarchyResponse(BaseModel):
category_id: str
brands: List[HierarchyBrandItem] = []
# --- Tag ---
class TagCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
class TagResponse(BaseModel):
tag_id: str
name: str
slug: str
is_active: bool
class Config:
from_attributes = True
# --- Collection ---
class CollectionCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
class CollectionResponse(BaseModel):
collection_id: str
name: str
slug: str
description: Optional[str]
is_active: bool
class Config:
from_attributes = True
# --- SEO Metadata ---
class SeoMetadataCreate(BaseModel):
entity_type: str = Field(..., description="product, category, brand, device_model, repair_service")
entity_id: str
meta_title: str = Field(..., max_length=255)
meta_description: str
canonical_url: Optional[str] = Field(None, max_length=500)
og_image: Optional[str] = Field(None, max_length=500)
schema_markup: Optional[Dict[str, Any]] = Field(None, alias="schema_json")
class Config:
populate_by_name = True
from_attributes = True
class SeoMetadataResponse(BaseModel):
seo_id: str
entity_type: str
entity_id: str
meta_title: str
meta_description: str
canonical_url: Optional[str]
og_image: Optional[str]
schema_markup: Optional[Dict[str, Any]] = Field(None, alias="schema_json")
class Config:
populate_by_name = True
from_attributes = True
# --- Part ---
class PartCreate(BaseModel):
sku: str = Field(..., min_length=1, max_length=100)
name: str = Field(..., min_length=1, max_length=255)
cost_price: Decimal
low_stock_alert: Optional[int] = 3
supplier: Optional[str] = Field(None, max_length=255)
barcode: Optional[str] = Field(None, max_length=100)
class PartUpdate(BaseModel):
sku: Optional[str] = Field(None, min_length=1, max_length=100)
name: Optional[str] = Field(None, min_length=1, max_length=255)
cost_price: Optional[Decimal] = None
low_stock_alert: Optional[int] = None
supplier: Optional[str] = None
barcode: Optional[str] = None
is_active: Optional[bool] = None
class PartResponse(BaseModel):
part_id: str
sku: str
name: str
cost_price: Decimal
low_stock_alert: int
supplier: Optional[str]
barcode: Optional[str]
is_active: bool
stock: int = 0 # computed from stock ledger
class Config:
from_attributes = True
# --- Part Compatibility ---
class PartDeviceCompatibilityCreate(BaseModel):
part_id: str
model_id: str
class PartDeviceCompatibilityResponse(BaseModel):
id: str
part_id: str
model_id: str
class Config:
from_attributes = True
# --- Stock Movement ---
class StockMovementCreate(BaseModel):
entity_type: str = Field(..., description="variant or part")
entity_id: str
movement_type: str = Field(..., description="Purchase, Sale, Repair, Adjustment, Return, Transfer, Damage")
quantity: int
reference_type: str = Field(..., description="Order, PurchaseOrder, RepairBooking, ManualAdjustment")
reference_id: str
class StockMovementResponse(BaseModel):
movement_id: str
entity_type: str
entity_id: str
movement_type: str
quantity: int
reference_type: str
reference_id: str
created_at: datetime
class Config:
from_attributes = True
# --- Purchase Order ---
class PurchaseOrderItemCreate(BaseModel):
part_id: str
quantity_ordered: int
unit_price: Decimal
class PurchaseOrderCreate(BaseModel):
supplier_name: str = Field(..., min_length=1, max_length=255)
items: List[PurchaseOrderItemCreate]
class PurchaseOrderUpdate(BaseModel):
status: Optional[str] = None # Draft, Ordered, Received, Cancelled
items_received: Optional[List[Dict[str, Any]]] = None # list of {"id": item_id, "quantity_received": int}
class PurchaseOrderItemResponse(BaseModel):
id: str
part_id: str
quantity_ordered: int
quantity_received: int
unit_price: Decimal
class Config:
from_attributes = True
class PurchaseOrderResponse(BaseModel):
purchase_order_id: str
po_number: str
supplier_name: str
status: str
total_amount: Decimal
created_at: datetime
items: List[PurchaseOrderItemResponse]
class Config:
from_attributes = True
# --- Device Series ---
class DeviceSeriesCreate(BaseModel):
brand_id: str
name: str = Field(..., min_length=1, max_length=100)
device_type: Optional[str] = None # laptop, tablet, mobile
sort_order: Optional[int] = 0
class DeviceSeriesUpdate(BaseModel):
brand_id: Optional[str] = None
name: Optional[str] = None
device_type: Optional[str] = None
sort_order: Optional[int] = None
is_active: Optional[bool] = None
class DeviceSeriesResponse(BaseModel):
series_id: str
brand_id: Optional[str] = None
name: str
slug: str
device_type: Optional[str] = None
sort_order: int
is_active: bool
class Config:
from_attributes = True
# --- Device Model ---
class DeviceModelCreate(BaseModel):
series_id: Optional[str] = None
brand_id: Optional[str] = None
name: str = Field(..., min_length=1, max_length=100)
device_type: Optional[str] = None # laptop, tablet, mobile
release_year: Optional[int] = None
image_url: Optional[str] = Field(None, max_length=500)
class DeviceModelUpdate(BaseModel):
series_id: Optional[str] = None
brand_id: Optional[str] = None
name: Optional[str] = None
device_type: Optional[str] = None
release_year: Optional[int] = None
image_url: Optional[str] = None
is_active: Optional[bool] = None
class DeviceModelResponse(BaseModel):
model_id: str
series_id: Optional[str] = None
brand_id: Optional[str] = None
name: str
slug: str
device_type: Optional[str] = None
full_path: str
release_year: Optional[int]
image_url: Optional[str]
is_active: bool
class Config:
from_attributes = True
# --- Service Type ---
class ServiceTypeCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
icon_url: Optional[str] = Field(None, max_length=500)
description: Optional[str] = Field(None, max_length=500)
class ServiceTypeUpdate(BaseModel):
name: Optional[str] = None
icon_url: Optional[str] = None
description: Optional[str] = None
is_active: Optional[bool] = None
class ServiceTypeResponse(BaseModel):
service_type_id: str
name: str
slug: str
icon_url: Optional[str]
description: Optional[str]
is_active: bool
class Config:
from_attributes = True
# --- Repair Service ---
class RepairServiceCreate(BaseModel):
model_id: str
service_type_id: str
description: Optional[str] = Field(None, max_length=1000)
class RepairServiceUpdate(BaseModel):
model_id: Optional[str] = None
service_type_id: Optional[str] = None
description: Optional[str] = None
class RepairServiceResponse(BaseModel):
repair_service_id: str
model_id: str
service_type_id: str
slug: str
full_path: str
description: Optional[str]
class Config:
from_attributes = True
# --- Repair Variant & BOM Part ---
class RepairVariantPartCreate(BaseModel):
part_id: str
quantity: Optional[int] = 1
class RepairVariantCreate(BaseModel):
repair_service_id: str
name: str = Field(..., min_length=1, max_length=100)
price: Decimal
cost: Optional[Decimal] = Decimal(0.0)
duration_minutes: Optional[int] = 45
warranty_days: Optional[int] = 90
parts: Optional[List[RepairVariantPartCreate]] = []
class RepairVariantUpdate(BaseModel):
repair_service_id: Optional[str] = None
name: Optional[str] = None
price: Optional[Decimal] = None
cost: Optional[Decimal] = None
duration_minutes: Optional[int] = None
warranty_days: Optional[int] = None
parts: Optional[List[RepairVariantPartCreate]] = None
status: Optional[str] = None
class RepairVariantPartResponse(BaseModel):
id: str
variant_id: str
part_id: str
quantity: int
class Config:
from_attributes = True
class RepairVariantResponse(BaseModel):
variant_id: str
repair_service_id: str
name: str
price: Decimal
cost: Decimal
duration_minutes: int
warranty_days: int
status: str
created_at: datetime
bom_parts: List[RepairVariantPartResponse] = []
class Config:
from_attributes = True
# --- Attribute Type ---
class AttributeTypeCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
code: str = Field(..., min_length=1, max_length=100)
preset_values: Optional[List[str]] = []
class AttributeTypeUpdate(BaseModel):
name: Optional[str] = None
code: Optional[str] = None
preset_values: Optional[List[str]] = None
class AttributeTypeResponse(BaseModel):
attribute_id: str
name: str
code: str
status: str
preset_values: Optional[List[str]] = []
class Config:
from_attributes = True
# --- Variant Attribute ---
class VariantAttributeCreate(BaseModel):
attribute_id: str
attribute_value: str = Field(..., min_length=1, max_length=255)
class VariantAttributeResponse(BaseModel):
id: str
variant_id: str
attribute_id: str
attribute_value: str
attribute_name: Optional[str] = None
attribute_code: Optional[str] = None
@model_validator(mode="before")
@classmethod
def resolve_attribute_type(cls, data: Any) -> Any:
if not isinstance(data, dict) and hasattr(data, "attribute_type") and data.attribute_type:
return {
"id": data.id,
"variant_id": data.variant_id,
"attribute_id": data.attribute_id,
"attribute_value": data.attribute_value,
"attribute_name": data.attribute_type.name,
"attribute_code": data.attribute_type.code
}
return data
class Config:
from_attributes = True
# --- Product Image ---
class ProductImageCreate(BaseModel):
image_url: str = Field(..., max_length=500)
alt_text: Optional[str] = Field(None, max_length=255)
sort_order: Optional[int] = 0
is_banner: Optional[bool] = False
class ProductImageResponse(BaseModel):
image_id: str
product_id: str
image_url: str
alt_text: Optional[str]
sort_order: int
is_banner: bool
class Config:
from_attributes = True
# --- Variant Image ---
class VariantImageCreate(BaseModel):
image_url: str = Field(..., max_length=500)
sort_order: Optional[int] = 0
is_primary: Optional[bool] = False
class VariantImageResponse(BaseModel):
image_id: str
variant_id: str
image_url: str
sort_order: int
is_primary: bool
class Config:
from_attributes = True
# --- Product Variant ---
class ProductVariantCreate(BaseModel):
sku: str = Field(..., min_length=1, max_length=100)
barcode: Optional[str] = Field(None, max_length=100)
price: Decimal
compare_price: Optional[Decimal] = None
cost_price: Decimal
low_stock_threshold: Optional[int] = 5
initial_stock: Optional[int] = Field(None, ge=0, description="Sellable units; create defaults to 0, update omitted means leave ledger unchanged")
attributes: List[VariantAttributeCreate]
images: Optional[List[VariantImageCreate]] = []
class ProductVariantResponse(BaseModel):
variant_id: str
product_id: str
sku: str
barcode: Optional[str]
price: Decimal
compare_price: Optional[Decimal]
cost_price: Decimal
low_stock_threshold: int
status: str
available_stock: int = 0
attributes: List[VariantAttributeResponse] = []
images: List[VariantImageResponse] = []
class Config:
from_attributes = True
# --- Product ---
class ProductCreate(BaseModel):
category_id: str
brand_id: Optional[str] = None
device_series_id: Optional[str] = None
device_model_id: Optional[str] = None
device_type: Optional[str] = None # laptop, tablet, mobile
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = Field(None, max_length=1000)
images: Optional[List[ProductImageCreate]] = []
variants: List[ProductVariantCreate]
class ProductResponse(BaseModel):
product_id: str
category_id: str
brand_id: Optional[str]
device_series_id: Optional[str]
device_model_id: Optional[str]
device_type: Optional[str] = None
name: str
slug: str
full_path: str
description: Optional[str]
status: str
created_at: Optional[datetime] = None
images: List[ProductImageResponse] = []
variants: List[ProductVariantResponse] = []
@model_validator(mode="after")
def populate_images(self) -> 'ProductResponse':
if not self.images and self.variants:
first_var = self.variants[0]
if first_var.images:
self.images = [
ProductImageResponse(
image_id=vi.image_id,
product_id=self.product_id,
image_url=vi.image_url,
alt_text=self.name,
sort_order=vi.sort_order,
is_banner=vi.is_primary
) for vi in first_var.images
]
return self
class Config:
from_attributes = True
class ProductCardResponse(BaseModel):
product_id: str
category_id: Optional[str] = None
slug: str
name: str
thumbnail_url: Optional[str] = None
price: Decimal
compare_price: Optional[Decimal] = None
discount_percent: int = 0
rating: float = 5.0
stock_count: int = 10
first_variant_id: Optional[str] = None
badge: Optional[str] = None
brand_name: Optional[str] = None
class Config:
from_attributes = True
class ProductPaginatedResponse(BaseModel):
total: int
page: int
limit: int
cursor: Optional[str] = None
products: List[ProductCardResponse]