shop product refactoring
This commit is contained in:
@@ -6,7 +6,7 @@ from .database.base import Base
|
||||
from .database.user import User
|
||||
from .database.marketplace_product import MarketplaceProduct
|
||||
from .database.stock import Stock
|
||||
from .database.shop import Shop
|
||||
from .database.vendor import Vendor
|
||||
from .database.product import Product
|
||||
from .database.marketplace_import_job import MarketplaceImportJob
|
||||
|
||||
@@ -19,7 +19,7 @@ __all__ = [
|
||||
"User",
|
||||
"MarketplaceProduct",
|
||||
"Stock",
|
||||
"Shop",
|
||||
"Vendor",
|
||||
"Product",
|
||||
"MarketplaceImportJob",
|
||||
"api", # API models namespace
|
||||
|
||||
@@ -5,7 +5,7 @@ from .base import Base
|
||||
from .user import User
|
||||
from .marketplace_product import MarketplaceProduct
|
||||
from .stock import Stock
|
||||
from .shop import Shop
|
||||
from .vendor import Vendor
|
||||
from .product import Product
|
||||
from .marketplace_import_job import MarketplaceImportJob
|
||||
|
||||
@@ -14,7 +14,7 @@ __all__ = [
|
||||
"User",
|
||||
"MarketplaceProduct",
|
||||
"Stock",
|
||||
"Shop",
|
||||
"Vendor",
|
||||
"Product",
|
||||
"MarketplaceImportJob",
|
||||
]
|
||||
|
||||
@@ -20,9 +20,9 @@ class MarketplaceImportJob(Base, TimestampMixin):
|
||||
marketplace = Column(
|
||||
String, nullable=False, index=True, default="Letzshop"
|
||||
) # Index for marketplace filtering
|
||||
shop_name = Column(String, nullable=False, index=True) # Index for shop filtering
|
||||
shop_id = Column(
|
||||
Integer, ForeignKey("shops.id"), nullable=False
|
||||
vendor_name = Column(String, nullable=False, index=True) # Index for vendor filtering
|
||||
vendor_id = Column(
|
||||
Integer, ForeignKey("vendors.id"), nullable=False
|
||||
) # Add proper foreign key
|
||||
user_id = Column(
|
||||
Integer, ForeignKey("users.id"), nullable=False
|
||||
@@ -44,19 +44,19 @@ class MarketplaceImportJob(Base, TimestampMixin):
|
||||
|
||||
# Relationship to user
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
shop = relationship("Shop", back_populates="marketplace_import_jobs")
|
||||
vendor = relationship("Vendor", back_populates="marketplace_import_jobs")
|
||||
|
||||
# Additional indexes for marketplace import job queries
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"idx_marketplace_import_user_marketplace", "user_id", "marketplace"
|
||||
), # User's marketplace imports
|
||||
Index("idx_marketplace_import_shop_status", "status"), # Shop import status
|
||||
Index("idx_marketplace_import_shop_id", "shop_id"),
|
||||
Index("idx_marketplace_import_vendor_status", "status"), # Vendor import status
|
||||
Index("idx_marketplace_import_vendor_id", "vendor_id"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<MarketplaceImportJob(id={self.id}, marketplace='{self.marketplace}', shop='{self.shop_name}', "
|
||||
f"<MarketplaceImportJob(id={self.id}, marketplace='{self.marketplace}', vendor='{self.vendor_name}', "
|
||||
f"status='{self.status}', imported={self.imported_count})>"
|
||||
)
|
||||
|
||||
@@ -55,7 +55,7 @@ class MarketplaceProduct(Base, TimestampMixin):
|
||||
marketplace = Column(
|
||||
String, index=True, nullable=True, default="Letzshop"
|
||||
) # Index for marketplace filtering
|
||||
shop_name = Column(String, index=True, nullable=True) # Index for shop filtering
|
||||
vendor_name = Column(String, index=True, nullable=True) # Index for vendor filtering
|
||||
|
||||
# Relationship to stock (one-to-many via GTIN)
|
||||
stock_entries = relationship(
|
||||
@@ -69,8 +69,8 @@ class MarketplaceProduct(Base, TimestampMixin):
|
||||
# Additional indexes for marketplace queries
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"idx_marketplace_shop", "marketplace", "shop_name"
|
||||
), # Composite index for marketplace+shop queries
|
||||
"idx_marketplace_vendor", "marketplace", "vendor_name"
|
||||
), # Composite index for marketplace+vendor queries
|
||||
Index(
|
||||
"idx_marketplace_brand", "marketplace", "brand"
|
||||
), # Composite index for marketplace+brand queries
|
||||
@@ -79,5 +79,5 @@ class MarketplaceProduct(Base, TimestampMixin):
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<MarketplaceProduct(marketplace_product_id='{self.marketplace_product_id}', title='{self.title}', marketplace='{self.marketplace}', "
|
||||
f"shop='{self.shop_name}')>"
|
||||
f"vendor='{self.vendor_name}')>"
|
||||
)
|
||||
|
||||
@@ -8,11 +8,11 @@ from sqlalchemy.orm import relationship
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
|
||||
class Product(Base):
|
||||
class Product(Base, TimestampMixin):
|
||||
__tablename__ = "products"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
shop_id = Column(Integer, ForeignKey("shops.id"), nullable=False)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
||||
marketplace_product_id = Column(Integer, ForeignKey("marketplace_products.id"), nullable=False)
|
||||
|
||||
# Shop-specific overrides (can override the main product data)
|
||||
@@ -32,17 +32,13 @@ class Product(Base):
|
||||
min_quantity = Column(Integer, default=1)
|
||||
max_quantity = Column(Integer)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
shop = relationship("Shop", back_populates="product")
|
||||
vendor = relationship("Vendor", back_populates="product")
|
||||
marketplace_product = relationship("MarketplaceProduct", back_populates="product")
|
||||
|
||||
# Constraints
|
||||
__table_args__ = (
|
||||
UniqueConstraint("shop_id", "marketplace_product_id", name="uq_product"),
|
||||
Index("idx_product_active", "shop_id", "is_active"),
|
||||
Index("idx_product_featured", "shop_id", "is_featured"),
|
||||
UniqueConstraint("vendor_id", "marketplace_product_id", name="uq_product"),
|
||||
Index("idx_product_active", "vendor_id", "is_active"),
|
||||
Index("idx_product_featured", "vendor_id", "is_featured"),
|
||||
)
|
||||
|
||||
@@ -18,10 +18,10 @@ class Stock(Base, TimestampMixin):
|
||||
location = Column(String, nullable=False, index=True)
|
||||
quantity = Column(Integer, nullable=False, default=0)
|
||||
reserved_quantity = Column(Integer, default=0) # For orders being processed
|
||||
shop_id = Column(Integer, ForeignKey("shops.id")) # Optional: shop-specific stock
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id")) # Optional: vendor -specific stock
|
||||
|
||||
# Relationships
|
||||
shop = relationship("Shop")
|
||||
vendor = relationship("Shop")
|
||||
|
||||
# Composite unique constraint to prevent duplicate GTIN-location combinations
|
||||
__table_args__ = (
|
||||
|
||||
@@ -15,7 +15,7 @@ class User(Base, TimestampMixin):
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
username = Column(String, unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
role = Column(String, nullable=False, default="user") # user, admin, shop_owner
|
||||
role = Column(String, nullable=False, default="user") # user, admin, vendor_owner
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
last_login = Column(DateTime, nullable=True)
|
||||
|
||||
@@ -23,7 +23,7 @@ class User(Base, TimestampMixin):
|
||||
marketplace_import_jobs = relationship(
|
||||
"MarketplaceImportJob", back_populates="user"
|
||||
)
|
||||
owned_shops = relationship("Shop", back_populates="owner")
|
||||
owned_vendors = relationship("Vendor", back_populates="owner")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(username='{self.username}', email='{self.email}', role='{self.role}')>"
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Index,
|
||||
Integer, String, Text, UniqueConstraint)
|
||||
from sqlalchemy import (Boolean, Column, ForeignKey, Integer, String, Text)
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
# Import Base from the central database module instead of creating a new one
|
||||
@@ -9,14 +6,14 @@ from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
|
||||
|
||||
class Shop(Base, TimestampMixin):
|
||||
__tablename__ = "shops"
|
||||
class Vendor(Base, TimestampMixin):
|
||||
__tablename__ = "vendors"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
shop_code = Column(
|
||||
vendor_code = Column(
|
||||
String, unique=True, index=True, nullable=False
|
||||
) # e.g., "TECHSTORE", "FASHIONHUB"
|
||||
shop_name = Column(String, nullable=False) # Display name
|
||||
vendor_name = Column(String, nullable=False) # Display name
|
||||
description = Column(Text)
|
||||
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
@@ -34,8 +31,8 @@ class Shop(Base, TimestampMixin):
|
||||
is_verified = Column(Boolean, default=False)
|
||||
|
||||
# Relationships
|
||||
owner = relationship("User", back_populates="owned_shops")
|
||||
product = relationship("Product", back_populates="shop")
|
||||
owner = relationship("User", back_populates="owned_vendors")
|
||||
product = relationship("Product", back_populates="vendor")
|
||||
marketplace_import_jobs = relationship(
|
||||
"MarketplaceImportJob", back_populates="shop"
|
||||
"MarketplaceImportJob", back_populates="vendor"
|
||||
)
|
||||
@@ -1,15 +1,14 @@
|
||||
# models/schemas/__init__.py
|
||||
"""API models package - Pydantic models for request/response validation."""
|
||||
|
||||
from . import auth
|
||||
# Import API model modules
|
||||
from . import base
|
||||
from . import auth
|
||||
from . import marketplace_product
|
||||
from . import stock
|
||||
from . import shop
|
||||
from . import marketplace_import_job
|
||||
from . import marketplace_product
|
||||
from . import stats
|
||||
|
||||
from . import stock
|
||||
from . import vendor
|
||||
# Common imports for convenience
|
||||
from .base import * # Base Pydantic models
|
||||
|
||||
@@ -18,7 +17,7 @@ __all__ = [
|
||||
"auth",
|
||||
"marketplace_product",
|
||||
"stock",
|
||||
"shop",
|
||||
"vendor",
|
||||
"marketplace_import_job",
|
||||
"stats",
|
||||
]
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
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")
|
||||
@@ -24,6 +27,7 @@ class UserRegister(BaseModel):
|
||||
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")
|
||||
@@ -33,6 +37,7 @@ class UserLogin(BaseModel):
|
||||
def validate_username(cls, v):
|
||||
return v.strip()
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
@@ -44,6 +49,7 @@ class UserResponse(BaseModel):
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
@@ -6,7 +6,7 @@ from pydantic import BaseModel, Field, field_validator
|
||||
class MarketplaceImportJobRequest(BaseModel):
|
||||
url: str = Field(..., description="URL to CSV file from marketplace")
|
||||
marketplace: str = Field(default="Letzshop", description="Marketplace name")
|
||||
shop_code: str = Field(..., description="Shop code to associate products with")
|
||||
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
|
||||
|
||||
@@ -18,7 +18,7 @@ class MarketplaceImportJobRequest(BaseModel):
|
||||
raise ValueError("URL must start with http:// or https://")
|
||||
return v
|
||||
|
||||
@field_validator("marketplace", "shop_code")
|
||||
@field_validator("marketplace", "vendor_code")
|
||||
@classmethod
|
||||
def validate_strings(cls, v):
|
||||
return v.strip()
|
||||
@@ -27,9 +27,9 @@ class MarketplaceImportJobResponse(BaseModel):
|
||||
job_id: int
|
||||
status: str
|
||||
marketplace: str
|
||||
shop_id: int
|
||||
shop_code: Optional[str] = None
|
||||
shop_name: str
|
||||
vendor_id: int
|
||||
vendor_code: Optional[str] = None
|
||||
vendor_name: str
|
||||
message: Optional[str] = None
|
||||
imported: Optional[int] = 0
|
||||
updated: Optional[int] = 0
|
||||
|
||||
@@ -43,7 +43,7 @@ class MarketplaceProductBase(BaseModel):
|
||||
shipping: Optional[str] = None
|
||||
currency: Optional[str] = None
|
||||
marketplace: Optional[str] = None
|
||||
shop_name: Optional[str] = None
|
||||
vendor_name: Optional[str] = None
|
||||
|
||||
class MarketplaceProductCreate(MarketplaceProductBase):
|
||||
marketplace_product_id: str = Field(..., description="MarketplaceProduct identifier")
|
||||
|
||||
@@ -6,7 +6,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from models.schemas.marketplace_product import MarketplaceProductResponse
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
marketplace_product_id: str = Field(..., description="MarketplaceProduct ID to add to shop")
|
||||
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
|
||||
@@ -21,7 +21,7 @@ class ProductCreate(BaseModel):
|
||||
class ProductResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
shop_id: int
|
||||
vendor_id: int
|
||||
marketplace_product: MarketplaceProductResponse
|
||||
product_id: Optional[str]
|
||||
price: Optional[float]
|
||||
|
||||
@@ -10,7 +10,7 @@ class StatsResponse(BaseModel):
|
||||
unique_brands: int
|
||||
unique_categories: int
|
||||
unique_marketplaces: int = 0
|
||||
unique_shops: int = 0
|
||||
unique_vendors: int = 0
|
||||
total_stock_entries: int = 0
|
||||
total_inventory_quantity: int = 0
|
||||
|
||||
@@ -18,5 +18,5 @@ class StatsResponse(BaseModel):
|
||||
class MarketplaceStatsResponse(BaseModel):
|
||||
marketplace: str
|
||||
total_products: int
|
||||
unique_shops: int
|
||||
unique_vendors: int
|
||||
unique_brands: int
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# shop.py - Keep basic format validation, remove business logic
|
||||
# vendor.py - Keep basic format validation, remove business logic
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
class ShopCreate(BaseModel):
|
||||
shop_code: str = Field(..., description="Unique shop identifier")
|
||||
shop_name: str = Field(..., description="Display name of the shop")
|
||||
description: Optional[str] = Field(None, description="Shop description")
|
||||
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
|
||||
contact_phone: Optional[str] = None
|
||||
website: Optional[str] = None
|
||||
@@ -23,8 +23,8 @@ class ShopCreate(BaseModel):
|
||||
raise ValueError("Invalid email format")
|
||||
return v.lower() if v else v
|
||||
|
||||
class ShopUpdate(BaseModel):
|
||||
shop_name: Optional[str] = None
|
||||
class VendorUpdate(BaseModel):
|
||||
vendor_name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
contact_email: Optional[str] = None
|
||||
contact_phone: Optional[str] = None
|
||||
@@ -39,11 +39,11 @@ class ShopUpdate(BaseModel):
|
||||
raise ValueError("Invalid email format")
|
||||
return v.lower() if v else v
|
||||
|
||||
class ShopResponse(BaseModel):
|
||||
class VendorResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
shop_code: str
|
||||
shop_name: str
|
||||
vendor_code: str
|
||||
vendor_name: str
|
||||
description: Optional[str]
|
||||
owner_id: int
|
||||
contact_email: Optional[str]
|
||||
@@ -56,8 +56,8 @@ class ShopResponse(BaseModel):
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class ShopListResponse(BaseModel):
|
||||
shops: List[ShopResponse]
|
||||
class VendorListResponse(BaseModel):
|
||||
vendors: List[VendorResponse]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
Reference in New Issue
Block a user