marketplace refactoring

This commit is contained in:
2025-10-04 13:38:10 +02:00
parent 32be301d83
commit c971674ec2
68 changed files with 1102 additions and 1128 deletions

View File

@@ -11,7 +11,7 @@ This FastAPI application provides a complete ecommerce backend solution designed
- **JWT Authentication** - Secure user registration, login, and role-based access control - **JWT Authentication** - Secure user registration, login, and role-based access control
- **Marketplace Integration** - Support for multiple marketplaces (Letzshop, Amazon, eBay, Etsy, Shopify, etc.) - **Marketplace Integration** - Support for multiple marketplaces (Letzshop, Amazon, eBay, Etsy, Shopify, etc.)
- **Multi-Shop Management** - Shop creation, ownership validation, and product catalog management - **Multi-Shop Management** - Shop creation, ownership validation, and product catalog management
- **Advanced Product Management** - GTIN validation, price processing, and comprehensive filtering - **Advanced MarketplaceProduct Management** - GTIN validation, price processing, and comprehensive filtering
- **Stock Management** - Multi-location inventory tracking with add/remove/set operations - **Stock Management** - Multi-location inventory tracking with add/remove/set operations
- **CSV Import/Export** - Background processing of marketplace CSV files with progress tracking - **CSV Import/Export** - Background processing of marketplace CSV files with progress tracking
- **Rate Limiting** - Built-in request rate limiting for API protection - **Rate Limiting** - Built-in request rate limiting for API protection
@@ -46,7 +46,7 @@ letzshop_api/
│ │ ├── main.py # API router setup │ │ ├── main.py # API router setup
│ │ └── v1/ # API version 1 routes │ │ └── v1/ # API version 1 routes
│ │ ├── auth.py # Authentication endpoints │ │ ├── auth.py # Authentication endpoints
│ │ ├── products.py # Product management │ │ ├── products.py # MarketplaceProduct management
│ │ ├── stock.py # Stock operations │ │ ├── stock.py # Stock operations
│ │ ├── shops.py # Shop management │ │ ├── shops.py # Shop management
│ │ ├── marketplace.py # Marketplace imports │ │ ├── marketplace.py # Marketplace imports
@@ -60,7 +60,7 @@ letzshop_api/
│ │ ├── base.py # Base model class and common mixins │ │ ├── base.py # Base model class and common mixins
│ │ ├── user.py # User, UserProfile models │ │ ├── user.py # User, UserProfile models
│ │ ├── auth.py # Authentication-related models │ │ ├── auth.py # Authentication-related models
│ │ ├── product.py # Product, ProductVariant models │ │ ├── product.py # MarketplaceProduct, ProductVariant models
│ │ ├── stock.py # Stock, StockMovement models │ │ ├── stock.py # Stock, StockMovement models
│ │ ├── shop.py # Shop, ShopLocation models │ │ ├── shop.py # Shop, ShopLocation models
│ │ ├── marketplace.py # Marketplace integration models │ │ ├── marketplace.py # Marketplace integration models
@@ -69,7 +69,7 @@ letzshop_api/
│ ├── __init__.py # Common imports │ ├── __init__.py # Common imports
│ ├── base.py # Base Pydantic models │ ├── base.py # Base Pydantic models
│ ├── auth.py # Login, Token, User response models │ ├── auth.py # Login, Token, User response models
│ ├── product.py # Product request/response models │ ├── product.py # MarketplaceProduct request/response models
│ ├── stock.py # Stock operation models │ ├── stock.py # Stock operation models
│ ├── shop.py # Shop management models │ ├── shop.py # Shop management models
│ ├── marketplace.py # Marketplace import models │ ├── marketplace.py # Marketplace import models
@@ -285,23 +285,23 @@ curl -X POST "http://localhost:8000/api/v1/auth/login" \
```bash ```bash
# Get token from login response and use in subsequent requests # Get token from login response and use in subsequent requests
curl -X GET "http://localhost:8000/api/v1/product" \ curl -X GET "http://localhost:8000/api/v1/marketplace/product" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" -H "Authorization: Bearer YOUR_JWT_TOKEN"
``` ```
## Core Features ## Core Features
### Product Management ### MarketplaceProduct Management
#### Create a Product #### Create a MarketplaceProduct
```bash ```bash
curl -X POST "http://localhost:8000/api/v1/product" \ curl -X POST "http://localhost:8000/api/v1/marketplace/product" \
-H "Authorization: Bearer YOUR_TOKEN" \ -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"product_id": "PROD001", "marketplace_product_id": "PROD001",
"title": "Amazing Product", "title": "Amazing MarketplaceProduct",
"description": "An amazing product description", "description": "An amazing product description",
"price": "29.99", "price": "29.99",
"currency": "EUR", "currency": "EUR",
@@ -317,15 +317,15 @@ curl -X POST "http://localhost:8000/api/v1/product" \
```bash ```bash
# Get all products # Get all products
curl -X GET "http://localhost:8000/api/v1/product" \ curl -X GET "http://localhost:8000/api/v1/marketplace/product" \
-H "Authorization: Bearer YOUR_TOKEN" -H "Authorization: Bearer YOUR_TOKEN"
# Filter by marketplace # Filter by marketplace
curl -X GET "http://localhost:8000/api/v1/product?marketplace=Amazon&limit=50" \ curl -X GET "http://localhost:8000/api/v1/marketplace/product?marketplace=Amazon&limit=50" \
-H "Authorization: Bearer YOUR_TOKEN" -H "Authorization: Bearer YOUR_TOKEN"
# Search products # Search products
curl -X GET "http://localhost:8000/api/v1/product?search=Amazing&brand=BrandName" \ curl -X GET "http://localhost:8000/api/v1/marketplace/product?search=Amazing&brand=BrandName" \
-H "Authorization: Bearer YOUR_TOKEN" -H "Authorization: Bearer YOUR_TOKEN"
``` ```
@@ -393,12 +393,12 @@ curl -X GET "http://localhost:8000/api/v1/marketplace/import-status/1" \
```bash ```bash
# Export all products # Export all products
curl -X GET "http://localhost:8000/api/v1/product/export-csv" \ curl -X GET "http://localhost:8000/api/v1/marketplace/product" \
-H "Authorization: Bearer YOUR_TOKEN" \ -H "Authorization: Bearer YOUR_TOKEN" \
-o products_export.csv -o products_export.csv
# Export with filters # Export with filters
curl -X GET "http://localhost:8000/api/v1/product/export-csv?marketplace=Amazon&shop_name=MyShop" \ curl -X GET "http://localhost:8000/api/v1/marketplace/product?marketplace=Amazon&shop_name=MyShop" \
-H "Authorization: Bearer YOUR_TOKEN" \ -H "Authorization: Bearer YOUR_TOKEN" \
-o amazon_products.csv -o amazon_products.csv
``` ```
@@ -409,18 +409,18 @@ The system supports CSV imports with the following headers:
### Required Fields ### Required Fields
- `product_id` - Unique product identifier - `product_id` - Unique product identifier
- `title` - Product title - `title` - MarketplaceProduct title
### Optional Fields ### Optional Fields
- `description` - Product description - `description` - MarketplaceProduct description
- `link` - Product URL - `link` - MarketplaceProduct URL
- `image_link` - Product image URL - `image_link` - MarketplaceProduct image URL
- `availability` - Stock availability (in stock, out of stock, preorder) - `availability` - Stock availability (in stock, out of stock, preorder)
- `price` - Product price - `price` - MarketplaceProduct price
- `currency` - Price currency (EUR, USD, etc.) - `currency` - Price currency (EUR, USD, etc.)
- `brand` - Product brand - `brand` - MarketplaceProduct brand
- `gtin` - Global Trade Item Number (EAN/UPC) - `gtin` - Global Trade Item Number (EAN/UPC)
- `google_product_category` - Product category - `google_product_category` - MarketplaceProduct category
- `marketplace` - Source marketplace - `marketplace` - Source marketplace
- `shop_name` - Shop/seller name - `shop_name` - Shop/seller name
@@ -438,13 +438,16 @@ PROD002,"Super Gadget","A fantastic gadget",19.99,EUR,GadgetInc,9876543210987,Am
- `POST /api/v1/auth/login` - Login user - `POST /api/v1/auth/login` - Login user
- `GET /api/v1/auth/me` - Get current user info - `GET /api/v1/auth/me` - Get current user info
### Product Endpoints ### Marketplace Endpoints
- `GET /api/v1/product` - List products with filtering - `GET /api/v1/marketplace/product` - List marketplace products with filtering
- `POST /api/v1/product` - Create new product - `POST /api/v1/marketplace/product` - Create new marketplace product
- `GET /api/v1/product/{product_id}` - Get specific product - `GET /api/v1/marketplace/product/{product_id}` - Get specific marketplace product
- `PUT /api/v1/product/{product_id}` - Update product - `PUT /api/v1/marketplace/product/{product_id}` - Update marketplace product
- `DELETE /api/v1/product/{product_id}` - Delete product - `DELETE /api/v1/marketplace/product/{product_id}` - Delete marketplace product
- `POST /api/v1/marketplace/import-product` - Start CSV import
- `GET /api/v1/marketplace/import-status/{job_id}` - Check import status
- `GET /api/v1/marketplace/import-jobs` - List import jobs
-
### Stock Endpoints ### Stock Endpoints
- `POST /api/v1/stock` - Set stock quantity - `POST /api/v1/stock` - Set stock quantity
- `POST /api/v1/stock/add` - Add to stock - `POST /api/v1/stock/add` - Add to stock
@@ -458,11 +461,6 @@ PROD002,"Super Gadget","A fantastic gadget",19.99,EUR,GadgetInc,9876543210987,Am
- `GET /api/v1/shop` - List shops - `GET /api/v1/shop` - List shops
- `GET /api/v1/shop/{shop_code}` - Get specific shop - `GET /api/v1/shop/{shop_code}` - Get specific shop
### Marketplace Endpoints
- `POST /api/v1/marketplace/import-product` - Start CSV import
- `GET /api/v1/marketplace/import-status/{job_id}` - Check import status
- `GET /api/v1/marketplace/import-jobs` - List import jobs
### Statistics Endpoints ### Statistics Endpoints
- `GET /api/v1/stats` - Get general statistics - `GET /api/v1/stats` - Get general statistics
- `GET /api/v1/stats/marketplace-stats` - Get marketplace statistics - `GET /api/v1/stats/marketplace-stats` - Get marketplace statistics
@@ -529,7 +527,7 @@ make docs-help
### Core Tables ### Core Tables
- **users** - User accounts and authentication - **users** - User accounts and authentication
- **products** - Product catalog with marketplace info - **products** - MarketplaceProduct catalog with marketplace info
- **stock** - Inventory tracking by location and GTIN - **stock** - Inventory tracking by location and GTIN
- **shops** - Shop/seller information - **shops** - Shop/seller information
- **shop_products** - Shop-specific product settings - **shop_products** - Shop-specific product settings
@@ -583,7 +581,7 @@ make test-slow
# Authentication tests # Authentication tests
make test-auth make test-auth
# Product management tests # MarketplaceProduct management tests
make test-products make test-products
# Stock management tests # Stock management tests

View File

@@ -23,10 +23,10 @@ except ImportError as e:
print(f" ✗ User model failed: {e}") print(f" ✗ User model failed: {e}")
try: try:
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
print(" ✓ Product model imported") print("MarketplaceProduct model imported")
except ImportError as e: except ImportError as e:
print(f" ✗ Product model failed: {e}") print(f"MarketplaceProduct model failed: {e}")
try: try:
from models.database.stock import Stock from models.database.stock import Stock
@@ -41,7 +41,7 @@ except ImportError as e:
print(f" ✗ Shop models failed: {e}") print(f" ✗ Shop models failed: {e}")
try: try:
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
print(" ✓ Marketplace model imported") print(" ✓ Marketplace model imported")
except ImportError as e: except ImportError as e:
print(f" ✗ Marketplace model failed: {e}") print(f" ✗ Marketplace model failed: {e}")

View File

@@ -22,7 +22,7 @@ def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('products', op.create_table('products',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.String(), nullable=False), sa.Column('marketplace_product_id', sa.String(), nullable=False),
sa.Column('title', sa.String(), nullable=False), sa.Column('title', sa.String(), nullable=False),
sa.Column('description', sa.String(), nullable=True), sa.Column('description', sa.String(), nullable=True),
sa.Column('link', sa.String(), nullable=True), sa.Column('link', sa.String(), nullable=True),
@@ -73,7 +73,7 @@ def upgrade() -> None:
op.create_index(op.f('ix_products_gtin'), 'products', ['gtin'], unique=False) op.create_index(op.f('ix_products_gtin'), 'products', ['gtin'], unique=False)
op.create_index(op.f('ix_products_id'), 'products', ['id'], unique=False) op.create_index(op.f('ix_products_id'), 'products', ['id'], unique=False)
op.create_index(op.f('ix_products_marketplace'), 'products', ['marketplace'], unique=False) op.create_index(op.f('ix_products_marketplace'), 'products', ['marketplace'], unique=False)
op.create_index(op.f('ix_products_product_id'), 'products', ['product_id'], unique=True) op.create_index(op.f('ix_products_product_id'), 'products', ['marketplace_product_id'], unique=True)
op.create_index(op.f('ix_products_shop_name'), 'products', ['shop_name'], unique=False) op.create_index(op.f('ix_products_shop_name'), 'products', ['shop_name'], unique=False)
op.create_table('users', op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
@@ -139,7 +139,7 @@ def upgrade() -> None:
op.create_table('shop_products', op.create_table('shop_products',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('shop_id', sa.Integer(), nullable=False), sa.Column('shop_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False), sa.Column('marketplace_product_id', sa.Integer(), nullable=False),
sa.Column('shop_product_id', sa.String(), nullable=True), sa.Column('shop_product_id', sa.String(), nullable=True),
sa.Column('shop_price', sa.Float(), nullable=True), sa.Column('shop_price', sa.Float(), nullable=True),
sa.Column('shop_sale_price', sa.Float(), nullable=True), sa.Column('shop_sale_price', sa.Float(), nullable=True),
@@ -153,10 +153,10 @@ def upgrade() -> None:
sa.Column('max_quantity', sa.Integer(), nullable=True), sa.Column('max_quantity', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ), sa.ForeignKeyConstraint(['marketplace_product_id'], ['products.id'], ),
sa.ForeignKeyConstraint(['shop_id'], ['shops.id'], ), sa.ForeignKeyConstraint(['shop_id'], ['shops.id'], ),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('shop_id', 'product_id', name='uq_shop_product') sa.UniqueConstraint('shop_id', 'marketplace_product_id', name='uq_shop_product')
) )
op.create_index('idx_shop_product_active', 'shop_products', ['shop_id', 'is_active'], unique=False) op.create_index('idx_shop_product_active', 'shop_products', ['shop_id', 'is_active'], unique=False)
op.create_index('idx_shop_product_featured', 'shop_products', ['shop_id', 'is_featured'], unique=False) op.create_index('idx_shop_product_featured', 'shop_products', ['shop_id', 'is_featured'], unique=False)

View File

@@ -9,7 +9,7 @@ This module provides classes and functions for:
from fastapi import APIRouter from fastapi import APIRouter
from app.api.v1 import admin, auth, marketplace, product, shop, stats, stock from app.api.v1 import admin, auth, marketplace, shop, stats, stock
api_router = APIRouter() api_router = APIRouter()
@@ -17,7 +17,6 @@ api_router = APIRouter()
api_router.include_router(admin.router, tags=["admin"]) api_router.include_router(admin.router, tags=["admin"])
api_router.include_router(auth.router, tags=["authentication"]) api_router.include_router(auth.router, tags=["authentication"])
api_router.include_router(marketplace.router, tags=["marketplace"]) api_router.include_router(marketplace.router, tags=["marketplace"])
api_router.include_router(product.router, tags=["product"])
api_router.include_router(shop.router, tags=["shop"]) api_router.include_router(shop.router, tags=["shop"])
api_router.include_router(stats.router, tags=["statistics"]) api_router.include_router(stats.router, tags=["statistics"])
api_router.include_router(stock.router, tags=["stock"]) api_router.include_router(stock.router, tags=["stock"])

View File

@@ -19,7 +19,7 @@ from app.api.deps import get_current_admin_user
from app.core.database import get_db from app.core.database import get_db
from app.services.admin_service import admin_service from app.services.admin_service import admin_service
from models.schemas.auth import UserResponse from models.schemas.auth import UserResponse
from models.schemas.marketplace import MarketplaceImportJobResponse from models.schemas.marketplace_import_job import MarketplaceImportJobResponse
from models.schemas.shop import ShopListResponse from models.schemas.shop import ShopListResponse
from models.database.user import User from models.database.user import User

View File

@@ -1,9 +1,12 @@
# app/api/v1/marketplace.py # app/api/v1/marketplace_products.py
""" """
Marketplace endpoints - simplified with service-level exception handling. MarketplaceProduct endpoints - simplified with service-level exception handling.
This module provides classes and functions for: This module provides classes and functions for:
- Product import from marketplace CSV files - MarketplaceProduct CRUD operations with marketplace support
- Advanced product filtering and search
- MarketplaceProduct export functionality
- MarketplaceProduct import from marketplace CSV files
- Import job management and monitoring - Import job management and monitoring
- Import statistics and job cancellation - Import statistics and job cancellation
""" """
@@ -12,25 +15,151 @@ import logging
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, BackgroundTasks, Depends, Query from fastapi import APIRouter, BackgroundTasks, Depends, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_user from app.api.deps import get_current_user
from app.core.database import get_db from app.core.database import get_db
from app.services.marketplace_service import marketplace_service from app.services.marketplace_import_job_service import marketplace_import_job_service
from app.services.marketplace_product_service import marketplace_product_service
from app.tasks.background_tasks import process_marketplace_import from app.tasks.background_tasks import process_marketplace_import
from middleware.decorators import rate_limit from middleware.decorators import rate_limit
from models.schemas.marketplace import (MarketplaceImportJobResponse, from models.schemas.marketplace_import_job import (MarketplaceImportJobResponse,
MarketplaceImportRequest) MarketplaceImportJobRequest)
from models.schemas.marketplace_product import (MarketplaceProductCreate, MarketplaceProductDetailResponse,
MarketplaceProductListResponse, MarketplaceProductResponse,
MarketplaceProductUpdate)
from models.database.user import User from models.database.user import User
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ============================================================================
# PRODUCT ENDPOINTS
# ============================================================================
@router.get("/marketplace/product/export-csv")
async def export_csv(
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Export products as CSV with streaming and marketplace filtering (Protected)."""
def generate_csv():
return marketplace_product_service.generate_csv_export(
db=db, marketplace=marketplace, shop_name=shop_name
)
filename = "marketplace_products_export"
if marketplace:
filename += f"_{marketplace}"
if shop_name:
filename += f"_{shop_name}"
filename += ".csv"
return StreamingResponse(
generate_csv(),
media_type="text/csv",
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
@router.get("/marketplace/product", response_model=MarketplaceProductListResponse)
def get_products(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
brand: Optional[str] = Query(None),
category: Optional[str] = Query(None),
availability: Optional[str] = Query(None),
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
search: Optional[str] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get products with advanced filtering including marketplace and shop (Protected)."""
products, total = marketplace_product_service.get_products_with_filters(
db=db,
skip=skip,
limit=limit,
brand=brand,
category=category,
availability=availability,
marketplace=marketplace,
shop_name=shop_name,
search=search,
)
return MarketplaceProductListResponse(
products=products, total=total, skip=skip, limit=limit
)
@router.post("/marketplace/product", response_model=MarketplaceProductResponse)
def create_product(
product: MarketplaceProductCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Create a new product with validation and marketplace support (Protected)."""
logger.info(f"Starting product creation for ID: {product.marketplace_product_id}")
db_product = marketplace_product_service.create_product(db, product)
logger.info("MarketplaceProduct created successfully")
return db_product
@router.get("/marketplace/product/{marketplace_product_id}", response_model=MarketplaceProductDetailResponse)
def get_product(
marketplace_product_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get product with stock information (Protected)."""
product = marketplace_product_service.get_product_by_id_or_raise(db, marketplace_product_id)
# Get stock information if GTIN exists
stock_info = None
if product.gtin:
stock_info = marketplace_product_service.get_stock_info(db, product.gtin)
return MarketplaceProductDetailResponse(product=product, stock_info=stock_info)
@router.put("/marketplace/product/{marketplace_product_id}", response_model=MarketplaceProductResponse)
def update_product(
marketplace_product_id: str,
product_update: MarketplaceProductUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Update product with validation and marketplace support (Protected)."""
updated_product = marketplace_product_service.update_product(db, marketplace_product_id, product_update)
return updated_product
@router.delete("/marketplace/product/{marketplace_product_id}")
def delete_product(
marketplace_product_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Delete product and associated stock (Protected)."""
marketplace_product_service.delete_product(db, marketplace_product_id)
return {"message": "MarketplaceProduct and associated stock deleted successfully"}
# ============================================================================
# IMPORT JOB ENDPOINTS
# ============================================================================
@router.post("/marketplace/import-product", response_model=MarketplaceImportJobResponse) @router.post("/marketplace/import-product", response_model=MarketplaceImportJobResponse)
@rate_limit(max_requests=10, window_seconds=3600) # Limit marketplace imports @rate_limit(max_requests=10, window_seconds=3600) # Limit marketplace imports
async def import_products_from_marketplace( async def import_products_from_marketplace(
request: MarketplaceImportRequest, request: MarketplaceImportJobRequest,
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
@@ -41,7 +170,7 @@ async def import_products_from_marketplace(
) )
# Create import job through service # Create import job through service
import_job = marketplace_service.create_import_job(db, request, current_user) import_job = marketplace_import_job_service.create_import_job(db, request, current_user)
# Process in background # Process in background
background_tasks.add_task( background_tasks.add_task(
@@ -74,8 +203,8 @@ def get_marketplace_import_status(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get status of marketplace import job (Protected).""" """Get status of marketplace import job (Protected)."""
job = marketplace_service.get_import_job_by_id(db, job_id, current_user) job = marketplace_import_job_service.get_import_job_by_id(db, job_id, current_user)
return marketplace_service.convert_to_response_model(job) return marketplace_import_job_service.convert_to_response_model(job)
@router.get( @router.get(
@@ -90,7 +219,7 @@ def get_marketplace_import_jobs(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Get marketplace import jobs with filtering (Protected).""" """Get marketplace import jobs with filtering (Protected)."""
jobs = marketplace_service.get_import_jobs( jobs = marketplace_import_job_service.get_import_jobs(
db=db, db=db,
user=current_user, user=current_user,
marketplace=marketplace, marketplace=marketplace,
@@ -99,7 +228,7 @@ def get_marketplace_import_jobs(
limit=limit, limit=limit,
) )
return [marketplace_service.convert_to_response_model(job) for job in jobs] return [marketplace_import_job_service.convert_to_response_model(job) for job in jobs]
@router.get("/marketplace/marketplace-import-stats") @router.get("/marketplace/marketplace-import-stats")
@@ -107,7 +236,7 @@ def get_marketplace_import_stats(
db: Session = Depends(get_db), current_user: User = Depends(get_current_user) db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
): ):
"""Get statistics about marketplace import jobs (Protected).""" """Get statistics about marketplace import jobs (Protected)."""
return marketplace_service.get_job_stats(db, current_user) return marketplace_import_job_service.get_job_stats(db, current_user)
@router.put( @router.put(
@@ -120,8 +249,8 @@ def cancel_marketplace_import_job(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Cancel a pending or running marketplace import job (Protected).""" """Cancel a pending or running marketplace import job (Protected)."""
job = marketplace_service.cancel_import_job(db, job_id, current_user) job = marketplace_import_job_service.cancel_import_job(db, job_id, current_user)
return marketplace_service.convert_to_response_model(job) return marketplace_import_job_service.convert_to_response_model(job)
@router.delete("/marketplace/import-jobs/{job_id}") @router.delete("/marketplace/import-jobs/{job_id}")
@@ -131,5 +260,5 @@ def delete_marketplace_import_job(
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
"""Delete a completed marketplace import job (Protected).""" """Delete a completed marketplace import job (Protected)."""
marketplace_service.delete_import_job(db, job_id, current_user) marketplace_import_job_service.delete_import_job(db, job_id, current_user)
return {"message": "Marketplace import job deleted successfully"} return {"message": "Marketplace import job deleted successfully"}

View File

@@ -1,142 +0,0 @@
# app/api/v1/product.py
"""
Product endpoints - simplified with service-level exception handling.
This module provides classes and functions for:
- Product CRUD operations with marketplace support
- Advanced product filtering and search
- Product export functionality
"""
import logging
from typing import Optional
from fastapi import APIRouter, Depends, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.api.deps import get_current_user
from app.core.database import get_db
from app.services.product_service import product_service
from models.schemas.product import (ProductCreate, ProductDetailResponse,
ProductListResponse, ProductResponse,
ProductUpdate)
from models.database.user import User
router = APIRouter()
logger = logging.getLogger(__name__)
@router.get("/product/export-csv")
async def export_csv(
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Export products as CSV with streaming and marketplace filtering (Protected)."""
def generate_csv():
return product_service.generate_csv_export(
db=db, marketplace=marketplace, shop_name=shop_name
)
filename = "products_export"
if marketplace:
filename += f"_{marketplace}"
if shop_name:
filename += f"_{shop_name}"
filename += ".csv"
return StreamingResponse(
generate_csv(),
media_type="text/csv",
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
@router.get("/product", response_model=ProductListResponse)
def get_products(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
brand: Optional[str] = Query(None),
category: Optional[str] = Query(None),
availability: Optional[str] = Query(None),
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
search: Optional[str] = Query(None),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get products with advanced filtering including marketplace and shop (Protected)."""
products, total = product_service.get_products_with_filters(
db=db,
skip=skip,
limit=limit,
brand=brand,
category=category,
availability=availability,
marketplace=marketplace,
shop_name=shop_name,
search=search,
)
return ProductListResponse(
products=products, total=total, skip=skip, limit=limit
)
@router.post("/product", response_model=ProductResponse)
def create_product(
product: ProductCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Create a new product with validation and marketplace support (Protected)."""
logger.info(f"Starting product creation for ID: {product.product_id}")
db_product = product_service.create_product(db, product)
logger.info("Product created successfully")
return db_product
@router.get("/product/{product_id}", response_model=ProductDetailResponse)
def get_product(
product_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get product with stock information (Protected)."""
product = product_service.get_product_by_id_or_raise(db, product_id)
# Get stock information if GTIN exists
stock_info = None
if product.gtin:
stock_info = product_service.get_stock_info(db, product.gtin)
return ProductDetailResponse(product=product, stock_info=stock_info)
@router.put("/product/{product_id}", response_model=ProductResponse)
def update_product(
product_id: str,
product_update: ProductUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Update product with validation and marketplace support (Protected)."""
updated_product = product_service.update_product(db, product_id, product_update)
return updated_product
@router.delete("/product/{product_id}")
def delete_product(
product_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Delete product and associated stock (Protected)."""
product_service.delete_product(db, product_id)
return {"message": "Product and associated stock deleted successfully"}

View File

@@ -29,13 +29,13 @@ from .auth import (
UserAlreadyExistsException UserAlreadyExistsException
) )
from .product import ( from .marketplace_product import (
ProductNotFoundException, MarketplaceProductNotFoundException,
ProductAlreadyExistsException, MarketplaceProductAlreadyExistsException,
InvalidProductDataException, InvalidMarketplaceProductDataException,
ProductValidationException, MarketplaceProductValidationException,
InvalidGTINException, InvalidGTINException,
ProductCSVImportException, MarketplaceProductCSVImportException,
) )
from .stock import ( from .stock import (
@@ -61,7 +61,7 @@ from .shop import (
ShopValidationException, ShopValidationException,
) )
from .marketplace import ( from .marketplace_import_job import (
MarketplaceImportException, MarketplaceImportException,
ImportJobNotFoundException, ImportJobNotFoundException,
ImportJobNotOwnedException, ImportJobNotOwnedException,
@@ -107,13 +107,13 @@ __all__ = [
"AdminRequiredException", "AdminRequiredException",
"UserAlreadyExistsException", "UserAlreadyExistsException",
# Product exceptions # MarketplaceProduct exceptions
"ProductNotFoundException", "MarketplaceProductNotFoundException",
"ProductAlreadyExistsException", "MarketplaceProductAlreadyExistsException",
"InvalidProductDataException", "InvalidMarketplaceProductDataException",
"ProductValidationException", "MarketplaceProductValidationException",
"InvalidGTINException", "InvalidGTINException",
"ProductCSVImportException", "MarketplaceProductCSVImportException",
# Stock exceptions # Stock exceptions
"StockNotFoundException", "StockNotFoundException",
@@ -136,7 +136,7 @@ __all__ = [
"MaxShopsReachedException", "MaxShopsReachedException",
"ShopValidationException", "ShopValidationException",
# Marketplace exceptions # Marketplace import exceptions
"MarketplaceImportException", "MarketplaceImportException",
"ImportJobNotFoundException", "ImportJobNotFoundException",
"ImportJobNotOwnedException", "ImportJobNotOwnedException",

View File

@@ -1,4 +1,4 @@
# app/exceptions/marketplace.py # app/exceptions/marketplace_import_job.py
""" """
Marketplace import specific exceptions. Marketplace import specific exceptions.
""" """

View File

@@ -1,36 +1,36 @@
# app/exceptions/product.py # app/exceptions/marketplace_products.py
""" """
Product management specific exceptions. MarketplaceProduct management specific exceptions.
""" """
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from .base import ResourceNotFoundException, ConflictException, ValidationException, BusinessLogicException from .base import ResourceNotFoundException, ConflictException, ValidationException, BusinessLogicException
class ProductNotFoundException(ResourceNotFoundException): class MarketplaceProductNotFoundException(ResourceNotFoundException):
"""Raised when a product is not found.""" """Raised when a product is not found."""
def __init__(self, product_id: str): def __init__(self, marketplace_product_id: str):
super().__init__( super().__init__(
resource_type="Product", resource_type="MarketplaceProduct",
identifier=product_id, identifier=marketplace_product_id,
message=f"Product with ID '{product_id}' not found", message=f"MarketplaceProduct with ID '{marketplace_product_id}' not found",
error_code="PRODUCT_NOT_FOUND", error_code="PRODUCT_NOT_FOUND",
) )
class ProductAlreadyExistsException(ConflictException): class MarketplaceProductAlreadyExistsException(ConflictException):
"""Raised when trying to create a product that already exists.""" """Raised when trying to create a product that already exists."""
def __init__(self, product_id: str): def __init__(self, marketplace_product_id: str):
super().__init__( super().__init__(
message=f"Product with ID '{product_id}' already exists", message=f"MarketplaceProduct with ID '{marketplace_product_id}' already exists",
error_code="PRODUCT_ALREADY_EXISTS", error_code="PRODUCT_ALREADY_EXISTS",
details={"product_id": product_id}, details={"marketplace_product_id": marketplace_product_id},
) )
class InvalidProductDataException(ValidationException): class InvalidMarketplaceProductDataException(ValidationException):
"""Raised when product data is invalid.""" """Raised when product data is invalid."""
def __init__( def __init__(
@@ -47,7 +47,7 @@ class InvalidProductDataException(ValidationException):
self.error_code = "INVALID_PRODUCT_DATA" self.error_code = "INVALID_PRODUCT_DATA"
class ProductValidationException(ValidationException): class MarketplaceProductValidationException(ValidationException):
"""Raised when product validation fails.""" """Raised when product validation fails."""
def __init__( def __init__(
@@ -80,12 +80,12 @@ class InvalidGTINException(ValidationException):
self.error_code = "INVALID_GTIN" self.error_code = "INVALID_GTIN"
class ProductCSVImportException(BusinessLogicException): class MarketplaceProductCSVImportException(BusinessLogicException):
"""Raised when product CSV import fails.""" """Raised when product CSV import fails."""
def __init__( def __init__(
self, self,
message: str = "Product CSV import failed", message: str = "MarketplaceProduct CSV import failed",
row_number: Optional[int] = None, row_number: Optional[int] = None,
errors: Optional[Dict[str, Any]] = None, errors: Optional[Dict[str, Any]] = None,
): ):

View File

@@ -98,13 +98,13 @@ class InvalidShopDataException(ValidationException):
class ShopProductAlreadyExistsException(ConflictException): class ShopProductAlreadyExistsException(ConflictException):
"""Raised when trying to add a product that already exists in shop.""" """Raised when trying to add a product that already exists in shop."""
def __init__(self, shop_code: str, product_id: str): def __init__(self, shop_code: str, marketplace_product_id: str):
super().__init__( super().__init__(
message=f"Product '{product_id}' already exists in shop '{shop_code}'", message=f"MarketplaceProduct '{marketplace_product_id}' already exists in shop '{shop_code}'",
error_code="SHOP_PRODUCT_ALREADY_EXISTS", error_code="SHOP_PRODUCT_ALREADY_EXISTS",
details={ details={
"shop_code": shop_code, "shop_code": shop_code,
"product_id": product_id, "marketplace_product_id": marketplace_product_id,
}, },
) )
@@ -112,11 +112,11 @@ class ShopProductAlreadyExistsException(ConflictException):
class ShopProductNotFoundException(ResourceNotFoundException): class ShopProductNotFoundException(ResourceNotFoundException):
"""Raised when a shop product relationship is not found.""" """Raised when a shop product relationship is not found."""
def __init__(self, shop_code: str, product_id: str): def __init__(self, shop_code: str, marketplace_product_id: str):
super().__init__( super().__init__(
resource_type="ShopProduct", resource_type="ShopProduct",
identifier=f"{shop_code}/{product_id}", identifier=f"{shop_code}/{marketplace_product_id}",
message=f"Product '{product_id}' not found in shop '{shop_code}'", message=f"MarketplaceProduct '{marketplace_product_id}' not found in shop '{shop_code}'",
error_code="SHOP_PRODUCT_NOT_FOUND", error_code="SHOP_PRODUCT_NOT_FOUND",
) )

View File

@@ -22,8 +22,8 @@ from app.exceptions import (
ShopVerificationException, ShopVerificationException,
AdminOperationException, AdminOperationException,
) )
from models.schemas.marketplace import MarketplaceImportJobResponse from models.schemas.marketplace_import_job import MarketplaceImportJobResponse
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.shop import Shop from models.database.shop import Shop
from models.database.user import User from models.database.user import User
@@ -338,5 +338,5 @@ class AdminService:
) )
# Create service instance following the same pattern as product_service # Create service instance following the same pattern as marketplace_product_service
admin_service = AdminService() admin_service = AdminService()

View File

@@ -1,4 +1,4 @@
# app/services/marketplace_service.py # app/services/marketplace_import_job_service.py
""" """
Marketplace service for managing import jobs and marketplace integrations. Marketplace service for managing import jobs and marketplace integrations.
@@ -24,16 +24,16 @@ from app.exceptions import (
ImportJobCannotBeDeletedException, ImportJobCannotBeDeletedException,
ValidationException, ValidationException,
) )
from models.schemas.marketplace import (MarketplaceImportJobResponse, from models.schemas.marketplace_import_job import (MarketplaceImportJobResponse,
MarketplaceImportRequest) MarketplaceImportJobRequest)
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.shop import Shop from models.database.shop import Shop
from models.database.user import User from models.database.user import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class MarketplaceService: class MarketplaceImportJobService:
"""Service class for Marketplace operations following the application's service pattern.""" """Service class for Marketplace operations following the application's service pattern."""
def validate_shop_access(self, db: Session, shop_code: str, user: User) -> Shop: def validate_shop_access(self, db: Session, shop_code: str, user: User) -> Shop:
@@ -76,7 +76,7 @@ class MarketplaceService:
raise ValidationException("Failed to validate shop access") raise ValidationException("Failed to validate shop access")
def create_import_job( def create_import_job(
self, db: Session, request: MarketplaceImportRequest, user: User self, db: Session, request: MarketplaceImportJobRequest, user: User
) -> MarketplaceImportJob: ) -> MarketplaceImportJob:
""" """
Create a new marketplace import job. Create a new marketplace import job.
@@ -414,4 +414,4 @@ class MarketplaceService:
# Create service instance # Create service instance
marketplace_service = MarketplaceService() marketplace_import_job_service = MarketplaceImportJobService()

View File

@@ -1,9 +1,9 @@
# app/services/product_service.py # app/services/marketplace_product_service.py
""" """
Product service for managing product operations and data processing. MarketplaceProduct service for managing product operations and data processing.
This module provides classes and functions for: This module provides classes and functions for:
- Product CRUD operations with validation - MarketplaceProduct CRUD operations with validation
- Advanced product filtering and search - Advanced product filtering and search
- Stock information integration - Stock information integration
- CSV export functionality - CSV export functionality
@@ -18,37 +18,38 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.exceptions import ( from app.exceptions import (
ProductNotFoundException, MarketplaceProductNotFoundException,
ProductAlreadyExistsException, MarketplaceProductAlreadyExistsException,
InvalidProductDataException, InvalidMarketplaceProductDataException,
ProductValidationException, MarketplaceProductValidationException,
ValidationException, ValidationException,
) )
from models.schemas.product import ProductCreate, ProductUpdate from app.services.marketplace_import_job_service import marketplace_import_job_service
from models.schemas.marketplace_product import MarketplaceProductCreate, MarketplaceProductUpdate
from models.schemas.stock import StockLocationResponse, StockSummaryResponse from models.schemas.stock import StockLocationResponse, StockSummaryResponse
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
from models.database.stock import Stock from models.database.stock import Stock
from app.utils.data_processing import GTINProcessor, PriceProcessor from app.utils.data_processing import GTINProcessor, PriceProcessor
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ProductService: class MarketplaceProductService:
"""Service class for Product operations following the application's service pattern.""" """Service class for MarketplaceProduct operations following the application's service pattern."""
def __init__(self): def __init__(self):
"""Class constructor.""" """Class constructor."""
self.gtin_processor = GTINProcessor() self.gtin_processor = GTINProcessor()
self.price_processor = PriceProcessor() self.price_processor = PriceProcessor()
def create_product(self, db: Session, product_data: ProductCreate) -> Product: def create_product(self, db: Session, product_data: MarketplaceProductCreate) -> MarketplaceProduct:
"""Create a new product with validation.""" """Create a new product with validation."""
try: try:
# Process and validate GTIN if provided # Process and validate GTIN if provided
if product_data.gtin: if product_data.gtin:
normalized_gtin = self.gtin_processor.normalize(product_data.gtin) normalized_gtin = self.gtin_processor.normalize(product_data.gtin)
if not normalized_gtin: if not normalized_gtin:
raise InvalidProductDataException("Invalid GTIN format", field="gtin") raise InvalidMarketplaceProductDataException("Invalid GTIN format", field="gtin")
product_data.gtin = normalized_gtin product_data.gtin = normalized_gtin
# Process price if provided # Process price if provided
@@ -62,67 +63,67 @@ class ProductService:
product_data.currency = currency product_data.currency = currency
except ValueError as e: except ValueError as e:
# Convert ValueError to domain-specific exception # Convert ValueError to domain-specific exception
raise InvalidProductDataException(str(e), field="price") raise InvalidMarketplaceProductDataException(str(e), field="price")
# Set default marketplace if not provided # Set default marketplace if not provided
if not product_data.marketplace: if not product_data.marketplace:
product_data.marketplace = "Letzshop" product_data.marketplace = "Letzshop"
# Validate required fields # Validate required fields
if not product_data.product_id or not product_data.product_id.strip(): if not product_data.marketplace_product_id or not product_data.marketplace_product_id.strip():
raise ProductValidationException("Product ID is required", field="product_id") raise MarketplaceProductValidationException("MarketplaceProduct ID is required", field="marketplace_product_id")
if not product_data.title or not product_data.title.strip(): if not product_data.title or not product_data.title.strip():
raise ProductValidationException("Product title is required", field="title") raise MarketplaceProductValidationException("MarketplaceProduct title is required", field="title")
db_product = Product(**product_data.model_dump()) db_product = MarketplaceProduct(**product_data.model_dump())
db.add(db_product) db.add(db_product)
db.commit() db.commit()
db.refresh(db_product) db.refresh(db_product)
logger.info(f"Created product {db_product.product_id}") logger.info(f"Created product {db_product.marketplace_product_id}")
return db_product return db_product
except (InvalidProductDataException, ProductValidationException): except (InvalidMarketplaceProductDataException, MarketplaceProductValidationException):
db.rollback() db.rollback()
raise # Re-raise custom exceptions raise # Re-raise custom exceptions
except IntegrityError as e: except IntegrityError as e:
db.rollback() db.rollback()
logger.error(f"Database integrity error: {str(e)}") logger.error(f"Database integrity error: {str(e)}")
if "product_id" in str(e).lower() or "unique" in str(e).lower(): if "marketplace_product_id" in str(e).lower() or "unique" in str(e).lower():
raise ProductAlreadyExistsException(product_data.product_id) raise MarketplaceProductAlreadyExistsException(product_data.marketplace_product_id)
else: else:
raise ProductValidationException("Data integrity constraint violation") raise MarketplaceProductValidationException("Data integrity constraint violation")
except Exception as e: except Exception as e:
db.rollback() db.rollback()
logger.error(f"Error creating product: {str(e)}") logger.error(f"Error creating product: {str(e)}")
raise ValidationException("Failed to create product") raise ValidationException("Failed to create product")
def get_product_by_id(self, db: Session, product_id: str) -> Optional[Product]: def get_product_by_id(self, db: Session, marketplace_product_id: str) -> Optional[MarketplaceProduct]:
"""Get a product by its ID.""" """Get a product by its ID."""
try: try:
return db.query(Product).filter(Product.product_id == product_id).first() return db.query(MarketplaceProduct).filter(MarketplaceProduct.marketplace_product_id == marketplace_product_id).first()
except Exception as e: except Exception as e:
logger.error(f"Error getting product {product_id}: {str(e)}") logger.error(f"Error getting product {marketplace_product_id}: {str(e)}")
return None return None
def get_product_by_id_or_raise(self, db: Session, product_id: str) -> Product: def get_product_by_id_or_raise(self, db: Session, marketplace_product_id: str) -> MarketplaceProduct:
""" """
Get a product by its ID or raise exception. Get a product by its ID or raise exception.
Args: Args:
db: Database session db: Database session
product_id: Product ID to find marketplace_product_id: MarketplaceProduct ID to find
Returns: Returns:
Product object MarketplaceProduct object
Raises: Raises:
ProductNotFoundException: If product doesn't exist MarketplaceProductNotFoundException: If product doesn't exist
""" """
product = self.get_product_by_id(db, product_id) product = self.get_product_by_id(db, marketplace_product_id)
if not product: if not product:
raise ProductNotFoundException(product_id) raise MarketplaceProductNotFoundException(marketplace_product_id)
return product return product
def get_products_with_filters( def get_products_with_filters(
@@ -136,7 +137,7 @@ class ProductService:
marketplace: Optional[str] = None, marketplace: Optional[str] = None,
shop_name: Optional[str] = None, shop_name: Optional[str] = None,
search: Optional[str] = None, search: Optional[str] = None,
) -> Tuple[List[Product], int]: ) -> Tuple[List[MarketplaceProduct], int]:
""" """
Get products with filtering and pagination. Get products with filtering and pagination.
@@ -155,27 +156,27 @@ class ProductService:
Tuple of (products_list, total_count) Tuple of (products_list, total_count)
""" """
try: try:
query = db.query(Product) query = db.query(MarketplaceProduct)
# Apply filters # Apply filters
if brand: if brand:
query = query.filter(Product.brand.ilike(f"%{brand}%")) query = query.filter(MarketplaceProduct.brand.ilike(f"%{brand}%"))
if category: if category:
query = query.filter(Product.google_product_category.ilike(f"%{category}%")) query = query.filter(MarketplaceProduct.google_product_category.ilike(f"%{category}%"))
if availability: if availability:
query = query.filter(Product.availability == availability) query = query.filter(MarketplaceProduct.availability == availability)
if marketplace: if marketplace:
query = query.filter(Product.marketplace.ilike(f"%{marketplace}%")) query = query.filter(MarketplaceProduct.marketplace.ilike(f"%{marketplace}%"))
if shop_name: if shop_name:
query = query.filter(Product.shop_name.ilike(f"%{shop_name}%")) query = query.filter(MarketplaceProduct.shop_name.ilike(f"%{shop_name}%"))
if search: if search:
# Search in title, description, marketplace, and shop_name # Search in title, description, marketplace, and shop_name
search_term = f"%{search}%" search_term = f"%{search}%"
query = query.filter( query = query.filter(
(Product.title.ilike(search_term)) (MarketplaceProduct.title.ilike(search_term))
| (Product.description.ilike(search_term)) | (MarketplaceProduct.description.ilike(search_term))
| (Product.marketplace.ilike(search_term)) | (MarketplaceProduct.marketplace.ilike(search_term))
| (Product.shop_name.ilike(search_term)) | (MarketplaceProduct.shop_name.ilike(search_term))
) )
total = query.count() total = query.count()
@@ -187,10 +188,10 @@ class ProductService:
logger.error(f"Error getting products with filters: {str(e)}") logger.error(f"Error getting products with filters: {str(e)}")
raise ValidationException("Failed to retrieve products") raise ValidationException("Failed to retrieve products")
def update_product(self, db: Session, product_id: str, product_update: ProductUpdate) -> Product: def update_product(self, db: Session, marketplace_product_id: str, product_update: MarketplaceProductUpdate) -> MarketplaceProduct:
"""Update product with validation.""" """Update product with validation."""
try: try:
product = self.get_product_by_id_or_raise(db, product_id) product = self.get_product_by_id_or_raise(db, marketplace_product_id)
# Update fields # Update fields
update_data = product_update.model_dump(exclude_unset=True) update_data = product_update.model_dump(exclude_unset=True)
@@ -199,7 +200,7 @@ class ProductService:
if "gtin" in update_data and update_data["gtin"]: if "gtin" in update_data and update_data["gtin"]:
normalized_gtin = self.gtin_processor.normalize(update_data["gtin"]) normalized_gtin = self.gtin_processor.normalize(update_data["gtin"])
if not normalized_gtin: if not normalized_gtin:
raise InvalidProductDataException("Invalid GTIN format", field="gtin") raise InvalidMarketplaceProductDataException("Invalid GTIN format", field="gtin")
update_data["gtin"] = normalized_gtin update_data["gtin"] = normalized_gtin
# Process price if being updated # Process price if being updated
@@ -213,11 +214,11 @@ class ProductService:
update_data["currency"] = currency update_data["currency"] = currency
except ValueError as e: except ValueError as e:
# Convert ValueError to domain-specific exception # Convert ValueError to domain-specific exception
raise InvalidProductDataException(str(e), field="price") raise InvalidMarketplaceProductDataException(str(e), field="price")
# Validate required fields if being updated # Validate required fields if being updated
if "title" in update_data and (not update_data["title"] or not update_data["title"].strip()): if "title" in update_data and (not update_data["title"] or not update_data["title"].strip()):
raise ProductValidationException("Product title cannot be empty", field="title") raise MarketplaceProductValidationException("MarketplaceProduct title cannot be empty", field="title")
for key, value in update_data.items(): for key, value in update_data.items():
setattr(product, key, value) setattr(product, key, value)
@@ -226,33 +227,33 @@ class ProductService:
db.commit() db.commit()
db.refresh(product) db.refresh(product)
logger.info(f"Updated product {product_id}") logger.info(f"Updated product {marketplace_product_id}")
return product return product
except (ProductNotFoundException, InvalidProductDataException, ProductValidationException): except (MarketplaceProductNotFoundException, InvalidMarketplaceProductDataException, MarketplaceProductValidationException):
db.rollback() db.rollback()
raise # Re-raise custom exceptions raise # Re-raise custom exceptions
except Exception as e: except Exception as e:
db.rollback() db.rollback()
logger.error(f"Error updating product {product_id}: {str(e)}") logger.error(f"Error updating product {marketplace_product_id}: {str(e)}")
raise ValidationException("Failed to update product") raise ValidationException("Failed to update product")
def delete_product(self, db: Session, product_id: str) -> bool: def delete_product(self, db: Session, marketplace_product_id: str) -> bool:
""" """
Delete product and associated stock. Delete product and associated stock.
Args: Args:
db: Database session db: Database session
product_id: Product ID to delete marketplace_product_id: MarketplaceProduct ID to delete
Returns: Returns:
True if deletion successful True if deletion successful
Raises: Raises:
ProductNotFoundException: If product doesn't exist MarketplaceProductNotFoundException: If product doesn't exist
""" """
try: try:
product = self.get_product_by_id_or_raise(db, product_id) product = self.get_product_by_id_or_raise(db, marketplace_product_id)
# Delete associated stock entries if GTIN exists # Delete associated stock entries if GTIN exists
if product.gtin: if product.gtin:
@@ -261,14 +262,14 @@ class ProductService:
db.delete(product) db.delete(product)
db.commit() db.commit()
logger.info(f"Deleted product {product_id}") logger.info(f"Deleted product {marketplace_product_id}")
return True return True
except ProductNotFoundException: except MarketplaceProductNotFoundException:
raise # Re-raise custom exceptions raise # Re-raise custom exceptions
except Exception as e: except Exception as e:
db.rollback() db.rollback()
logger.error(f"Error deleting product {product_id}: {str(e)}") logger.error(f"Error deleting product {marketplace_product_id}: {str(e)}")
raise ValidationException("Failed to delete product") raise ValidationException("Failed to delete product")
def get_stock_info(self, db: Session, gtin: str) -> Optional[StockSummaryResponse]: def get_stock_info(self, db: Session, gtin: str) -> Optional[StockSummaryResponse]:
@@ -330,7 +331,7 @@ class ProductService:
# Write header row # Write header row
headers = [ headers = [
"product_id", "title", "description", "link", "image_link", "marketplace_product_id", "title", "description", "link", "image_link",
"availability", "price", "currency", "brand", "gtin", "availability", "price", "currency", "brand", "gtin",
"marketplace", "shop_name" "marketplace", "shop_name"
] ]
@@ -345,13 +346,13 @@ class ProductService:
offset = 0 offset = 0
while True: while True:
query = db.query(Product) query = db.query(MarketplaceProduct)
# Apply marketplace filters # Apply marketplace filters
if marketplace: if marketplace:
query = query.filter(Product.marketplace.ilike(f"%{marketplace}%")) query = query.filter(MarketplaceProduct.marketplace.ilike(f"%{marketplace}%"))
if shop_name: if shop_name:
query = query.filter(Product.shop_name.ilike(f"%{shop_name}%")) query = query.filter(MarketplaceProduct.shop_name.ilike(f"%{shop_name}%"))
products = query.offset(offset).limit(batch_size).all() products = query.offset(offset).limit(batch_size).all()
if not products: if not products:
@@ -360,7 +361,7 @@ class ProductService:
for product in products: for product in products:
# Create CSV row with proper escaping # Create CSV row with proper escaping
row_data = [ row_data = [
product.product_id or "", product.marketplace_product_id or "",
product.title or "", product.title or "",
product.description or "", product.description or "",
product.link or "", product.link or "",
@@ -387,11 +388,11 @@ class ProductService:
logger.error(f"Error generating CSV export: {str(e)}") logger.error(f"Error generating CSV export: {str(e)}")
raise ValidationException("Failed to generate CSV export") raise ValidationException("Failed to generate CSV export")
def product_exists(self, db: Session, product_id: str) -> bool: def product_exists(self, db: Session, marketplace_product_id: str) -> bool:
"""Check if product exists by ID.""" """Check if product exists by ID."""
try: try:
return ( return (
db.query(Product).filter(Product.product_id == product_id).first() db.query(MarketplaceProduct).filter(MarketplaceProduct.marketplace_product_id == marketplace_product_id).first()
is not None is not None
) )
except Exception as e: except Exception as e:
@@ -401,18 +402,18 @@ class ProductService:
# Private helper methods # Private helper methods
def _validate_product_data(self, product_data: dict) -> None: def _validate_product_data(self, product_data: dict) -> None:
"""Validate product data structure.""" """Validate product data structure."""
required_fields = ['product_id', 'title'] required_fields = ['marketplace_product_id', 'title']
for field in required_fields: for field in required_fields:
if field not in product_data or not product_data[field]: if field not in product_data or not product_data[field]:
raise ProductValidationException(f"{field} is required", field=field) raise MarketplaceProductValidationException(f"{field} is required", field=field)
def _normalize_product_data(self, product_data: dict) -> dict: def _normalize_product_data(self, product_data: dict) -> dict:
"""Normalize and clean product data.""" """Normalize and clean product data."""
normalized = product_data.copy() normalized = product_data.copy()
# Trim whitespace from string fields # Trim whitespace from string fields
string_fields = ['product_id', 'title', 'description', 'brand', 'marketplace', 'shop_name'] string_fields = ['marketplace_product_id', 'title', 'description', 'brand', 'marketplace', 'shop_name']
for field in string_fields: for field in string_fields:
if field in normalized and normalized[field]: if field in normalized and normalized[field]:
normalized[field] = normalized[field].strip() normalized[field] = normalized[field].strip()
@@ -421,4 +422,4 @@ class ProductService:
# Create service instance # Create service instance
product_service = ProductService() marketplace_product_service = MarketplaceProductService()

View File

@@ -20,13 +20,13 @@ from app.exceptions import (
ShopAlreadyExistsException, ShopAlreadyExistsException,
UnauthorizedShopAccessException, UnauthorizedShopAccessException,
InvalidShopDataException, InvalidShopDataException,
ProductNotFoundException, MarketplaceProductNotFoundException,
ShopProductAlreadyExistsException, ShopProductAlreadyExistsException,
MaxShopsReachedException, MaxShopsReachedException,
ValidationException, ValidationException,
) )
from models.schemas.shop import ShopCreate, ShopProductCreate from models.schemas.shop import ShopCreate, ShopProductCreate
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
from models.database.shop import Shop, ShopProduct from models.database.shop import Shop, ShopProduct
from models.database.user import User from models.database.user import User
@@ -198,22 +198,22 @@ class ShopService:
Created ShopProduct object Created ShopProduct object
Raises: Raises:
ProductNotFoundException: If product not found MarketplaceProductNotFoundException: If product not found
ShopProductAlreadyExistsException: If product already in shop ShopProductAlreadyExistsException: If product already in shop
""" """
try: try:
# Check if product exists # Check if product exists
product = self._get_product_by_id_or_raise(db, shop_product.product_id) marketplace_product = self._get_product_by_id_or_raise(db, shop_product.marketplace_product_id)
# Check if product already in shop # Check if product already in shop
if self._product_in_shop(db, shop.id, product.id): if self._product_in_shop(db, shop.id, marketplace_product.id):
raise ShopProductAlreadyExistsException(shop.shop_code, shop_product.product_id) raise ShopProductAlreadyExistsException(shop.shop_code, shop_product.marketplace_product_id)
# Create shop-product association # Create shop-product association
new_shop_product = ShopProduct( new_shop_product = ShopProduct(
shop_id=shop.id, shop_id=shop.id,
product_id=product.id, marketplace_product_id=marketplace_product.id,
**shop_product.model_dump(exclude={"product_id"}), **shop_product.model_dump(exclude={"marketplace_product_id"}),
) )
db.add(new_shop_product) db.add(new_shop_product)
@@ -223,10 +223,10 @@ class ShopService:
# Load the product relationship # Load the product relationship
db.refresh(new_shop_product) db.refresh(new_shop_product)
logger.info(f"Product {shop_product.product_id} added to shop {shop.shop_code}") logger.info(f"MarketplaceProduct {shop_product.marketplace_product_id} added to shop {shop.shop_code}")
return new_shop_product return new_shop_product
except (ProductNotFoundException, ShopProductAlreadyExistsException): except (MarketplaceProductNotFoundException, ShopProductAlreadyExistsException):
db.rollback() db.rollback()
raise # Re-raise custom exceptions raise # Re-raise custom exceptions
except Exception as e: except Exception as e:
@@ -322,20 +322,20 @@ class ShopService:
.first() is not None .first() is not None
) )
def _get_product_by_id_or_raise(self, db: Session, product_id: str) -> Product: def _get_product_by_id_or_raise(self, db: Session, marketplace_product_id: str) -> MarketplaceProduct:
"""Get product by ID or raise exception.""" """Get product by ID or raise exception."""
product = db.query(Product).filter(Product.product_id == product_id).first() product = db.query(MarketplaceProduct).filter(MarketplaceProduct.marketplace_product_id == marketplace_product_id).first()
if not product: if not product:
raise ProductNotFoundException(product_id) raise MarketplaceProductNotFoundException(marketplace_product_id)
return product return product
def _product_in_shop(self, db: Session, shop_id: int, product_id: int) -> bool: def _product_in_shop(self, db: Session, shop_id: int, marketplace_product_id: int) -> bool:
"""Check if product is already in shop.""" """Check if product is already in shop."""
return ( return (
db.query(ShopProduct) db.query(ShopProduct)
.filter( .filter(
ShopProduct.shop_id == shop_id, ShopProduct.shop_id == shop_id,
ShopProduct.product_id == product_id ShopProduct.marketplace_product_id == marketplace_product_id
) )
.first() is not None .first() is not None
) )

View File

@@ -16,7 +16,7 @@ from sqlalchemy import func
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.exceptions import ValidationException from app.exceptions import ValidationException
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
from models.database.stock import Stock from models.database.stock import Stock
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -85,13 +85,13 @@ class StatsService:
# Query to get stats per marketplace # Query to get stats per marketplace
marketplace_stats = ( marketplace_stats = (
db.query( db.query(
Product.marketplace, MarketplaceProduct.marketplace,
func.count(Product.id).label("total_products"), func.count(MarketplaceProduct.id).label("total_products"),
func.count(func.distinct(Product.shop_name)).label("unique_shops"), func.count(func.distinct(MarketplaceProduct.shop_name)).label("unique_shops"),
func.count(func.distinct(Product.brand)).label("unique_brands"), func.count(func.distinct(MarketplaceProduct.brand)).label("unique_brands"),
) )
.filter(Product.marketplace.isnot(None)) .filter(MarketplaceProduct.marketplace.isnot(None))
.group_by(Product.marketplace) .group_by(MarketplaceProduct.marketplace)
.all() .all()
) )
@@ -195,13 +195,13 @@ class StatsService:
# Private helper methods # Private helper methods
def _get_product_count(self, db: Session) -> int: def _get_product_count(self, db: Session) -> int:
"""Get total product count.""" """Get total product count."""
return db.query(Product).count() return db.query(MarketplaceProduct).count()
def _get_unique_brands_count(self, db: Session) -> int: def _get_unique_brands_count(self, db: Session) -> int:
"""Get count of unique brands.""" """Get count of unique brands."""
return ( return (
db.query(Product.brand) db.query(MarketplaceProduct.brand)
.filter(Product.brand.isnot(None), Product.brand != "") .filter(MarketplaceProduct.brand.isnot(None), MarketplaceProduct.brand != "")
.distinct() .distinct()
.count() .count()
) )
@@ -209,10 +209,10 @@ class StatsService:
def _get_unique_categories_count(self, db: Session) -> int: def _get_unique_categories_count(self, db: Session) -> int:
"""Get count of unique categories.""" """Get count of unique categories."""
return ( return (
db.query(Product.google_product_category) db.query(MarketplaceProduct.google_product_category)
.filter( .filter(
Product.google_product_category.isnot(None), MarketplaceProduct.google_product_category.isnot(None),
Product.google_product_category != "", MarketplaceProduct.google_product_category != "",
) )
.distinct() .distinct()
.count() .count()
@@ -221,8 +221,8 @@ class StatsService:
def _get_unique_marketplaces_count(self, db: Session) -> int: def _get_unique_marketplaces_count(self, db: Session) -> int:
"""Get count of unique marketplaces.""" """Get count of unique marketplaces."""
return ( return (
db.query(Product.marketplace) db.query(MarketplaceProduct.marketplace)
.filter(Product.marketplace.isnot(None), Product.marketplace != "") .filter(MarketplaceProduct.marketplace.isnot(None), MarketplaceProduct.marketplace != "")
.distinct() .distinct()
.count() .count()
) )
@@ -230,8 +230,8 @@ class StatsService:
def _get_unique_shops_count(self, db: Session) -> int: def _get_unique_shops_count(self, db: Session) -> int:
"""Get count of unique shops.""" """Get count of unique shops."""
return ( return (
db.query(Product.shop_name) db.query(MarketplaceProduct.shop_name)
.filter(Product.shop_name.isnot(None), Product.shop_name != "") .filter(MarketplaceProduct.shop_name.isnot(None), MarketplaceProduct.shop_name != "")
.distinct() .distinct()
.count() .count()
) )
@@ -239,16 +239,16 @@ class StatsService:
def _get_products_with_gtin_count(self, db: Session) -> int: def _get_products_with_gtin_count(self, db: Session) -> int:
"""Get count of products with GTIN.""" """Get count of products with GTIN."""
return ( return (
db.query(Product) db.query(MarketplaceProduct)
.filter(Product.gtin.isnot(None), Product.gtin != "") .filter(MarketplaceProduct.gtin.isnot(None), MarketplaceProduct.gtin != "")
.count() .count()
) )
def _get_products_with_images_count(self, db: Session) -> int: def _get_products_with_images_count(self, db: Session) -> int:
"""Get count of products with images.""" """Get count of products with images."""
return ( return (
db.query(Product) db.query(MarketplaceProduct)
.filter(Product.image_link.isnot(None), Product.image_link != "") .filter(MarketplaceProduct.image_link.isnot(None), MarketplaceProduct.image_link != "")
.count() .count()
) )
@@ -265,11 +265,11 @@ class StatsService:
def _get_brands_by_marketplace(self, db: Session, marketplace: str) -> List[str]: def _get_brands_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
"""Get unique brands for a specific marketplace.""" """Get unique brands for a specific marketplace."""
brands = ( brands = (
db.query(Product.brand) db.query(MarketplaceProduct.brand)
.filter( .filter(
Product.marketplace == marketplace, MarketplaceProduct.marketplace == marketplace,
Product.brand.isnot(None), MarketplaceProduct.brand.isnot(None),
Product.brand != "", MarketplaceProduct.brand != "",
) )
.distinct() .distinct()
.all() .all()
@@ -279,11 +279,11 @@ class StatsService:
def _get_shops_by_marketplace(self, db: Session, marketplace: str) -> List[str]: def _get_shops_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
"""Get unique shops for a specific marketplace.""" """Get unique shops for a specific marketplace."""
shops = ( shops = (
db.query(Product.shop_name) db.query(MarketplaceProduct.shop_name)
.filter( .filter(
Product.marketplace == marketplace, MarketplaceProduct.marketplace == marketplace,
Product.shop_name.isnot(None), MarketplaceProduct.shop_name.isnot(None),
Product.shop_name != "", MarketplaceProduct.shop_name != "",
) )
.distinct() .distinct()
.all() .all()
@@ -292,7 +292,7 @@ class StatsService:
def _get_products_by_marketplace_count(self, db: Session, marketplace: str) -> int: def _get_products_by_marketplace_count(self, db: Session, marketplace: str) -> int:
"""Get product count for a specific marketplace.""" """Get product count for a specific marketplace."""
return db.query(Product).filter(Product.marketplace == marketplace).count() return db.query(MarketplaceProduct).filter(MarketplaceProduct.marketplace == marketplace).count()
# Create service instance following the same pattern as other services # Create service instance following the same pattern as other services
stats_service = StatsService() stats_service = StatsService()

View File

@@ -26,7 +26,7 @@ from app.exceptions import (
) )
from models.schemas.stock import (StockAdd, StockCreate, StockLocationResponse, from models.schemas.stock import (StockAdd, StockCreate, StockLocationResponse,
StockSummaryResponse, StockUpdate) StockSummaryResponse, StockUpdate)
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
from models.database.stock import Stock from models.database.stock import Stock
from app.utils.data_processing import GTINProcessor from app.utils.data_processing import GTINProcessor
@@ -261,7 +261,7 @@ class StockService:
) )
# Try to get product title for reference # Try to get product title for reference
product = db.query(Product).filter(Product.gtin == normalized_gtin).first() product = db.query(MarketplaceProduct).filter(MarketplaceProduct.gtin == normalized_gtin).first()
product_title = product.title if product else None product_title = product.title if product else None
return StockSummaryResponse( return StockSummaryResponse(
@@ -304,7 +304,7 @@ class StockService:
total_quantity = sum(entry.quantity for entry in total_stock) total_quantity = sum(entry.quantity for entry in total_stock)
# Get product info for context # Get product info for context
product = db.query(Product).filter(Product.gtin == normalized_gtin).first() product = db.query(MarketplaceProduct).filter(MarketplaceProduct.gtin == normalized_gtin).first()
return { return {
"gtin": normalized_gtin, "gtin": normalized_gtin,
@@ -491,14 +491,14 @@ class StockService:
low_stock_items = [] low_stock_items = []
for entry in low_stock_entries: for entry in low_stock_entries:
# Get product info if available # Get product info if available
product = db.query(Product).filter(Product.gtin == entry.gtin).first() product = db.query(MarketplaceProduct).filter(MarketplaceProduct.gtin == entry.gtin).first()
low_stock_items.append({ low_stock_items.append({
"gtin": entry.gtin, "gtin": entry.gtin,
"location": entry.location, "location": entry.location,
"current_quantity": entry.quantity, "current_quantity": entry.quantity,
"product_title": product.title if product else None, "product_title": product.title if product else None,
"product_id": product.product_id if product else None, "marketplace_product_id": product.marketplace_product_id if product else None,
}) })
return low_stock_items return low_stock_items

View File

@@ -11,7 +11,7 @@ import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from app.core.database import SessionLocal from app.core.database import SessionLocal
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
from app.utils.csv_processor import CSVProcessor from app.utils.csv_processor import CSVProcessor
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -17,7 +17,7 @@ import requests
from sqlalchemy import literal from sqlalchemy import literal
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -40,15 +40,15 @@ class CSVProcessor:
COLUMN_MAPPING = { COLUMN_MAPPING = {
# Standard variations # Standard variations
"id": "product_id", "id": "marketplace_product_id",
"ID": "product_id", "ID": "marketplace_product_id",
"Product ID": "product_id", "MarketplaceProduct ID": "marketplace_product_id",
"name": "title", "name": "title",
"Name": "title", "Name": "title",
"product_name": "title", "product_name": "title",
"Product Name": "title", "MarketplaceProduct Name": "title",
# Google Shopping feed standard # Google Shopping feed standard
"g:id": "product_id", "g:id": "marketplace_product_id",
"g:title": "title", "g:title": "title",
"g:description": "description", "g:description": "description",
"g:link": "link", "g:link": "link",
@@ -266,8 +266,8 @@ class CSVProcessor:
product_data["shop_name"] = shop_name product_data["shop_name"] = shop_name
# Validate required fields # Validate required fields
if not product_data.get("product_id"): if not product_data.get("marketplace_product_id"):
logger.warning(f"Row {index}: Missing product_id, skipping") logger.warning(f"Row {index}: Missing marketplace_product_id, skipping")
errors += 1 errors += 1
continue continue
@@ -278,8 +278,8 @@ class CSVProcessor:
# Check if product exists # Check if product exists
existing_product = ( existing_product = (
db.query(Product) db.query(MarketplaceProduct)
.filter(Product.product_id == literal(product_data["product_id"])) .filter(MarketplaceProduct.marketplace_product_id == literal(product_data["marketplace_product_id"]))
.first() .first()
) )
@@ -293,7 +293,7 @@ class CSVProcessor:
existing_product.updated_at = datetime.now(timezone.utc) existing_product.updated_at = datetime.now(timezone.utc)
updated += 1 updated += 1
logger.debug( logger.debug(
f"Updated product {product_data['product_id']} for " f"Updated product {product_data['marketplace_product_id']} for "
f"{marketplace} and shop {shop_name}" f"{marketplace} and shop {shop_name}"
) )
else: else:
@@ -302,13 +302,13 @@ class CSVProcessor:
k: v k: v
for k, v in product_data.items() for k, v in product_data.items()
if k not in ["id", "created_at", "updated_at"] if k not in ["id", "created_at", "updated_at"]
and hasattr(Product, k) and hasattr(MarketplaceProduct, k)
} }
new_product = Product(**filtered_data) new_product = MarketplaceProduct(**filtered_data)
db.add(new_product) db.add(new_product)
imported += 1 imported += 1
logger.debug( logger.debug(
f"Imported new product {product_data['product_id']} " f"Imported new product {product_data['marketplace_product_id']} "
f"for {marketplace} and shop {shop_name}" f"for {marketplace} and shop {shop_name}"
) )

View File

@@ -51,7 +51,7 @@ Response:
Include the JWT token in the Authorization header: Include the JWT token in the Authorization header:
```http ```http
GET /api/v1/product GET /api/v1/marketplace/product
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
``` ```

View File

@@ -37,8 +37,8 @@ All API endpoints are versioned using URL path versioning:
- Configuration management - Configuration management
### Products (`/products/`) ### Products (`/products/`)
- Product CRUD operations - MarketplaceProduct CRUD operations
- Product search and filtering - MarketplaceProduct search and filtering
- Bulk operations - Bulk operations
### Shops (`/shops/`) ### Shops (`/shops/`)
@@ -98,7 +98,7 @@ Content-Type: application/json
Most list endpoints support pagination: Most list endpoints support pagination:
```bash ```bash
GET /api/v1/product?skip=0&limit=20 GET /api/v1/marketplace/product?skip=0&limit=20
``` ```
Response includes pagination metadata: Response includes pagination metadata:
@@ -116,14 +116,14 @@ Response includes pagination metadata:
Many endpoints support filtering and search: Many endpoints support filtering and search:
```bash ```bash
GET /api/v1/product?search=laptop&category=electronics&min_price=100 GET /api/v1/marketplace/product?search=laptop&category=electronics&min_price=100
``` ```
### Sorting ### Sorting
Use the `sort` parameter with field names: Use the `sort` parameter with field names:
```bash ```bash
GET /api/v1/product?sort=name&order=desc GET /api/v1/marketplace/product?sort=name&order=desc
``` ```
## Status Codes ## Status Codes

View File

@@ -30,7 +30,7 @@ app/exceptions/
├── auth.py # Authentication/authorization exceptions ├── auth.py # Authentication/authorization exceptions
├── admin.py # Admin operation exceptions ├── admin.py # Admin operation exceptions
├── marketplace.py # Import/marketplace exceptions ├── marketplace.py # Import/marketplace exceptions
├── product.py # Product management exceptions ├── product.py # MarketplaceProduct management exceptions
├── shop.py # Shop management exceptions ├── shop.py # Shop management exceptions
└── stock.py # Stock management exceptions └── stock.py # Stock management exceptions
``` ```
@@ -48,10 +48,10 @@ All custom exceptions return a consistent JSON structure:
```json ```json
{ {
"error_code": "PRODUCT_NOT_FOUND", "error_code": "PRODUCT_NOT_FOUND",
"message": "Product with ID 'ABC123' not found", "message": "MarketplaceProduct with ID 'ABC123' not found",
"status_code": 404, "status_code": 404,
"details": { "details": {
"resource_type": "Product", "resource_type": "MarketplaceProduct",
"identifier": "ABC123" "identifier": "ABC123"
} }
} }
@@ -360,7 +360,7 @@ def test_get_all_users_non_admin(client, auth_headers):
### Resource Not Found (404) ### Resource Not Found (404)
- `USER_NOT_FOUND`: User with specified ID not found - `USER_NOT_FOUND`: User with specified ID not found
- `SHOP_NOT_FOUND`: Shop with specified code/ID not found - `SHOP_NOT_FOUND`: Shop with specified code/ID not found
- `PRODUCT_NOT_FOUND`: Product with specified ID not found - `PRODUCT_NOT_FOUND`: MarketplaceProduct with specified ID not found
### Business Logic (400) ### Business Logic (400)
- `CANNOT_MODIFY_SELF`: Admin cannot modify own account - `CANNOT_MODIFY_SELF`: Admin cannot modify own account
@@ -370,7 +370,7 @@ def test_get_all_users_non_admin(client, auth_headers):
### Validation (422) ### Validation (422)
- `VALIDATION_ERROR`: Pydantic request validation failed - `VALIDATION_ERROR`: Pydantic request validation failed
- `INVALID_PRODUCT_DATA`: Product data validation failed - `INVALID_PRODUCT_DATA`: MarketplaceProduct data validation failed
- `INVALID_SHOP_DATA`: Shop data validation failed - `INVALID_SHOP_DATA`: Shop data validation failed
### Rate Limiting (429) ### Rate Limiting (429)

View File

@@ -7,11 +7,11 @@ Your API returns consistent error responses with this structure:
```json ```json
{ {
"error_code": "PRODUCT_NOT_FOUND", "error_code": "PRODUCT_NOT_FOUND",
"message": "Product with ID 'ABC123' not found", "message": "MarketplaceProduct with ID 'ABC123' not found",
"status_code": 404, "status_code": 404,
"field": "product_id", "field": "marketplace_product_id",
"details": { "details": {
"resource_type": "Product", "resource_type": "MarketplaceProduct",
"identifier": "ABC123" "identifier": "ABC123"
} }
} }
@@ -131,8 +131,8 @@ export const ERROR_MESSAGES = {
TOKEN_EXPIRED: 'Your session has expired. Please log in again.', TOKEN_EXPIRED: 'Your session has expired. Please log in again.',
USER_NOT_ACTIVE: 'Your account has been deactivated. Contact support.', USER_NOT_ACTIVE: 'Your account has been deactivated. Contact support.',
// Product errors // MarketplaceProduct errors
PRODUCT_NOT_FOUND: 'Product not found. It may have been removed.', PRODUCT_NOT_FOUND: 'MarketplaceProduct not found. It may have been removed.',
PRODUCT_ALREADY_EXISTS: 'A product with this ID already exists.', PRODUCT_ALREADY_EXISTS: 'A product with this ID already exists.',
INVALID_PRODUCT_DATA: 'Please check the product information and try again.', INVALID_PRODUCT_DATA: 'Please check the product information and try again.',
@@ -226,7 +226,7 @@ const ProductForm = () => {
try { try {
await handleApiCall(() => createProduct(formData)); await handleApiCall(() => createProduct(formData));
// Success handling // Success handling
alert('Product created successfully!'); alert('MarketplaceProduct created successfully!');
setFormData({ product_id: '', name: '', price: '' }); setFormData({ product_id: '', name: '', price: '' });
} catch (apiError) { } catch (apiError) {
// Handle field-specific errors // Handle field-specific errors
@@ -260,7 +260,7 @@ const ProductForm = () => {
)} )}
<div className="form-field"> <div className="form-field">
<label>Product ID</label> <label>MarketplaceProduct ID</label>
<input <input
type="text" type="text"
value={formData.product_id} value={formData.product_id}
@@ -273,7 +273,7 @@ const ProductForm = () => {
</div> </div>
<div className="form-field"> <div className="form-field">
<label>Product Name</label> <label>MarketplaceProduct Name</label>
<input <input
type="text" type="text"
value={formData.name} value={formData.name}
@@ -286,7 +286,7 @@ const ProductForm = () => {
</div> </div>
<button type="submit" disabled={isLoading}> <button type="submit" disabled={isLoading}>
{isLoading ? 'Creating...' : 'Create Product'} {isLoading ? 'Creating...' : 'Create MarketplaceProduct'}
</button> </button>
</form> </form>
); );

View File

@@ -92,8 +92,8 @@ if __name__ == "__main__":
# 4. Create a sample product # 4. Create a sample product
print("\n4. Creating a sample product...") print("\n4. Creating a sample product...")
sample_product = { sample_product = {
"product_id": "TEST001", "marketplace_product_id": "TEST001",
"title": "Test Product", "title": "Test MarketplaceProduct",
"description": "A test product for demonstration", "description": "A test product for demonstration",
"price": "19.99", "price": "19.99",
"brand": "Test Brand", "brand": "Test Brand",
@@ -101,7 +101,7 @@ if __name__ == "__main__":
} }
product_result = create_product(admin_token, sample_product) product_result = create_product(admin_token, sample_product)
print(f"Product created: {product_result}") print(f"MarketplaceProduct created: {product_result}")
# 5. Get products list # 5. Get products list
print("\n5. Getting products list...") print("\n5. Getting products list...")
@@ -195,15 +195,15 @@ curl -X GET "http://localhost:8000/products" \
-H "Authorization: Bearer YOUR_JWT_TOKEN_HERE" -H "Authorization: Bearer YOUR_JWT_TOKEN_HERE"
``` ```
#### Create a Product #### Create a MarketplaceProduct
```bash ```bash
curl -X POST "http://localhost:8000/products" \ curl -X POST "http://localhost:8000/products" \
-H "Authorization: Bearer YOUR_JWT_TOKEN_HERE" \ -H "Authorization: Bearer YOUR_JWT_TOKEN_HERE" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"product_id": "TEST001", "marketplace_product_id": "TEST001",
"title": "Test Product", "title": "Test MarketplaceProduct",
"description": "A test product for demonstration", "description": "A test product for demonstration",
"price": "19.99", "price": "19.99",
"brand": "Test Brand", "brand": "Test Brand",

View File

@@ -6,7 +6,7 @@ Welcome to the complete documentation for the Letzshop Import application - a co
Letzshop Import is a powerful web application that enables: Letzshop Import is a powerful web application that enables:
- **Product Management**: Create, update, and manage product catalogs - **MarketplaceProduct Management**: Create, update, and manage product catalogs
- **Shop Management**: Multi-shop support with individual configurations - **Shop Management**: Multi-shop support with individual configurations
- **CSV Import**: Bulk import products from various marketplace formats - **CSV Import**: Bulk import products from various marketplace formats
- **Stock Management**: Track inventory across multiple locations - **Stock Management**: Track inventory across multiple locations
@@ -28,7 +28,7 @@ Letzshop Import is a powerful web application that enables:
### 📖 User Guides ### 📖 User Guides
- [**User Management**](guides/user-management.md) - Managing users and roles - [**User Management**](guides/user-management.md) - Managing users and roles
- [**Product Management**](guides/product-management.md) - Working with products - [**MarketplaceProduct Management**](guides/product-management.md) - Working with products
- [**CSV Import**](guides/csv-import.md) - Bulk import workflows - [**CSV Import**](guides/csv-import.md) - Bulk import workflows
- [**Shop Setup**](guides/shop-setup.md) - Configuring shops - [**Shop Setup**](guides/shop-setup.md) - Configuring shops
@@ -53,7 +53,7 @@ graph TB
Client[Web Client/API Consumer] Client[Web Client/API Consumer]
API[FastAPI Application] API[FastAPI Application]
Auth[Authentication Service] Auth[Authentication Service]
Products[Product Service] Products[MarketplaceProduct Service]
Shops[Shop Service] Shops[Shop Service]
Import[Import Service] Import[Import Service]
DB[(PostgreSQL Database)] DB[(PostgreSQL Database)]
@@ -71,7 +71,7 @@ graph TB
## Key Features ## Key Features
=== "Product Management" === "MarketplaceProduct Management"
- CRUD operations for products - CRUD operations for products
- GTIN validation and normalization - GTIN validation and normalization
- Price management with currency support - Price management with currency support

View File

@@ -4,10 +4,10 @@
# Database models (SQLAlchemy) # Database models (SQLAlchemy)
from .database.base import Base from .database.base import Base
from .database.user import User from .database.user import User
from .database.product import Product from .database.marketplace_product import MarketplaceProduct
from .database.stock import Stock from .database.stock import Stock
from .database.shop import Shop, ShopProduct from .database.shop import Shop, ShopProduct
from .database.marketplace import MarketplaceImportJob from .database.marketplace_import_job import MarketplaceImportJob
# API models (Pydantic) - import the modules, not all classes # API models (Pydantic) - import the modules, not all classes
from . import schemas from . import schemas
@@ -16,7 +16,7 @@ from . import schemas
__all__ = [ __all__ = [
"Base", "Base",
"User", "User",
"Product", "MarketplaceProduct",
"Stock", "Stock",
"Shop", "Shop",
"ShopProduct", "ShopProduct",

View File

@@ -3,15 +3,15 @@
from .base import Base from .base import Base
from .user import User from .user import User
from .product import Product from .marketplace_product import MarketplaceProduct
from .stock import Stock from .stock import Stock
from .shop import Shop, ShopProduct from .shop import Shop, ShopProduct
from .marketplace import MarketplaceImportJob from .marketplace_import_job import MarketplaceImportJob
__all__ = [ __all__ = [
"Base", "Base",
"User", "User",
"Product", "MarketplaceProduct",
"Stock", "Stock",
"Shop", "Shop",
"ShopProduct", "ShopProduct",

View File

@@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timezone
from sqlalchemy import Column, DateTime from sqlalchemy import Column, DateTime
@@ -8,7 +8,7 @@ from app.core.database import Base
class TimestampMixin: class TimestampMixin:
"""Mixin to add created_at and updated_at timestamps to models""" """Mixin to add created_at and updated_at timestamps to models"""
created_at = Column(DateTime, default=datetime.utcnow, nullable=False) created_at = Column(DateTime, default=datetime.now(timezone.utc), nullable=False)
updated_at = Column( updated_at = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc), nullable=False
) )

View File

@@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timezone
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Index, from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Index,
Integer, String, Text, UniqueConstraint) Integer, String, Text, UniqueConstraint)
@@ -6,9 +6,10 @@ from sqlalchemy.orm import relationship
# Import Base from the central database module instead of creating a new one # Import Base from the central database module instead of creating a new one
from app.core.database import Base from app.core.database import Base
from models.database.base import TimestampMixin
class MarketplaceImportJob(Base): class MarketplaceImportJob(Base, TimestampMixin):
__tablename__ = "marketplace_import_jobs" __tablename__ = "marketplace_import_jobs"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
@@ -37,7 +38,7 @@ class MarketplaceImportJob(Base):
error_message = Column(String) error_message = Column(String)
# Timestamps # Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
started_at = Column(DateTime) started_at = Column(DateTime)
completed_at = Column(DateTime) completed_at = Column(DateTime)

View File

@@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timezone
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Index, from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Index,
Integer, String, Text, UniqueConstraint) Integer, String, Text, UniqueConstraint)
@@ -6,13 +6,14 @@ from sqlalchemy.orm import relationship
# Import Base from the central database module instead of creating a new one # Import Base from the central database module instead of creating a new one
from app.core.database import Base from app.core.database import Base
from models.database.base import TimestampMixin
class Product(Base): class MarketplaceProduct(Base, TimestampMixin):
__tablename__ = "products" __tablename__ = "marketplace_products"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
product_id = Column(String, unique=True, index=True, nullable=False) marketplace_product_id = Column(String, unique=True, index=True, nullable=False)
title = Column(String, nullable=False) title = Column(String, nullable=False)
description = Column(String) description = Column(String)
link = Column(String) link = Column(String)
@@ -56,16 +57,11 @@ class Product(Base):
) # Index for marketplace filtering ) # Index for marketplace filtering
shop_name = Column(String, index=True, nullable=True) # Index for shop 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) # Relationship to stock (one-to-many via GTIN)
stock_entries = relationship( stock_entries = relationship(
"Stock", "Stock",
foreign_keys="Stock.gtin", foreign_keys="Stock.gtin",
primaryjoin="Product.gtin == Stock.gtin", primaryjoin="MarketplaceProduct.gtin == Stock.gtin",
viewonly=True, viewonly=True,
) )
shop_products = relationship("ShopProduct", back_populates="product") shop_products = relationship("ShopProduct", back_populates="product")
@@ -82,6 +78,6 @@ class Product(Base):
def __repr__(self): def __repr__(self):
return ( return (
f"<Product(product_id='{self.product_id}', title='{self.title}', marketplace='{self.marketplace}', " f"<MarketplaceProduct(marketplace_product_id='{self.marketplace_product_id}', title='{self.title}', marketplace='{self.marketplace}', "
f"shop='{self.shop_name}')>" f"shop='{self.shop_name}')>"
) )

View File

@@ -6,9 +6,10 @@ from sqlalchemy.orm import relationship
# Import Base from the central database module instead of creating a new one # Import Base from the central database module instead of creating a new one
from app.core.database import Base from app.core.database import Base
from models.database.base import TimestampMixin
class Shop(Base): class Shop(Base, TimestampMixin):
__tablename__ = "shops" __tablename__ = "shops"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
@@ -32,10 +33,6 @@ class Shop(Base):
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
is_verified = Column(Boolean, default=False) 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 # Relationships
owner = relationship("User", back_populates="owned_shops") owner = relationship("User", back_populates="owned_shops")
shop_products = relationship("ShopProduct", back_populates="shop") shop_products = relationship("ShopProduct", back_populates="shop")
@@ -49,7 +46,7 @@ class ShopProduct(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
shop_id = Column(Integer, ForeignKey("shops.id"), nullable=False) shop_id = Column(Integer, ForeignKey("shops.id"), nullable=False)
product_id = Column(Integer, ForeignKey("products.id"), nullable=False) marketplace_product_id = Column(Integer, ForeignKey("marketplace_products.id"), nullable=False)
# Shop-specific overrides (can override the main product data) # Shop-specific overrides (can override the main product data)
shop_product_id = Column(String) # Shop's internal product ID shop_product_id = Column(String) # Shop's internal product ID
@@ -74,11 +71,11 @@ class ShopProduct(Base):
# Relationships # Relationships
shop = relationship("Shop", back_populates="shop_products") shop = relationship("Shop", back_populates="shop_products")
product = relationship("Product", back_populates="shop_products") product = relationship("MarketplaceProduct", back_populates="shop_products")
# Constraints # Constraints
__table_args__ = ( __table_args__ = (
UniqueConstraint("shop_id", "product_id", name="uq_shop_product"), UniqueConstraint("shop_id", "marketplace_product_id", name="uq_shop_product"),
Index("idx_shop_product_active", "shop_id", "is_active"), Index("idx_shop_product_active", "shop_id", "is_active"),
Index("idx_shop_product_featured", "shop_id", "is_featured"), Index("idx_shop_product_featured", "shop_id", "is_featured"),
) )

View File

@@ -6,9 +6,9 @@ from sqlalchemy.orm import relationship
# Import Base from the central database module instead of creating a new one # Import Base from the central database module instead of creating a new one
from app.core.database import Base from app.core.database import Base
from models.database.base import TimestampMixin
class Stock(Base, TimestampMixin):
class Stock(Base):
__tablename__ = "stock" __tablename__ = "stock"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
@@ -20,11 +20,6 @@ class Stock(Base):
reserved_quantity = Column(Integer, default=0) # For orders being processed reserved_quantity = Column(Integer, default=0) # For orders being processed
shop_id = Column(Integer, ForeignKey("shops.id")) # Optional: shop-specific stock 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 # Relationships
shop = relationship("Shop") shop = relationship("Shop")

View File

@@ -6,9 +6,9 @@ from sqlalchemy.orm import relationship
# Import Base from the central database module instead of creating a new one # Import Base from the central database module instead of creating a new one
from app.core.database import Base from app.core.database import Base
from models.database.base import TimestampMixin
class User(Base, TimestampMixin):
class User(Base):
__tablename__ = "users" __tablename__ = "users"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
@@ -18,10 +18,6 @@ class User(Base):
role = Column(String, nullable=False, default="user") # user, admin, shop_owner role = Column(String, nullable=False, default="user") # user, admin, shop_owner
is_active = Column(Boolean, default=True, nullable=False) is_active = Column(Boolean, default=True, nullable=False)
last_login = Column(DateTime, nullable=True) 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 # Relationships
marketplace_import_jobs = relationship( marketplace_import_jobs = relationship(

View File

@@ -1,13 +1,13 @@
# models/api/__init__.py # models/schemas/__init__.py
"""API models package - Pydantic models for request/response validation.""" """API models package - Pydantic models for request/response validation."""
# Import API model modules # Import API model modules
from . import base from . import base
from . import auth from . import auth
from . import product from . import marketplace_product
from . import stock from . import stock
from . import shop from . import shop
from . import marketplace from . import marketplace_import_job
from . import stats from . import stats
# Common imports for convenience # Common imports for convenience
@@ -16,9 +16,9 @@ from .base import * # Base Pydantic models
__all__ = [ __all__ = [
"base", "base",
"auth", "auth",
"product", "marketplace_product",
"stock", "stock",
"shop", "shop",
"marketplace", "marketplace_import_job",
"stats", "stats",
] ]

View File

@@ -1,9 +1,9 @@
# marketplace.py - Keep URL validation, remove business constraints # models/schemas/marketplace_import_job.py - Keep URL validation, remove business constraints
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
class MarketplaceImportRequest(BaseModel): class MarketplaceImportJobRequest(BaseModel):
url: str = Field(..., description="URL to CSV file from marketplace") url: str = Field(..., description="URL to CSV file from marketplace")
marketplace: str = Field(default="Letzshop", description="Marketplace name") marketplace: str = Field(default="Letzshop", description="Marketplace name")
shop_code: str = Field(..., description="Shop code to associate products with") shop_code: str = Field(..., description="Shop code to associate products with")

View File

@@ -1,11 +1,11 @@
# product.py - Simplified validation # models/schemas/marketplace_products.py - Simplified validation
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from models.schemas.stock import StockSummaryResponse from models.schemas.stock import StockSummaryResponse
class ProductBase(BaseModel): class MarketplaceProductBase(BaseModel):
product_id: Optional[str] = None marketplace_product_id: Optional[str] = None
title: Optional[str] = None title: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
link: Optional[str] = None link: Optional[str] = None
@@ -45,27 +45,27 @@ class ProductBase(BaseModel):
marketplace: Optional[str] = None marketplace: Optional[str] = None
shop_name: Optional[str] = None shop_name: Optional[str] = None
class ProductCreate(ProductBase): class MarketplaceProductCreate(MarketplaceProductBase):
product_id: str = Field(..., description="Product identifier") marketplace_product_id: str = Field(..., description="MarketplaceProduct identifier")
title: str = Field(..., description="Product title") title: str = Field(..., description="MarketplaceProduct title")
# Removed: min_length constraints and custom validators # Removed: min_length constraints and custom validators
# Service will handle empty string validation with proper domain exceptions # Service will handle empty string validation with proper domain exceptions
class ProductUpdate(ProductBase): class MarketplaceProductUpdate(MarketplaceProductBase):
pass pass
class ProductResponse(ProductBase): class MarketplaceProductResponse(MarketplaceProductBase):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
id: int id: int
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
class ProductListResponse(BaseModel): class MarketplaceProductListResponse(BaseModel):
products: List[ProductResponse] products: List[MarketplaceProductResponse]
total: int total: int
skip: int skip: int
limit: int limit: int
class ProductDetailResponse(BaseModel): class MarketplaceProductDetailResponse(BaseModel):
product: ProductResponse product: MarketplaceProductResponse
stock_info: Optional[StockSummaryResponse] = None stock_info: Optional[StockSummaryResponse] = None

View File

@@ -3,7 +3,7 @@ import re
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic import BaseModel, ConfigDict, Field, field_validator
from models.schemas.product import ProductResponse from models.schemas.marketplace_product import MarketplaceProductResponse
class ShopCreate(BaseModel): class ShopCreate(BaseModel):
shop_code: str = Field(..., description="Unique shop identifier") shop_code: str = Field(..., description="Unique shop identifier")
@@ -64,7 +64,7 @@ class ShopListResponse(BaseModel):
limit: int limit: int
class ShopProductCreate(BaseModel): class ShopProductCreate(BaseModel):
product_id: str = Field(..., description="Product ID to add to shop") marketplace_product_id: str = Field(..., description="MarketplaceProduct ID to add to shop")
shop_product_id: Optional[str] = None shop_product_id: Optional[str] = None
shop_price: Optional[float] = None # Removed: ge=0 constraint shop_price: Optional[float] = None # Removed: ge=0 constraint
shop_sale_price: Optional[float] = None # Removed: ge=0 constraint shop_sale_price: Optional[float] = None # Removed: ge=0 constraint
@@ -80,7 +80,7 @@ class ShopProductResponse(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
id: int id: int
shop_id: int shop_id: int
product: ProductResponse product: MarketplaceProductResponse
shop_product_id: Optional[str] shop_product_id: Optional[str]
shop_price: Optional[float] shop_price: Optional[float]
shop_sale_price: Optional[float] shop_sale_price: Optional[float]

View File

@@ -131,10 +131,10 @@ def verify_model_structure():
# Import specific models # Import specific models
from models.database.user import User from models.database.user import User
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
from models.database.stock import Stock from models.database.stock import Stock
from models.database.shop import Shop, ShopProduct from models.database.shop import Shop, ShopProduct
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
print("[OK] All database models imported successfully") print("[OK] All database models imported successfully")
@@ -171,7 +171,7 @@ def check_project_structure():
critical_paths = [ critical_paths = [
"models/database/base.py", "models/database/base.py",
"models/database/user.py", "models/database/user.py",
"models/database/product.py", "models/database/marketplace_products.py",
"models/database/stock.py", "models/database/stock.py",
"app/core/config.py", "app/core/config.py",
"alembic/env.py", "alembic/env.py",

Binary file not shown.

View File

@@ -8,8 +8,8 @@ from sqlalchemy.pool import StaticPool
from app.core.database import Base, get_db from app.core.database import Base, get_db
from main import app from main import app
# Import all models to ensure they're registered with Base metadata # Import all models to ensure they're registered with Base metadata
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
from models.database.shop import Shop, ShopProduct from models.database.shop import Shop, ShopProduct
from models.database.stock import Stock from models.database.stock import Stock
from models.database.user import User from models.database.user import User
@@ -87,8 +87,8 @@ def cleanup():
# Import fixtures from fixture modules # Import fixtures from fixture modules
pytest_plugins = [ pytest_plugins = [
"tests.fixtures.auth_fixtures", "tests.fixtures.auth_fixtures",
"tests.fixtures.product_fixtures", "tests.fixtures.marketplace_product_fixtures",
"tests.fixtures.shop_fixtures", "tests.fixtures.shop_fixtures",
"tests.fixtures.marketplace_fixtures", "tests.fixtures.marketplace_import_job_fixtures",
"tests.fixtures.testing_fixtures", "tests.fixtures.testing_fixtures",
] ]

View File

@@ -1,11 +1,11 @@
# tests/fixtures/marketplace_fixtures.py # tests/fixtures/marketplace_import_job_fixtures.py
import pytest import pytest
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
@pytest.fixture @pytest.fixture
def test_marketplace_job(db, test_shop, test_user): def test_marketplace_import_job(db, test_shop, test_user):
"""Create a test marketplace import job""" """Create a test marketplace import job"""
job = MarketplaceImportJob( job = MarketplaceImportJob(
marketplace="amazon", marketplace="amazon",
@@ -26,7 +26,7 @@ def test_marketplace_job(db, test_shop, test_user):
return job return job
def create_test_import_job(db, shop_id, user_id, **kwargs): def create_test_marketplace_import_job(db, shop_id, user_id, **kwargs):
"""Helper function to create MarketplaceImportJob with defaults""" """Helper function to create MarketplaceImportJob with defaults"""
defaults = { defaults = {
"marketplace": "test", "marketplace": "test",

View File

@@ -1,17 +1,17 @@
# tests/fixtures/product_fixtures.py # tests/fixtures/marketplace_product_fixtures.py
import uuid import uuid
import pytest import pytest
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
@pytest.fixture @pytest.fixture
def test_product(db): def test_marketplace_product(db):
"""Create a test product""" """Create a test product"""
product = Product( marketplace_product = MarketplaceProduct(
product_id="TEST001", marketplace_product_id="TEST001",
title="Test Product", title="Test MarketplaceProduct",
description="A test product", description="A test product",
price="10.99", price="10.99",
currency="EUR", currency="EUR",
@@ -21,19 +21,19 @@ def test_product(db):
marketplace="Letzshop", marketplace="Letzshop",
shop_name="TestShop", shop_name="TestShop",
) )
db.add(product) db.add(marketplace_product)
db.commit() db.commit()
db.refresh(product) db.refresh(marketplace_product)
return product return marketplace_product
@pytest.fixture @pytest.fixture
def unique_product(db): def unique_product(db):
"""Create a unique product for tests that need isolated product data""" """Create a unique product for tests that need isolated product data"""
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]
product = Product( marketplace_product = MarketplaceProduct(
product_id=f"UNIQUE_{unique_id}", marketplace_product_id=f"UNIQUE_{unique_id}",
title=f"Unique Product {unique_id}", title=f"Unique MarketplaceProduct {unique_id}",
description=f"A unique test product {unique_id}", description=f"A unique test product {unique_id}",
price="19.99", price="19.99",
currency="EUR", currency="EUR",
@@ -44,22 +44,22 @@ def unique_product(db):
shop_name=f"UniqueShop_{unique_id}", shop_name=f"UniqueShop_{unique_id}",
google_product_category=f"UniqueCategory_{unique_id}", google_product_category=f"UniqueCategory_{unique_id}",
) )
db.add(product) db.add(marketplace_product)
db.commit() db.commit()
db.refresh(product) db.refresh(marketplace_product)
return product return marketplace_product
@pytest.fixture @pytest.fixture
def multiple_products(db): def multiple_products(db):
"""Create multiple products for testing statistics and pagination""" """Create multiple products for testing statistics and pagination"""
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]
products = [] marketplace_products = []
for i in range(5): for i in range(5):
product = Product( marketplace_product = MarketplaceProduct(
product_id=f"MULTI_{unique_id}_{i}", marketplace_product_id=f"MULTI_{unique_id}_{i}",
title=f"Multi Product {i} {unique_id}", title=f"Multi MarketplaceProduct {i} {unique_id}",
description=f"Multi test product {i}", description=f"Multi test product {i}",
price=f"{10 + i}.99", price=f"{10 + i}.99",
currency="EUR", currency="EUR",
@@ -69,23 +69,23 @@ def multiple_products(db):
google_product_category=f"MultiCategory_{i % 2}", # Create 2 different categories google_product_category=f"MultiCategory_{i % 2}", # Create 2 different categories
gtin=f"1234567890{i}{unique_id[:2]}", gtin=f"1234567890{i}{unique_id[:2]}",
) )
products.append(product) marketplace_products.append(marketplace_product)
db.add_all(products) db.add_all(marketplace_products)
db.commit() db.commit()
for product in products: for product in marketplace_products:
db.refresh(product) db.refresh(product)
return products return marketplace_products
def create_unique_product_factory(): def create_unique_marketplace_product_factory():
"""Factory function to create unique products in tests""" """Factory function to create unique products in tests"""
def _create_product(db, **kwargs): def _marketplace_create_product(db, **kwargs):
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]
defaults = { defaults = {
"product_id": f"FACTORY_{unique_id}", "marketplace_product_id": f"FACTORY_{unique_id}",
"title": f"Factory Product {unique_id}", "title": f"Factory MarketplaceProduct {unique_id}",
"price": "15.99", "price": "15.99",
"currency": "EUR", "currency": "EUR",
"marketplace": "TestMarket", "marketplace": "TestMarket",
@@ -93,31 +93,31 @@ def create_unique_product_factory():
} }
defaults.update(kwargs) defaults.update(kwargs)
product = Product(**defaults) marketplace_product = MarketplaceProduct(**defaults)
db.add(product) db.add(marketplace_product)
db.commit() db.commit()
db.refresh(product) db.refresh(marketplace_product)
return product return marketplace_product
return _create_product return _marketplace_create_product
@pytest.fixture @pytest.fixture
def product_factory(): def marketplace_product_factory():
"""Fixture that provides a product factory function""" """Fixture that provides a product factory function"""
return create_unique_product_factory() return create_unique_marketplace_product_factory()
@pytest.fixture @pytest.fixture
def test_product_with_stock(db, test_product, test_stock): def test_marketplace_product_with_stock(db, test_marketplace_product, test_stock):
"""Product with associated stock record.""" """MarketplaceProduct with associated stock record."""
# Ensure they're linked by GTIN # Ensure they're linked by GTIN
if test_product.gtin != test_stock.gtin: if test_marketplace_product.gtin != test_stock.gtin:
test_stock.gtin = test_product.gtin test_stock.gtin = test_marketplace_product.gtin
db.commit() db.commit()
db.refresh(test_stock) db.refresh(test_stock)
return { return {
'product': test_product, 'marketplace_product': test_marketplace_product,
'stock': test_stock 'stock': test_stock
} }

View File

@@ -80,7 +80,7 @@ def verified_shop(db, other_user):
def shop_product(db, test_shop, unique_product): def shop_product(db, test_shop, unique_product):
"""Create a shop product relationship""" """Create a shop product relationship"""
shop_product = ShopProduct( shop_product = ShopProduct(
shop_id=test_shop.id, product_id=unique_product.id, is_active=True shop_id=test_shop.id, marketplace_product_id=unique_product.id, is_active=True
) )
# Add optional fields if they exist in your model # Add optional fields if they exist in your model
if hasattr(ShopProduct, "shop_price"): if hasattr(ShopProduct, "shop_price"):
@@ -97,11 +97,11 @@ def shop_product(db, test_shop, unique_product):
@pytest.fixture @pytest.fixture
def test_stock(db, test_product, test_shop): def test_stock(db, test_marketplace_product, test_shop):
"""Create test stock entry""" """Create test stock entry"""
unique_id = str(uuid.uuid4())[:8].upper() # Short unique identifier unique_id = str(uuid.uuid4())[:8].upper() # Short unique identifier
stock = Stock( stock = Stock(
gtin=test_product.gtin, # Use gtin instead of product_id gtin=test_marketplace_product.gtin, # Use gtin instead of marketplace_product_id
location=f"WAREHOUSE_A_{unique_id}", location=f"WAREHOUSE_A_{unique_id}",
quantity=10, quantity=10,
reserved_quantity=0, reserved_quantity=0,

View File

@@ -137,7 +137,7 @@ class TestAdminAPI:
assert data["error_code"] == "SHOP_NOT_FOUND" assert data["error_code"] == "SHOP_NOT_FOUND"
def test_get_marketplace_import_jobs_admin( def test_get_marketplace_import_jobs_admin(
self, client, admin_headers, test_marketplace_job self, client, admin_headers, test_marketplace_import_job
): ):
"""Test admin getting marketplace import jobs""" """Test admin getting marketplace import jobs"""
response = client.get( response = client.get(
@@ -148,17 +148,17 @@ class TestAdminAPI:
data = response.json() data = response.json()
assert len(data) >= 1 assert len(data) >= 1
# Check that test_marketplace_job is in the response # Check that test_marketplace_import_job is in the response
job_ids = [job["job_id"] for job in data if "job_id" in job] job_ids = [job["job_id"] for job in data if "job_id" in job]
assert test_marketplace_job.id in job_ids assert test_marketplace_import_job.id in job_ids
def test_get_marketplace_import_jobs_with_filters( def test_get_marketplace_import_jobs_with_filters(
self, client, admin_headers, test_marketplace_job self, client, admin_headers, test_marketplace_import_job
): ):
"""Test admin getting marketplace import jobs with filters""" """Test admin getting marketplace import jobs with filters"""
response = client.get( response = client.get(
"/api/v1/admin/marketplace-import-jobs", "/api/v1/admin/marketplace-import-jobs",
params={"marketplace": test_marketplace_job.marketplace}, params={"marketplace": test_marketplace_import_job.marketplace},
headers=admin_headers, headers=admin_headers,
) )
@@ -166,7 +166,7 @@ class TestAdminAPI:
data = response.json() data = response.json()
assert len(data) >= 1 assert len(data) >= 1
assert all( assert all(
job["marketplace"] == test_marketplace_job.marketplace for job in data job["marketplace"] == test_marketplace_import_job.marketplace for job in data
) )
def test_get_marketplace_import_jobs_non_admin(self, client, auth_headers): def test_get_marketplace_import_jobs_non_admin(self, client, auth_headers):

View File

@@ -1,12 +1,13 @@
# tests/integration/api/v1/test_filtering.py # tests/integration/api/v1/test_filtering.py
import pytest import pytest
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.api @pytest.mark.api
@pytest.mark.products @pytest.mark.products
@pytest.mark.marketplace
class TestFiltering: class TestFiltering:
def test_product_brand_filter_success(self, client, auth_headers, db): def test_product_brand_filter_success(self, client, auth_headers, db):
@@ -16,27 +17,27 @@ class TestFiltering:
unique_suffix = str(uuid.uuid4())[:8] unique_suffix = str(uuid.uuid4())[:8]
products = [ products = [
Product(product_id=f"BRAND1_{unique_suffix}", title="Product 1", brand="BrandA"), MarketplaceProduct(marketplace_product_id=f"BRAND1_{unique_suffix}", title="MarketplaceProduct 1", brand="BrandA"),
Product(product_id=f"BRAND2_{unique_suffix}", title="Product 2", brand="BrandB"), MarketplaceProduct(marketplace_product_id=f"BRAND2_{unique_suffix}", title="MarketplaceProduct 2", brand="BrandB"),
Product(product_id=f"BRAND3_{unique_suffix}", title="Product 3", brand="BrandA"), MarketplaceProduct(marketplace_product_id=f"BRAND3_{unique_suffix}", title="MarketplaceProduct 3", brand="BrandA"),
] ]
db.add_all(products) db.add_all(products)
db.commit() db.commit()
# Filter by BrandA # Filter by BrandA
response = client.get("/api/v1/product?brand=BrandA", headers=auth_headers) response = client.get("/api/v1/marketplace/product?brand=BrandA", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] >= 2 # At least our test products assert data["total"] >= 2 # At least our test products
# Verify all returned products have BrandA # Verify all returned products have BrandA
for product in data["products"]: for product in data["products"]:
if product["product_id"].endswith(unique_suffix): if product["marketplace_product_id"].endswith(unique_suffix):
assert product["brand"] == "BrandA" assert product["brand"] == "BrandA"
# Filter by BrandB # Filter by BrandB
response = client.get("/api/v1/product?brand=BrandB", headers=auth_headers) response = client.get("/api/v1/marketplace/product?brand=BrandB", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] >= 1 # At least our test product assert data["total"] >= 1 # At least our test product
@@ -47,21 +48,21 @@ class TestFiltering:
unique_suffix = str(uuid.uuid4())[:8] unique_suffix = str(uuid.uuid4())[:8]
products = [ products = [
Product(product_id=f"MKT1_{unique_suffix}", title="Product 1", marketplace="Amazon"), MarketplaceProduct(marketplace_product_id=f"MKT1_{unique_suffix}", title="MarketplaceProduct 1", marketplace="Amazon"),
Product(product_id=f"MKT2_{unique_suffix}", title="Product 2", marketplace="eBay"), MarketplaceProduct(marketplace_product_id=f"MKT2_{unique_suffix}", title="MarketplaceProduct 2", marketplace="eBay"),
Product(product_id=f"MKT3_{unique_suffix}", title="Product 3", marketplace="Amazon"), MarketplaceProduct(marketplace_product_id=f"MKT3_{unique_suffix}", title="MarketplaceProduct 3", marketplace="Amazon"),
] ]
db.add_all(products) db.add_all(products)
db.commit() db.commit()
response = client.get("/api/v1/product?marketplace=Amazon", headers=auth_headers) response = client.get("/api/v1/marketplace/product?marketplace=Amazon", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] >= 2 # At least our test products assert data["total"] >= 2 # At least our test products
# Verify all returned products have Amazon marketplace # Verify all returned products have Amazon marketplace
amazon_products = [p for p in data["products"] if p["product_id"].endswith(unique_suffix)] amazon_products = [p for p in data["products"] if p["marketplace_product_id"].endswith(unique_suffix)]
for product in amazon_products: for product in amazon_products:
assert product["marketplace"] == "Amazon" assert product["marketplace"] == "Amazon"
@@ -71,18 +72,18 @@ class TestFiltering:
unique_suffix = str(uuid.uuid4())[:8] unique_suffix = str(uuid.uuid4())[:8]
products = [ products = [
Product( MarketplaceProduct(
product_id=f"SEARCH1_{unique_suffix}", marketplace_product_id=f"SEARCH1_{unique_suffix}",
title=f"Apple iPhone {unique_suffix}", title=f"Apple iPhone {unique_suffix}",
description="Smartphone" description="Smartphone"
), ),
Product( MarketplaceProduct(
product_id=f"SEARCH2_{unique_suffix}", marketplace_product_id=f"SEARCH2_{unique_suffix}",
title=f"Samsung Galaxy {unique_suffix}", title=f"Samsung Galaxy {unique_suffix}",
description="Android phone", description="Android phone",
), ),
Product( MarketplaceProduct(
product_id=f"SEARCH3_{unique_suffix}", marketplace_product_id=f"SEARCH3_{unique_suffix}",
title=f"iPad Tablet {unique_suffix}", title=f"iPad Tablet {unique_suffix}",
description="Apple tablet" description="Apple tablet"
), ),
@@ -92,13 +93,13 @@ class TestFiltering:
db.commit() db.commit()
# Search for "Apple" # Search for "Apple"
response = client.get(f"/api/v1/product?search=Apple", headers=auth_headers) response = client.get(f"/api/v1/marketplace/product?search=Apple", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] >= 2 # iPhone and iPad assert data["total"] >= 2 # iPhone and iPad
# Search for "phone" # Search for "phone"
response = client.get(f"/api/v1/product?search=phone", headers=auth_headers) response = client.get(f"/api/v1/marketplace/product?search=phone", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] >= 2 # iPhone and Galaxy assert data["total"] >= 2 # iPhone and Galaxy
@@ -109,20 +110,20 @@ class TestFiltering:
unique_suffix = str(uuid.uuid4())[:8] unique_suffix = str(uuid.uuid4())[:8]
products = [ products = [
Product( MarketplaceProduct(
product_id=f"COMBO1_{unique_suffix}", marketplace_product_id=f"COMBO1_{unique_suffix}",
title=f"Apple iPhone {unique_suffix}", title=f"Apple iPhone {unique_suffix}",
brand="Apple", brand="Apple",
marketplace="Amazon", marketplace="Amazon",
), ),
Product( MarketplaceProduct(
product_id=f"COMBO2_{unique_suffix}", marketplace_product_id=f"COMBO2_{unique_suffix}",
title=f"Apple iPad {unique_suffix}", title=f"Apple iPad {unique_suffix}",
brand="Apple", brand="Apple",
marketplace="eBay", marketplace="eBay",
), ),
Product( MarketplaceProduct(
product_id=f"COMBO3_{unique_suffix}", marketplace_product_id=f"COMBO3_{unique_suffix}",
title=f"Samsung Phone {unique_suffix}", title=f"Samsung Phone {unique_suffix}",
brand="Samsung", brand="Samsung",
marketplace="Amazon", marketplace="Amazon",
@@ -134,14 +135,14 @@ class TestFiltering:
# Filter by brand AND marketplace # Filter by brand AND marketplace
response = client.get( response = client.get(
"/api/v1/product?brand=Apple&marketplace=Amazon", headers=auth_headers "/api/v1/marketplace/product?brand=Apple&marketplace=Amazon", headers=auth_headers
) )
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] >= 1 # At least iPhone matches both assert data["total"] >= 1 # At least iPhone matches both
# Find our specific test product # Find our specific test product
matching_products = [p for p in data["products"] if p["product_id"].endswith(unique_suffix)] matching_products = [p for p in data["products"] if p["marketplace_product_id"].endswith(unique_suffix)]
for product in matching_products: for product in matching_products:
assert product["brand"] == "Apple" assert product["brand"] == "Apple"
assert product["marketplace"] == "Amazon" assert product["marketplace"] == "Amazon"
@@ -149,7 +150,7 @@ class TestFiltering:
def test_filter_with_no_results(self, client, auth_headers): def test_filter_with_no_results(self, client, auth_headers):
"""Test filtering with criteria that returns no results""" """Test filtering with criteria that returns no results"""
response = client.get( response = client.get(
"/api/v1/product?brand=NonexistentBrand123456", headers=auth_headers "/api/v1/marketplace/product?brand=NonexistentBrand123456", headers=auth_headers
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -162,9 +163,9 @@ class TestFiltering:
import uuid import uuid
unique_suffix = str(uuid.uuid4())[:8] unique_suffix = str(uuid.uuid4())[:8]
product = Product( product = MarketplaceProduct(
product_id=f"CASE_{unique_suffix}", marketplace_product_id=f"CASE_{unique_suffix}",
title="Test Product", title="Test MarketplaceProduct",
brand="TestBrand", brand="TestBrand",
marketplace="TestMarket", marketplace="TestMarket",
) )
@@ -173,7 +174,7 @@ class TestFiltering:
# Test different case variations # Test different case variations
for brand_filter in ["TestBrand", "testbrand", "TESTBRAND"]: for brand_filter in ["TestBrand", "testbrand", "TESTBRAND"]:
response = client.get(f"/api/v1/product?brand={brand_filter}", headers=auth_headers) response = client.get(f"/api/v1/marketplace/product?brand={brand_filter}", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] >= 1 assert data["total"] >= 1
@@ -182,9 +183,9 @@ class TestFiltering:
"""Test behavior with invalid filter parameters""" """Test behavior with invalid filter parameters"""
# Test with very long filter values # Test with very long filter values
long_brand = "A" * 1000 long_brand = "A" * 1000
response = client.get(f"/api/v1/product?brand={long_brand}", headers=auth_headers) response = client.get(f"/api/v1/marketplace/product?brand={long_brand}", headers=auth_headers)
assert response.status_code == 200 # Should handle gracefully assert response.status_code == 200 # Should handle gracefully
# Test with special characters # Test with special characters
response = client.get("/api/v1/product?brand=<script>alert('test')</script>", headers=auth_headers) response = client.get("/api/v1/marketplace/product?brand=<script>alert('test')</script>", headers=auth_headers)
assert response.status_code == 200 # Should handle gracefully assert response.status_code == 200 # Should handle gracefully

View File

@@ -1,4 +1,4 @@
# tests/integration/api/v1/test_marketplace_endpoints.py # tests/integration/api/v1/test_marketplace_import_job_endpoints.py
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -7,7 +7,7 @@ import pytest
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.api @pytest.mark.api
@pytest.mark.marketplace @pytest.mark.marketplace
class TestMarketplaceAPI: class TestMarketplaceImportJobAPI:
def test_import_from_marketplace(self, client, auth_headers, test_shop, test_user): def test_import_from_marketplace(self, client, auth_headers, test_shop, test_user):
"""Test marketplace import endpoint - just test job creation""" """Test marketplace import endpoint - just test job creation"""
# Ensure user owns the shop # Ensure user owns the shop
@@ -102,18 +102,18 @@ class TestMarketplaceAPI:
assert data["marketplace"] == "AdminMarket" assert data["marketplace"] == "AdminMarket"
assert data["shop_code"] == test_shop.shop_code assert data["shop_code"] == test_shop.shop_code
def test_get_marketplace_import_status(self, client, auth_headers, test_marketplace_job): def test_get_marketplace_import_status(self, client, auth_headers, test_marketplace_import_job):
"""Test getting marketplace import status""" """Test getting marketplace import status"""
response = client.get( response = client.get(
f"/api/v1/marketplace/import-status/{test_marketplace_job.id}", f"/api/v1/marketplace/import-status/{test_marketplace_import_job.id}",
headers=auth_headers headers=auth_headers
) )
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["job_id"] == test_marketplace_job.id assert data["job_id"] == test_marketplace_import_job.id
assert data["status"] == test_marketplace_job.status assert data["status"] == test_marketplace_import_job.status
assert data["marketplace"] == test_marketplace_job.marketplace assert data["marketplace"] == test_marketplace_import_job.marketplace
def test_get_marketplace_import_status_not_found(self, client, auth_headers): def test_get_marketplace_import_status_not_found(self, client, auth_headers):
"""Test getting status of non-existent import job""" """Test getting status of non-existent import job"""
@@ -127,13 +127,13 @@ class TestMarketplaceAPI:
assert data["error_code"] == "IMPORT_JOB_NOT_FOUND" assert data["error_code"] == "IMPORT_JOB_NOT_FOUND"
assert "99999" in data["message"] assert "99999" in data["message"]
def test_get_marketplace_import_status_unauthorized(self, client, auth_headers, test_marketplace_job, other_user): def test_get_marketplace_import_status_unauthorized(self, client, auth_headers, test_marketplace_import_job, other_user):
"""Test getting status of unauthorized import job""" """Test getting status of unauthorized import job"""
# Change job owner to other user # Change job owner to other user
test_marketplace_job.user_id = other_user.id test_marketplace_import_job.user_id = other_user.id
response = client.get( response = client.get(
f"/api/v1/marketplace/import-status/{test_marketplace_job.id}", f"/api/v1/marketplace/import-status/{test_marketplace_import_job.id}",
headers=auth_headers headers=auth_headers
) )
@@ -141,7 +141,7 @@ class TestMarketplaceAPI:
data = response.json() data = response.json()
assert data["error_code"] == "IMPORT_JOB_NOT_OWNED" assert data["error_code"] == "IMPORT_JOB_NOT_OWNED"
def test_get_marketplace_import_jobs(self, client, auth_headers, test_marketplace_job): def test_get_marketplace_import_jobs(self, client, auth_headers, test_marketplace_import_job):
"""Test getting marketplace import jobs""" """Test getting marketplace import jobs"""
response = client.get("/api/v1/marketplace/import-jobs", headers=auth_headers) response = client.get("/api/v1/marketplace/import-jobs", headers=auth_headers)
@@ -152,12 +152,12 @@ class TestMarketplaceAPI:
# Find our test job in the results # Find our test job in the results
job_ids = [job["job_id"] for job in data] job_ids = [job["job_id"] for job in data]
assert test_marketplace_job.id in job_ids assert test_marketplace_import_job.id in job_ids
def test_get_marketplace_import_jobs_with_filters(self, client, auth_headers, test_marketplace_job): def test_get_marketplace_import_jobs_with_filters(self, client, auth_headers, test_marketplace_import_job):
"""Test getting import jobs with filters""" """Test getting import jobs with filters"""
response = client.get( response = client.get(
f"/api/v1/marketplace/import-jobs?marketplace={test_marketplace_job.marketplace}", f"/api/v1/marketplace/import-jobs?marketplace={test_marketplace_import_job.marketplace}",
headers=auth_headers headers=auth_headers
) )
@@ -167,7 +167,7 @@ class TestMarketplaceAPI:
assert len(data) >= 1 assert len(data) >= 1
for job in data: for job in data:
assert test_marketplace_job.marketplace.lower() in job["marketplace"].lower() assert test_marketplace_import_job.marketplace.lower() in job["marketplace"].lower()
def test_get_marketplace_import_jobs_pagination(self, client, auth_headers): def test_get_marketplace_import_jobs_pagination(self, client, auth_headers):
"""Test import jobs pagination""" """Test import jobs pagination"""
@@ -181,7 +181,7 @@ class TestMarketplaceAPI:
assert isinstance(data, list) assert isinstance(data, list)
assert len(data) <= 5 assert len(data) <= 5
def test_get_marketplace_import_stats(self, client, auth_headers, test_marketplace_job): def test_get_marketplace_import_stats(self, client, auth_headers, test_marketplace_import_job):
"""Test getting marketplace import statistics""" """Test getting marketplace import statistics"""
response = client.get("/api/v1/marketplace/marketplace-import-stats", headers=auth_headers) response = client.get("/api/v1/marketplace/marketplace-import-stats", headers=auth_headers)
@@ -198,7 +198,7 @@ class TestMarketplaceAPI:
def test_cancel_marketplace_import_job(self, client, auth_headers, test_user, test_shop, db): def test_cancel_marketplace_import_job(self, client, auth_headers, test_user, test_shop, db):
"""Test cancelling a marketplace import job""" """Test cancelling a marketplace import job"""
# Create a pending job that can be cancelled # Create a pending job that can be cancelled
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
import uuid import uuid
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]
@@ -240,14 +240,14 @@ class TestMarketplaceAPI:
data = response.json() data = response.json()
assert data["error_code"] == "IMPORT_JOB_NOT_FOUND" assert data["error_code"] == "IMPORT_JOB_NOT_FOUND"
def test_cancel_marketplace_import_job_cannot_cancel(self, client, auth_headers, test_marketplace_job, db): def test_cancel_marketplace_import_job_cannot_cancel(self, client, auth_headers, test_marketplace_import_job, db):
"""Test cancelling a job that cannot be cancelled""" """Test cancelling a job that cannot be cancelled"""
# Set job to completed status # Set job to completed status
test_marketplace_job.status = "completed" test_marketplace_import_job.status = "completed"
db.commit() db.commit()
response = client.put( response = client.put(
f"/api/v1/marketplace/import-jobs/{test_marketplace_job.id}/cancel", f"/api/v1/marketplace/import-jobs/{test_marketplace_import_job.id}/cancel",
headers=auth_headers headers=auth_headers
) )
@@ -259,7 +259,7 @@ class TestMarketplaceAPI:
def test_delete_marketplace_import_job(self, client, auth_headers, test_user, test_shop, db): def test_delete_marketplace_import_job(self, client, auth_headers, test_user, test_shop, db):
"""Test deleting a marketplace import job""" """Test deleting a marketplace import job"""
# Create a completed job that can be deleted # Create a completed job that can be deleted
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
import uuid import uuid
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]
@@ -302,7 +302,7 @@ class TestMarketplaceAPI:
def test_delete_marketplace_import_job_cannot_delete(self, client, auth_headers, test_user, test_shop, db): def test_delete_marketplace_import_job_cannot_delete(self, client, auth_headers, test_user, test_shop, db):
"""Test deleting a job that cannot be deleted""" """Test deleting a job that cannot be deleted"""
# Create a pending job that cannot be deleted # Create a pending job that cannot be deleted
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
import uuid import uuid
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]
@@ -352,7 +352,7 @@ class TestMarketplaceAPI:
data = response.json() data = response.json()
assert data["error_code"] == "INVALID_TOKEN" assert data["error_code"] == "INVALID_TOKEN"
def test_admin_can_access_all_jobs(self, client, admin_headers, test_marketplace_job): def test_admin_can_access_all_jobs(self, client, admin_headers, test_marketplace_import_job):
"""Test that admin can access all import jobs""" """Test that admin can access all import jobs"""
response = client.get("/api/v1/marketplace/import-jobs", headers=admin_headers) response = client.get("/api/v1/marketplace/import-jobs", headers=admin_headers)
@@ -361,23 +361,23 @@ class TestMarketplaceAPI:
assert isinstance(data, list) assert isinstance(data, list)
# Admin should see all jobs, including the test job # Admin should see all jobs, including the test job
job_ids = [job["job_id"] for job in data] job_ids = [job["job_id"] for job in data]
assert test_marketplace_job.id in job_ids assert test_marketplace_import_job.id in job_ids
def test_admin_can_view_any_job_status(self, client, admin_headers, test_marketplace_job): def test_admin_can_view_any_job_status(self, client, admin_headers, test_marketplace_import_job):
"""Test that admin can view any job status""" """Test that admin can view any job status"""
response = client.get( response = client.get(
f"/api/v1/marketplace/import-status/{test_marketplace_job.id}", f"/api/v1/marketplace/import-status/{test_marketplace_import_job.id}",
headers=admin_headers headers=admin_headers
) )
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["job_id"] == test_marketplace_job.id assert data["job_id"] == test_marketplace_import_job.id
def test_admin_can_cancel_any_job(self, client, admin_headers, test_user, test_shop, db): def test_admin_can_cancel_any_job(self, client, admin_headers, test_user, test_shop, db):
"""Test that admin can cancel any job""" """Test that admin can cancel any job"""
# Create a pending job owned by different user # Create a pending job owned by different user
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
import uuid import uuid
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]

View File

@@ -5,7 +5,7 @@ import uuid
import pytest import pytest
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
@pytest.mark.integration @pytest.mark.integration
@@ -13,9 +13,9 @@ from models.database.product import Product
@pytest.mark.performance # for the performance test @pytest.mark.performance # for the performance test
class TestExportFunctionality: class TestExportFunctionality:
def test_csv_export_basic_success(self, client, auth_headers, test_product): def test_csv_export_basic_success(self, client, auth_headers, test_marketplace_product):
"""Test basic CSV export functionality successfully""" """Test basic CSV export functionality successfully"""
response = client.get("/api/v1/product/export-csv", headers=auth_headers) response = client.get("/api/v1/marketplace/product/export-csv", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8" assert response.headers["content-type"] == "text/csv; charset=utf-8"
@@ -26,13 +26,13 @@ class TestExportFunctionality:
# Check header row # Check header row
header = next(csv_reader) header = next(csv_reader)
expected_fields = ["product_id", "title", "description", "price", "marketplace"] expected_fields = ["marketplace_product_id", "title", "description", "price", "marketplace"]
for field in expected_fields: for field in expected_fields:
assert field in header assert field in header
# Verify test product appears in export # Verify test product appears in export
csv_lines = csv_content.split('\n') csv_lines = csv_content.split('\n')
test_product_found = any(test_product.product_id in line for line in csv_lines) test_product_found = any(test_marketplace_product.marketplace_product_id in line for line in csv_lines)
assert test_product_found, "Test product should appear in CSV export" assert test_product_found, "Test product should appear in CSV export"
def test_csv_export_with_marketplace_filter_success(self, client, auth_headers, db): def test_csv_export_with_marketplace_filter_success(self, client, auth_headers, db):
@@ -40,14 +40,14 @@ class TestExportFunctionality:
# Create products in different marketplaces with unique IDs # Create products in different marketplaces with unique IDs
unique_suffix = str(uuid.uuid4())[:8] unique_suffix = str(uuid.uuid4())[:8]
products = [ products = [
Product( MarketplaceProduct(
product_id=f"EXP1_{unique_suffix}", marketplace_product_id=f"EXP1_{unique_suffix}",
title=f"Amazon Product {unique_suffix}", title=f"Amazon MarketplaceProduct {unique_suffix}",
marketplace="Amazon" marketplace="Amazon"
), ),
Product( MarketplaceProduct(
product_id=f"EXP2_{unique_suffix}", marketplace_product_id=f"EXP2_{unique_suffix}",
title=f"eBay Product {unique_suffix}", title=f"eBay MarketplaceProduct {unique_suffix}",
marketplace="eBay" marketplace="eBay"
), ),
] ]
@@ -56,7 +56,7 @@ class TestExportFunctionality:
db.commit() db.commit()
response = client.get( response = client.get(
"/api/v1/product/export-csv?marketplace=Amazon", headers=auth_headers "/api/v1/marketplace/product/export-csv?marketplace=Amazon", headers=auth_headers
) )
assert response.status_code == 200 assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8" assert response.headers["content-type"] == "text/csv; charset=utf-8"
@@ -69,14 +69,14 @@ class TestExportFunctionality:
"""Test CSV export with shop name filtering successfully""" """Test CSV export with shop name filtering successfully"""
unique_suffix = str(uuid.uuid4())[:8] unique_suffix = str(uuid.uuid4())[:8]
products = [ products = [
Product( MarketplaceProduct(
product_id=f"SHOP1_{unique_suffix}", marketplace_product_id=f"SHOP1_{unique_suffix}",
title=f"Shop1 Product {unique_suffix}", title=f"Shop1 MarketplaceProduct {unique_suffix}",
shop_name="TestShop1" shop_name="TestShop1"
), ),
Product( MarketplaceProduct(
product_id=f"SHOP2_{unique_suffix}", marketplace_product_id=f"SHOP2_{unique_suffix}",
title=f"Shop2 Product {unique_suffix}", title=f"Shop2 MarketplaceProduct {unique_suffix}",
shop_name="TestShop2" shop_name="TestShop2"
), ),
] ]
@@ -85,7 +85,7 @@ class TestExportFunctionality:
db.commit() db.commit()
response = client.get( response = client.get(
"/api/v1/product/export-csv?shop_name=TestShop1", headers=auth_headers "/api/v1/marketplace/product?shop_name=TestShop1", headers=auth_headers
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -97,21 +97,21 @@ class TestExportFunctionality:
"""Test CSV export with combined marketplace and shop filters successfully""" """Test CSV export with combined marketplace and shop filters successfully"""
unique_suffix = str(uuid.uuid4())[:8] unique_suffix = str(uuid.uuid4())[:8]
products = [ products = [
Product( MarketplaceProduct(
product_id=f"COMBO1_{unique_suffix}", marketplace_product_id=f"COMBO1_{unique_suffix}",
title=f"Combo Product 1 {unique_suffix}", title=f"Combo MarketplaceProduct 1 {unique_suffix}",
marketplace="Amazon", marketplace="Amazon",
shop_name="TestShop" shop_name="TestShop"
), ),
Product( MarketplaceProduct(
product_id=f"COMBO2_{unique_suffix}", marketplace_product_id=f"COMBO2_{unique_suffix}",
title=f"Combo Product 2 {unique_suffix}", title=f"Combo MarketplaceProduct 2 {unique_suffix}",
marketplace="eBay", marketplace="eBay",
shop_name="TestShop" shop_name="TestShop"
), ),
Product( MarketplaceProduct(
product_id=f"COMBO3_{unique_suffix}", marketplace_product_id=f"COMBO3_{unique_suffix}",
title=f"Combo Product 3 {unique_suffix}", title=f"Combo MarketplaceProduct 3 {unique_suffix}",
marketplace="Amazon", marketplace="Amazon",
shop_name="OtherShop" shop_name="OtherShop"
), ),
@@ -121,7 +121,7 @@ class TestExportFunctionality:
db.commit() db.commit()
response = client.get( response = client.get(
"/api/v1/product/export-csv?marketplace=Amazon&shop_name=TestShop", "/api/v1/marketplace/product?marketplace=Amazon&shop_name=TestShop",
headers=auth_headers headers=auth_headers
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -134,7 +134,7 @@ class TestExportFunctionality:
def test_csv_export_no_results(self, client, auth_headers): def test_csv_export_no_results(self, client, auth_headers):
"""Test CSV export with filters that return no results""" """Test CSV export with filters that return no results"""
response = client.get( response = client.get(
"/api/v1/product/export-csv?marketplace=NonexistentMarketplace12345", "/api/v1/marketplace/product/export-csv?marketplace=NonexistentMarketplace12345",
headers=auth_headers headers=auth_headers
) )
@@ -146,7 +146,7 @@ class TestExportFunctionality:
# Should have header row even with no data # Should have header row even with no data
assert len(csv_lines) >= 1 assert len(csv_lines) >= 1
# First line should be headers # First line should be headers
assert "product_id" in csv_lines[0] assert "marketplace_product_id" in csv_lines[0]
def test_csv_export_performance_large_dataset(self, client, auth_headers, db): def test_csv_export_performance_large_dataset(self, client, auth_headers, db):
"""Test CSV export performance with many products""" """Test CSV export performance with many products"""
@@ -156,9 +156,9 @@ class TestExportFunctionality:
products = [] products = []
batch_size = 100 # Reduced from 1000 for faster test execution batch_size = 100 # Reduced from 1000 for faster test execution
for i in range(batch_size): for i in range(batch_size):
product = Product( product = MarketplaceProduct(
product_id=f"PERF{i:04d}_{unique_suffix}", marketplace_product_id=f"PERF{i:04d}_{unique_suffix}",
title=f"Performance Product {i}", title=f"Performance MarketplaceProduct {i}",
marketplace="Performance", marketplace="Performance",
description=f"Performance test product {i}", description=f"Performance test product {i}",
price="10.99" price="10.99"
@@ -174,7 +174,7 @@ class TestExportFunctionality:
import time import time
start_time = time.time() start_time = time.time()
response = client.get("/api/v1/product/export-csv", headers=auth_headers) response = client.get("/api/v1/marketplace/product/export-csv", headers=auth_headers)
end_time = time.time() end_time = time.time()
execution_time = end_time - start_time execution_time = end_time - start_time
@@ -186,20 +186,20 @@ class TestExportFunctionality:
# Verify content contains our test data # Verify content contains our test data
csv_content = response.content.decode("utf-8") csv_content = response.content.decode("utf-8")
assert f"PERF0000_{unique_suffix}" in csv_content assert f"PERF0000_{unique_suffix}" in csv_content
assert "Performance Product" in csv_content assert "Performance MarketplaceProduct" in csv_content
def test_csv_export_without_auth_returns_invalid_token(self, client): def test_csv_export_without_auth_returns_invalid_token(self, client):
"""Test that CSV export requires authentication returns InvalidTokenException""" """Test that CSV export requires authentication returns InvalidTokenException"""
response = client.get("/api/v1/product/export-csv") response = client.get("/api/v1/marketplace/product")
assert response.status_code == 401 assert response.status_code == 401
data = response.json() data = response.json()
assert data["error_code"] == "INVALID_TOKEN" assert data["error_code"] == "INVALID_TOKEN"
assert data["status_code"] == 401 assert data["status_code"] == 401
def test_csv_export_streaming_response_format(self, client, auth_headers, test_product): def test_csv_export_streaming_response_format(self, client, auth_headers, test_marketplace_product):
"""Test that CSV export returns proper streaming response with correct headers""" """Test that CSV export returns proper streaming response with correct headers"""
response = client.get("/api/v1/product/export-csv", headers=auth_headers) response = client.get("/api/v1/marketplace/product/export-csv", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
assert response.headers["content-type"] == "text/csv; charset=utf-8" assert response.headers["content-type"] == "text/csv; charset=utf-8"
@@ -215,9 +215,9 @@ class TestExportFunctionality:
unique_suffix = str(uuid.uuid4())[:8] unique_suffix = str(uuid.uuid4())[:8]
# Create product with special characters that might break CSV # Create product with special characters that might break CSV
product = Product( product = MarketplaceProduct(
product_id=f"SPECIAL_{unique_suffix}", marketplace_product_id=f"SPECIAL_{unique_suffix}",
title=f'Product with quotes and commas {unique_suffix}', # Simplified to avoid CSV escaping issues title=f'MarketplaceProduct with quotes and commas {unique_suffix}', # Simplified to avoid CSV escaping issues
description=f"Description with special chars {unique_suffix}", description=f"Description with special chars {unique_suffix}",
marketplace="Test Market", marketplace="Test Market",
price="19.99" price="19.99"
@@ -226,14 +226,14 @@ class TestExportFunctionality:
db.add(product) db.add(product)
db.commit() db.commit()
response = client.get("/api/v1/product/export-csv", headers=auth_headers) response = client.get("/api/v1/marketplace/product/export-csv", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
csv_content = response.content.decode("utf-8") csv_content = response.content.decode("utf-8")
# Verify our test product appears in the CSV content # Verify our test product appears in the CSV content
assert f"SPECIAL_{unique_suffix}" in csv_content assert f"SPECIAL_{unique_suffix}" in csv_content
assert f"Product with quotes and commas {unique_suffix}" in csv_content assert f"MarketplaceProduct with quotes and commas {unique_suffix}" in csv_content
assert "Test Market" in csv_content assert "Test Market" in csv_content
assert "19.99" in csv_content assert "19.99" in csv_content
@@ -243,7 +243,7 @@ class TestExportFunctionality:
header = next(csv_reader) header = next(csv_reader)
# Verify header contains expected fields # Verify header contains expected fields
expected_fields = ["product_id", "title", "marketplace", "price"] expected_fields = ["marketplace_product_id", "title", "marketplace", "price"]
for field in expected_fields: for field in expected_fields:
assert field in header assert field in header
@@ -274,7 +274,7 @@ class TestExportFunctionality:
# This would require access to your service instance to mock properly # This would require access to your service instance to mock properly
# For now, we test that the endpoint structure supports error handling # For now, we test that the endpoint structure supports error handling
response = client.get("/api/v1/product/export-csv", headers=auth_headers) response = client.get("/api/v1/marketplace/product", headers=auth_headers)
# Should either succeed or return proper error response # Should either succeed or return proper error response
assert response.status_code in [200, 400, 500] assert response.status_code in [200, 400, 500]
@@ -293,7 +293,7 @@ class TestExportFunctionality:
def test_csv_export_filename_generation(self, client, auth_headers): def test_csv_export_filename_generation(self, client, auth_headers):
"""Test CSV export generates appropriate filenames based on filters""" """Test CSV export generates appropriate filenames based on filters"""
# Test basic export filename # Test basic export filename
response = client.get("/api/v1/product/export-csv", headers=auth_headers) response = client.get("/api/v1/marketplace/product/export-csv", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
content_disposition = response.headers.get("content-disposition", "") content_disposition = response.headers.get("content-disposition", "")
@@ -301,7 +301,7 @@ class TestExportFunctionality:
# Test with marketplace filter # Test with marketplace filter
response = client.get( response = client.get(
"/api/v1/product/export-csv?marketplace=Amazon", "/api/v1/marketplace/product/export-csv?marketplace=Amazon",
headers=auth_headers headers=auth_headers
) )
assert response.status_code == 200 assert response.status_code == 200

View File

@@ -1,49 +1,49 @@
# tests/integration/api/v1/test_product_endpoints.py # tests/integration/api/v1/test_marketplace_products_endpoints.py
import pytest import pytest
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.api @pytest.mark.api
@pytest.mark.products @pytest.mark.products
class TestProductsAPI: class TestMarketplaceProductsAPI:
def test_get_products_empty(self, client, auth_headers): def test_get_products_empty(self, client, auth_headers):
"""Test getting products when none exist""" """Test getting products when none exist"""
response = client.get("/api/v1/product", headers=auth_headers) response = client.get("/api/v1/marketplace/product", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["products"] == [] assert data["products"] == []
assert data["total"] == 0 assert data["total"] == 0
def test_get_products_with_data(self, client, auth_headers, test_product): def test_get_products_with_data(self, client, auth_headers, test_marketplace_product):
"""Test getting products with data""" """Test getting products with data"""
response = client.get("/api/v1/product", headers=auth_headers) response = client.get("/api/v1/marketplace/product", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert len(data["products"]) >= 1 assert len(data["products"]) >= 1
assert data["total"] >= 1 assert data["total"] >= 1
# Find our test product # Find our test product
test_product_found = any(p["product_id"] == test_product.product_id for p in data["products"]) test_product_found = any(p["marketplace_product_id"] == test_marketplace_product.marketplace_product_id for p in data["products"])
assert test_product_found assert test_product_found
def test_get_products_with_filters(self, client, auth_headers, test_product): def test_get_products_with_filters(self, client, auth_headers, test_marketplace_product):
"""Test filtering products""" """Test filtering products"""
# Test brand filter # Test brand filter
response = client.get(f"/api/v1/product?brand={test_product.brand}", headers=auth_headers) response = client.get(f"/api/v1/marketplace/product?brand={test_marketplace_product.brand}", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] >= 1 assert data["total"] >= 1
# Test marketplace filter # Test marketplace filter
response = client.get(f"/api/v1/product?marketplace={test_product.marketplace}", headers=auth_headers) response = client.get(f"/api/v1/marketplace/product?marketplace={test_marketplace_product.marketplace}", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] >= 1 assert data["total"] >= 1
# Test search # Test search
response = client.get("/api/v1/product?search=Test", headers=auth_headers) response = client.get("/api/v1/marketplace/product?search=Test", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["total"] >= 1 assert data["total"] >= 1
@@ -51,8 +51,8 @@ class TestProductsAPI:
def test_create_product_success(self, client, auth_headers): def test_create_product_success(self, client, auth_headers):
"""Test creating a new product successfully""" """Test creating a new product successfully"""
product_data = { product_data = {
"product_id": "NEW001", "marketplace_product_id": "NEW001",
"title": "New Product", "title": "New MarketplaceProduct",
"description": "A new product", "description": "A new product",
"price": "15.99", "price": "15.99",
"brand": "NewBrand", "brand": "NewBrand",
@@ -62,19 +62,19 @@ class TestProductsAPI:
} }
response = client.post( response = client.post(
"/api/v1/product", headers=auth_headers, json=product_data "/api/v1/marketplace/product", headers=auth_headers, json=product_data
) )
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["product_id"] == "NEW001" assert data["marketplace_product_id"] == "NEW001"
assert data["title"] == "New Product" assert data["title"] == "New MarketplaceProduct"
assert data["marketplace"] == "Amazon" assert data["marketplace"] == "Amazon"
def test_create_product_duplicate_id_returns_conflict(self, client, auth_headers, test_product): def test_create_product_duplicate_id_returns_conflict(self, client, auth_headers, test_marketplace_product):
"""Test creating product with duplicate ID returns ProductAlreadyExistsException""" """Test creating product with duplicate ID returns MarketplaceProductAlreadyExistsException"""
product_data = { product_data = {
"product_id": test_product.product_id, "marketplace_product_id": test_marketplace_product.marketplace_product_id,
"title": "Different Title", "title": "Different Title",
"description": "A new product", "description": "A new product",
"price": "15.99", "price": "15.99",
@@ -85,65 +85,65 @@ class TestProductsAPI:
} }
response = client.post( response = client.post(
"/api/v1/product", headers=auth_headers, json=product_data "/api/v1/marketplace/product", headers=auth_headers, json=product_data
) )
assert response.status_code == 409 assert response.status_code == 409
data = response.json() data = response.json()
assert data["error_code"] == "PRODUCT_ALREADY_EXISTS" assert data["error_code"] == "PRODUCT_ALREADY_EXISTS"
assert data["status_code"] == 409 assert data["status_code"] == 409
assert test_product.product_id in data["message"] assert test_marketplace_product.marketplace_product_id in data["message"]
assert data["details"]["product_id"] == test_product.product_id assert data["details"]["marketplace_product_id"] == test_marketplace_product.marketplace_product_id
def test_create_product_missing_title_validation_error(self, client, auth_headers): def test_create_product_missing_title_validation_error(self, client, auth_headers):
"""Test creating product without title returns ValidationException""" """Test creating product without title returns ValidationException"""
product_data = { product_data = {
"product_id": "VALID001", "marketplace_product_id": "VALID001",
"title": "", # Empty title "title": "", # Empty title
"price": "15.99", "price": "15.99",
} }
response = client.post( response = client.post(
"/api/v1/product", headers=auth_headers, json=product_data "/api/v1/marketplace/product", headers=auth_headers, json=product_data
) )
assert response.status_code == 422 # Pydantic validation error assert response.status_code == 422 # Pydantic validation error
data = response.json() data = response.json()
assert data["error_code"] == "PRODUCT_VALIDATION_FAILED" assert data["error_code"] == "PRODUCT_VALIDATION_FAILED"
assert data["status_code"] == 422 assert data["status_code"] == 422
assert "Product title is required" in data["message"] assert "MarketplaceProduct title is required" in data["message"]
assert data["details"]["field"] == "title" assert data["details"]["field"] == "title"
def test_create_product_missing_product_id_validation_error(self, client, auth_headers): def test_create_product_missing_product_id_validation_error(self, client, auth_headers):
"""Test creating product without product_id returns ValidationException""" """Test creating product without marketplace_product_id returns ValidationException"""
product_data = { product_data = {
"product_id": "", # Empty product ID "marketplace_product_id": "", # Empty product ID
"title": "Valid Title", "title": "Valid Title",
"price": "15.99", "price": "15.99",
} }
response = client.post( response = client.post(
"/api/v1/product", headers=auth_headers, json=product_data "/api/v1/marketplace/product", headers=auth_headers, json=product_data
) )
assert response.status_code == 422 assert response.status_code == 422
data = response.json() data = response.json()
assert data["error_code"] == "PRODUCT_VALIDATION_FAILED" assert data["error_code"] == "PRODUCT_VALIDATION_FAILED"
assert data["status_code"] == 422 assert data["status_code"] == 422
assert "Product ID is required" in data["message"] assert "MarketplaceProduct ID is required" in data["message"]
assert data["details"]["field"] == "product_id" assert data["details"]["field"] == "marketplace_product_id"
def test_create_product_invalid_gtin_data_error(self, client, auth_headers): def test_create_product_invalid_gtin_data_error(self, client, auth_headers):
"""Test creating product with invalid GTIN returns InvalidProductDataException""" """Test creating product with invalid GTIN returns InvalidMarketplaceProductDataException"""
product_data = { product_data = {
"product_id": "GTIN001", "marketplace_product_id": "GTIN001",
"title": "GTIN Test Product", "title": "GTIN Test MarketplaceProduct",
"price": "15.99", "price": "15.99",
"gtin": "invalid_gtin", "gtin": "invalid_gtin",
} }
response = client.post( response = client.post(
"/api/v1/product", headers=auth_headers, json=product_data "/api/v1/marketplace/product", headers=auth_headers, json=product_data
) )
assert response.status_code == 422 assert response.status_code == 422
@@ -154,15 +154,15 @@ class TestProductsAPI:
assert data["details"]["field"] == "gtin" assert data["details"]["field"] == "gtin"
def test_create_product_invalid_price_data_error(self, client, auth_headers): def test_create_product_invalid_price_data_error(self, client, auth_headers):
"""Test creating product with invalid price returns InvalidProductDataException""" """Test creating product with invalid price returns InvalidMarketplaceProductDataException"""
product_data = { product_data = {
"product_id": "PRICE001", "marketplace_product_id": "PRICE001",
"title": "Price Test Product", "title": "Price Test MarketplaceProduct",
"price": "invalid_price", "price": "invalid_price",
} }
response = client.post( response = client.post(
"/api/v1/product", headers=auth_headers, json=product_data "/api/v1/marketplace/product", headers=auth_headers, json=product_data
) )
assert response.status_code == 422 assert response.status_code == 422
@@ -181,7 +181,7 @@ class TestProductsAPI:
} }
response = client.post( response = client.post(
"/api/v1/product", headers=auth_headers, json=product_data "/api/v1/marketplace/product", headers=auth_headers, json=product_data
) )
assert response.status_code == 422 assert response.status_code == 422
@@ -191,50 +191,50 @@ class TestProductsAPI:
assert "Request validation failed" in data["message"] assert "Request validation failed" in data["message"]
assert "validation_errors" in data["details"] assert "validation_errors" in data["details"]
def test_get_product_by_id_success(self, client, auth_headers, test_product): def test_get_product_by_id_success(self, client, auth_headers, test_marketplace_product):
"""Test getting specific product successfully""" """Test getting specific product successfully"""
response = client.get( response = client.get(
f"/api/v1/product/{test_product.product_id}", headers=auth_headers f"/api/v1/marketplace/product/{test_marketplace_product.marketplace_product_id}", headers=auth_headers
) )
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["product"]["product_id"] == test_product.product_id assert data["product"]["marketplace_product_id"] == test_marketplace_product.marketplace_product_id
assert data["product"]["title"] == test_product.title assert data["product"]["title"] == test_marketplace_product.title
def test_get_nonexistent_product_returns_not_found(self, client, auth_headers): def test_get_nonexistent_product_returns_not_found(self, client, auth_headers):
"""Test getting nonexistent product returns ProductNotFoundException""" """Test getting nonexistent product returns MarketplaceProductNotFoundException"""
response = client.get("/api/v1/product/NONEXISTENT", headers=auth_headers) response = client.get("/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers)
assert response.status_code == 404 assert response.status_code == 404
data = response.json() data = response.json()
assert data["error_code"] == "PRODUCT_NOT_FOUND" assert data["error_code"] == "PRODUCT_NOT_FOUND"
assert data["status_code"] == 404 assert data["status_code"] == 404
assert "NONEXISTENT" in data["message"] assert "NONEXISTENT" in data["message"]
assert data["details"]["resource_type"] == "Product" assert data["details"]["resource_type"] == "MarketplaceProduct"
assert data["details"]["identifier"] == "NONEXISTENT" assert data["details"]["identifier"] == "NONEXISTENT"
def test_update_product_success(self, client, auth_headers, test_product): def test_update_product_success(self, client, auth_headers, test_marketplace_product):
"""Test updating product successfully""" """Test updating product successfully"""
update_data = {"title": "Updated Product Title", "price": "25.99"} update_data = {"title": "Updated MarketplaceProduct Title", "price": "25.99"}
response = client.put( response = client.put(
f"/api/v1/product/{test_product.product_id}", f"/api/v1/marketplace/product/{test_marketplace_product.marketplace_product_id}",
headers=auth_headers, headers=auth_headers,
json=update_data, json=update_data,
) )
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["title"] == "Updated Product Title" assert data["title"] == "Updated MarketplaceProduct Title"
assert data["price"] == "25.99" assert data["price"] == "25.99"
def test_update_nonexistent_product_returns_not_found(self, client, auth_headers): def test_update_nonexistent_product_returns_not_found(self, client, auth_headers):
"""Test updating nonexistent product returns ProductNotFoundException""" """Test updating nonexistent product returns MarketplaceProductNotFoundException"""
update_data = {"title": "Updated Product Title"} update_data = {"title": "Updated MarketplaceProduct Title"}
response = client.put( response = client.put(
"/api/v1/product/NONEXISTENT", "/api/v1/marketplace/product/NONEXISTENT",
headers=auth_headers, headers=auth_headers,
json=update_data, json=update_data,
) )
@@ -244,15 +244,15 @@ class TestProductsAPI:
assert data["error_code"] == "PRODUCT_NOT_FOUND" assert data["error_code"] == "PRODUCT_NOT_FOUND"
assert data["status_code"] == 404 assert data["status_code"] == 404
assert "NONEXISTENT" in data["message"] assert "NONEXISTENT" in data["message"]
assert data["details"]["resource_type"] == "Product" assert data["details"]["resource_type"] == "MarketplaceProduct"
assert data["details"]["identifier"] == "NONEXISTENT" assert data["details"]["identifier"] == "NONEXISTENT"
def test_update_product_empty_title_validation_error(self, client, auth_headers, test_product): def test_update_product_empty_title_validation_error(self, client, auth_headers, test_marketplace_product):
"""Test updating product with empty title returns ProductValidationException""" """Test updating product with empty title returns MarketplaceProductValidationException"""
update_data = {"title": ""} update_data = {"title": ""}
response = client.put( response = client.put(
f"/api/v1/product/{test_product.product_id}", f"/api/v1/marketplace/product/{test_marketplace_product.marketplace_product_id}",
headers=auth_headers, headers=auth_headers,
json=update_data, json=update_data,
) )
@@ -261,15 +261,15 @@ class TestProductsAPI:
data = response.json() data = response.json()
assert data["error_code"] == "PRODUCT_VALIDATION_FAILED" assert data["error_code"] == "PRODUCT_VALIDATION_FAILED"
assert data["status_code"] == 422 assert data["status_code"] == 422
assert "Product title cannot be empty" in data["message"] assert "MarketplaceProduct title cannot be empty" in data["message"]
assert data["details"]["field"] == "title" assert data["details"]["field"] == "title"
def test_update_product_invalid_gtin_data_error(self, client, auth_headers, test_product): def test_update_product_invalid_gtin_data_error(self, client, auth_headers, test_marketplace_product):
"""Test updating product with invalid GTIN returns InvalidProductDataException""" """Test updating product with invalid GTIN returns InvalidMarketplaceProductDataException"""
update_data = {"gtin": "invalid_gtin"} update_data = {"gtin": "invalid_gtin"}
response = client.put( response = client.put(
f"/api/v1/product/{test_product.product_id}", f"/api/v1/marketplace/product/{test_marketplace_product.marketplace_product_id}",
headers=auth_headers, headers=auth_headers,
json=update_data, json=update_data,
) )
@@ -281,12 +281,12 @@ class TestProductsAPI:
assert "Invalid GTIN format" in data["message"] assert "Invalid GTIN format" in data["message"]
assert data["details"]["field"] == "gtin" assert data["details"]["field"] == "gtin"
def test_update_product_invalid_price_data_error(self, client, auth_headers, test_product): def test_update_product_invalid_price_data_error(self, client, auth_headers, test_marketplace_product):
"""Test updating product with invalid price returns InvalidProductDataException""" """Test updating product with invalid price returns InvalidMarketplaceProductDataException"""
update_data = {"price": "invalid_price"} update_data = {"price": "invalid_price"}
response = client.put( response = client.put(
f"/api/v1/product/{test_product.product_id}", f"/api/v1/marketplace/product/{test_marketplace_product.marketplace_product_id}",
headers=auth_headers, headers=auth_headers,
json=update_data, json=update_data,
) )
@@ -298,30 +298,30 @@ class TestProductsAPI:
assert "Invalid price format" in data["message"] assert "Invalid price format" in data["message"]
assert data["details"]["field"] == "price" assert data["details"]["field"] == "price"
def test_delete_product_success(self, client, auth_headers, test_product): def test_delete_product_success(self, client, auth_headers, test_marketplace_product):
"""Test deleting product successfully""" """Test deleting product successfully"""
response = client.delete( response = client.delete(
f"/api/v1/product/{test_product.product_id}", headers=auth_headers f"/api/v1/marketplace/product/{test_marketplace_product.marketplace_product_id}", headers=auth_headers
) )
assert response.status_code == 200 assert response.status_code == 200
assert "deleted successfully" in response.json()["message"] assert "deleted successfully" in response.json()["message"]
def test_delete_nonexistent_product_returns_not_found(self, client, auth_headers): def test_delete_nonexistent_product_returns_not_found(self, client, auth_headers):
"""Test deleting nonexistent product returns ProductNotFoundException""" """Test deleting nonexistent product returns MarketplaceProductNotFoundException"""
response = client.delete("/api/v1/product/NONEXISTENT", headers=auth_headers) response = client.delete("/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers)
assert response.status_code == 404 assert response.status_code == 404
data = response.json() data = response.json()
assert data["error_code"] == "PRODUCT_NOT_FOUND" assert data["error_code"] == "PRODUCT_NOT_FOUND"
assert data["status_code"] == 404 assert data["status_code"] == 404
assert "NONEXISTENT" in data["message"] assert "NONEXISTENT" in data["message"]
assert data["details"]["resource_type"] == "Product" assert data["details"]["resource_type"] == "MarketplaceProduct"
assert data["details"]["identifier"] == "NONEXISTENT" assert data["details"]["identifier"] == "NONEXISTENT"
def test_get_product_without_auth_returns_invalid_token(self, client): def test_get_product_without_auth_returns_invalid_token(self, client):
"""Test that product endpoints require authentication returns InvalidTokenException""" """Test that product endpoints require authentication returns InvalidTokenException"""
response = client.get("/api/v1/product") response = client.get("/api/v1/marketplace/product")
assert response.status_code == 401 assert response.status_code == 401
data = response.json() data = response.json()
@@ -331,7 +331,7 @@ class TestProductsAPI:
def test_exception_structure_consistency(self, client, auth_headers): def test_exception_structure_consistency(self, client, auth_headers):
"""Test that all exceptions follow the consistent LetzShopException structure""" """Test that all exceptions follow the consistent LetzShopException structure"""
# Test with a known error case # Test with a known error case
response = client.get("/api/v1/product/NONEXISTENT", headers=auth_headers) response = client.get("/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers)
assert response.status_code == 404 assert response.status_code == 404
data = response.json() data = response.json()

View File

@@ -1,7 +1,7 @@
# tests/integration/api/v1/test_pagination.py # tests/integration/api/v1/test_pagination.py
import pytest import pytest
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
from models.database.shop import Shop from models.database.shop import Shop
@pytest.mark.integration @pytest.mark.integration
@@ -18,9 +18,9 @@ class TestPagination:
# Create multiple products # Create multiple products
products = [] products = []
for i in range(25): for i in range(25):
product = Product( product = MarketplaceProduct(
product_id=f"PAGE{i:03d}_{unique_suffix}", marketplace_product_id=f"PAGE{i:03d}_{unique_suffix}",
title=f"Pagination Test Product {i}", title=f"Pagination Test MarketplaceProduct {i}",
marketplace="PaginationTest", marketplace="PaginationTest",
) )
products.append(product) products.append(product)
@@ -29,7 +29,7 @@ class TestPagination:
db.commit() db.commit()
# Test first page # Test first page
response = client.get("/api/v1/product?limit=10&skip=0", headers=auth_headers) response = client.get("/api/v1/marketplace/product?limit=10&skip=0", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert len(data["products"]) == 10 assert len(data["products"]) == 10
@@ -38,21 +38,21 @@ class TestPagination:
assert data["limit"] == 10 assert data["limit"] == 10
# Test second page # Test second page
response = client.get("/api/v1/product?limit=10&skip=10", headers=auth_headers) response = client.get("/api/v1/marketplace/product?limit=10&skip=10", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert len(data["products"]) == 10 assert len(data["products"]) == 10
assert data["skip"] == 10 assert data["skip"] == 10
# Test last page (should have remaining products) # Test last page (should have remaining products)
response = client.get("/api/v1/product?limit=10&skip=20", headers=auth_headers) response = client.get("/api/v1/marketplace/product?limit=10&skip=20", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert len(data["products"]) >= 5 # At least 5 remaining from our test set assert len(data["products"]) >= 5 # At least 5 remaining from our test set
def test_pagination_boundary_negative_skip_validation_error(self, client, auth_headers): def test_pagination_boundary_negative_skip_validation_error(self, client, auth_headers):
"""Test negative skip parameter returns ValidationException""" """Test negative skip parameter returns ValidationException"""
response = client.get("/api/v1/product?skip=-1", headers=auth_headers) response = client.get("/api/v1/marketplace/product?skip=-1", headers=auth_headers)
assert response.status_code == 422 assert response.status_code == 422
data = response.json() data = response.json()
@@ -63,7 +63,7 @@ class TestPagination:
def test_pagination_boundary_zero_limit_validation_error(self, client, auth_headers): def test_pagination_boundary_zero_limit_validation_error(self, client, auth_headers):
"""Test zero limit parameter returns ValidationException""" """Test zero limit parameter returns ValidationException"""
response = client.get("/api/v1/product?limit=0", headers=auth_headers) response = client.get("/api/v1/marketplace/product?limit=0", headers=auth_headers)
assert response.status_code == 422 assert response.status_code == 422
data = response.json() data = response.json()
@@ -73,7 +73,7 @@ class TestPagination:
def test_pagination_boundary_excessive_limit_validation_error(self, client, auth_headers): def test_pagination_boundary_excessive_limit_validation_error(self, client, auth_headers):
"""Test excessive limit parameter returns ValidationException""" """Test excessive limit parameter returns ValidationException"""
response = client.get("/api/v1/product?limit=10000", headers=auth_headers) response = client.get("/api/v1/marketplace/product?limit=10000", headers=auth_headers)
assert response.status_code == 422 assert response.status_code == 422
data = response.json() data = response.json()
@@ -84,7 +84,7 @@ class TestPagination:
def test_pagination_beyond_available_records(self, client, auth_headers, db): def test_pagination_beyond_available_records(self, client, auth_headers, db):
"""Test pagination beyond available records returns empty results""" """Test pagination beyond available records returns empty results"""
# Test skip beyond available records # Test skip beyond available records
response = client.get("/api/v1/product?skip=10000&limit=10", headers=auth_headers) response = client.get("/api/v1/marketplace/product?skip=10000&limit=10", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
@@ -101,9 +101,9 @@ class TestPagination:
# Create products with same brand for filtering # Create products with same brand for filtering
products = [] products = []
for i in range(15): for i in range(15):
product = Product( product = MarketplaceProduct(
product_id=f"FILTPAGE{i:03d}_{unique_suffix}", marketplace_product_id=f"FILTPAGE{i:03d}_{unique_suffix}",
title=f"Filter Page Product {i}", title=f"Filter Page MarketplaceProduct {i}",
brand="FilterBrand", brand="FilterBrand",
marketplace="FilterMarket", marketplace="FilterMarket",
) )
@@ -114,7 +114,7 @@ class TestPagination:
# Test first page with filter # Test first page with filter
response = client.get( response = client.get(
"/api/v1/product?brand=FilterBrand&limit=5&skip=0", "/api/v1/marketplace/product?brand=FilterBrand&limit=5&skip=0",
headers=auth_headers headers=auth_headers
) )
@@ -124,13 +124,13 @@ class TestPagination:
assert data["total"] >= 15 # At least our test products assert data["total"] >= 15 # At least our test products
# Verify all products have the filtered brand # Verify all products have the filtered brand
test_products = [p for p in data["products"] if p["product_id"].endswith(unique_suffix)] test_products = [p for p in data["products"] if p["marketplace_product_id"].endswith(unique_suffix)]
for product in test_products: for product in test_products:
assert product["brand"] == "FilterBrand" assert product["brand"] == "FilterBrand"
# Test second page with same filter # Test second page with same filter
response = client.get( response = client.get(
"/api/v1/product?brand=FilterBrand&limit=5&skip=5", "/api/v1/marketplace/product?brand=FilterBrand&limit=5&skip=5",
headers=auth_headers headers=auth_headers
) )
@@ -141,7 +141,7 @@ class TestPagination:
def test_pagination_default_values(self, client, auth_headers): def test_pagination_default_values(self, client, auth_headers):
"""Test pagination with default values""" """Test pagination with default values"""
response = client.get("/api/v1/product", headers=auth_headers) response = client.get("/api/v1/marketplace/product", headers=auth_headers)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
@@ -157,9 +157,9 @@ class TestPagination:
# Create products with predictable ordering # Create products with predictable ordering
products = [] products = []
for i in range(10): for i in range(10):
product = Product( product = MarketplaceProduct(
product_id=f"CONSIST{i:03d}_{unique_suffix}", marketplace_product_id=f"CONSIST{i:03d}_{unique_suffix}",
title=f"Consistent Product {i:03d}", title=f"Consistent MarketplaceProduct {i:03d}",
marketplace="ConsistentMarket", marketplace="ConsistentMarket",
) )
products.append(product) products.append(product)
@@ -168,14 +168,14 @@ class TestPagination:
db.commit() db.commit()
# Get first page # Get first page
response1 = client.get("/api/v1/product?limit=5&skip=0", headers=auth_headers) response1 = client.get("/api/v1/marketplace/product?limit=5&skip=0", headers=auth_headers)
assert response1.status_code == 200 assert response1.status_code == 200
first_page_ids = [p["product_id"] for p in response1.json()["products"]] first_page_ids = [p["marketplace_product_id"] for p in response1.json()["products"]]
# Get second page # Get second page
response2 = client.get("/api/v1/product?limit=5&skip=5", headers=auth_headers) response2 = client.get("/api/v1/marketplace/product?limit=5&skip=5", headers=auth_headers)
assert response2.status_code == 200 assert response2.status_code == 200
second_page_ids = [p["product_id"] for p in response2.json()["products"]] second_page_ids = [p["marketplace_product_id"] for p in response2.json()["products"]]
# Verify no overlap between pages # Verify no overlap between pages
overlap = set(first_page_ids) & set(second_page_ids) overlap = set(first_page_ids) & set(second_page_ids)
@@ -249,7 +249,7 @@ class TestPagination:
import time import time
start_time = time.time() start_time = time.time()
response = client.get("/api/v1/product?skip=1000&limit=10", headers=auth_headers) response = client.get("/api/v1/marketplace/product?skip=1000&limit=10", headers=auth_headers)
end_time = time.time() end_time = time.time()
assert response.status_code == 200 assert response.status_code == 200
@@ -262,26 +262,26 @@ class TestPagination:
def test_pagination_with_invalid_parameters_types(self, client, auth_headers): def test_pagination_with_invalid_parameters_types(self, client, auth_headers):
"""Test pagination with invalid parameter types returns ValidationException""" """Test pagination with invalid parameter types returns ValidationException"""
# Test non-numeric skip # Test non-numeric skip
response = client.get("/api/v1/product?skip=invalid", headers=auth_headers) response = client.get("/api/v1/marketplace/product?skip=invalid", headers=auth_headers)
assert response.status_code == 422 assert response.status_code == 422
data = response.json() data = response.json()
assert data["error_code"] == "VALIDATION_ERROR" assert data["error_code"] == "VALIDATION_ERROR"
# Test non-numeric limit # Test non-numeric limit
response = client.get("/api/v1/product?limit=invalid", headers=auth_headers) response = client.get("/api/v1/marketplace/product?limit=invalid", headers=auth_headers)
assert response.status_code == 422 assert response.status_code == 422
data = response.json() data = response.json()
assert data["error_code"] == "VALIDATION_ERROR" assert data["error_code"] == "VALIDATION_ERROR"
# Test float values (should be converted or rejected) # Test float values (should be converted or rejected)
response = client.get("/api/v1/product?skip=10.5&limit=5.5", headers=auth_headers) response = client.get("/api/v1/marketplace/product?skip=10.5&limit=5.5", headers=auth_headers)
assert response.status_code in [200, 422] # Depends on implementation assert response.status_code in [200, 422] # Depends on implementation
def test_empty_dataset_pagination(self, client, auth_headers): def test_empty_dataset_pagination(self, client, auth_headers):
"""Test pagination behavior with empty dataset""" """Test pagination behavior with empty dataset"""
# Use a filter that should return no results # Use a filter that should return no results
response = client.get( response = client.get(
"/api/v1/product?brand=NonexistentBrand999&limit=10&skip=0", "/api/v1/marketplace/product?brand=NonexistentBrand999&limit=10&skip=0",
headers=auth_headers headers=auth_headers
) )
@@ -294,7 +294,7 @@ class TestPagination:
def test_exception_structure_in_pagination_errors(self, client, auth_headers): def test_exception_structure_in_pagination_errors(self, client, auth_headers):
"""Test that pagination validation errors follow consistent exception structure""" """Test that pagination validation errors follow consistent exception structure"""
response = client.get("/api/v1/product?skip=-1", headers=auth_headers) response = client.get("/api/v1/marketplace/product?skip=-1", headers=auth_headers)
assert response.status_code == 422 assert response.status_code == 422
data = response.json() data = response.json()

View File

@@ -184,7 +184,7 @@ class TestShopsAPI:
def test_add_product_to_shop_success(self, client, auth_headers, test_shop, unique_product): def test_add_product_to_shop_success(self, client, auth_headers, test_shop, unique_product):
"""Test adding product to shop successfully""" """Test adding product to shop successfully"""
shop_product_data = { shop_product_data = {
"product_id": unique_product.product_id, # Use string product_id, not database id "marketplace_product_id": unique_product.marketplace_product_id, # Use string marketplace_product_id, not database id
"shop_price": 29.99, "shop_price": 29.99,
"is_active": True, "is_active": True,
"is_featured": False, "is_featured": False,
@@ -205,18 +205,18 @@ class TestShopsAPI:
assert data["is_active"] is True assert data["is_active"] is True
assert data["is_featured"] is False assert data["is_featured"] is False
# Product details are nested in the 'product' field # MarketplaceProduct details are nested in the 'product' field
assert "product" in data assert "product" in data
assert data["product"]["product_id"] == unique_product.product_id assert data["product"]["marketplace_product_id"] == unique_product.marketplace_product_id
assert data["product"]["id"] == unique_product.id assert data["product"]["id"] == unique_product.id
def test_add_product_to_shop_already_exists_conflict(self, client, auth_headers, test_shop, shop_product): def test_add_product_to_shop_already_exists_conflict(self, client, auth_headers, test_shop, shop_product):
"""Test adding product that already exists in shop returns ShopProductAlreadyExistsException""" """Test adding product that already exists in shop returns ShopProductAlreadyExistsException"""
# shop_product fixture already creates a relationship, get the product_id string # shop_product fixture already creates a relationship, get the marketplace_product_id string
existing_product = shop_product.product existing_product = shop_product.product
shop_product_data = { shop_product_data = {
"product_id": existing_product.product_id, # Use string product_id "marketplace_product_id": existing_product.marketplace_product_id, # Use string marketplace_product_id
"shop_price": 29.99, "shop_price": 29.99,
} }
@@ -231,12 +231,12 @@ class TestShopsAPI:
assert data["error_code"] == "SHOP_PRODUCT_ALREADY_EXISTS" assert data["error_code"] == "SHOP_PRODUCT_ALREADY_EXISTS"
assert data["status_code"] == 409 assert data["status_code"] == 409
assert test_shop.shop_code in data["message"] assert test_shop.shop_code in data["message"]
assert existing_product.product_id in data["message"] assert existing_product.marketplace_product_id in data["message"]
def test_add_nonexistent_product_to_shop_not_found(self, client, auth_headers, test_shop): def test_add_nonexistent_product_to_shop_not_found(self, client, auth_headers, test_shop):
"""Test adding nonexistent product to shop returns ProductNotFoundException""" """Test adding nonexistent product to shop returns MarketplaceProductNotFoundException"""
shop_product_data = { shop_product_data = {
"product_id": "NONEXISTENT_PRODUCT", # Use string product_id that doesn't exist "marketplace_product_id": "NONEXISTENT_PRODUCT", # Use string marketplace_product_id that doesn't exist
"shop_price": 29.99, "shop_price": 29.99,
} }
@@ -321,7 +321,7 @@ class TestShopsAPI:
# Test adding products (might require verification) # Test adding products (might require verification)
product_data = { product_data = {
"product_id": 1, "marketplace_product_id": 1,
"shop_price": 29.99, "shop_price": 29.99,
} }

View File

@@ -1,9 +1,11 @@
# tests/integration/api/v1/test_stats_endpoints.py # tests/integration/api/v1/test_stats_endpoints.py
import pytest import pytest
@pytest.mark.integration
@pytest.mark.api
@pytest.mark.stats
class TestStatsAPI: class TestStatsAPI:
def test_get_basic_stats(self, client, auth_headers, test_product): def test_get_basic_stats(self, client, auth_headers, test_marketplace_product):
"""Test getting basic statistics""" """Test getting basic statistics"""
response = client.get("/api/v1/stats", headers=auth_headers) response = client.get("/api/v1/stats", headers=auth_headers)
@@ -16,7 +18,7 @@ class TestStatsAPI:
assert "unique_shops" in data assert "unique_shops" in data
assert data["total_products"] >= 1 assert data["total_products"] >= 1
def test_get_marketplace_stats(self, client, auth_headers, test_product): def test_get_marketplace_stats(self, client, auth_headers, test_marketplace_product):
"""Test getting marketplace statistics""" """Test getting marketplace statistics"""
response = client.get("/api/v1/stats/marketplace", headers=auth_headers) response = client.get("/api/v1/stats/marketplace", headers=auth_headers)

View File

@@ -12,7 +12,7 @@ class TestAuthentication:
"/api/v1/admin/users", "/api/v1/admin/users",
"/api/v1/admin/shops", "/api/v1/admin/shops",
"/api/v1/marketplace/import-jobs", "/api/v1/marketplace/import-jobs",
"/api/v1/product", "/api/v1/marketplace/product",
"/api/v1/shop", "/api/v1/shop",
"/api/v1/stats", "/api/v1/stats",
"/api/v1/stock", "/api/v1/stock",
@@ -26,7 +26,7 @@ class TestAuthentication:
"""Test protected endpoints with invalid token""" """Test protected endpoints with invalid token"""
headers = {"Authorization": "Bearer invalid_token_here"} headers = {"Authorization": "Bearer invalid_token_here"}
response = client.get("/api/v1/product", headers=headers) response = client.get("/api/v1/marketplace/product", headers=headers)
assert response.status_code == 401 # Token is not valid assert response.status_code == 401 # Token is not valid
def test_debug_direct_bearer(self, client): def test_debug_direct_bearer(self, client):
@@ -49,7 +49,7 @@ class TestAuthentication:
# Test 2: Try a regular endpoint that uses get_current_user # Test 2: Try a regular endpoint that uses get_current_user
response2 = client.get( response2 = client.get(
"/api/v1/product" "/api/v1/marketplace/product"
) # or any endpoint with get_current_user ) # or any endpoint with get_current_user
print(f"Regular endpoint - Status: {response2.status_code}") print(f"Regular endpoint - Status: {response2.status_code}")
try: try:
@@ -64,12 +64,12 @@ class TestAuthentication:
if hasattr(route, "path") and hasattr(route, "methods"): if hasattr(route, "path") and hasattr(route, "methods"):
print(f"{list(route.methods)} {route.path}") print(f"{list(route.methods)} {route.path}")
print("\n=== Testing Product Endpoint Variations ===") print("\n=== Testing MarketplaceProduct Endpoint Variations ===")
variations = [ variations = [
"/api/v1/product", # Your current attempt "/api/v1/marketplace/product", # Your current attempt
"/api/v1/product/", # With trailing slash "/api/v1/marketplace/product/", # With trailing slash
"/api/v1/product/list", # With list endpoint "/api/v1/marketplace/product/list", # With list endpoint
"/api/v1/product/all", # With all endpoint "/api/v1/marketplace/product/all", # With all endpoint
] ]
for path in variations: for path in variations:

View File

@@ -27,7 +27,7 @@ class TestAuthorization:
def test_regular_endpoints_with_user_access(self, client, auth_headers): def test_regular_endpoints_with_user_access(self, client, auth_headers):
"""Test that regular users can access non-admin endpoints""" """Test that regular users can access non-admin endpoints"""
user_endpoints = [ user_endpoints = [
"/api/v1/product", "/api/v1/marketplace/product",
"/api/v1/stats", "/api/v1/stats",
"/api/v1/stock", "/api/v1/stock",
] ]

View File

@@ -11,7 +11,7 @@ class TestInputValidation:
malicious_search = "'; DROP TABLE products; --" malicious_search = "'; DROP TABLE products; --"
response = client.get( response = client.get(
f"/api/v1/product?search={malicious_search}", headers=auth_headers f"/api/v1/marketplace/product?search={malicious_search}", headers=auth_headers
) )
# Should not crash and should return normal response # Should not crash and should return normal response
@@ -25,12 +25,12 @@ class TestInputValidation:
# xss_payload = "<script>alert('xss')</script>" # xss_payload = "<script>alert('xss')</script>"
# #
# product_data = { # product_data = {
# "product_id": "XSS_TEST", # "marketplace_product_id": "XSS_TEST",
# "title": xss_payload, # "title": xss_payload,
# "description": xss_payload, # "description": xss_payload,
# } # }
# #
# response = client.post("/api/v1/product", headers=auth_headers, json=product_data) # response = client.post("/api/v1/marketplace/product", headers=auth_headers, json=product_data)
# #
# assert response.status_code == 200 # assert response.status_code == 200
# data = response.json() # data = response.json()
@@ -40,24 +40,24 @@ class TestInputValidation:
def test_parameter_validation(self, client, auth_headers): def test_parameter_validation(self, client, auth_headers):
"""Test parameter validation for API endpoints""" """Test parameter validation for API endpoints"""
# Test invalid pagination parameters # Test invalid pagination parameters
response = client.get("/api/v1/product?limit=-1", headers=auth_headers) response = client.get("/api/v1/marketplace/product?limit=-1", headers=auth_headers)
assert response.status_code == 422 # Validation error assert response.status_code == 422 # Validation error
response = client.get("/api/v1/product?skip=-1", headers=auth_headers) response = client.get("/api/v1/marketplace/product?skip=-1", headers=auth_headers)
assert response.status_code == 422 # Validation error assert response.status_code == 422 # Validation error
def test_json_validation(self, client, auth_headers): def test_json_validation(self, client, auth_headers):
"""Test JSON validation for POST requests""" """Test JSON validation for POST requests"""
# Test invalid JSON structure # Test invalid JSON structure
response = client.post( response = client.post(
"/api/v1/product", headers=auth_headers, content="invalid json content" "/api/v1/marketplace/product", headers=auth_headers, content="invalid json content"
) )
assert response.status_code == 422 # JSON decode error assert response.status_code == 422 # JSON decode error
# Test missing required fields # Test missing required fields
response = client.post( response = client.post(
"/api/v1/product", "/api/v1/marketplace/product",
headers=auth_headers, headers=auth_headers,
json={"title": "Test Product"}, # Missing required product_id json={"title": "Test MarketplaceProduct"}, # Missing required marketplace_product_id
) )
assert response.status_code == 422 # Validation error assert response.status_code == 422 # Validation error

View File

@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch
import pytest import pytest
from app.tasks.background_tasks import process_marketplace_import from app.tasks.background_tasks import process_marketplace_import
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
@pytest.mark.integration @pytest.mark.integration

View File

@@ -10,8 +10,8 @@ class TestIntegrationFlows:
"""Test complete product creation and management workflow""" """Test complete product creation and management workflow"""
# 1. Create a product # 1. Create a product
product_data = { product_data = {
"product_id": "FLOW001", "marketplace_product_id": "FLOW001",
"title": "Integration Test Product", "title": "Integration Test MarketplaceProduct",
"description": "Testing full workflow", "description": "Testing full workflow",
"price": "29.99", "price": "29.99",
"brand": "FlowBrand", "brand": "FlowBrand",
@@ -21,7 +21,7 @@ class TestIntegrationFlows:
} }
response = client.post( response = client.post(
"/api/v1/product", headers=auth_headers, json=product_data "/api/v1/marketplace/product", headers=auth_headers, json=product_data
) )
assert response.status_code == 200 assert response.status_code == 200
product = response.json() product = response.json()
@@ -38,16 +38,16 @@ class TestIntegrationFlows:
# 3. Get product with stock info # 3. Get product with stock info
response = client.get( response = client.get(
f"/api/v1/product/{product['product_id']}", headers=auth_headers f"/api/v1/marketplace/product/{product['marketplace_product_id']}", headers=auth_headers
) )
assert response.status_code == 200 assert response.status_code == 200
product_detail = response.json() product_detail = response.json()
assert product_detail["stock_info"]["total_quantity"] == 50 assert product_detail["stock_info"]["total_quantity"] == 50
# 4. Update product # 4. Update product
update_data = {"title": "Updated Integration Test Product"} update_data = {"title": "Updated Integration Test MarketplaceProduct"}
response = client.put( response = client.put(
f"/api/v1/product/{product['product_id']}", f"/api/v1/marketplace/product/{product['marketplace_product_id']}",
headers=auth_headers, headers=auth_headers,
json=update_data, json=update_data,
) )
@@ -55,7 +55,7 @@ class TestIntegrationFlows:
# 5. Search for product # 5. Search for product
response = client.get( response = client.get(
"/api/v1/product?search=Updated Integration", headers=auth_headers "/api/v1/marketplace/product?search=Updated Integration", headers=auth_headers
) )
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["total"] == 1 assert response.json()["total"] == 1
@@ -75,14 +75,14 @@ class TestIntegrationFlows:
# 2. Create a product # 2. Create a product
product_data = { product_data = {
"product_id": "SHOPFLOW001", "marketplace_product_id": "SHOPFLOW001",
"title": "Shop Flow Product", "title": "Shop Flow MarketplaceProduct",
"price": "15.99", "price": "15.99",
"marketplace": "ShopFlow", "marketplace": "ShopFlow",
} }
response = client.post( response = client.post(
"/api/v1/product", headers=auth_headers, json=product_data "/api/v1/marketplace/product", headers=auth_headers, json=product_data
) )
assert response.status_code == 200 assert response.status_code == 200
product = response.json() product = response.json()

View File

@@ -3,7 +3,7 @@ import time
import pytest import pytest
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
@pytest.mark.performance @pytest.mark.performance
@@ -15,9 +15,9 @@ class TestPerformance:
# Create multiple products # Create multiple products
products = [] products = []
for i in range(100): for i in range(100):
product = Product( product = MarketplaceProduct(
product_id=f"PERF{i:03d}", marketplace_product_id=f"PERF{i:03d}",
title=f"Performance Test Product {i}", title=f"Performance Test MarketplaceProduct {i}",
price=f"{i}.99", price=f"{i}.99",
marketplace="Performance", marketplace="Performance",
) )
@@ -28,7 +28,7 @@ class TestPerformance:
# Time the request # Time the request
start_time = time.time() start_time = time.time()
response = client.get("/api/v1/product?limit=100", headers=auth_headers) response = client.get("/api/v1/marketplace/product?limit=100", headers=auth_headers)
end_time = time.time() end_time = time.time()
assert response.status_code == 200 assert response.status_code == 200
@@ -40,9 +40,9 @@ class TestPerformance:
# Create products with searchable content # Create products with searchable content
products = [] products = []
for i in range(50): for i in range(50):
product = Product( product = MarketplaceProduct(
product_id=f"SEARCH{i:03d}", marketplace_product_id=f"SEARCH{i:03d}",
title=f"Searchable Product {i}", title=f"Searchable MarketplaceProduct {i}",
description=f"This is a searchable product number {i}", description=f"This is a searchable product number {i}",
brand="SearchBrand", brand="SearchBrand",
marketplace="SearchMarket", marketplace="SearchMarket",
@@ -54,7 +54,7 @@ class TestPerformance:
# Time search request # Time search request
start_time = time.time() start_time = time.time()
response = client.get("/api/v1/product?search=Searchable", headers=auth_headers) response = client.get("/api/v1/marketplace/product?search=Searchable", headers=auth_headers)
end_time = time.time() end_time = time.time()
assert response.status_code == 200 assert response.status_code == 200
@@ -69,9 +69,9 @@ class TestPerformance:
marketplaces = ["Market1", "Market2"] marketplaces = ["Market1", "Market2"]
for i in range(200): for i in range(200):
product = Product( product = MarketplaceProduct(
product_id=f"COMPLEX{i:03d}", marketplace_product_id=f"COMPLEX{i:03d}",
title=f"Complex Product {i}", title=f"Complex MarketplaceProduct {i}",
brand=brands[i % 3], brand=brands[i % 3],
marketplace=marketplaces[i % 2], marketplace=marketplaces[i % 2],
price=f"{10 + (i % 50)}.99", price=f"{10 + (i % 50)}.99",
@@ -85,7 +85,7 @@ class TestPerformance:
# Test complex filtering performance # Test complex filtering performance
start_time = time.time() start_time = time.time()
response = client.get( response = client.get(
"/api/v1/product?brand=Brand1&marketplace=Market1&limit=50", "/api/v1/marketplace/product?brand=Brand1&marketplace=Market1&limit=50",
headers=auth_headers, headers=auth_headers,
) )
end_time = time.time() end_time = time.time()
@@ -100,9 +100,9 @@ class TestPerformance:
# Create a large dataset # Create a large dataset
products = [] products = []
for i in range(500): for i in range(500):
product = Product( product = MarketplaceProduct(
product_id=f"LARGE{i:04d}", marketplace_product_id=f"LARGE{i:04d}",
title=f"Large Dataset Product {i}", title=f"Large Dataset MarketplaceProduct {i}",
marketplace="LargeTest", marketplace="LargeTest",
) )
products.append(product) products.append(product)
@@ -115,7 +115,7 @@ class TestPerformance:
for offset in offsets: for offset in offsets:
start_time = time.time() start_time = time.time()
response = client.get( response = client.get(
f"/api/v1/product?skip={offset}&limit=20", headers=auth_headers f"/api/v1/marketplace/product?skip={offset}&limit=20", headers=auth_headers
) )
end_time = time.time() end_time = time.time()

View File

@@ -104,13 +104,13 @@ class TestErrorHandling:
def test_product_not_found(self, client, auth_headers): def test_product_not_found(self, client, auth_headers):
"""Test accessing non-existent product""" """Test accessing non-existent product"""
response = client.get("/api/v1/product/NONEXISTENT", headers=auth_headers) response = client.get("/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers)
assert response.status_code == 404 assert response.status_code == 404
data = response.json() data = response.json()
assert data["error_code"] == "PRODUCT_NOT_FOUND" assert data["error_code"] == "PRODUCT_NOT_FOUND"
assert data["status_code"] == 404 assert data["status_code"] == 404
assert data["details"]["resource_type"] == "Product" assert data["details"]["resource_type"] == "MarketplaceProduct"
assert data["details"]["identifier"] == "NONEXISTENT" assert data["details"]["identifier"] == "NONEXISTENT"
def test_duplicate_shop_creation(self, client, auth_headers, test_shop): def test_duplicate_shop_creation(self, client, auth_headers, test_shop):
@@ -128,21 +128,21 @@ class TestErrorHandling:
assert data["status_code"] == 409 assert data["status_code"] == 409
assert data["details"]["shop_code"] == test_shop.shop_code.upper() assert data["details"]["shop_code"] == test_shop.shop_code.upper()
def test_duplicate_product_creation(self, client, auth_headers, test_product): def test_duplicate_product_creation(self, client, auth_headers, test_marketplace_product):
"""Test creating product with duplicate product ID""" """Test creating product with duplicate product ID"""
product_data = { product_data = {
"product_id": test_product.product_id, "marketplace_product_id": test_marketplace_product.marketplace_product_id,
"title": "Duplicate Product", "title": "Duplicate MarketplaceProduct",
"gtin": "1234567890123" "gtin": "1234567890123"
} }
response = client.post("/api/v1/product", headers=auth_headers, json=product_data) response = client.post("/api/v1/marketplace/product", headers=auth_headers, json=product_data)
assert response.status_code == 409 assert response.status_code == 409
data = response.json() data = response.json()
assert data["error_code"] == "PRODUCT_ALREADY_EXISTS" assert data["error_code"] == "PRODUCT_ALREADY_EXISTS"
assert data["status_code"] == 409 assert data["status_code"] == 409
assert data["details"]["product_id"] == test_product.product_id assert data["details"]["marketplace_product_id"] == test_marketplace_product.marketplace_product_id
def test_unauthorized_shop_access(self, client, auth_headers, inactive_shop): def test_unauthorized_shop_access(self, client, auth_headers, inactive_shop):
"""Test accessing shop without proper permissions""" """Test accessing shop without proper permissions"""
@@ -191,12 +191,12 @@ class TestErrorHandling:
def test_validation_error_invalid_gtin(self, client, auth_headers): def test_validation_error_invalid_gtin(self, client, auth_headers):
"""Test validation error for invalid GTIN format""" """Test validation error for invalid GTIN format"""
product_data = { product_data = {
"product_id": "TESTPROD001", "marketplace_product_id": "TESTPROD001",
"title": "Test Product", "title": "Test MarketplaceProduct",
"gtin": "invalid_gtin_format" "gtin": "invalid_gtin_format"
} }
response = client.post("/api/v1/product", headers=auth_headers, json=product_data) response = client.post("/api/v1/marketplace/product", headers=auth_headers, json=product_data)
assert response.status_code == 422 assert response.status_code == 422
data = response.json() data = response.json()
@@ -204,11 +204,11 @@ class TestErrorHandling:
assert data["status_code"] == 422 assert data["status_code"] == 422
assert data["details"]["field"] == "gtin" assert data["details"]["field"] == "gtin"
def test_stock_insufficient_quantity(self, client, auth_headers, test_shop, test_product): def test_stock_insufficient_quantity(self, client, auth_headers, test_shop, test_marketplace_product):
"""Test business logic error for insufficient stock""" """Test business logic error for insufficient stock"""
# First create some stock # First create some stock
stock_data = { stock_data = {
"gtin": test_product.gtin, "gtin": test_marketplace_product.gtin,
"location": "WAREHOUSE_A", "location": "WAREHOUSE_A",
"quantity": 5 "quantity": 5
} }
@@ -216,7 +216,7 @@ class TestErrorHandling:
# Try to remove more than available using your remove endpoint # Try to remove more than available using your remove endpoint
remove_data = { remove_data = {
"gtin": test_product.gtin, "gtin": test_marketplace_product.gtin,
"location": "WAREHOUSE_A", "location": "WAREHOUSE_A",
"quantity": 10 # More than the 5 we added "quantity": 10 # More than the 5 we added
} }
@@ -345,7 +345,7 @@ class TestErrorHandling:
"""Test that all error responses follow consistent structure""" """Test that all error responses follow consistent structure"""
test_cases = [ test_cases = [
("/api/v1/shop/NONEXISTENT", 404), ("/api/v1/shop/NONEXISTENT", 404),
("/api/v1/product/NONEXISTENT", 404), ("/api/v1/marketplace/product/NONEXISTENT", 404),
] ]
for endpoint, expected_status in test_cases: for endpoint, expected_status in test_cases:

View File

@@ -1,4 +1,4 @@
product_id,title,price,currency,brand,marketplace product_id,title,price,currency,brand,marketplace
TEST001,Sample Product 1,19.99,EUR,TestBrand,TestMarket TEST001,Sample MarketplaceProduct 1,19.99,EUR,TestBrand,TestMarket
TEST002,Sample Product 2,29.99,EUR,TestBrand,TestMarket TEST002,Sample MarketplaceProduct 2,29.99,EUR,TestBrand,TestMarket
TEST003,Sample Product 3,39.99,USD,AnotherBrand,TestMarket TEST003,Sample MarketplaceProduct 3,39.99,USD,AnotherBrand,TestMarket
1 product_id title price currency brand marketplace
2 TEST001 Sample Product 1 Sample MarketplaceProduct 1 19.99 EUR TestBrand TestMarket
3 TEST002 Sample Product 2 Sample MarketplaceProduct 2 29.99 EUR TestBrand TestMarket
4 TEST003 Sample Product 3 Sample MarketplaceProduct 3 39.99 USD AnotherBrand TestMarket

View File

@@ -41,7 +41,8 @@ class TestRateLimiter:
# Next request should be blocked # Next request should be blocked
assert limiter.allow_request(client_id, max_requests, 3600) is False assert limiter.allow_request(client_id, max_requests, 3600) is False
@pytest.mark.unit
@pytest.mark.auth # for auth manager tests
class TestAuthManager: class TestAuthManager:
def test_password_hashing_and_verification(self): def test_password_hashing_and_verification(self):
"""Test password hashing and verification""" """Test password hashing and verification"""

View File

@@ -1,7 +1,7 @@
# tests/unit/models/test_database_models.py # tests/unit/models/test_database_models.py
import pytest import pytest
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
from models.database.shop import Shop from models.database.shop import Shop
from models.database.stock import Stock from models.database.stock import Stock
from models.database.user import User from models.database.user import User
@@ -30,10 +30,10 @@ class TestDatabaseModels:
assert user.updated_at is not None assert user.updated_at is not None
def test_product_model(self, db): def test_product_model(self, db):
"""Test Product model creation""" """Test MarketplaceProduct model creation"""
product = Product( marketplace_product = MarketplaceProduct(
product_id="DB_TEST_001", marketplace_product_id="DB_TEST_001",
title="Database Test Product", title="Database Test MarketplaceProduct",
description="Testing product model", description="Testing product model",
price="25.99", price="25.99",
currency="USD", currency="USD",
@@ -44,13 +44,13 @@ class TestDatabaseModels:
shop_name="DBTestShop", shop_name="DBTestShop",
) )
db.add(product) db.add(marketplace_product)
db.commit() db.commit()
db.refresh(product) db.refresh(marketplace_product)
assert product.id is not None assert marketplace_product.id is not None
assert product.product_id == "DB_TEST_001" assert marketplace_product.marketplace_product_id == "DB_TEST_001"
assert product.created_at is not None assert marketplace_product.created_at is not None
def test_stock_model(self, db): def test_stock_model(self, db):
"""Test Stock model creation""" """Test Stock model creation"""
@@ -87,13 +87,13 @@ class TestDatabaseModels:
def test_database_constraints(self, db): def test_database_constraints(self, db):
"""Test database constraints and unique indexes""" """Test database constraints and unique indexes"""
# Test unique product_id constraint # Test unique marketplace_product_id constraint
product1 = Product(product_id="UNIQUE_001", title="Product 1") product1 = MarketplaceProduct(marketplace_product_id="UNIQUE_001", title="MarketplaceProduct 1")
db.add(product1) db.add(product1)
db.commit() db.commit()
# This should raise an integrity error # This should raise an integrity error
with pytest.raises(Exception): # Could be IntegrityError or similar with pytest.raises(Exception): # Could be IntegrityError or similar
product2 = Product(product_id="UNIQUE_001", title="Product 2") product2 = MarketplaceProduct(marketplace_product_id="UNIQUE_001", title="MarketplaceProduct 2")
db.add(product2) db.add(product2)
db.commit() db.commit()

View File

@@ -10,7 +10,7 @@ from app.exceptions import (
AdminOperationException, AdminOperationException,
) )
from app.services.admin_service import AdminService from app.services.admin_service import AdminService
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.shop import Shop from models.database.shop import Shop
@@ -169,51 +169,51 @@ class TestAdminService:
assert exception.error_code == "SHOP_NOT_FOUND" assert exception.error_code == "SHOP_NOT_FOUND"
# Marketplace Import Jobs Tests # Marketplace Import Jobs Tests
def test_get_marketplace_import_jobs_no_filters(self, db, test_marketplace_job): def test_get_marketplace_import_jobs_no_filters(self, db, test_marketplace_import_job):
"""Test getting marketplace import jobs without filters""" """Test getting marketplace import jobs without filters"""
result = self.service.get_marketplace_import_jobs(db, skip=0, limit=10) result = self.service.get_marketplace_import_jobs(db, skip=0, limit=10)
assert len(result) >= 1 assert len(result) >= 1
# Find our test job in the results # Find our test job in the results
test_job = next( test_job = next(
(job for job in result if job.job_id == test_marketplace_job.id), None (job for job in result if job.job_id == test_marketplace_import_job.id), None
) )
assert test_job is not None assert test_job is not None
assert test_job.marketplace == test_marketplace_job.marketplace assert test_job.marketplace == test_marketplace_import_job.marketplace
assert test_job.shop_name == test_marketplace_job.shop_name assert test_job.shop_name == test_marketplace_import_job.shop_name
assert test_job.status == test_marketplace_job.status assert test_job.status == test_marketplace_import_job.status
def test_get_marketplace_import_jobs_with_marketplace_filter(self, db, test_marketplace_job): def test_get_marketplace_import_jobs_with_marketplace_filter(self, db, test_marketplace_import_job):
"""Test filtering marketplace import jobs by marketplace""" """Test filtering marketplace import jobs by marketplace"""
result = self.service.get_marketplace_import_jobs( result = self.service.get_marketplace_import_jobs(
db, marketplace=test_marketplace_job.marketplace, skip=0, limit=10 db, marketplace=test_marketplace_import_job.marketplace, skip=0, limit=10
) )
assert len(result) >= 1 assert len(result) >= 1
for job in result: for job in result:
assert test_marketplace_job.marketplace.lower() in job.marketplace.lower() assert test_marketplace_import_job.marketplace.lower() in job.marketplace.lower()
def test_get_marketplace_import_jobs_with_shop_filter(self, db, test_marketplace_job): def test_get_marketplace_import_jobs_with_shop_filter(self, db, test_marketplace_import_job):
"""Test filtering marketplace import jobs by shop name""" """Test filtering marketplace import jobs by shop name"""
result = self.service.get_marketplace_import_jobs( result = self.service.get_marketplace_import_jobs(
db, shop_name=test_marketplace_job.shop_name, skip=0, limit=10 db, shop_name=test_marketplace_import_job.shop_name, skip=0, limit=10
) )
assert len(result) >= 1 assert len(result) >= 1
for job in result: for job in result:
assert test_marketplace_job.shop_name.lower() in job.shop_name.lower() assert test_marketplace_import_job.shop_name.lower() in job.shop_name.lower()
def test_get_marketplace_import_jobs_with_status_filter(self, db, test_marketplace_job): def test_get_marketplace_import_jobs_with_status_filter(self, db, test_marketplace_import_job):
"""Test filtering marketplace import jobs by status""" """Test filtering marketplace import jobs by status"""
result = self.service.get_marketplace_import_jobs( result = self.service.get_marketplace_import_jobs(
db, status=test_marketplace_job.status, skip=0, limit=10 db, status=test_marketplace_import_job.status, skip=0, limit=10
) )
assert len(result) >= 1 assert len(result) >= 1
for job in result: for job in result:
assert job.status == test_marketplace_job.status assert job.status == test_marketplace_import_job.status
def test_get_marketplace_import_jobs_pagination(self, db, test_marketplace_job): def test_get_marketplace_import_jobs_pagination(self, db, test_marketplace_import_job):
"""Test marketplace import jobs pagination""" """Test marketplace import jobs pagination"""
result_page1 = self.service.get_marketplace_import_jobs(db, skip=0, limit=1) result_page1 = self.service.get_marketplace_import_jobs(db, skip=0, limit=1)
result_page2 = self.service.get_marketplace_import_jobs(db, skip=1, limit=1) result_page2 = self.service.get_marketplace_import_jobs(db, skip=1, limit=1)

View File

@@ -4,7 +4,7 @@ from datetime import datetime
import pytest import pytest
from app.exceptions.marketplace import ( from app.exceptions.marketplace_import_job import (
ImportJobNotFoundException, ImportJobNotFoundException,
ImportJobNotOwnedException, ImportJobNotOwnedException,
ImportJobCannotBeCancelledException, ImportJobCannotBeCancelledException,
@@ -12,9 +12,9 @@ from app.exceptions.marketplace import (
) )
from app.exceptions.shop import ShopNotFoundException, UnauthorizedShopAccessException from app.exceptions.shop import ShopNotFoundException, UnauthorizedShopAccessException
from app.exceptions.base import ValidationException from app.exceptions.base import ValidationException
from app.services.marketplace_service import MarketplaceService from app.services.marketplace_import_job_service import MarketplaceImportJobService
from models.schemas.marketplace import MarketplaceImportRequest from models.schemas.marketplace_import_job import MarketplaceImportJobRequest
from models.database.marketplace import MarketplaceImportJob from models.database.marketplace_import_job import MarketplaceImportJob
from models.database.shop import Shop from models.database.shop import Shop
from models.database.user import User from models.database.user import User
@@ -23,7 +23,7 @@ from models.database.user import User
@pytest.mark.marketplace @pytest.mark.marketplace
class TestMarketplaceService: class TestMarketplaceService:
def setup_method(self): def setup_method(self):
self.service = MarketplaceService() self.service = MarketplaceImportJobService()
def test_validate_shop_access_success(self, db, test_shop, test_user): def test_validate_shop_access_success(self, db, test_shop, test_user):
"""Test successful shop access validation""" """Test successful shop access validation"""
@@ -76,7 +76,7 @@ class TestMarketplaceService:
test_shop.owner_id = test_user.id test_shop.owner_id = test_user.id
db.commit() db.commit()
request = MarketplaceImportRequest( request = MarketplaceImportJobRequest(
url="https://example.com/products.csv", url="https://example.com/products.csv",
marketplace="Amazon", marketplace="Amazon",
shop_code=test_shop.shop_code, shop_code=test_shop.shop_code,
@@ -94,7 +94,7 @@ class TestMarketplaceService:
def test_create_import_job_invalid_shop(self, db, test_user): def test_create_import_job_invalid_shop(self, db, test_user):
"""Test import job creation with invalid shop""" """Test import job creation with invalid shop"""
request = MarketplaceImportRequest( request = MarketplaceImportJobRequest(
url="https://example.com/products.csv", url="https://example.com/products.csv",
marketplace="Amazon", marketplace="Amazon",
shop_code="INVALID_SHOP", shop_code="INVALID_SHOP",
@@ -114,7 +114,7 @@ class TestMarketplaceService:
test_shop.owner_id = other_user.id test_shop.owner_id = other_user.id
db.commit() db.commit()
request = MarketplaceImportRequest( request = MarketplaceImportJobRequest(
url="https://example.com/products.csv", url="https://example.com/products.csv",
marketplace="Amazon", marketplace="Amazon",
shop_code=test_shop.shop_code, shop_code=test_shop.shop_code,
@@ -127,24 +127,24 @@ class TestMarketplaceService:
exception = exc_info.value exception = exc_info.value
assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS" assert exception.error_code == "UNAUTHORIZED_SHOP_ACCESS"
def test_get_import_job_by_id_success(self, db, test_marketplace_job, test_user): def test_get_import_job_by_id_success(self, db, test_marketplace_import_job, test_user):
"""Test getting import job by ID for job owner""" """Test getting import job by ID for job owner"""
result = self.service.get_import_job_by_id( result = self.service.get_import_job_by_id(
db, test_marketplace_job.id, test_user db, test_marketplace_import_job.id, test_user
) )
assert result.id == test_marketplace_job.id assert result.id == test_marketplace_import_job.id
assert result.user_id == test_user.id assert result.user_id == test_user.id
def test_get_import_job_by_id_admin_access( def test_get_import_job_by_id_admin_access(
self, db, test_marketplace_job, test_admin self, db, test_marketplace_import_job, test_admin
): ):
"""Test that admin can access any import job""" """Test that admin can access any import job"""
result = self.service.get_import_job_by_id( result = self.service.get_import_job_by_id(
db, test_marketplace_job.id, test_admin db, test_marketplace_import_job.id, test_admin
) )
assert result.id == test_marketplace_job.id assert result.id == test_marketplace_import_job.id
def test_get_import_job_by_id_not_found(self, db, test_user): def test_get_import_job_by_id_not_found(self, db, test_user):
"""Test getting non-existent import job""" """Test getting non-existent import job"""
@@ -157,42 +157,42 @@ class TestMarketplaceService:
assert "99999" in exception.message assert "99999" in exception.message
def test_get_import_job_by_id_access_denied( def test_get_import_job_by_id_access_denied(
self, db, test_marketplace_job, other_user self, db, test_marketplace_import_job, other_user
): ):
"""Test access denied when user doesn't own the job""" """Test access denied when user doesn't own the job"""
with pytest.raises(ImportJobNotOwnedException) as exc_info: with pytest.raises(ImportJobNotOwnedException) as exc_info:
self.service.get_import_job_by_id(db, test_marketplace_job.id, other_user) self.service.get_import_job_by_id(db, test_marketplace_import_job.id, other_user)
exception = exc_info.value exception = exc_info.value
assert exception.error_code == "IMPORT_JOB_NOT_OWNED" assert exception.error_code == "IMPORT_JOB_NOT_OWNED"
assert exception.status_code == 403 assert exception.status_code == 403
assert str(test_marketplace_job.id) in exception.message assert str(test_marketplace_import_job.id) in exception.message
def test_get_import_jobs_user_filter(self, db, test_marketplace_job, test_user): def test_get_import_jobs_user_filter(self, db, test_marketplace_import_job, test_user):
"""Test getting import jobs filtered by user""" """Test getting import jobs filtered by user"""
jobs = self.service.get_import_jobs(db, test_user) jobs = self.service.get_import_jobs(db, test_user)
assert len(jobs) >= 1 assert len(jobs) >= 1
assert any(job.id == test_marketplace_job.id for job in jobs) assert any(job.id == test_marketplace_import_job.id for job in jobs)
assert test_marketplace_job.user_id == test_user.id assert test_marketplace_import_job.user_id == test_user.id
def test_get_import_jobs_admin_sees_all(self, db, test_marketplace_job, test_admin): def test_get_import_jobs_admin_sees_all(self, db, test_marketplace_import_job, test_admin):
"""Test that admin sees all import jobs""" """Test that admin sees all import jobs"""
jobs = self.service.get_import_jobs(db, test_admin) jobs = self.service.get_import_jobs(db, test_admin)
assert len(jobs) >= 1 assert len(jobs) >= 1
assert any(job.id == test_marketplace_job.id for job in jobs) assert any(job.id == test_marketplace_import_job.id for job in jobs)
def test_get_import_jobs_with_marketplace_filter( def test_get_import_jobs_with_marketplace_filter(
self, db, test_marketplace_job, test_user self, db, test_marketplace_import_job, test_user
): ):
"""Test getting import jobs with marketplace filter""" """Test getting import jobs with marketplace filter"""
jobs = self.service.get_import_jobs( jobs = self.service.get_import_jobs(
db, test_user, marketplace=test_marketplace_job.marketplace db, test_user, marketplace=test_marketplace_import_job.marketplace
) )
assert len(jobs) >= 1 assert len(jobs) >= 1
assert any(job.marketplace == test_marketplace_job.marketplace for job in jobs) assert any(job.marketplace == test_marketplace_import_job.marketplace for job in jobs)
def test_get_import_jobs_with_pagination(self, db, test_user, test_shop): def test_get_import_jobs_with_pagination(self, db, test_user, test_shop):
"""Test getting import jobs with pagination""" """Test getting import jobs with pagination"""
@@ -228,11 +228,11 @@ class TestMarketplaceService:
assert exception.error_code == "VALIDATION_ERROR" assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to retrieve import jobs" in exception.message assert "Failed to retrieve import jobs" in exception.message
def test_update_job_status_success(self, db, test_marketplace_job): def test_update_job_status_success(self, db, test_marketplace_import_job):
"""Test updating job status""" """Test updating job status"""
result = self.service.update_job_status( result = self.service.update_job_status(
db, db,
test_marketplace_job.id, test_marketplace_import_job.id,
"completed", "completed",
imported_count=100, imported_count=100,
total_processed=100, total_processed=100,
@@ -260,7 +260,7 @@ class TestMarketplaceService:
assert exception.error_code == "VALIDATION_ERROR" assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to update job status" in exception.message assert "Failed to update job status" in exception.message
def test_get_job_stats_user(self, db, test_marketplace_job, test_user): def test_get_job_stats_user(self, db, test_marketplace_import_job, test_user):
"""Test getting job statistics for user""" """Test getting job statistics for user"""
stats = self.service.get_job_stats(db, test_user) stats = self.service.get_job_stats(db, test_user)
@@ -271,7 +271,7 @@ class TestMarketplaceService:
assert "failed_jobs" in stats assert "failed_jobs" in stats
assert isinstance(stats["total_jobs"], int) assert isinstance(stats["total_jobs"], int)
def test_get_job_stats_admin(self, db, test_marketplace_job, test_admin): def test_get_job_stats_admin(self, db, test_marketplace_import_job, test_admin):
"""Test getting job statistics for admin""" """Test getting job statistics for admin"""
stats = self.service.get_job_stats(db, test_admin) stats = self.service.get_job_stats(db, test_admin)
@@ -287,14 +287,14 @@ class TestMarketplaceService:
assert exception.error_code == "VALIDATION_ERROR" assert exception.error_code == "VALIDATION_ERROR"
assert "Failed to retrieve job statistics" in exception.message assert "Failed to retrieve job statistics" in exception.message
def test_convert_to_response_model(self, test_marketplace_job): def test_convert_to_response_model(self, test_marketplace_import_job):
"""Test converting database model to response model""" """Test converting database model to response model"""
response = self.service.convert_to_response_model(test_marketplace_job) response = self.service.convert_to_response_model(test_marketplace_import_job)
assert response.job_id == test_marketplace_job.id assert response.job_id == test_marketplace_import_job.id
assert response.status == test_marketplace_job.status assert response.status == test_marketplace_import_job.status
assert response.marketplace == test_marketplace_job.marketplace assert response.marketplace == test_marketplace_import_job.marketplace
assert response.imported == (test_marketplace_job.imported_count or 0) assert response.imported == (test_marketplace_import_job.imported_count or 0)
def test_cancel_import_job_success(self, db, test_user, test_shop): def test_cancel_import_job_success(self, db, test_user, test_shop):
"""Test cancelling a pending import job""" """Test cancelling a pending import job"""
@@ -330,24 +330,24 @@ class TestMarketplaceService:
exception = exc_info.value exception = exc_info.value
assert exception.error_code == "IMPORT_JOB_NOT_FOUND" assert exception.error_code == "IMPORT_JOB_NOT_FOUND"
def test_cancel_import_job_access_denied(self, db, test_marketplace_job, other_user): def test_cancel_import_job_access_denied(self, db, test_marketplace_import_job, other_user):
"""Test cancelling import job without access""" """Test cancelling import job without access"""
with pytest.raises(ImportJobNotOwnedException) as exc_info: with pytest.raises(ImportJobNotOwnedException) as exc_info:
self.service.cancel_import_job(db, test_marketplace_job.id, other_user) self.service.cancel_import_job(db, test_marketplace_import_job.id, other_user)
exception = exc_info.value exception = exc_info.value
assert exception.error_code == "IMPORT_JOB_NOT_OWNED" assert exception.error_code == "IMPORT_JOB_NOT_OWNED"
def test_cancel_import_job_invalid_status( def test_cancel_import_job_invalid_status(
self, db, test_marketplace_job, test_user self, db, test_marketplace_import_job, test_user
): ):
"""Test cancelling a job that can't be cancelled""" """Test cancelling a job that can't be cancelled"""
# Set job status to completed # Set job status to completed
test_marketplace_job.status = "completed" test_marketplace_import_job.status = "completed"
db.commit() db.commit()
with pytest.raises(ImportJobCannotBeCancelledException) as exc_info: with pytest.raises(ImportJobCannotBeCancelledException) as exc_info:
self.service.cancel_import_job(db, test_marketplace_job.id, test_user) self.service.cancel_import_job(db, test_marketplace_import_job.id, test_user)
exception = exc_info.value exception = exc_info.value
assert exception.error_code == "IMPORT_JOB_CANNOT_BE_CANCELLED" assert exception.error_code == "IMPORT_JOB_CANNOT_BE_CANCELLED"
@@ -396,10 +396,10 @@ class TestMarketplaceService:
exception = exc_info.value exception = exc_info.value
assert exception.error_code == "IMPORT_JOB_NOT_FOUND" assert exception.error_code == "IMPORT_JOB_NOT_FOUND"
def test_delete_import_job_access_denied(self, db, test_marketplace_job, other_user): def test_delete_import_job_access_denied(self, db, test_marketplace_import_job, other_user):
"""Test deleting import job without access""" """Test deleting import job without access"""
with pytest.raises(ImportJobNotOwnedException) as exc_info: with pytest.raises(ImportJobNotOwnedException) as exc_info:
self.service.delete_import_job(db, test_marketplace_job.id, other_user) self.service.delete_import_job(db, test_marketplace_import_job.id, other_user)
exception = exc_info.value exception = exc_info.value
assert exception.error_code == "IMPORT_JOB_NOT_OWNED" assert exception.error_code == "IMPORT_JOB_NOT_OWNED"
@@ -449,7 +449,7 @@ class TestMarketplaceService:
def test_create_import_job_database_error(self, db_with_error, test_user): def test_create_import_job_database_error(self, db_with_error, test_user):
"""Test import job creation handles database errors""" """Test import job creation handles database errors"""
request = MarketplaceImportRequest( request = MarketplaceImportJobRequest(
url="https://example.com/products.csv", url="https://example.com/products.csv",
marketplace="Amazon", marketplace="Amazon",
shop_code="TEST_SHOP", shop_code="TEST_SHOP",

View File

@@ -1,29 +1,29 @@
# tests/test_product_service.py # tests/test_product_service.py
import pytest import pytest
from app.services.product_service import ProductService from app.services.marketplace_product_service import MarketplaceProductService
from app.exceptions import ( from app.exceptions import (
ProductNotFoundException, MarketplaceProductNotFoundException,
ProductAlreadyExistsException, MarketplaceProductAlreadyExistsException,
InvalidProductDataException, InvalidMarketplaceProductDataException,
ProductValidationException, MarketplaceProductValidationException,
ValidationException, ValidationException,
) )
from models.schemas.product import ProductCreate, ProductUpdate from models.schemas.marketplace_product import MarketplaceProductCreate, MarketplaceProductUpdate
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.products @pytest.mark.products
class TestProductService: class TestProductService:
def setup_method(self): def setup_method(self):
self.service = ProductService() self.service = MarketplaceProductService()
def test_create_product_success(self, db): def test_create_product_success(self, db):
"""Test successful product creation with valid data""" """Test successful product creation with valid data"""
product_data = ProductCreate( product_data = MarketplaceProductCreate(
product_id="SVC001", marketplace_product_id="SVC001",
title="Service Test Product", title="Service Test MarketplaceProduct",
gtin="1234567890123", gtin="1234567890123",
price="19.99", price="19.99",
marketplace="TestMarket", marketplace="TestMarket",
@@ -31,22 +31,22 @@ class TestProductService:
product = self.service.create_product(db, product_data) product = self.service.create_product(db, product_data)
assert product.product_id == "SVC001" assert product.marketplace_product_id == "SVC001"
assert product.title == "Service Test Product" assert product.title == "Service Test MarketplaceProduct"
assert product.gtin == "1234567890123" assert product.gtin == "1234567890123"
assert product.marketplace == "TestMarket" assert product.marketplace == "TestMarket"
assert product.price == "19.99" # Price is stored as string after processing assert product.price == "19.99" # Price is stored as string after processing
def test_create_product_invalid_gtin(self, db): def test_create_product_invalid_gtin(self, db):
"""Test product creation with invalid GTIN raises InvalidProductDataException""" """Test product creation with invalid GTIN raises InvalidMarketplaceProductDataException"""
product_data = ProductCreate( product_data = MarketplaceProductCreate(
product_id="SVC002", marketplace_product_id="SVC002",
title="Service Test Product", title="Service Test MarketplaceProduct",
gtin="invalid_gtin", gtin="invalid_gtin",
price="19.99", price="19.99",
) )
with pytest.raises(InvalidProductDataException) as exc_info: with pytest.raises(InvalidMarketplaceProductDataException) as exc_info:
self.service.create_product(db, product_data) self.service.create_product(db, product_data)
assert exc_info.value.error_code == "INVALID_PRODUCT_DATA" assert exc_info.value.error_code == "INVALID_PRODUCT_DATA"
@@ -55,201 +55,201 @@ class TestProductService:
assert exc_info.value.details.get("field") == "gtin" assert exc_info.value.details.get("field") == "gtin"
def test_create_product_missing_product_id(self, db): def test_create_product_missing_product_id(self, db):
"""Test product creation without product_id raises ProductValidationException""" """Test product creation without marketplace_product_id raises MarketplaceProductValidationException"""
product_data = ProductCreate( product_data = MarketplaceProductCreate(
product_id="", # Empty product ID marketplace_product_id="", # Empty product ID
title="Service Test Product", title="Service Test MarketplaceProduct",
price="19.99", price="19.99",
) )
with pytest.raises(ProductValidationException) as exc_info: with pytest.raises(MarketplaceProductValidationException) as exc_info:
self.service.create_product(db, product_data) self.service.create_product(db, product_data)
assert exc_info.value.error_code == "PRODUCT_VALIDATION_FAILED" assert exc_info.value.error_code == "PRODUCT_VALIDATION_FAILED"
assert "Product ID is required" in str(exc_info.value) assert "MarketplaceProduct ID is required" in str(exc_info.value)
assert exc_info.value.details.get("field") == "product_id" assert exc_info.value.details.get("field") == "marketplace_product_id"
def test_create_product_missing_title(self, db): def test_create_product_missing_title(self, db):
"""Test product creation without title raises ProductValidationException""" """Test product creation without title raises MarketplaceProductValidationException"""
product_data = ProductCreate( product_data = MarketplaceProductCreate(
product_id="SVC003", marketplace_product_id="SVC003",
title="", # Empty title title="", # Empty title
price="19.99", price="19.99",
) )
with pytest.raises(ProductValidationException) as exc_info: with pytest.raises(MarketplaceProductValidationException) as exc_info:
self.service.create_product(db, product_data) self.service.create_product(db, product_data)
assert exc_info.value.error_code == "PRODUCT_VALIDATION_FAILED" assert exc_info.value.error_code == "PRODUCT_VALIDATION_FAILED"
assert "Product title is required" in str(exc_info.value) assert "MarketplaceProduct title is required" in str(exc_info.value)
assert exc_info.value.details.get("field") == "title" assert exc_info.value.details.get("field") == "title"
def test_create_product_already_exists(self, db, test_product): def test_create_product_already_exists(self, db, test_marketplace_product):
"""Test creating product with existing ID raises ProductAlreadyExistsException""" """Test creating product with existing ID raises MarketplaceProductAlreadyExistsException"""
product_data = ProductCreate( product_data = MarketplaceProductCreate(
product_id=test_product.product_id, # Use existing product ID marketplace_product_id=test_marketplace_product.marketplace_product_id, # Use existing product ID
title="Duplicate Product", title="Duplicate MarketplaceProduct",
price="29.99", price="29.99",
) )
with pytest.raises(ProductAlreadyExistsException) as exc_info: with pytest.raises(MarketplaceProductAlreadyExistsException) as exc_info:
self.service.create_product(db, product_data) self.service.create_product(db, product_data)
assert exc_info.value.error_code == "PRODUCT_ALREADY_EXISTS" assert exc_info.value.error_code == "PRODUCT_ALREADY_EXISTS"
assert test_product.product_id in str(exc_info.value) assert test_marketplace_product.marketplace_product_id in str(exc_info.value)
assert exc_info.value.status_code == 409 assert exc_info.value.status_code == 409
assert exc_info.value.details.get("product_id") == test_product.product_id assert exc_info.value.details.get("marketplace_product_id") == test_marketplace_product.marketplace_product_id
def test_create_product_invalid_price(self, db): def test_create_product_invalid_price(self, db):
"""Test product creation with invalid price raises InvalidProductDataException""" """Test product creation with invalid price raises InvalidMarketplaceProductDataException"""
product_data = ProductCreate( product_data = MarketplaceProductCreate(
product_id="SVC004", marketplace_product_id="SVC004",
title="Service Test Product", title="Service Test MarketplaceProduct",
price="invalid_price", price="invalid_price",
) )
with pytest.raises(InvalidProductDataException) as exc_info: with pytest.raises(InvalidMarketplaceProductDataException) as exc_info:
self.service.create_product(db, product_data) self.service.create_product(db, product_data)
assert exc_info.value.error_code == "INVALID_PRODUCT_DATA" assert exc_info.value.error_code == "INVALID_PRODUCT_DATA"
assert "Invalid price format" in str(exc_info.value) assert "Invalid price format" in str(exc_info.value)
assert exc_info.value.details.get("field") == "price" assert exc_info.value.details.get("field") == "price"
def test_get_product_by_id_or_raise_success(self, db, test_product): def test_get_product_by_id_or_raise_success(self, db, test_marketplace_product):
"""Test successful product retrieval by ID""" """Test successful product retrieval by ID"""
product = self.service.get_product_by_id_or_raise(db, test_product.product_id) product = self.service.get_product_by_id_or_raise(db, test_marketplace_product.marketplace_product_id)
assert product.product_id == test_product.product_id assert product.marketplace_product_id == test_marketplace_product.marketplace_product_id
assert product.title == test_product.title assert product.title == test_marketplace_product.title
def test_get_product_by_id_or_raise_not_found(self, db): def test_get_product_by_id_or_raise_not_found(self, db):
"""Test product retrieval with non-existent ID raises ProductNotFoundException""" """Test product retrieval with non-existent ID raises MarketplaceProductNotFoundException"""
with pytest.raises(ProductNotFoundException) as exc_info: with pytest.raises(MarketplaceProductNotFoundException) as exc_info:
self.service.get_product_by_id_or_raise(db, "NONEXISTENT") self.service.get_product_by_id_or_raise(db, "NONEXISTENT")
assert exc_info.value.error_code == "PRODUCT_NOT_FOUND" assert exc_info.value.error_code == "PRODUCT_NOT_FOUND"
assert "NONEXISTENT" in str(exc_info.value) assert "NONEXISTENT" in str(exc_info.value)
assert exc_info.value.status_code == 404 assert exc_info.value.status_code == 404
assert exc_info.value.details.get("resource_type") == "Product" assert exc_info.value.details.get("resource_type") == "MarketplaceProduct"
assert exc_info.value.details.get("identifier") == "NONEXISTENT" assert exc_info.value.details.get("identifier") == "NONEXISTENT"
def test_get_products_with_filters_success(self, db, test_product): def test_get_products_with_filters_success(self, db, test_marketplace_product):
"""Test getting products with various filters""" """Test getting products with various filters"""
products, total = self.service.get_products_with_filters( products, total = self.service.get_products_with_filters(
db, brand=test_product.brand db, brand=test_marketplace_product.brand
) )
assert total == 1 assert total == 1
assert len(products) == 1 assert len(products) == 1
assert products[0].brand == test_product.brand assert products[0].brand == test_marketplace_product.brand
def test_get_products_with_search(self, db, test_product): def test_get_products_with_search(self, db, test_marketplace_product):
"""Test getting products with search term""" """Test getting products with search term"""
products, total = self.service.get_products_with_filters( products, total = self.service.get_products_with_filters(
db, search="Test Product" db, search="Test MarketplaceProduct"
) )
assert total >= 1 assert total >= 1
assert len(products) >= 1 assert len(products) >= 1
# Verify search worked by checking that title contains search term # Verify search worked by checking that title contains search term
found_product = next((p for p in products if p.product_id == test_product.product_id), None) found_product = next((p for p in products if p.marketplace_product_id == test_marketplace_product.marketplace_product_id), None)
assert found_product is not None assert found_product is not None
def test_update_product_success(self, db, test_product): def test_update_product_success(self, db, test_marketplace_product):
"""Test successful product update""" """Test successful product update"""
update_data = ProductUpdate( update_data = MarketplaceProductUpdate(
title="Updated Product Title", title="Updated MarketplaceProduct Title",
price="39.99" price="39.99"
) )
updated_product = self.service.update_product(db, test_product.product_id, update_data) updated_product = self.service.update_product(db, test_marketplace_product.marketplace_product_id, update_data)
assert updated_product.title == "Updated Product Title" assert updated_product.title == "Updated MarketplaceProduct Title"
assert updated_product.price == "39.99" # Price is stored as string after processing assert updated_product.price == "39.99" # Price is stored as string after processing
assert updated_product.product_id == test_product.product_id # ID unchanged assert updated_product.marketplace_product_id == test_marketplace_product.marketplace_product_id # ID unchanged
def test_update_product_not_found(self, db): def test_update_product_not_found(self, db):
"""Test updating non-existent product raises ProductNotFoundException""" """Test updating non-existent product raises MarketplaceProductNotFoundException"""
update_data = ProductUpdate(title="Updated Title") update_data = MarketplaceProductUpdate(title="Updated Title")
with pytest.raises(ProductNotFoundException) as exc_info: with pytest.raises(MarketplaceProductNotFoundException) as exc_info:
self.service.update_product(db, "NONEXISTENT", update_data) self.service.update_product(db, "NONEXISTENT", update_data)
assert exc_info.value.error_code == "PRODUCT_NOT_FOUND" assert exc_info.value.error_code == "PRODUCT_NOT_FOUND"
assert "NONEXISTENT" in str(exc_info.value) assert "NONEXISTENT" in str(exc_info.value)
def test_update_product_invalid_gtin(self, db, test_product): def test_update_product_invalid_gtin(self, db, test_marketplace_product):
"""Test updating product with invalid GTIN raises InvalidProductDataException""" """Test updating product with invalid GTIN raises InvalidMarketplaceProductDataException"""
update_data = ProductUpdate(gtin="invalid_gtin") update_data = MarketplaceProductUpdate(gtin="invalid_gtin")
with pytest.raises(InvalidProductDataException) as exc_info: with pytest.raises(InvalidMarketplaceProductDataException) as exc_info:
self.service.update_product(db, test_product.product_id, update_data) self.service.update_product(db, test_marketplace_product.marketplace_product_id, update_data)
assert exc_info.value.error_code == "INVALID_PRODUCT_DATA" assert exc_info.value.error_code == "INVALID_PRODUCT_DATA"
assert "Invalid GTIN format" in str(exc_info.value) assert "Invalid GTIN format" in str(exc_info.value)
assert exc_info.value.details.get("field") == "gtin" assert exc_info.value.details.get("field") == "gtin"
def test_update_product_empty_title(self, db, test_product): def test_update_product_empty_title(self, db, test_marketplace_product):
"""Test updating product with empty title raises ProductValidationException""" """Test updating product with empty title raises MarketplaceProductValidationException"""
update_data = ProductUpdate(title="") update_data = MarketplaceProductUpdate(title="")
with pytest.raises(ProductValidationException) as exc_info: with pytest.raises(MarketplaceProductValidationException) as exc_info:
self.service.update_product(db, test_product.product_id, update_data) self.service.update_product(db, test_marketplace_product.marketplace_product_id, update_data)
assert exc_info.value.error_code == "PRODUCT_VALIDATION_FAILED" assert exc_info.value.error_code == "PRODUCT_VALIDATION_FAILED"
assert "Product title cannot be empty" in str(exc_info.value) assert "MarketplaceProduct title cannot be empty" in str(exc_info.value)
assert exc_info.value.details.get("field") == "title" assert exc_info.value.details.get("field") == "title"
def test_update_product_invalid_price(self, db, test_product): def test_update_product_invalid_price(self, db, test_marketplace_product):
"""Test updating product with invalid price raises InvalidProductDataException""" """Test updating product with invalid price raises InvalidMarketplaceProductDataException"""
update_data = ProductUpdate(price="invalid_price") update_data = MarketplaceProductUpdate(price="invalid_price")
with pytest.raises(InvalidProductDataException) as exc_info: with pytest.raises(InvalidMarketplaceProductDataException) as exc_info:
self.service.update_product(db, test_product.product_id, update_data) self.service.update_product(db, test_marketplace_product.marketplace_product_id, update_data)
assert exc_info.value.error_code == "INVALID_PRODUCT_DATA" assert exc_info.value.error_code == "INVALID_PRODUCT_DATA"
assert "Invalid price format" in str(exc_info.value) assert "Invalid price format" in str(exc_info.value)
assert exc_info.value.details.get("field") == "price" assert exc_info.value.details.get("field") == "price"
def test_delete_product_success(self, db, test_product): def test_delete_product_success(self, db, test_marketplace_product):
"""Test successful product deletion""" """Test successful product deletion"""
result = self.service.delete_product(db, test_product.product_id) result = self.service.delete_product(db, test_marketplace_product.marketplace_product_id)
assert result is True assert result is True
# Verify product is deleted # Verify product is deleted
deleted_product = self.service.get_product_by_id(db, test_product.product_id) deleted_product = self.service.get_product_by_id(db, test_marketplace_product.marketplace_product_id)
assert deleted_product is None assert deleted_product is None
def test_delete_product_not_found(self, db): def test_delete_product_not_found(self, db):
"""Test deleting non-existent product raises ProductNotFoundException""" """Test deleting non-existent product raises MarketplaceProductNotFoundException"""
with pytest.raises(ProductNotFoundException) as exc_info: with pytest.raises(MarketplaceProductNotFoundException) as exc_info:
self.service.delete_product(db, "NONEXISTENT") self.service.delete_product(db, "NONEXISTENT")
assert exc_info.value.error_code == "PRODUCT_NOT_FOUND" assert exc_info.value.error_code == "PRODUCT_NOT_FOUND"
assert "NONEXISTENT" in str(exc_info.value) assert "NONEXISTENT" in str(exc_info.value)
def test_get_stock_info_success(self, db, test_product_with_stock): def test_get_stock_info_success(self, db, test_marketplace_product_with_stock):
"""Test getting stock info for product with stock""" """Test getting stock info for product with stock"""
# Extract the product from the dictionary # Extract the product from the dictionary
product = test_product_with_stock['product'] marketplace_product = test_marketplace_product_with_stock['marketplace_product']
stock_info = self.service.get_stock_info(db, product.gtin) stock_info = self.service.get_stock_info(db, marketplace_product.gtin)
assert stock_info is not None assert stock_info is not None
assert stock_info.gtin == product.gtin assert stock_info.gtin == marketplace_product.gtin
assert stock_info.total_quantity > 0 assert stock_info.total_quantity > 0
assert len(stock_info.locations) > 0 assert len(stock_info.locations) > 0
def test_get_stock_info_no_stock(self, db, test_product): def test_get_stock_info_no_stock(self, db, test_marketplace_product):
"""Test getting stock info for product without stock""" """Test getting stock info for product without stock"""
stock_info = self.service.get_stock_info(db, test_product.gtin or "1234567890123") stock_info = self.service.get_stock_info(db, test_marketplace_product.gtin or "1234567890123")
assert stock_info is None assert stock_info is None
def test_product_exists_true(self, db, test_product): def test_product_exists_true(self, db, test_marketplace_product):
"""Test product_exists returns True for existing product""" """Test product_exists returns True for existing product"""
exists = self.service.product_exists(db, test_product.product_id) exists = self.service.product_exists(db, test_marketplace_product.marketplace_product_id)
assert exists is True assert exists is True
def test_product_exists_false(self, db): def test_product_exists_false(self, db):
@@ -257,7 +257,7 @@ class TestProductService:
exists = self.service.product_exists(db, "NONEXISTENT") exists = self.service.product_exists(db, "NONEXISTENT")
assert exists is False assert exists is False
def test_generate_csv_export_success(self, db, test_product): def test_generate_csv_export_success(self, db, test_marketplace_product):
"""Test CSV export generation""" """Test CSV export generation"""
csv_generator = self.service.generate_csv_export(db) csv_generator = self.service.generate_csv_export(db)
@@ -265,17 +265,17 @@ class TestProductService:
csv_lines = list(csv_generator) csv_lines = list(csv_generator)
assert len(csv_lines) > 1 # Header + at least one data row assert len(csv_lines) > 1 # Header + at least one data row
assert csv_lines[0].startswith("product_id,title,description") # Check header assert csv_lines[0].startswith("marketplace_product_id,title,description") # Check header
# Check that test product appears in CSV # Check that test product appears in CSV
csv_content = "".join(csv_lines) csv_content = "".join(csv_lines)
assert test_product.product_id in csv_content assert test_marketplace_product.marketplace_product_id in csv_content
def test_generate_csv_export_with_filters(self, db, test_product): def test_generate_csv_export_with_filters(self, db, test_marketplace_product):
"""Test CSV export with marketplace filter""" """Test CSV export with marketplace filter"""
csv_generator = self.service.generate_csv_export( csv_generator = self.service.generate_csv_export(
db, db,
marketplace=test_product.marketplace marketplace=test_marketplace_product.marketplace
) )
csv_lines = list(csv_generator) csv_lines = list(csv_generator)
@@ -283,4 +283,4 @@ class TestProductService:
if len(csv_lines) > 1: # If there's data if len(csv_lines) > 1: # If there's data
csv_content = "".join(csv_lines) csv_content = "".join(csv_lines)
assert test_product.marketplace in csv_content assert test_marketplace_product.marketplace in csv_content

View File

@@ -7,7 +7,7 @@ from app.exceptions import (
ShopAlreadyExistsException, ShopAlreadyExistsException,
UnauthorizedShopAccessException, UnauthorizedShopAccessException,
InvalidShopDataException, InvalidShopDataException,
ProductNotFoundException, MarketplaceProductNotFoundException,
ShopProductAlreadyExistsException, ShopProductAlreadyExistsException,
MaxShopsReachedException, MaxShopsReachedException,
ValidationException, ValidationException,
@@ -179,7 +179,7 @@ class TestShopService:
def test_add_product_to_shop_success(self, db, test_shop, unique_product): def test_add_product_to_shop_success(self, db, test_shop, unique_product):
"""Test successfully adding product to shop""" """Test successfully adding product to shop"""
shop_product_data = ShopProductCreate( shop_product_data = ShopProductCreate(
product_id=unique_product.product_id, marketplace_product_id=unique_product.marketplace_product_id,
price="15.99", price="15.99",
is_featured=True, is_featured=True,
stock_quantity=5, stock_quantity=5,
@@ -191,25 +191,25 @@ class TestShopService:
assert shop_product is not None assert shop_product is not None
assert shop_product.shop_id == test_shop.id assert shop_product.shop_id == test_shop.id
assert shop_product.product_id == unique_product.id assert shop_product.marketplace_product_id == unique_product.id
def test_add_product_to_shop_product_not_found(self, db, test_shop): def test_add_product_to_shop_product_not_found(self, db, test_shop):
"""Test adding non-existent product to shop fails""" """Test adding non-existent product to shop fails"""
shop_product_data = ShopProductCreate(product_id="NONEXISTENT", price="15.99") shop_product_data = ShopProductCreate(marketplace_product_id="NONEXISTENT", price="15.99")
with pytest.raises(ProductNotFoundException) as exc_info: with pytest.raises(MarketplaceProductNotFoundException) as exc_info:
self.service.add_product_to_shop(db, test_shop, shop_product_data) self.service.add_product_to_shop(db, test_shop, shop_product_data)
exception = exc_info.value exception = exc_info.value
assert exception.status_code == 404 assert exception.status_code == 404
assert exception.error_code == "PRODUCT_NOT_FOUND" assert exception.error_code == "PRODUCT_NOT_FOUND"
assert exception.details["resource_type"] == "Product" assert exception.details["resource_type"] == "MarketplaceProduct"
assert exception.details["identifier"] == "NONEXISTENT" assert exception.details["identifier"] == "NONEXISTENT"
def test_add_product_to_shop_already_exists(self, db, test_shop, shop_product): def test_add_product_to_shop_already_exists(self, db, test_shop, shop_product):
"""Test adding product that's already in shop fails""" """Test adding product that's already in shop fails"""
shop_product_data = ShopProductCreate( shop_product_data = ShopProductCreate(
product_id=shop_product.product.product_id, price="15.99" marketplace_product_id=shop_product.product.marketplace_product_id, price="15.99"
) )
with pytest.raises(ShopProductAlreadyExistsException) as exc_info: with pytest.raises(ShopProductAlreadyExistsException) as exc_info:
@@ -219,7 +219,7 @@ class TestShopService:
assert exception.status_code == 409 assert exception.status_code == 409
assert exception.error_code == "SHOP_PRODUCT_ALREADY_EXISTS" assert exception.error_code == "SHOP_PRODUCT_ALREADY_EXISTS"
assert exception.details["shop_code"] == test_shop.shop_code assert exception.details["shop_code"] == test_shop.shop_code
assert exception.details["product_id"] == shop_product.product.product_id assert exception.details["marketplace_product_id"] == shop_product.product.marketplace_product_id
def test_get_shop_products_owner_access( def test_get_shop_products_owner_access(
self, db, test_user, test_shop, shop_product self, db, test_user, test_shop, shop_product
@@ -229,8 +229,8 @@ class TestShopService:
assert total >= 1 assert total >= 1
assert len(products) >= 1 assert len(products) >= 1
product_ids = [p.product_id for p in products] product_ids = [p.marketplace_product_id for p in products]
assert shop_product.product_id in product_ids assert shop_product.marketplace_product_id in product_ids
def test_get_shop_products_access_denied(self, db, test_user, inactive_shop): def test_get_shop_products_access_denied(self, db, test_user, inactive_shop):
"""Test non-owner cannot access unverified shop products""" """Test non-owner cannot access unverified shop products"""
@@ -300,7 +300,7 @@ class TestShopService:
monkeypatch.setattr(db, "commit", mock_commit) monkeypatch.setattr(db, "commit", mock_commit)
shop_product_data = ShopProductCreate( shop_product_data = ShopProductCreate(
product_id=unique_product.product_id, price="15.99" marketplace_product_id=unique_product.marketplace_product_id, price="15.99"
) )
with pytest.raises(ValidationException) as exc_info: with pytest.raises(ValidationException) as exc_info:

View File

@@ -2,7 +2,7 @@
import pytest import pytest
from app.services.stats_service import StatsService from app.services.stats_service import StatsService
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
from models.database.stock import Stock from models.database.stock import Stock
@@ -15,7 +15,7 @@ class TestStatsService:
"""Setup method following the same pattern as other service tests""" """Setup method following the same pattern as other service tests"""
self.service = StatsService() self.service = StatsService()
def test_get_comprehensive_stats_basic(self, db, test_product, test_stock): def test_get_comprehensive_stats_basic(self, db, test_marketplace_product, test_stock):
"""Test getting comprehensive stats with basic data""" """Test getting comprehensive stats with basic data"""
stats = self.service.get_comprehensive_stats(db) stats = self.service.get_comprehensive_stats(db)
@@ -31,13 +31,13 @@ class TestStatsService:
assert stats["total_stock_entries"] >= 1 assert stats["total_stock_entries"] >= 1
assert stats["total_inventory_quantity"] >= 10 # test_stock has quantity 10 assert stats["total_inventory_quantity"] >= 10 # test_stock has quantity 10
def test_get_comprehensive_stats_multiple_products(self, db, test_product): def test_get_comprehensive_stats_multiple_products(self, db, test_marketplace_product):
"""Test comprehensive stats with multiple products across different dimensions""" """Test comprehensive stats with multiple products across different dimensions"""
# Create products with different brands, categories, marketplaces # Create products with different brands, categories, marketplaces
additional_products = [ additional_products = [
Product( MarketplaceProduct(
product_id="PROD002", marketplace_product_id="PROD002",
title="Product 2", title="MarketplaceProduct 2",
brand="DifferentBrand", brand="DifferentBrand",
google_product_category="Different Category", google_product_category="Different Category",
marketplace="Amazon", marketplace="Amazon",
@@ -45,9 +45,9 @@ class TestStatsService:
price="15.99", price="15.99",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="PROD003", marketplace_product_id="PROD003",
title="Product 3", title="MarketplaceProduct 3",
brand="ThirdBrand", brand="ThirdBrand",
google_product_category="Third Category", google_product_category="Third Category",
marketplace="eBay", marketplace="eBay",
@@ -55,12 +55,12 @@ class TestStatsService:
price="25.99", price="25.99",
currency="USD", currency="USD",
), ),
Product( MarketplaceProduct(
product_id="PROD004", marketplace_product_id="PROD004",
title="Product 4", title="MarketplaceProduct 4",
brand="TestBrand", # Same as test_product brand="TestBrand", # Same as test_marketplace_product
google_product_category="Different Category", google_product_category="Different Category",
marketplace="Letzshop", # Same as test_product marketplace="Letzshop", # Same as test_marketplace_product
shop_name="DifferentShop", shop_name="DifferentShop",
price="35.99", price="35.99",
currency="EUR", currency="EUR",
@@ -71,7 +71,7 @@ class TestStatsService:
stats = self.service.get_comprehensive_stats(db) stats = self.service.get_comprehensive_stats(db)
assert stats["total_products"] >= 4 # test_product + 3 additional assert stats["total_products"] >= 4 # test_marketplace_product + 3 additional
assert stats["unique_brands"] >= 3 # TestBrand, DifferentBrand, ThirdBrand assert stats["unique_brands"] >= 3 # TestBrand, DifferentBrand, ThirdBrand
assert stats["unique_categories"] >= 2 # At least 2 different categories assert stats["unique_categories"] >= 2 # At least 2 different categories
assert stats["unique_marketplaces"] >= 3 # Letzshop, Amazon, eBay assert stats["unique_marketplaces"] >= 3 # Letzshop, Amazon, eBay
@@ -81,9 +81,9 @@ class TestStatsService:
"""Test comprehensive stats handles null/empty values correctly""" """Test comprehensive stats handles null/empty values correctly"""
# Create products with null/empty values # Create products with null/empty values
products_with_nulls = [ products_with_nulls = [
Product( MarketplaceProduct(
product_id="NULL001", marketplace_product_id="NULL001",
title="Product with Nulls", title="MarketplaceProduct with Nulls",
brand=None, # Null brand brand=None, # Null brand
google_product_category=None, # Null category google_product_category=None, # Null category
marketplace=None, # Null marketplace marketplace=None, # Null marketplace
@@ -91,9 +91,9 @@ class TestStatsService:
price="10.00", price="10.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="EMPTY001", marketplace_product_id="EMPTY001",
title="Product with Empty Values", title="MarketplaceProduct with Empty Values",
brand="", # Empty brand brand="", # Empty brand
google_product_category="", # Empty category google_product_category="", # Empty category
marketplace="", # Empty marketplace marketplace="", # Empty marketplace
@@ -115,7 +115,7 @@ class TestStatsService:
assert isinstance(stats["unique_marketplaces"], int) assert isinstance(stats["unique_marketplaces"], int)
assert isinstance(stats["unique_shops"], int) assert isinstance(stats["unique_shops"], int)
def test_get_marketplace_breakdown_stats_basic(self, db, test_product): def test_get_marketplace_breakdown_stats_basic(self, db, test_marketplace_product):
"""Test getting marketplace breakdown stats with basic data""" """Test getting marketplace breakdown stats with basic data"""
stats = self.service.get_marketplace_breakdown_stats(db) stats = self.service.get_marketplace_breakdown_stats(db)
@@ -124,7 +124,7 @@ class TestStatsService:
# Find our test marketplace in the results # Find our test marketplace in the results
test_marketplace_stat = next( test_marketplace_stat = next(
(stat for stat in stats if stat["marketplace"] == test_product.marketplace), (stat for stat in stats if stat["marketplace"] == test_marketplace_product.marketplace),
None, None,
) )
assert test_marketplace_stat is not None assert test_marketplace_stat is not None
@@ -133,32 +133,32 @@ class TestStatsService:
assert test_marketplace_stat["unique_brands"] >= 1 assert test_marketplace_stat["unique_brands"] >= 1
def test_get_marketplace_breakdown_stats_multiple_marketplaces( def test_get_marketplace_breakdown_stats_multiple_marketplaces(
self, db, test_product self, db, test_marketplace_product
): ):
"""Test marketplace breakdown with multiple marketplaces""" """Test marketplace breakdown with multiple marketplaces"""
# Create products for different marketplaces # Create products for different marketplaces
marketplace_products = [ marketplace_products = [
Product( MarketplaceProduct(
product_id="AMAZON001", marketplace_product_id="AMAZON001",
title="Amazon Product 1", title="Amazon MarketplaceProduct 1",
brand="AmazonBrand1", brand="AmazonBrand1",
marketplace="Amazon", marketplace="Amazon",
shop_name="AmazonShop1", shop_name="AmazonShop1",
price="20.00", price="20.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="AMAZON002", marketplace_product_id="AMAZON002",
title="Amazon Product 2", title="Amazon MarketplaceProduct 2",
brand="AmazonBrand2", brand="AmazonBrand2",
marketplace="Amazon", marketplace="Amazon",
shop_name="AmazonShop2", shop_name="AmazonShop2",
price="25.00", price="25.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="EBAY001", marketplace_product_id="EBAY001",
title="eBay Product", title="eBay MarketplaceProduct",
brand="eBayBrand", brand="eBayBrand",
marketplace="eBay", marketplace="eBay",
shop_name="eBayShop", shop_name="eBayShop",
@@ -171,11 +171,11 @@ class TestStatsService:
stats = self.service.get_marketplace_breakdown_stats(db) stats = self.service.get_marketplace_breakdown_stats(db)
# Should have at least 3 marketplaces: test_product.marketplace, Amazon, eBay # Should have at least 3 marketplaces: test_marketplace_product.marketplace, Amazon, eBay
marketplace_names = [stat["marketplace"] for stat in stats] marketplace_names = [stat["marketplace"] for stat in stats]
assert "Amazon" in marketplace_names assert "Amazon" in marketplace_names
assert "eBay" in marketplace_names assert "eBay" in marketplace_names
assert test_product.marketplace in marketplace_names assert test_marketplace_product.marketplace in marketplace_names
# Check Amazon stats specifically # Check Amazon stats specifically
amazon_stat = next(stat for stat in stats if stat["marketplace"] == "Amazon") amazon_stat = next(stat for stat in stats if stat["marketplace"] == "Amazon")
@@ -192,9 +192,9 @@ class TestStatsService:
def test_get_marketplace_breakdown_stats_excludes_nulls(self, db): def test_get_marketplace_breakdown_stats_excludes_nulls(self, db):
"""Test marketplace breakdown excludes products with null marketplaces""" """Test marketplace breakdown excludes products with null marketplaces"""
# Create product with null marketplace # Create product with null marketplace
null_marketplace_product = Product( null_marketplace_product = MarketplaceProduct(
product_id="NULLMARKET001", marketplace_product_id="NULLMARKET001",
title="Product without marketplace", title="MarketplaceProduct without marketplace",
marketplace=None, marketplace=None,
shop_name="SomeShop", shop_name="SomeShop",
brand="SomeBrand", brand="SomeBrand",
@@ -212,29 +212,29 @@ class TestStatsService:
] ]
assert None not in marketplace_names assert None not in marketplace_names
def test_get_product_count(self, db, test_product): def test_get_product_count(self, db, test_marketplace_product):
"""Test getting total product count""" """Test getting total product count"""
count = self.service._get_product_count(db) count = self.service._get_product_count(db)
assert count >= 1 assert count >= 1
assert isinstance(count, int) assert isinstance(count, int)
def test_get_unique_brands_count(self, db, test_product): def test_get_unique_brands_count(self, db, test_marketplace_product):
"""Test getting unique brands count""" """Test getting unique brands count"""
# Add products with different brands # Add products with different brands
brand_products = [ brand_products = [
Product( MarketplaceProduct(
product_id="BRAND001", marketplace_product_id="BRAND001",
title="Brand Product 1", title="Brand MarketplaceProduct 1",
brand="BrandA", brand="BrandA",
marketplace="Test", marketplace="Test",
shop_name="TestShop", shop_name="TestShop",
price="10.00", price="10.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="BRAND002", marketplace_product_id="BRAND002",
title="Brand Product 2", title="Brand MarketplaceProduct 2",
brand="BrandB", brand="BrandB",
marketplace="Test", marketplace="Test",
shop_name="TestShop", shop_name="TestShop",
@@ -249,25 +249,25 @@ class TestStatsService:
assert ( assert (
count >= 2 count >= 2
) # At least BrandA and BrandB, plus possibly test_product brand ) # At least BrandA and BrandB, plus possibly test_marketplace_product brand
assert isinstance(count, int) assert isinstance(count, int)
def test_get_unique_categories_count(self, db, test_product): def test_get_unique_categories_count(self, db, test_marketplace_product):
"""Test getting unique categories count""" """Test getting unique categories count"""
# Add products with different categories # Add products with different categories
category_products = [ category_products = [
Product( MarketplaceProduct(
product_id="CAT001", marketplace_product_id="CAT001",
title="Category Product 1", title="Category MarketplaceProduct 1",
google_product_category="Electronics", google_product_category="Electronics",
marketplace="Test", marketplace="Test",
shop_name="TestShop", shop_name="TestShop",
price="10.00", price="10.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="CAT002", marketplace_product_id="CAT002",
title="Category Product 2", title="Category MarketplaceProduct 2",
google_product_category="Books", google_product_category="Books",
marketplace="Test", marketplace="Test",
shop_name="TestShop", shop_name="TestShop",
@@ -283,21 +283,21 @@ class TestStatsService:
assert count >= 2 # At least Electronics and Books assert count >= 2 # At least Electronics and Books
assert isinstance(count, int) assert isinstance(count, int)
def test_get_unique_marketplaces_count(self, db, test_product): def test_get_unique_marketplaces_count(self, db, test_marketplace_product):
"""Test getting unique marketplaces count""" """Test getting unique marketplaces count"""
# Add products with different marketplaces # Add products with different marketplaces
marketplace_products = [ marketplace_products = [
Product( MarketplaceProduct(
product_id="MARKET001", marketplace_product_id="MARKET001",
title="Marketplace Product 1", title="Marketplace MarketplaceProduct 1",
marketplace="Amazon", marketplace="Amazon",
shop_name="AmazonShop", shop_name="AmazonShop",
price="10.00", price="10.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="MARKET002", marketplace_product_id="MARKET002",
title="Marketplace Product 2", title="Marketplace MarketplaceProduct 2",
marketplace="eBay", marketplace="eBay",
shop_name="eBayShop", shop_name="eBayShop",
price="15.00", price="15.00",
@@ -309,24 +309,24 @@ class TestStatsService:
count = self.service._get_unique_marketplaces_count(db) count = self.service._get_unique_marketplaces_count(db)
assert count >= 2 # At least Amazon and eBay, plus test_product marketplace assert count >= 2 # At least Amazon and eBay, plus test_marketplace_product marketplace
assert isinstance(count, int) assert isinstance(count, int)
def test_get_unique_shops_count(self, db, test_product): def test_get_unique_shops_count(self, db, test_marketplace_product):
"""Test getting unique shops count""" """Test getting unique shops count"""
# Add products with different shop names # Add products with different shop names
shop_products = [ shop_products = [
Product( MarketplaceProduct(
product_id="SHOP001", marketplace_product_id="SHOP001",
title="Shop Product 1", title="Shop MarketplaceProduct 1",
marketplace="Test", marketplace="Test",
shop_name="ShopA", shop_name="ShopA",
price="10.00", price="10.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="SHOP002", marketplace_product_id="SHOP002",
title="Shop Product 2", title="Shop MarketplaceProduct 2",
marketplace="Test", marketplace="Test",
shop_name="ShopB", shop_name="ShopB",
price="15.00", price="15.00",
@@ -338,7 +338,7 @@ class TestStatsService:
count = self.service._get_unique_shops_count(db) count = self.service._get_unique_shops_count(db)
assert count >= 2 # At least ShopA and ShopB, plus test_product shop assert count >= 2 # At least ShopA and ShopB, plus test_marketplace_product shop
assert isinstance(count, int) assert isinstance(count, int)
def test_get_stock_statistics(self, db, test_stock): def test_get_stock_statistics(self, db, test_stock):
@@ -374,27 +374,27 @@ class TestStatsService:
"""Test getting brands for a specific marketplace""" """Test getting brands for a specific marketplace"""
# Create products for specific marketplace # Create products for specific marketplace
marketplace_products = [ marketplace_products = [
Product( MarketplaceProduct(
product_id="SPECIFIC001", marketplace_product_id="SPECIFIC001",
title="Specific Product 1", title="Specific MarketplaceProduct 1",
brand="SpecificBrand1", brand="SpecificBrand1",
marketplace="SpecificMarket", marketplace="SpecificMarket",
shop_name="SpecificShop1", shop_name="SpecificShop1",
price="10.00", price="10.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="SPECIFIC002", marketplace_product_id="SPECIFIC002",
title="Specific Product 2", title="Specific MarketplaceProduct 2",
brand="SpecificBrand2", brand="SpecificBrand2",
marketplace="SpecificMarket", marketplace="SpecificMarket",
shop_name="SpecificShop2", shop_name="SpecificShop2",
price="15.00", price="15.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="OTHER001", marketplace_product_id="OTHER001",
title="Other Product", title="Other MarketplaceProduct",
brand="OtherBrand", brand="OtherBrand",
marketplace="OtherMarket", marketplace="OtherMarket",
shop_name="OtherShop", shop_name="OtherShop",
@@ -416,18 +416,18 @@ class TestStatsService:
"""Test getting shops for a specific marketplace""" """Test getting shops for a specific marketplace"""
# Create products for specific marketplace # Create products for specific marketplace
marketplace_products = [ marketplace_products = [
Product( MarketplaceProduct(
product_id="SHOPTEST001", marketplace_product_id="SHOPTEST001",
title="Shop Test Product 1", title="Shop Test MarketplaceProduct 1",
brand="TestBrand", brand="TestBrand",
marketplace="TestMarketplace", marketplace="TestMarketplace",
shop_name="TestShop1", shop_name="TestShop1",
price="10.00", price="10.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="SHOPTEST002", marketplace_product_id="SHOPTEST002",
title="Shop Test Product 2", title="Shop Test MarketplaceProduct 2",
brand="TestBrand", brand="TestBrand",
marketplace="TestMarketplace", marketplace="TestMarketplace",
shop_name="TestShop2", shop_name="TestShop2",
@@ -448,25 +448,25 @@ class TestStatsService:
"""Test getting product count for a specific marketplace""" """Test getting product count for a specific marketplace"""
# Create products for specific marketplace # Create products for specific marketplace
marketplace_products = [ marketplace_products = [
Product( MarketplaceProduct(
product_id="COUNT001", marketplace_product_id="COUNT001",
title="Count Product 1", title="Count MarketplaceProduct 1",
marketplace="CountMarketplace", marketplace="CountMarketplace",
shop_name="CountShop", shop_name="CountShop",
price="10.00", price="10.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="COUNT002", marketplace_product_id="COUNT002",
title="Count Product 2", title="Count MarketplaceProduct 2",
marketplace="CountMarketplace", marketplace="CountMarketplace",
shop_name="CountShop", shop_name="CountShop",
price="15.00", price="15.00",
currency="EUR", currency="EUR",
), ),
Product( MarketplaceProduct(
product_id="COUNT003", marketplace_product_id="COUNT003",
title="Count Product 3", title="Count MarketplaceProduct 3",
marketplace="CountMarketplace", marketplace="CountMarketplace",
shop_name="CountShop", shop_name="CountShop",
price="20.00", price="20.00",

View File

@@ -14,7 +14,7 @@ from app.exceptions import (
ValidationException, ValidationException,
) )
from models.schemas.stock import StockAdd, StockCreate, StockUpdate from models.schemas.stock import StockAdd, StockCreate, StockUpdate
from models.database.product import Product from models.database.marketplace_product import MarketplaceProduct
from models.database.stock import Stock from models.database.stock import Stock
@@ -254,7 +254,7 @@ class TestStockService:
# The service prevents negative stock through InsufficientStockException # The service prevents negative stock through InsufficientStockException
assert exc_info.value.error_code == "INSUFFICIENT_STOCK" assert exc_info.value.error_code == "INSUFFICIENT_STOCK"
def test_get_stock_by_gtin_success(self, db, test_stock, test_product): def test_get_stock_by_gtin_success(self, db, test_stock, test_marketplace_product):
"""Test getting stock summary by GTIN successfully.""" """Test getting stock summary by GTIN successfully."""
result = self.service.get_stock_by_gtin(db, test_stock.gtin) result = self.service.get_stock_by_gtin(db, test_stock.gtin)
@@ -263,11 +263,11 @@ class TestStockService:
assert len(result.locations) == 1 assert len(result.locations) == 1
assert result.locations[0].location == test_stock.location assert result.locations[0].location == test_stock.location
assert result.locations[0].quantity == test_stock.quantity assert result.locations[0].quantity == test_stock.quantity
assert result.product_title == test_product.title assert result.product_title == test_marketplace_product.title
def test_get_stock_by_gtin_multiple_locations_success(self, db, test_product): def test_get_stock_by_gtin_multiple_locations_success(self, db, test_marketplace_product):
"""Test getting stock summary with multiple locations successfully.""" """Test getting stock summary with multiple locations successfully."""
unique_gtin = test_product.gtin unique_gtin = test_marketplace_product.gtin
unique_id = str(uuid.uuid4())[:8] unique_id = str(uuid.uuid4())[:8]
# Create multiple stock entries for the same GTIN with unique locations # Create multiple stock entries for the same GTIN with unique locations
@@ -301,13 +301,13 @@ class TestStockService:
assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED" assert exc_info.value.error_code == "STOCK_VALIDATION_FAILED"
assert "Invalid GTIN format" in str(exc_info.value) assert "Invalid GTIN format" in str(exc_info.value)
def test_get_total_stock_success(self, db, test_stock, test_product): def test_get_total_stock_success(self, db, test_stock, test_marketplace_product):
"""Test getting total stock for a GTIN successfully.""" """Test getting total stock for a GTIN successfully."""
result = self.service.get_total_stock(db, test_stock.gtin) result = self.service.get_total_stock(db, test_stock.gtin)
assert result["gtin"] == test_stock.gtin assert result["gtin"] == test_stock.gtin
assert result["total_quantity"] == test_stock.quantity assert result["total_quantity"] == test_stock.quantity
assert result["product_title"] == test_product.title assert result["product_title"] == test_marketplace_product.title
assert result["locations_count"] == 1 assert result["locations_count"] == 1
def test_get_total_stock_invalid_gtin_validation_error(self, db): def test_get_total_stock_invalid_gtin_validation_error(self, db):
@@ -415,7 +415,7 @@ class TestStockService:
assert exc_info.value.error_code == "STOCK_NOT_FOUND" assert exc_info.value.error_code == "STOCK_NOT_FOUND"
assert "99999" in str(exc_info.value) assert "99999" in str(exc_info.value)
def test_get_low_stock_items_success(self, db, test_stock, test_product): def test_get_low_stock_items_success(self, db, test_stock, test_marketplace_product):
"""Test getting low stock items successfully.""" """Test getting low stock items successfully."""
# Set stock to a low value # Set stock to a low value
test_stock.quantity = 5 test_stock.quantity = 5
@@ -428,7 +428,7 @@ class TestStockService:
assert low_stock_item is not None assert low_stock_item is not None
assert low_stock_item["current_quantity"] == 5 assert low_stock_item["current_quantity"] == 5
assert low_stock_item["location"] == test_stock.location assert low_stock_item["location"] == test_stock.location
assert low_stock_item["product_title"] == test_product.title assert low_stock_item["product_title"] == test_marketplace_product.title
def test_get_low_stock_items_invalid_threshold_error(self, db): def test_get_low_stock_items_invalid_threshold_error(self, db):
"""Test getting low stock items with invalid threshold returns InvalidQuantityException.""" """Test getting low stock items with invalid threshold returns InvalidQuantityException."""
@@ -491,9 +491,9 @@ class TestStockService:
@pytest.fixture @pytest.fixture
def test_product_with_stock(db, test_stock): def test_product_with_stock(db, test_stock):
"""Create a test product that corresponds to the test stock.""" """Create a test product that corresponds to the test stock."""
product = Product( product = MarketplaceProduct(
product_id="STOCK_TEST_001", marketplace_product_id="STOCK_TEST_001",
title="Stock Test Product", title="Stock Test MarketplaceProduct",
gtin=test_stock.gtin, gtin=test_stock.gtin,
price="29.99", price="29.99",
brand="TestBrand", brand="TestBrand",

View File

@@ -18,7 +18,7 @@ class TestCSVProcessor:
def test_download_csv_encoding_fallback(self, mock_get): def test_download_csv_encoding_fallback(self, mock_get):
"""Test CSV download with encoding fallback""" """Test CSV download with encoding fallback"""
# Create content with special characters that would fail UTF-8 if not properly encoded # Create content with special characters that would fail UTF-8 if not properly encoded
special_content = "product_id,title,price\nTEST001,Café Product,10.99" special_content = "marketplace_product_id,title,price\nTEST001,Café MarketplaceProduct,10.99"
mock_response = Mock() mock_response = Mock()
mock_response.status_code = 200 mock_response.status_code = 200
@@ -31,7 +31,7 @@ class TestCSVProcessor:
mock_get.assert_called_once_with("http://example.com/test.csv", timeout=30) mock_get.assert_called_once_with("http://example.com/test.csv", timeout=30)
assert isinstance(csv_content, str) assert isinstance(csv_content, str)
assert "Café Product" in csv_content assert "Café MarketplaceProduct" in csv_content
@patch("requests.get") @patch("requests.get")
def test_download_csv_encoding_ignore_fallback(self, mock_get): def test_download_csv_encoding_ignore_fallback(self, mock_get):
@@ -41,7 +41,7 @@ class TestCSVProcessor:
mock_response.status_code = 200 mock_response.status_code = 200
# Create bytes that will fail most encodings # Create bytes that will fail most encodings
mock_response.content = ( mock_response.content = (
b"product_id,title,price\nTEST001,\xff\xfe Product,10.99" b"marketplace_product_id,title,price\nTEST001,\xff\xfe MarketplaceProduct,10.99"
) )
mock_response.raise_for_status.return_value = None mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response mock_get.return_value = mock_response
@@ -51,7 +51,7 @@ class TestCSVProcessor:
mock_get.assert_called_once_with("http://example.com/test.csv", timeout=30) mock_get.assert_called_once_with("http://example.com/test.csv", timeout=30)
assert isinstance(csv_content, str) assert isinstance(csv_content, str)
# Should still contain basic content even with ignored errors # Should still contain basic content even with ignored errors
assert "product_id,title,price" in csv_content assert "marketplace_product_id,title,price" in csv_content
assert "TEST001" in csv_content assert "TEST001" in csv_content
@patch("requests.get") @patch("requests.get")
@@ -91,15 +91,15 @@ class TestCSVProcessor:
def test_parse_csv_content(self): def test_parse_csv_content(self):
"""Test CSV content parsing""" """Test CSV content parsing"""
csv_content = """product_id,title,price,marketplace csv_content = """marketplace_product_id,title,price,marketplace
TEST001,Test Product 1,10.99,TestMarket TEST001,Test MarketplaceProduct 1,10.99,TestMarket
TEST002,Test Product 2,15.99,TestMarket""" TEST002,Test MarketplaceProduct 2,15.99,TestMarket"""
df = self.processor.parse_csv(csv_content) df = self.processor.parse_csv(csv_content)
assert len(df) == 2 assert len(df) == 2
assert "product_id" in df.columns assert "marketplace_product_id" in df.columns
assert df.iloc[0]["product_id"] == "TEST001" assert df.iloc[0]["marketplace_product_id"] == "TEST001"
assert df.iloc[1]["price"] == 15.99 assert df.iloc[1]["price"] == 15.99
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -112,8 +112,8 @@ TEST002,Test Product 2,15.99,TestMarket"""
mock_download.return_value = "csv_content" mock_download.return_value = "csv_content"
mock_df = pd.DataFrame( mock_df = pd.DataFrame(
{ {
"product_id": ["TEST001", "TEST002"], "marketplace_product_id": ["TEST001", "TEST002"],
"title": ["Product 1", "Product 2"], "title": ["MarketplaceProduct 1", "MarketplaceProduct 2"],
"price": ["10.99", "15.99"], "price": ["10.99", "15.99"],
"marketplace": ["TestMarket", "TestMarket"], "marketplace": ["TestMarket", "TestMarket"],
"shop_name": ["TestShop", "TestShop"], "shop_name": ["TestShop", "TestShop"],