diff --git a/backup/api_models.py b/backup/api_models.py deleted file mode 100644 index 499abda0..00000000 --- a/backup/api_models.py +++ /dev/null @@ -1,385 +0,0 @@ -# models/api_models.py - Updated with Marketplace Support and Pydantic v2 -import re -from datetime import datetime -from typing import List, Optional - -from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator - - -# User Authentication Models -class UserRegister(BaseModel): - email: EmailStr = Field(..., description="Valid email address") - username: str = Field( - ..., min_length=3, max_length=50, description="Username (3-50 characters)" - ) - password: str = Field( - ..., min_length=6, description="Password (minimum 6 characters)" - ) - - @field_validator("username") - @classmethod - def validate_username(cls, v): - if not 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 - - -# NEW: Shop models -class ShopCreate(BaseModel): - shop_code: str = Field( - ..., - min_length=3, - max_length=50, - description="Unique shop code (e.g., TECHSTORE)", - ) - shop_name: str = Field( - ..., min_length=1, max_length=200, description="Display name of the shop" - ) - description: Optional[str] = Field( - None, max_length=2000, description="Shop description" - ) - contact_email: Optional[str] = None - contact_phone: Optional[str] = None - website: Optional[str] = None - business_address: Optional[str] = None - tax_number: Optional[str] = None - - @field_validator("shop_code") - def validate_shop_code(cls, v): - # Convert to uppercase and check format - v = v.upper().strip() - if not v.replace("_", "").replace("-", "").isalnum(): - raise ValueError( - "Shop code must be alphanumeric (underscores and hyphens allowed)" - ) - return v - - @field_validator("contact_email") - def validate_contact_email(cls, v): - if v and ("@" not in v or "." not in v): - raise ValueError("Invalid email format") - return v.lower() if v else v - - -class ShopUpdate(BaseModel): - shop_name: Optional[str] = Field(None, min_length=1, max_length=200) - description: Optional[str] = Field(None, max_length=2000) - contact_email: Optional[str] = None - contact_phone: Optional[str] = None - website: Optional[str] = None - business_address: Optional[str] = None - tax_number: Optional[str] = None - - @field_validator("contact_email") - def validate_contact_email(cls, v): - if v and ("@" not in v or "." not in v): - raise ValueError("Invalid email format") - return v.lower() if v else v - - -class ShopResponse(BaseModel): - model_config = ConfigDict(from_attributes=True) - - id: int - shop_code: str - shop_name: str - description: Optional[str] - owner_id: int - contact_email: Optional[str] - contact_phone: Optional[str] - website: Optional[str] - business_address: Optional[str] - tax_number: Optional[str] - is_active: bool - is_verified: bool - created_at: datetime - updated_at: datetime - - -class ShopListResponse(BaseModel): - shops: List[ShopResponse] - total: int - skip: int - limit: int - - -# Base Product Models with Marketplace Support -class ProductBase(BaseModel): - product_id: Optional[str] = None - title: Optional[str] = None - description: Optional[str] = None - link: Optional[str] = None - image_link: Optional[str] = None - availability: Optional[str] = None - price: Optional[str] = None - brand: Optional[str] = None - gtin: Optional[str] = None - mpn: Optional[str] = None - condition: Optional[str] = None - adult: Optional[str] = None - multipack: Optional[int] = None - is_bundle: Optional[str] = None - age_group: Optional[str] = None - color: Optional[str] = None - gender: Optional[str] = None - material: Optional[str] = None - pattern: Optional[str] = None - size: Optional[str] = None - size_type: Optional[str] = None - size_system: Optional[str] = None - item_group_id: Optional[str] = None - google_product_category: Optional[str] = None - product_type: Optional[str] = None - custom_label_0: Optional[str] = None - custom_label_1: Optional[str] = None - custom_label_2: Optional[str] = None - custom_label_3: Optional[str] = None - custom_label_4: Optional[str] = None - additional_image_link: Optional[str] = None - sale_price: Optional[str] = None - unit_pricing_measure: Optional[str] = None - unit_pricing_base_measure: Optional[str] = None - identifier_exists: Optional[str] = None - shipping: Optional[str] = None - currency: Optional[str] = None - # New marketplace fields - marketplace: Optional[str] = None - shop_name: Optional[str] = None - - -class ProductCreate(ProductBase): - product_id: str = Field(..., min_length=1, description="Product ID is required") - title: str = Field(..., min_length=1, description="Title is required") - - @field_validator("product_id", "title") - @classmethod - def validate_required_fields(cls, v): - if not v or not v.strip(): - raise ValueError("Field cannot be empty") - return v.strip() - - -class ProductUpdate(ProductBase): - pass - - -class ProductResponse(ProductBase): - model_config = ConfigDict(from_attributes=True) - - id: int - created_at: datetime - updated_at: datetime - - -# NEW: Shop Product models -class ShopProductCreate(BaseModel): - product_id: str = Field(..., description="Product ID to add to shop") - shop_product_id: Optional[str] = Field( - None, description="Shop's internal product ID" - ) - shop_price: Optional[float] = Field( - None, ge=0, description="Shop-specific price override" - ) - shop_sale_price: Optional[float] = Field( - None, ge=0, description="Shop-specific sale price" - ) - shop_currency: Optional[str] = Field(None, description="Shop-specific currency") - shop_availability: Optional[str] = Field( - None, description="Shop-specific availability" - ) - shop_condition: Optional[str] = Field(None, description="Shop-specific condition") - is_featured: bool = Field(False, description="Featured product flag") - min_quantity: int = Field(1, ge=1, description="Minimum order quantity") - max_quantity: Optional[int] = Field( - None, ge=1, description="Maximum order quantity" - ) - - -class ShopProductResponse(BaseModel): - model_config = ConfigDict(from_attributes=True) - - id: int - shop_id: int - product: ProductResponse - shop_product_id: Optional[str] - shop_price: Optional[float] - shop_sale_price: Optional[float] - shop_currency: Optional[str] - shop_availability: Optional[str] - shop_condition: Optional[str] - is_featured: bool - is_active: bool - min_quantity: int - max_quantity: Optional[int] - created_at: datetime - updated_at: datetime - - -# Stock Models -class StockBase(BaseModel): - gtin: str = Field(..., min_length=1, description="GTIN is required") - location: str = Field(..., min_length=1, description="Location is required") - - -class StockCreate(StockBase): - quantity: int = Field(ge=0, description="Quantity must be non-negative") - - -class StockAdd(StockBase): - quantity: int = Field(gt=0, description="Quantity to add must be positive") - - -class StockUpdate(BaseModel): - quantity: int = Field(ge=0, description="Quantity must be non-negative") - - -class StockResponse(BaseModel): - 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 - - -# Marketplace Import Models -class MarketplaceImportRequest(BaseModel): - url: str = Field(..., description="URL to CSV file from marketplace") - marketplace: str = Field( - default="Letzshop", - description="Name of the marketplace (e.g., Letzshop, Amazon, eBay)", - ) - shop_code: str = Field(..., description="Shop code to associate products with") - batch_size: Optional[int] = Field( - 1000, gt=0, le=10000, description="Batch size for processing" - ) - - @field_validator("url") - @classmethod - def validate_url(cls, v): - if not v.startswith(("http://", "https://")): - raise ValueError("URL must start with http:// or https://") - return v - - @field_validator("marketplace") - @classmethod - def validate_marketplace(cls, v): - # You can add validation for supported marketplaces here - supported_marketplaces = [ - "Letzshop", - "Amazon", - "eBay", - "Etsy", - "Shopify", - "Other", - ] - if v not in supported_marketplaces: - # For now, allow any marketplace but log it - pass - return v.strip() - - @field_validator("shop_code") - @classmethod - def validate_shop_code(cls, v): - return v.upper().strip() - - -class MarketplaceImportJobResponse(BaseModel): - job_id: int - status: str - marketplace: str - shop_id: int - shop_code: Optional[str] = None # Will be populated from shop relationship - shop_name: str - message: Optional[str] = None - imported: Optional[int] = 0 - updated: Optional[int] = 0 - total_processed: Optional[int] = 0 - error_count: Optional[int] = 0 - error_message: Optional[str] = None - created_at: Optional[datetime] = None - started_at: Optional[datetime] = None - completed_at: Optional[datetime] = None - - -# Response Models -class ProductListResponse(BaseModel): - products: List[ProductResponse] - total: int - skip: int - limit: int - - -class ProductDetailResponse(BaseModel): - product: ProductResponse - stock_info: Optional[StockSummaryResponse] = None - - -class StatsResponse(BaseModel): - total_products: int - unique_brands: int - unique_categories: int - unique_marketplaces: int = 0 - unique_shops: int = 0 - total_stock_entries: int = 0 - total_inventory_quantity: int = 0 - - -class MarketplaceStatsResponse(BaseModel): - marketplace: str - total_products: int - unique_shops: int - unique_brands: int diff --git a/backup/database_models.py b/backup/database_models.py deleted file mode 100644 index 3fa75f19..00000000 --- a/backup/database_models.py +++ /dev/null @@ -1,274 +0,0 @@ -# models/database_models.py - Updated with Marketplace Support -from datetime import datetime - -from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Index, - Integer, String, Text, UniqueConstraint) -from sqlalchemy.orm import relationship - -# Import Base from the central database module instead of creating a new one -from app.core.database import Base - - -class User(Base): - __tablename__ = "users" - - id = Column(Integer, primary_key=True, index=True) - 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 - is_active = Column(Boolean, default=True, nullable=False) - last_login = Column(DateTime, nullable=True) - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) - updated_at = Column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False - ) - - # Relationships - marketplace_import_jobs = relationship( - "MarketplaceImportJob", back_populates="user" - ) - owned_shops = relationship("Shop", back_populates="owner") - - def __repr__(self): - return f"" - - -class Shop(Base): - __tablename__ = "shops" - - id = Column(Integer, primary_key=True, index=True) - shop_code = Column( - String, unique=True, index=True, nullable=False - ) # e.g., "TECHSTORE", "FASHIONHUB" - shop_name = Column(String, nullable=False) # Display name - description = Column(Text) - owner_id = Column(Integer, ForeignKey("users.id"), nullable=False) - - # Contact information - contact_email = Column(String) - contact_phone = Column(String) - website = Column(String) - - # Business information - business_address = Column(Text) - tax_number = Column(String) - - # Status - is_active = Column(Boolean, default=True) - is_verified = Column(Boolean, default=False) - - # Timestamps - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - # Relationships - owner = relationship("User", back_populates="owned_shops") - shop_products = relationship("ShopProduct", back_populates="shop") - marketplace_import_jobs = relationship( - "MarketplaceImportJob", back_populates="shop" - ) - - -class Product(Base): - __tablename__ = "products" - - id = Column(Integer, primary_key=True, index=True) - product_id = Column(String, unique=True, index=True, nullable=False) - title = Column(String, nullable=False) - description = Column(String) - link = Column(String) - image_link = Column(String) - availability = Column(String, index=True) # Index for filtering - price = Column(String) - brand = Column(String, index=True) # Index for filtering - gtin = Column(String, index=True) # Index for stock lookups - mpn = Column(String) - condition = Column(String) - adult = Column(String) - multipack = Column(Integer) - is_bundle = Column(String) - age_group = Column(String) - color = Column(String) - gender = Column(String) - material = Column(String) - pattern = Column(String) - size = Column(String) - size_type = Column(String) - size_system = Column(String) - item_group_id = Column(String) - google_product_category = Column(String, index=True) # Index for filtering - product_type = Column(String) - custom_label_0 = Column(String) - custom_label_1 = Column(String) - custom_label_2 = Column(String) - custom_label_3 = Column(String) - custom_label_4 = Column(String) - additional_image_link = Column(String) - sale_price = Column(String) - unit_pricing_measure = Column(String) - unit_pricing_base_measure = Column(String) - identifier_exists = Column(String) - shipping = Column(String) - currency = Column(String) - - # New marketplace fields - 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 - - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) - updated_at = Column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False - ) - - # Relationship to stock (one-to-many via GTIN) - stock_entries = relationship( - "Stock", - foreign_keys="Stock.gtin", - primaryjoin="Product.gtin == Stock.gtin", - viewonly=True, - ) - shop_products = relationship("ShopProduct", back_populates="product") - - # Additional indexes for marketplace queries - __table_args__ = ( - Index( - "idx_marketplace_shop", "marketplace", "shop_name" - ), # Composite index for marketplace+shop queries - Index( - "idx_marketplace_brand", "marketplace", "brand" - ), # Composite index for marketplace+brand queries - ) - - def __repr__(self): - return ( - f"" - ) - - -class ShopProduct(Base): - __tablename__ = "shop_products" - - id = Column(Integer, primary_key=True, index=True) - shop_id = Column(Integer, ForeignKey("shops.id"), nullable=False) - product_id = Column(Integer, ForeignKey("products.id"), nullable=False) - - # Shop-specific overrides (can override the main product data) - shop_product_id = Column(String) # Shop's internal product ID - shop_price = Column(Float) # Override main product price - shop_sale_price = Column(Float) - shop_currency = Column(String) - shop_availability = Column(String) # Override availability - shop_condition = Column(String) - - # Shop-specific metadata - is_featured = Column(Boolean, default=False) - is_active = Column(Boolean, default=True) - display_order = Column(Integer, default=0) - - # Inventory management - 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="shop_products") - product = relationship("Product", back_populates="shop_products") - - # Constraints - __table_args__ = ( - UniqueConstraint("shop_id", "product_id", name="uq_shop_product"), - Index("idx_shop_product_active", "shop_id", "is_active"), - Index("idx_shop_product_featured", "shop_id", "is_featured"), - ) - - -class Stock(Base): - __tablename__ = "stock" - - id = Column(Integer, primary_key=True, index=True) - gtin = Column( - String, index=True, nullable=False - ) # Foreign key relationship would be ideal - 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 - - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) - updated_at = Column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False - ) - - # Relationships - shop = relationship("Shop") - - # Composite unique constraint to prevent duplicate GTIN-location combinations - __table_args__ = ( - UniqueConstraint("gtin", "location", name="uq_stock_gtin_location"), - Index( - "idx_stock_gtin_location", "gtin", "location" - ), # Composite index for efficient queries - ) - - def __repr__(self): - return f"" - - -class MarketplaceImportJob(Base): - __tablename__ = "marketplace_import_jobs" - - id = Column(Integer, primary_key=True, index=True) - status = Column( - String, nullable=False, default="pending" - ) # pending, processing, completed, failed, completed_with_errors - source_url = Column(String, nullable=False) - 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 - ) # Add proper foreign key - user_id = Column( - Integer, ForeignKey("users.id"), nullable=False - ) # Foreign key to users table - - # Results - imported_count = Column(Integer, default=0) - updated_count = Column(Integer, default=0) - error_count = Column(Integer, default=0) - total_processed = Column(Integer, default=0) - - # Error handling - error_message = Column(String) - - # Timestamps - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) - started_at = Column(DateTime) - completed_at = Column(DateTime) - - # Relationship to user - user = relationship("User", foreign_keys=[user_id]) - shop = relationship("Shop", 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"), - ) - - def __repr__(self): - return ( - f"" - ) diff --git a/backup/test_export.py b/tests/integration/api/v1/test_export.py similarity index 96% rename from backup/test_export.py rename to tests/integration/api/v1/test_export.py index fb3bc699..7df3bc6c 100644 --- a/backup/test_export.py +++ b/tests/integration/api/v1/test_export.py @@ -7,6 +7,9 @@ import pytest from models.database.product import Product +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.performance # for the performance test class TestExportFunctionality: def test_csv_export_basic(self, client, auth_headers, test_product): """Test basic CSV export functionality""" diff --git a/backup/test_filtering.py b/tests/integration/api/v1/test_filtering.py similarity index 98% rename from backup/test_filtering.py rename to tests/integration/api/v1/test_filtering.py index 9c5f16a7..5302fd16 100644 --- a/backup/test_filtering.py +++ b/tests/integration/api/v1/test_filtering.py @@ -4,6 +4,9 @@ import pytest from models.database.product import Product +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.products class TestFiltering: def test_product_brand_filter(self, client, auth_headers, db): """Test filtering products by brand""" diff --git a/backup/test_background_tasks.py b/tests/integration/tasks/test_background_tasks.py similarity index 99% rename from backup/test_background_tasks.py rename to tests/integration/tasks/test_background_tasks.py index 06dd3c22..20b6a927 100644 --- a/backup/test_background_tasks.py +++ b/tests/integration/tasks/test_background_tasks.py @@ -7,7 +7,9 @@ import pytest from app.tasks.background_tasks import process_marketplace_import from models.database.marketplace import MarketplaceImportJob - +@pytest.mark.integration +@pytest.mark.database +@pytest.mark.marketplace class TestBackgroundTasks: @pytest.mark.asyncio async def test_marketplace_import_success(self, db, test_user, test_shop): diff --git a/backup/test_integration.py b/tests/integration/workflows/test_integration.py similarity index 98% rename from backup/test_integration.py rename to tests/integration/workflows/test_integration.py index fc82ced3..3902a774 100644 --- a/backup/test_integration.py +++ b/tests/integration/workflows/test_integration.py @@ -1,7 +1,9 @@ # tests/test_integration.py import pytest - +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.e2e class TestIntegrationFlows: def test_full_product_workflow(self, client, auth_headers): """Test complete product creation and management workflow""" diff --git a/backup/test_middleware.py b/tests/unit/middleware/test_middleware.py similarity index 97% rename from backup/test_middleware.py rename to tests/unit/middleware/test_middleware.py index f224eb13..4e4bdcb2 100644 --- a/backup/test_middleware.py +++ b/tests/unit/middleware/test_middleware.py @@ -6,7 +6,8 @@ import pytest from middleware.auth import AuthManager from middleware.rate_limiter import RateLimiter - +@pytest.mark.unit +@pytest.mark.auth # for auth manager tests class TestRateLimiter: def test_rate_limiter_allows_requests(self): """Test rate limiter allows requests within limit"""