From c971674ec2c56646ed2c1547a1870a3e32444fc7 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sat, 4 Oct 2025 13:38:10 +0200 Subject: [PATCH] marketplace refactoring --- README.md | 70 +++--- alembic/env.py | 8 +- .../dbe48f596a44_initial_complete_schema.py | 10 +- app/api/main.py | 3 +- app/api/v1/admin.py | 2 +- app/api/v1/marketplace.py | 161 ++++++++++++-- app/api/v1/product.py | 142 ------------- app/exceptions/__init__.py | 28 +-- ...rketplace.py => marketplace_import_job.py} | 2 +- .../{product.py => marketplace_product.py} | 30 +-- app/exceptions/shop.py | 12 +- app/services/admin_service.py | 6 +- ...e.py => marketplace_import_job_service.py} | 14 +- ...vice.py => marketplace_product_service.py} | 139 ++++++------ app/services/shop_service.py | 30 +-- app/services/stats_service.py | 60 +++--- app/services/stock_service.py | 10 +- app/tasks/background_tasks.py | 2 +- app/utils/csv_processor.py | 28 +-- docs/api/authentication.md | 2 +- docs/api/index.md | 10 +- docs/development/exception-handling.md | 10 +- .../frontend-exception-handling.md | 18 +- docs/guides/user-management.md | 12 +- docs/index.md | 8 +- models/__init__.py | 6 +- models/database/__init__.py | 6 +- models/database/base.py | 6 +- ...rketplace.py => marketplace_import_job.py} | 7 +- .../{product.py => marketplace_product.py} | 18 +- models/database/shop.py | 13 +- models/database/stock.py | 9 +- models/database/user.py | 8 +- models/schemas/__init__.py | 10 +- ...rketplace.py => marketplace_import_job.py} | 4 +- .../{product.py => marketplace_product.py} | 24 +-- models/schemas/shop.py | 6 +- scripts/verify_setup.py | 6 +- tests/.coverage | Bin 53248 -> 53248 bytes tests/conftest.py | 8 +- ....py => marketplace_import_job_fixtures.py} | 8 +- ...res.py => marketplace_product_fixtures.py} | 78 +++---- tests/fixtures/shop_fixtures.py | 6 +- .../api/v1/test_admin_endpoints.py | 12 +- tests/integration/api/v1/test_filtering.py | 71 ++++--- ... test_marketplace_import_job_endpoints.py} | 56 ++--- ....py => test_marketplace_product_export.py} | 96 ++++----- ...=> test_marketplace_products_endpoints.py} | 140 ++++++------ tests/integration/api/v1/test_pagination.py | 62 +++--- .../integration/api/v1/test_shop_endpoints.py | 18 +- .../api/v1/test_stats_endpoints.py | 8 +- .../security/test_authentication.py | 16 +- .../security/test_authorization.py | 2 +- .../security/test_input_validation.py | 16 +- .../tasks/test_background_tasks.py | 2 +- .../integration/workflows/test_integration.py | 20 +- tests/performance/test_api_performance.py | 34 +-- tests/system/test_error_handling.py | 28 +-- tests/test_data/csv/sample_products.csv | 6 +- tests/unit/middleware/test_middleware.py | 3 +- tests/unit/models/test_database_models.py | 26 +-- tests/unit/services/test_admin_service.py | 32 +-- .../unit/services/test_marketplace_service.py | 86 ++++---- tests/unit/services/test_product_service.py | 200 +++++++++--------- tests/unit/services/test_shop_service.py | 22 +- tests/unit/services/test_stats_service.py | 188 ++++++++-------- tests/unit/services/test_stock_service.py | 24 +-- tests/unit/utils/test_csv_processor.py | 22 +- 68 files changed, 1102 insertions(+), 1128 deletions(-) delete mode 100644 app/api/v1/product.py rename app/exceptions/{marketplace.py => marketplace_import_job.py} (99%) rename app/exceptions/{product.py => marketplace_product.py} (69%) rename app/services/{marketplace_service.py => marketplace_import_job_service.py} (96%) rename app/services/{product_service.py => marketplace_product_service.py} (65%) rename models/database/{marketplace.py => marketplace_import_job.py} (93%) rename models/database/{product.py => marketplace_product.py} (83%) rename models/schemas/{marketplace.py => marketplace_import_job.py} (90%) rename models/schemas/{product.py => marketplace_product.py} (74%) rename tests/fixtures/{marketplace_fixtures.py => marketplace_import_job_fixtures.py} (81%) rename tests/fixtures/{product_fixtures.py => marketplace_product_fixtures.py} (53%) rename tests/integration/api/v1/{test_marketplace_endpoints.py => test_marketplace_import_job_endpoints.py} (90%) rename tests/integration/api/v1/{test_product_export.py => test_marketplace_product_export.py} (74%) rename tests/integration/api/v1/{test_product_endpoints.py => test_marketplace_products_endpoints.py} (66%) diff --git a/README.md b/README.md index 1a425a47..f7d84237 100644 --- a/README.md +++ b/README.md @@ -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 - **Marketplace Integration** - Support for multiple marketplaces (Letzshop, Amazon, eBay, Etsy, Shopify, etc.) - **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 - **CSV Import/Export** - Background processing of marketplace CSV files with progress tracking - **Rate Limiting** - Built-in request rate limiting for API protection @@ -46,7 +46,7 @@ letzshop_api/ │ │ ├── main.py # API router setup │ │ └── v1/ # API version 1 routes │ │ ├── auth.py # Authentication endpoints -│ │ ├── products.py # Product management +│ │ ├── products.py # MarketplaceProduct management │ │ ├── stock.py # Stock operations │ │ ├── shops.py # Shop management │ │ ├── marketplace.py # Marketplace imports @@ -60,7 +60,7 @@ letzshop_api/ │ │ ├── base.py # Base model class and common mixins │ │ ├── user.py # User, UserProfile models │ │ ├── auth.py # Authentication-related models -│ │ ├── product.py # Product, ProductVariant models +│ │ ├── product.py # MarketplaceProduct, ProductVariant models │ │ ├── stock.py # Stock, StockMovement models │ │ ├── shop.py # Shop, ShopLocation models │ │ ├── marketplace.py # Marketplace integration models @@ -69,7 +69,7 @@ letzshop_api/ │ ├── __init__.py # Common imports │ ├── base.py # Base Pydantic 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 │ ├── shop.py # Shop management models │ ├── marketplace.py # Marketplace import models @@ -285,23 +285,23 @@ curl -X POST "http://localhost:8000/api/v1/auth/login" \ ```bash # 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" ``` ## Core Features -### Product Management +### MarketplaceProduct Management -#### Create a Product +#### Create a MarketplaceProduct ```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 "Content-Type: application/json" \ -d '{ - "product_id": "PROD001", - "title": "Amazing Product", + "marketplace_product_id": "PROD001", + "title": "Amazing MarketplaceProduct", "description": "An amazing product description", "price": "29.99", "currency": "EUR", @@ -317,15 +317,15 @@ curl -X POST "http://localhost:8000/api/v1/product" \ ```bash # 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" # 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" # 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" ``` @@ -393,12 +393,12 @@ curl -X GET "http://localhost:8000/api/v1/marketplace/import-status/1" \ ```bash # 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" \ -o products_export.csv # 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" \ -o amazon_products.csv ``` @@ -409,18 +409,18 @@ The system supports CSV imports with the following headers: ### Required Fields - `product_id` - Unique product identifier -- `title` - Product title +- `title` - MarketplaceProduct title ### Optional Fields -- `description` - Product description -- `link` - Product URL -- `image_link` - Product image URL +- `description` - MarketplaceProduct description +- `link` - MarketplaceProduct URL +- `image_link` - MarketplaceProduct image URL - `availability` - Stock availability (in stock, out of stock, preorder) -- `price` - Product price +- `price` - MarketplaceProduct price - `currency` - Price currency (EUR, USD, etc.) -- `brand` - Product brand +- `brand` - MarketplaceProduct brand - `gtin` - Global Trade Item Number (EAN/UPC) -- `google_product_category` - Product category +- `google_product_category` - MarketplaceProduct category - `marketplace` - Source marketplace - `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 - `GET /api/v1/auth/me` - Get current user info -### Product Endpoints -- `GET /api/v1/product` - List products with filtering -- `POST /api/v1/product` - Create new product -- `GET /api/v1/product/{product_id}` - Get specific product -- `PUT /api/v1/product/{product_id}` - Update product -- `DELETE /api/v1/product/{product_id}` - Delete product - +### Marketplace Endpoints +- `GET /api/v1/marketplace/product` - List marketplace products with filtering +- `POST /api/v1/marketplace/product` - Create new marketplace product +- `GET /api/v1/marketplace/product/{product_id}` - Get specific marketplace product +- `PUT /api/v1/marketplace/product/{product_id}` - Update marketplace 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 - `POST /api/v1/stock` - Set stock quantity - `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/{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 - `GET /api/v1/stats` - Get general statistics - `GET /api/v1/stats/marketplace-stats` - Get marketplace statistics @@ -529,7 +527,7 @@ make docs-help ### Core Tables - **users** - User accounts and authentication -- **products** - Product catalog with marketplace info +- **products** - MarketplaceProduct catalog with marketplace info - **stock** - Inventory tracking by location and GTIN - **shops** - Shop/seller information - **shop_products** - Shop-specific product settings @@ -583,7 +581,7 @@ make test-slow # Authentication tests make test-auth -# Product management tests +# MarketplaceProduct management tests make test-products # Stock management tests diff --git a/alembic/env.py b/alembic/env.py index b6db6883..b2cf6cce 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -23,10 +23,10 @@ except ImportError as e: print(f" ✗ User model failed: {e}") try: - from models.database.product import Product - print(" ✓ Product model imported") + from models.database.marketplace_product import MarketplaceProduct + print(" ✓ MarketplaceProduct model imported") except ImportError as e: - print(f" ✗ Product model failed: {e}") + print(f" ✗ MarketplaceProduct model failed: {e}") try: from models.database.stock import Stock @@ -41,7 +41,7 @@ except ImportError as e: print(f" ✗ Shop models failed: {e}") try: - from models.database.marketplace import MarketplaceImportJob + from models.database.marketplace_import_job import MarketplaceImportJob print(" ✓ Marketplace model imported") except ImportError as e: print(f" ✗ Marketplace model failed: {e}") diff --git a/alembic/versions/dbe48f596a44_initial_complete_schema.py b/alembic/versions/dbe48f596a44_initial_complete_schema.py index 716b381d..d95cf015 100644 --- a/alembic/versions/dbe48f596a44_initial_complete_schema.py +++ b/alembic/versions/dbe48f596a44_initial_complete_schema.py @@ -22,7 +22,7 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('products', 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('description', 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_id'), 'products', ['id'], 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_table('users', sa.Column('id', sa.Integer(), nullable=False), @@ -139,7 +139,7 @@ def upgrade() -> None: op.create_table('shop_products', sa.Column('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_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('created_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.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_featured', 'shop_products', ['shop_id', 'is_featured'], unique=False) diff --git a/app/api/main.py b/app/api/main.py index b76d4d73..31d9bed2 100644 --- a/app/api/main.py +++ b/app/api/main.py @@ -9,7 +9,7 @@ This module provides classes and functions for: 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() @@ -17,7 +17,6 @@ api_router = APIRouter() api_router.include_router(admin.router, tags=["admin"]) api_router.include_router(auth.router, tags=["authentication"]) 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(stats.router, tags=["statistics"]) api_router.include_router(stock.router, tags=["stock"]) diff --git a/app/api/v1/admin.py b/app/api/v1/admin.py index 6997e914..67cef1be 100644 --- a/app/api/v1/admin.py +++ b/app/api/v1/admin.py @@ -19,7 +19,7 @@ from app.api.deps import get_current_admin_user from app.core.database import get_db from app.services.admin_service import admin_service 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.database.user import User diff --git a/app/api/v1/marketplace.py b/app/api/v1/marketplace.py index 4d7f0686..90b879ae 100644 --- a/app/api/v1/marketplace.py +++ b/app/api/v1/marketplace.py @@ -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: -- 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 statistics and job cancellation """ @@ -12,25 +15,151 @@ import logging from typing import List, Optional from fastapi import APIRouter, BackgroundTasks, 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.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 middleware.decorators import rate_limit -from models.schemas.marketplace import (MarketplaceImportJobResponse, - MarketplaceImportRequest) +from models.schemas.marketplace_import_job import (MarketplaceImportJobResponse, + MarketplaceImportJobRequest) +from models.schemas.marketplace_product import (MarketplaceProductCreate, MarketplaceProductDetailResponse, + MarketplaceProductListResponse, MarketplaceProductResponse, + MarketplaceProductUpdate) from models.database.user import User router = APIRouter() 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) @rate_limit(max_requests=10, window_seconds=3600) # Limit marketplace imports async def import_products_from_marketplace( - request: MarketplaceImportRequest, + request: MarketplaceImportJobRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), @@ -41,7 +170,7 @@ async def import_products_from_marketplace( ) # 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 background_tasks.add_task( @@ -74,8 +203,8 @@ def get_marketplace_import_status( current_user: User = Depends(get_current_user), ): """Get status of marketplace import job (Protected).""" - job = marketplace_service.get_import_job_by_id(db, job_id, current_user) - return marketplace_service.convert_to_response_model(job) + job = marketplace_import_job_service.get_import_job_by_id(db, job_id, current_user) + return marketplace_import_job_service.convert_to_response_model(job) @router.get( @@ -90,7 +219,7 @@ def get_marketplace_import_jobs( current_user: User = Depends(get_current_user), ): """Get marketplace import jobs with filtering (Protected).""" - jobs = marketplace_service.get_import_jobs( + jobs = marketplace_import_job_service.get_import_jobs( db=db, user=current_user, marketplace=marketplace, @@ -99,7 +228,7 @@ def get_marketplace_import_jobs( 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") @@ -107,7 +236,7 @@ def get_marketplace_import_stats( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """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( @@ -120,8 +249,8 @@ def cancel_marketplace_import_job( current_user: User = Depends(get_current_user), ): """Cancel a pending or running marketplace import job (Protected).""" - job = marketplace_service.cancel_import_job(db, job_id, current_user) - return marketplace_service.convert_to_response_model(job) + job = marketplace_import_job_service.cancel_import_job(db, job_id, current_user) + return marketplace_import_job_service.convert_to_response_model(job) @router.delete("/marketplace/import-jobs/{job_id}") @@ -131,5 +260,5 @@ def delete_marketplace_import_job( current_user: User = Depends(get_current_user), ): """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"} diff --git a/app/api/v1/product.py b/app/api/v1/product.py deleted file mode 100644 index a5ed0948..00000000 --- a/app/api/v1/product.py +++ /dev/null @@ -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"} - diff --git a/app/exceptions/__init__.py b/app/exceptions/__init__.py index 4b2160dd..9f6b715b 100644 --- a/app/exceptions/__init__.py +++ b/app/exceptions/__init__.py @@ -29,13 +29,13 @@ from .auth import ( UserAlreadyExistsException ) -from .product import ( - ProductNotFoundException, - ProductAlreadyExistsException, - InvalidProductDataException, - ProductValidationException, +from .marketplace_product import ( + MarketplaceProductNotFoundException, + MarketplaceProductAlreadyExistsException, + InvalidMarketplaceProductDataException, + MarketplaceProductValidationException, InvalidGTINException, - ProductCSVImportException, + MarketplaceProductCSVImportException, ) from .stock import ( @@ -61,7 +61,7 @@ from .shop import ( ShopValidationException, ) -from .marketplace import ( +from .marketplace_import_job import ( MarketplaceImportException, ImportJobNotFoundException, ImportJobNotOwnedException, @@ -107,13 +107,13 @@ __all__ = [ "AdminRequiredException", "UserAlreadyExistsException", - # Product exceptions - "ProductNotFoundException", - "ProductAlreadyExistsException", - "InvalidProductDataException", - "ProductValidationException", + # MarketplaceProduct exceptions + "MarketplaceProductNotFoundException", + "MarketplaceProductAlreadyExistsException", + "InvalidMarketplaceProductDataException", + "MarketplaceProductValidationException", "InvalidGTINException", - "ProductCSVImportException", + "MarketplaceProductCSVImportException", # Stock exceptions "StockNotFoundException", @@ -136,7 +136,7 @@ __all__ = [ "MaxShopsReachedException", "ShopValidationException", - # Marketplace exceptions + # Marketplace import exceptions "MarketplaceImportException", "ImportJobNotFoundException", "ImportJobNotOwnedException", diff --git a/app/exceptions/marketplace.py b/app/exceptions/marketplace_import_job.py similarity index 99% rename from app/exceptions/marketplace.py rename to app/exceptions/marketplace_import_job.py index dd50d005..1773ce6b 100644 --- a/app/exceptions/marketplace.py +++ b/app/exceptions/marketplace_import_job.py @@ -1,4 +1,4 @@ -# app/exceptions/marketplace.py +# app/exceptions/marketplace_import_job.py """ Marketplace import specific exceptions. """ diff --git a/app/exceptions/product.py b/app/exceptions/marketplace_product.py similarity index 69% rename from app/exceptions/product.py rename to app/exceptions/marketplace_product.py index 88b77585..a153a717 100644 --- a/app/exceptions/product.py +++ b/app/exceptions/marketplace_product.py @@ -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 .base import ResourceNotFoundException, ConflictException, ValidationException, BusinessLogicException -class ProductNotFoundException(ResourceNotFoundException): +class MarketplaceProductNotFoundException(ResourceNotFoundException): """Raised when a product is not found.""" - def __init__(self, product_id: str): + def __init__(self, marketplace_product_id: str): super().__init__( - resource_type="Product", - identifier=product_id, - message=f"Product with ID '{product_id}' not found", + resource_type="MarketplaceProduct", + identifier=marketplace_product_id, + message=f"MarketplaceProduct with ID '{marketplace_product_id}' not found", error_code="PRODUCT_NOT_FOUND", ) -class ProductAlreadyExistsException(ConflictException): +class MarketplaceProductAlreadyExistsException(ConflictException): """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__( - message=f"Product with ID '{product_id}' already exists", + message=f"MarketplaceProduct with ID '{marketplace_product_id}' 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.""" def __init__( @@ -47,7 +47,7 @@ class InvalidProductDataException(ValidationException): self.error_code = "INVALID_PRODUCT_DATA" -class ProductValidationException(ValidationException): +class MarketplaceProductValidationException(ValidationException): """Raised when product validation fails.""" def __init__( @@ -80,12 +80,12 @@ class InvalidGTINException(ValidationException): self.error_code = "INVALID_GTIN" -class ProductCSVImportException(BusinessLogicException): +class MarketplaceProductCSVImportException(BusinessLogicException): """Raised when product CSV import fails.""" def __init__( self, - message: str = "Product CSV import failed", + message: str = "MarketplaceProduct CSV import failed", row_number: Optional[int] = None, errors: Optional[Dict[str, Any]] = None, ): diff --git a/app/exceptions/shop.py b/app/exceptions/shop.py index 95aeb613..8373fe05 100644 --- a/app/exceptions/shop.py +++ b/app/exceptions/shop.py @@ -98,13 +98,13 @@ class InvalidShopDataException(ValidationException): class ShopProductAlreadyExistsException(ConflictException): """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__( - 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", details={ "shop_code": shop_code, - "product_id": product_id, + "marketplace_product_id": marketplace_product_id, }, ) @@ -112,11 +112,11 @@ class ShopProductAlreadyExistsException(ConflictException): class ShopProductNotFoundException(ResourceNotFoundException): """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__( resource_type="ShopProduct", - identifier=f"{shop_code}/{product_id}", - message=f"Product '{product_id}' not found in shop '{shop_code}'", + identifier=f"{shop_code}/{marketplace_product_id}", + message=f"MarketplaceProduct '{marketplace_product_id}' not found in shop '{shop_code}'", error_code="SHOP_PRODUCT_NOT_FOUND", ) diff --git a/app/services/admin_service.py b/app/services/admin_service.py index a4289f25..27ca8a82 100644 --- a/app/services/admin_service.py +++ b/app/services/admin_service.py @@ -22,8 +22,8 @@ from app.exceptions import ( ShopVerificationException, AdminOperationException, ) -from models.schemas.marketplace import MarketplaceImportJobResponse -from models.database.marketplace import MarketplaceImportJob +from models.schemas.marketplace_import_job import MarketplaceImportJobResponse +from models.database.marketplace_import_job import MarketplaceImportJob from models.database.shop import Shop 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() diff --git a/app/services/marketplace_service.py b/app/services/marketplace_import_job_service.py similarity index 96% rename from app/services/marketplace_service.py rename to app/services/marketplace_import_job_service.py index bb331a65..2f56abbf 100644 --- a/app/services/marketplace_service.py +++ b/app/services/marketplace_import_job_service.py @@ -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. @@ -24,16 +24,16 @@ from app.exceptions import ( ImportJobCannotBeDeletedException, ValidationException, ) -from models.schemas.marketplace import (MarketplaceImportJobResponse, - MarketplaceImportRequest) -from models.database.marketplace import MarketplaceImportJob +from models.schemas.marketplace_import_job import (MarketplaceImportJobResponse, + MarketplaceImportJobRequest) +from models.database.marketplace_import_job import MarketplaceImportJob from models.database.shop import Shop from models.database.user import User logger = logging.getLogger(__name__) -class MarketplaceService: +class MarketplaceImportJobService: """Service class for Marketplace operations following the application's service pattern.""" 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") def create_import_job( - self, db: Session, request: MarketplaceImportRequest, user: User + self, db: Session, request: MarketplaceImportJobRequest, user: User ) -> MarketplaceImportJob: """ Create a new marketplace import job. @@ -414,4 +414,4 @@ class MarketplaceService: # Create service instance -marketplace_service = MarketplaceService() +marketplace_import_job_service = MarketplaceImportJobService() diff --git a/app/services/product_service.py b/app/services/marketplace_product_service.py similarity index 65% rename from app/services/product_service.py rename to app/services/marketplace_product_service.py index fe9b3bca..628decac 100644 --- a/app/services/product_service.py +++ b/app/services/marketplace_product_service.py @@ -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: -- Product CRUD operations with validation +- MarketplaceProduct CRUD operations with validation - Advanced product filtering and search - Stock information integration - CSV export functionality @@ -18,37 +18,38 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.exceptions import ( - ProductNotFoundException, - ProductAlreadyExistsException, - InvalidProductDataException, - ProductValidationException, + MarketplaceProductNotFoundException, + MarketplaceProductAlreadyExistsException, + InvalidMarketplaceProductDataException, + MarketplaceProductValidationException, 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.database.product import Product +from models.database.marketplace_product import MarketplaceProduct from models.database.stock import Stock from app.utils.data_processing import GTINProcessor, PriceProcessor logger = logging.getLogger(__name__) -class ProductService: - """Service class for Product operations following the application's service pattern.""" +class MarketplaceProductService: + """Service class for MarketplaceProduct operations following the application's service pattern.""" def __init__(self): """Class constructor.""" self.gtin_processor = GTINProcessor() 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.""" try: # Process and validate GTIN if provided if product_data.gtin: normalized_gtin = self.gtin_processor.normalize(product_data.gtin) if not normalized_gtin: - raise InvalidProductDataException("Invalid GTIN format", field="gtin") + raise InvalidMarketplaceProductDataException("Invalid GTIN format", field="gtin") product_data.gtin = normalized_gtin # Process price if provided @@ -62,67 +63,67 @@ class ProductService: product_data.currency = currency except ValueError as e: # Convert ValueError to domain-specific exception - raise InvalidProductDataException(str(e), field="price") + raise InvalidMarketplaceProductDataException(str(e), field="price") # Set default marketplace if not provided if not product_data.marketplace: product_data.marketplace = "Letzshop" # Validate required fields - if not product_data.product_id or not product_data.product_id.strip(): - raise ProductValidationException("Product ID is required", field="product_id") + if not product_data.marketplace_product_id or not product_data.marketplace_product_id.strip(): + raise MarketplaceProductValidationException("MarketplaceProduct ID is required", field="marketplace_product_id") 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.commit() 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 - except (InvalidProductDataException, ProductValidationException): + except (InvalidMarketplaceProductDataException, MarketplaceProductValidationException): db.rollback() raise # Re-raise custom exceptions except IntegrityError as e: db.rollback() logger.error(f"Database integrity error: {str(e)}") - if "product_id" in str(e).lower() or "unique" in str(e).lower(): - raise ProductAlreadyExistsException(product_data.product_id) + if "marketplace_product_id" in str(e).lower() or "unique" in str(e).lower(): + raise MarketplaceProductAlreadyExistsException(product_data.marketplace_product_id) else: - raise ProductValidationException("Data integrity constraint violation") + raise MarketplaceProductValidationException("Data integrity constraint violation") except Exception as e: db.rollback() logger.error(f"Error creating product: {str(e)}") 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.""" 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: - logger.error(f"Error getting product {product_id}: {str(e)}") + logger.error(f"Error getting product {marketplace_product_id}: {str(e)}") 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. Args: db: Database session - product_id: Product ID to find + marketplace_product_id: MarketplaceProduct ID to find Returns: - Product object + MarketplaceProduct object 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: - raise ProductNotFoundException(product_id) + raise MarketplaceProductNotFoundException(marketplace_product_id) return product def get_products_with_filters( @@ -136,7 +137,7 @@ class ProductService: marketplace: Optional[str] = None, shop_name: Optional[str] = None, search: Optional[str] = None, - ) -> Tuple[List[Product], int]: + ) -> Tuple[List[MarketplaceProduct], int]: """ Get products with filtering and pagination. @@ -155,27 +156,27 @@ class ProductService: Tuple of (products_list, total_count) """ try: - query = db.query(Product) + query = db.query(MarketplaceProduct) # Apply filters if brand: - query = query.filter(Product.brand.ilike(f"%{brand}%")) + query = query.filter(MarketplaceProduct.brand.ilike(f"%{brand}%")) if category: - query = query.filter(Product.google_product_category.ilike(f"%{category}%")) + query = query.filter(MarketplaceProduct.google_product_category.ilike(f"%{category}%")) if availability: - query = query.filter(Product.availability == availability) + query = query.filter(MarketplaceProduct.availability == availability) if marketplace: - query = query.filter(Product.marketplace.ilike(f"%{marketplace}%")) + query = query.filter(MarketplaceProduct.marketplace.ilike(f"%{marketplace}%")) 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: # Search in title, description, marketplace, and shop_name search_term = f"%{search}%" query = query.filter( - (Product.title.ilike(search_term)) - | (Product.description.ilike(search_term)) - | (Product.marketplace.ilike(search_term)) - | (Product.shop_name.ilike(search_term)) + (MarketplaceProduct.title.ilike(search_term)) + | (MarketplaceProduct.description.ilike(search_term)) + | (MarketplaceProduct.marketplace.ilike(search_term)) + | (MarketplaceProduct.shop_name.ilike(search_term)) ) total = query.count() @@ -187,10 +188,10 @@ class ProductService: logger.error(f"Error getting products with filters: {str(e)}") 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.""" 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_data = product_update.model_dump(exclude_unset=True) @@ -199,7 +200,7 @@ class ProductService: if "gtin" in update_data and update_data["gtin"]: normalized_gtin = self.gtin_processor.normalize(update_data["gtin"]) if not normalized_gtin: - raise InvalidProductDataException("Invalid GTIN format", field="gtin") + raise InvalidMarketplaceProductDataException("Invalid GTIN format", field="gtin") update_data["gtin"] = normalized_gtin # Process price if being updated @@ -213,11 +214,11 @@ class ProductService: update_data["currency"] = currency except ValueError as e: # Convert ValueError to domain-specific exception - raise InvalidProductDataException(str(e), field="price") + raise InvalidMarketplaceProductDataException(str(e), field="price") # Validate required fields if being updated 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(): setattr(product, key, value) @@ -226,33 +227,33 @@ class ProductService: db.commit() db.refresh(product) - logger.info(f"Updated product {product_id}") + logger.info(f"Updated product {marketplace_product_id}") return product - except (ProductNotFoundException, InvalidProductDataException, ProductValidationException): + except (MarketplaceProductNotFoundException, InvalidMarketplaceProductDataException, MarketplaceProductValidationException): db.rollback() raise # Re-raise custom exceptions except Exception as e: 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") - 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. Args: db: Database session - product_id: Product ID to delete + marketplace_product_id: MarketplaceProduct ID to delete Returns: True if deletion successful Raises: - ProductNotFoundException: If product doesn't exist + MarketplaceProductNotFoundException: If product doesn't exist """ 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 if product.gtin: @@ -261,14 +262,14 @@ class ProductService: db.delete(product) db.commit() - logger.info(f"Deleted product {product_id}") + logger.info(f"Deleted product {marketplace_product_id}") return True - except ProductNotFoundException: + except MarketplaceProductNotFoundException: raise # Re-raise custom exceptions except Exception as e: 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") def get_stock_info(self, db: Session, gtin: str) -> Optional[StockSummaryResponse]: @@ -330,7 +331,7 @@ class ProductService: # Write header row headers = [ - "product_id", "title", "description", "link", "image_link", + "marketplace_product_id", "title", "description", "link", "image_link", "availability", "price", "currency", "brand", "gtin", "marketplace", "shop_name" ] @@ -345,13 +346,13 @@ class ProductService: offset = 0 while True: - query = db.query(Product) + query = db.query(MarketplaceProduct) # Apply marketplace filters if marketplace: - query = query.filter(Product.marketplace.ilike(f"%{marketplace}%")) + query = query.filter(MarketplaceProduct.marketplace.ilike(f"%{marketplace}%")) 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() if not products: @@ -360,7 +361,7 @@ class ProductService: for product in products: # Create CSV row with proper escaping row_data = [ - product.product_id or "", + product.marketplace_product_id or "", product.title or "", product.description or "", product.link or "", @@ -387,11 +388,11 @@ class ProductService: logger.error(f"Error generating CSV export: {str(e)}") 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.""" try: 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 ) except Exception as e: @@ -401,18 +402,18 @@ class ProductService: # Private helper methods def _validate_product_data(self, product_data: dict) -> None: """Validate product data structure.""" - required_fields = ['product_id', 'title'] + required_fields = ['marketplace_product_id', 'title'] for field in required_fields: 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: """Normalize and clean product data.""" normalized = product_data.copy() # 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: if field in normalized and normalized[field]: normalized[field] = normalized[field].strip() @@ -421,4 +422,4 @@ class ProductService: # Create service instance -product_service = ProductService() +marketplace_product_service = MarketplaceProductService() diff --git a/app/services/shop_service.py b/app/services/shop_service.py index 23d75be3..fd60fd1f 100644 --- a/app/services/shop_service.py +++ b/app/services/shop_service.py @@ -20,13 +20,13 @@ from app.exceptions import ( ShopAlreadyExistsException, UnauthorizedShopAccessException, InvalidShopDataException, - ProductNotFoundException, + MarketplaceProductNotFoundException, ShopProductAlreadyExistsException, MaxShopsReachedException, ValidationException, ) 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.user import User @@ -198,22 +198,22 @@ class ShopService: Created ShopProduct object Raises: - ProductNotFoundException: If product not found + MarketplaceProductNotFoundException: If product not found ShopProductAlreadyExistsException: If product already in shop """ try: # 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 - if self._product_in_shop(db, shop.id, product.id): - raise ShopProductAlreadyExistsException(shop.shop_code, shop_product.product_id) + if self._product_in_shop(db, shop.id, marketplace_product.id): + raise ShopProductAlreadyExistsException(shop.shop_code, shop_product.marketplace_product_id) # Create shop-product association new_shop_product = ShopProduct( shop_id=shop.id, - product_id=product.id, - **shop_product.model_dump(exclude={"product_id"}), + marketplace_product_id=marketplace_product.id, + **shop_product.model_dump(exclude={"marketplace_product_id"}), ) db.add(new_shop_product) @@ -223,10 +223,10 @@ class ShopService: # Load the product relationship 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 - except (ProductNotFoundException, ShopProductAlreadyExistsException): + except (MarketplaceProductNotFoundException, ShopProductAlreadyExistsException): db.rollback() raise # Re-raise custom exceptions except Exception as e: @@ -322,20 +322,20 @@ class ShopService: .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.""" - 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: - raise ProductNotFoundException(product_id) + raise MarketplaceProductNotFoundException(marketplace_product_id) 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.""" return ( db.query(ShopProduct) .filter( ShopProduct.shop_id == shop_id, - ShopProduct.product_id == product_id + ShopProduct.marketplace_product_id == marketplace_product_id ) .first() is not None ) diff --git a/app/services/stats_service.py b/app/services/stats_service.py index 17e55eed..5c577414 100644 --- a/app/services/stats_service.py +++ b/app/services/stats_service.py @@ -16,7 +16,7 @@ from sqlalchemy import func from sqlalchemy.orm import Session from app.exceptions import ValidationException -from models.database.product import Product +from models.database.marketplace_product import MarketplaceProduct from models.database.stock import Stock logger = logging.getLogger(__name__) @@ -85,13 +85,13 @@ class StatsService: # Query to get stats per marketplace marketplace_stats = ( db.query( - Product.marketplace, - func.count(Product.id).label("total_products"), - func.count(func.distinct(Product.shop_name)).label("unique_shops"), - func.count(func.distinct(Product.brand)).label("unique_brands"), + MarketplaceProduct.marketplace, + func.count(MarketplaceProduct.id).label("total_products"), + func.count(func.distinct(MarketplaceProduct.shop_name)).label("unique_shops"), + func.count(func.distinct(MarketplaceProduct.brand)).label("unique_brands"), ) - .filter(Product.marketplace.isnot(None)) - .group_by(Product.marketplace) + .filter(MarketplaceProduct.marketplace.isnot(None)) + .group_by(MarketplaceProduct.marketplace) .all() ) @@ -195,13 +195,13 @@ class StatsService: # Private helper methods def _get_product_count(self, db: Session) -> int: """Get total product count.""" - return db.query(Product).count() + return db.query(MarketplaceProduct).count() def _get_unique_brands_count(self, db: Session) -> int: """Get count of unique brands.""" return ( - db.query(Product.brand) - .filter(Product.brand.isnot(None), Product.brand != "") + db.query(MarketplaceProduct.brand) + .filter(MarketplaceProduct.brand.isnot(None), MarketplaceProduct.brand != "") .distinct() .count() ) @@ -209,10 +209,10 @@ class StatsService: def _get_unique_categories_count(self, db: Session) -> int: """Get count of unique categories.""" return ( - db.query(Product.google_product_category) + db.query(MarketplaceProduct.google_product_category) .filter( - Product.google_product_category.isnot(None), - Product.google_product_category != "", + MarketplaceProduct.google_product_category.isnot(None), + MarketplaceProduct.google_product_category != "", ) .distinct() .count() @@ -221,8 +221,8 @@ class StatsService: def _get_unique_marketplaces_count(self, db: Session) -> int: """Get count of unique marketplaces.""" return ( - db.query(Product.marketplace) - .filter(Product.marketplace.isnot(None), Product.marketplace != "") + db.query(MarketplaceProduct.marketplace) + .filter(MarketplaceProduct.marketplace.isnot(None), MarketplaceProduct.marketplace != "") .distinct() .count() ) @@ -230,8 +230,8 @@ class StatsService: def _get_unique_shops_count(self, db: Session) -> int: """Get count of unique shops.""" return ( - db.query(Product.shop_name) - .filter(Product.shop_name.isnot(None), Product.shop_name != "") + db.query(MarketplaceProduct.shop_name) + .filter(MarketplaceProduct.shop_name.isnot(None), MarketplaceProduct.shop_name != "") .distinct() .count() ) @@ -239,16 +239,16 @@ class StatsService: def _get_products_with_gtin_count(self, db: Session) -> int: """Get count of products with GTIN.""" return ( - db.query(Product) - .filter(Product.gtin.isnot(None), Product.gtin != "") + db.query(MarketplaceProduct) + .filter(MarketplaceProduct.gtin.isnot(None), MarketplaceProduct.gtin != "") .count() ) def _get_products_with_images_count(self, db: Session) -> int: """Get count of products with images.""" return ( - db.query(Product) - .filter(Product.image_link.isnot(None), Product.image_link != "") + db.query(MarketplaceProduct) + .filter(MarketplaceProduct.image_link.isnot(None), MarketplaceProduct.image_link != "") .count() ) @@ -265,11 +265,11 @@ class StatsService: def _get_brands_by_marketplace(self, db: Session, marketplace: str) -> List[str]: """Get unique brands for a specific marketplace.""" brands = ( - db.query(Product.brand) + db.query(MarketplaceProduct.brand) .filter( - Product.marketplace == marketplace, - Product.brand.isnot(None), - Product.brand != "", + MarketplaceProduct.marketplace == marketplace, + MarketplaceProduct.brand.isnot(None), + MarketplaceProduct.brand != "", ) .distinct() .all() @@ -279,11 +279,11 @@ class StatsService: def _get_shops_by_marketplace(self, db: Session, marketplace: str) -> List[str]: """Get unique shops for a specific marketplace.""" shops = ( - db.query(Product.shop_name) + db.query(MarketplaceProduct.shop_name) .filter( - Product.marketplace == marketplace, - Product.shop_name.isnot(None), - Product.shop_name != "", + MarketplaceProduct.marketplace == marketplace, + MarketplaceProduct.shop_name.isnot(None), + MarketplaceProduct.shop_name != "", ) .distinct() .all() @@ -292,7 +292,7 @@ class StatsService: def _get_products_by_marketplace_count(self, db: Session, marketplace: str) -> int: """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 stats_service = StatsService() diff --git a/app/services/stock_service.py b/app/services/stock_service.py index 6b590ae6..41404ed1 100644 --- a/app/services/stock_service.py +++ b/app/services/stock_service.py @@ -26,7 +26,7 @@ from app.exceptions import ( ) from models.schemas.stock import (StockAdd, StockCreate, StockLocationResponse, StockSummaryResponse, StockUpdate) -from models.database.product import Product +from models.database.marketplace_product import MarketplaceProduct from models.database.stock import Stock from app.utils.data_processing import GTINProcessor @@ -261,7 +261,7 @@ class StockService: ) # 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 return StockSummaryResponse( @@ -304,7 +304,7 @@ class StockService: total_quantity = sum(entry.quantity for entry in total_stock) # 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 { "gtin": normalized_gtin, @@ -491,14 +491,14 @@ class StockService: low_stock_items = [] for entry in low_stock_entries: # 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({ "gtin": entry.gtin, "location": entry.location, "current_quantity": entry.quantity, "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 diff --git a/app/tasks/background_tasks.py b/app/tasks/background_tasks.py index 83aeab91..703e3da2 100644 --- a/app/tasks/background_tasks.py +++ b/app/tasks/background_tasks.py @@ -11,7 +11,7 @@ import logging from datetime import datetime, timezone 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 logger = logging.getLogger(__name__) diff --git a/app/utils/csv_processor.py b/app/utils/csv_processor.py index 331793e7..f37e6f75 100644 --- a/app/utils/csv_processor.py +++ b/app/utils/csv_processor.py @@ -17,7 +17,7 @@ import requests from sqlalchemy import literal from sqlalchemy.orm import Session -from models.database.product import Product +from models.database.marketplace_product import MarketplaceProduct logger = logging.getLogger(__name__) @@ -40,15 +40,15 @@ class CSVProcessor: COLUMN_MAPPING = { # Standard variations - "id": "product_id", - "ID": "product_id", - "Product ID": "product_id", + "id": "marketplace_product_id", + "ID": "marketplace_product_id", + "MarketplaceProduct ID": "marketplace_product_id", "name": "title", "Name": "title", "product_name": "title", - "Product Name": "title", + "MarketplaceProduct Name": "title", # Google Shopping feed standard - "g:id": "product_id", + "g:id": "marketplace_product_id", "g:title": "title", "g:description": "description", "g:link": "link", @@ -266,8 +266,8 @@ class CSVProcessor: product_data["shop_name"] = shop_name # Validate required fields - if not product_data.get("product_id"): - logger.warning(f"Row {index}: Missing product_id, skipping") + if not product_data.get("marketplace_product_id"): + logger.warning(f"Row {index}: Missing marketplace_product_id, skipping") errors += 1 continue @@ -278,8 +278,8 @@ class CSVProcessor: # Check if product exists existing_product = ( - db.query(Product) - .filter(Product.product_id == literal(product_data["product_id"])) + db.query(MarketplaceProduct) + .filter(MarketplaceProduct.marketplace_product_id == literal(product_data["marketplace_product_id"])) .first() ) @@ -293,7 +293,7 @@ class CSVProcessor: existing_product.updated_at = datetime.now(timezone.utc) updated += 1 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}" ) else: @@ -302,13 +302,13 @@ class CSVProcessor: k: v for k, v in product_data.items() 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) imported += 1 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}" ) diff --git a/docs/api/authentication.md b/docs/api/authentication.md index d2011f87..d2ca4f30 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -51,7 +51,7 @@ Response: Include the JWT token in the Authorization header: ```http -GET /api/v1/product +GET /api/v1/marketplace/product Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... ``` diff --git a/docs/api/index.md b/docs/api/index.md index 44577859..2f33f9b2 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -37,8 +37,8 @@ All API endpoints are versioned using URL path versioning: - Configuration management ### Products (`/products/`) -- Product CRUD operations -- Product search and filtering +- MarketplaceProduct CRUD operations +- MarketplaceProduct search and filtering - Bulk operations ### Shops (`/shops/`) @@ -98,7 +98,7 @@ Content-Type: application/json Most list endpoints support pagination: ```bash -GET /api/v1/product?skip=0&limit=20 +GET /api/v1/marketplace/product?skip=0&limit=20 ``` Response includes pagination metadata: @@ -116,14 +116,14 @@ Response includes pagination metadata: Many endpoints support filtering and search: ```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 Use the `sort` parameter with field names: ```bash -GET /api/v1/product?sort=name&order=desc +GET /api/v1/marketplace/product?sort=name&order=desc ``` ## Status Codes diff --git a/docs/development/exception-handling.md b/docs/development/exception-handling.md index 34c8962f..e0335217 100644 --- a/docs/development/exception-handling.md +++ b/docs/development/exception-handling.md @@ -30,7 +30,7 @@ app/exceptions/ ├── auth.py # Authentication/authorization exceptions ├── admin.py # Admin operation exceptions ├── marketplace.py # Import/marketplace exceptions -├── product.py # Product management exceptions +├── product.py # MarketplaceProduct management exceptions ├── shop.py # Shop management exceptions └── stock.py # Stock management exceptions ``` @@ -48,10 +48,10 @@ All custom exceptions return a consistent JSON structure: ```json { "error_code": "PRODUCT_NOT_FOUND", - "message": "Product with ID 'ABC123' not found", + "message": "MarketplaceProduct with ID 'ABC123' not found", "status_code": 404, "details": { - "resource_type": "Product", + "resource_type": "MarketplaceProduct", "identifier": "ABC123" } } @@ -360,7 +360,7 @@ def test_get_all_users_non_admin(client, auth_headers): ### Resource Not Found (404) - `USER_NOT_FOUND`: User with specified 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) - `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_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 ### Rate Limiting (429) diff --git a/docs/development/frontend-exception-handling.md b/docs/development/frontend-exception-handling.md index c6a67b9d..c7e61400 100644 --- a/docs/development/frontend-exception-handling.md +++ b/docs/development/frontend-exception-handling.md @@ -7,11 +7,11 @@ Your API returns consistent error responses with this structure: ```json { "error_code": "PRODUCT_NOT_FOUND", - "message": "Product with ID 'ABC123' not found", + "message": "MarketplaceProduct with ID 'ABC123' not found", "status_code": 404, - "field": "product_id", + "field": "marketplace_product_id", "details": { - "resource_type": "Product", + "resource_type": "MarketplaceProduct", "identifier": "ABC123" } } @@ -131,8 +131,8 @@ export const ERROR_MESSAGES = { TOKEN_EXPIRED: 'Your session has expired. Please log in again.', USER_NOT_ACTIVE: 'Your account has been deactivated. Contact support.', - // Product errors - PRODUCT_NOT_FOUND: 'Product not found. It may have been removed.', + // MarketplaceProduct errors + PRODUCT_NOT_FOUND: 'MarketplaceProduct not found. It may have been removed.', PRODUCT_ALREADY_EXISTS: 'A product with this ID already exists.', INVALID_PRODUCT_DATA: 'Please check the product information and try again.', @@ -226,7 +226,7 @@ const ProductForm = () => { try { await handleApiCall(() => createProduct(formData)); // Success handling - alert('Product created successfully!'); + alert('MarketplaceProduct created successfully!'); setFormData({ product_id: '', name: '', price: '' }); } catch (apiError) { // Handle field-specific errors @@ -260,7 +260,7 @@ const ProductForm = () => { )}
- + {
- + {
); diff --git a/docs/guides/user-management.md b/docs/guides/user-management.md index 88858b4c..78eb7c68 100644 --- a/docs/guides/user-management.md +++ b/docs/guides/user-management.md @@ -92,8 +92,8 @@ if __name__ == "__main__": # 4. Create a sample product print("\n4. Creating a sample product...") sample_product = { - "product_id": "TEST001", - "title": "Test Product", + "marketplace_product_id": "TEST001", + "title": "Test MarketplaceProduct", "description": "A test product for demonstration", "price": "19.99", "brand": "Test Brand", @@ -101,7 +101,7 @@ if __name__ == "__main__": } product_result = create_product(admin_token, sample_product) - print(f"Product created: {product_result}") + print(f"MarketplaceProduct created: {product_result}") # 5. Get 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" ``` -#### Create a Product +#### Create a MarketplaceProduct ```bash curl -X POST "http://localhost:8000/products" \ -H "Authorization: Bearer YOUR_JWT_TOKEN_HERE" \ -H "Content-Type: application/json" \ -d '{ - "product_id": "TEST001", - "title": "Test Product", + "marketplace_product_id": "TEST001", + "title": "Test MarketplaceProduct", "description": "A test product for demonstration", "price": "19.99", "brand": "Test Brand", diff --git a/docs/index.md b/docs/index.md index 35651090..1575d0c7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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: -- **Product Management**: Create, update, and manage product catalogs +- **MarketplaceProduct Management**: Create, update, and manage product catalogs - **Shop Management**: Multi-shop support with individual configurations - **CSV Import**: Bulk import products from various marketplace formats - **Stock Management**: Track inventory across multiple locations @@ -28,7 +28,7 @@ Letzshop Import is a powerful web application that enables: ### 📖 User Guides - [**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 - [**Shop Setup**](guides/shop-setup.md) - Configuring shops @@ -53,7 +53,7 @@ graph TB Client[Web Client/API Consumer] API[FastAPI Application] Auth[Authentication Service] - Products[Product Service] + Products[MarketplaceProduct Service] Shops[Shop Service] Import[Import Service] DB[(PostgreSQL Database)] @@ -71,7 +71,7 @@ graph TB ## Key Features -=== "Product Management" +=== "MarketplaceProduct Management" - CRUD operations for products - GTIN validation and normalization - Price management with currency support diff --git a/models/__init__.py b/models/__init__.py index d0d1346f..811951f0 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -4,10 +4,10 @@ # Database models (SQLAlchemy) from .database.base import Base from .database.user import User -from .database.product import Product +from .database.marketplace_product import MarketplaceProduct from .database.stock import Stock 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 from . import schemas @@ -16,7 +16,7 @@ from . import schemas __all__ = [ "Base", "User", - "Product", + "MarketplaceProduct", "Stock", "Shop", "ShopProduct", diff --git a/models/database/__init__.py b/models/database/__init__.py index ae9173a4..5d3c55e8 100644 --- a/models/database/__init__.py +++ b/models/database/__init__.py @@ -3,15 +3,15 @@ from .base import Base from .user import User -from .product import Product +from .marketplace_product import MarketplaceProduct from .stock import Stock from .shop import Shop, ShopProduct -from .marketplace import MarketplaceImportJob +from .marketplace_import_job import MarketplaceImportJob __all__ = [ "Base", "User", - "Product", + "MarketplaceProduct", "Stock", "Shop", "ShopProduct", diff --git a/models/database/base.py b/models/database/base.py index eebacf20..5c675ba2 100644 --- a/models/database/base.py +++ b/models/database/base.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import Column, DateTime @@ -8,7 +8,7 @@ from app.core.database import Base class TimestampMixin: """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( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False + DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc), nullable=False ) diff --git a/models/database/marketplace.py b/models/database/marketplace_import_job.py similarity index 93% rename from models/database/marketplace.py rename to models/database/marketplace_import_job.py index 8c347da0..874851e3 100644 --- a/models/database/marketplace.py +++ b/models/database/marketplace_import_job.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Index, 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 from app.core.database import Base +from models.database.base import TimestampMixin -class MarketplaceImportJob(Base): +class MarketplaceImportJob(Base, TimestampMixin): __tablename__ = "marketplace_import_jobs" id = Column(Integer, primary_key=True, index=True) @@ -37,7 +38,7 @@ class MarketplaceImportJob(Base): error_message = Column(String) # Timestamps - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + started_at = Column(DateTime) completed_at = Column(DateTime) diff --git a/models/database/product.py b/models/database/marketplace_product.py similarity index 83% rename from models/database/product.py rename to models/database/marketplace_product.py index d070a6d0..db1f51c1 100644 --- a/models/database/product.py +++ b/models/database/marketplace_product.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Index, 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 from app.core.database import Base +from models.database.base import TimestampMixin -class Product(Base): - __tablename__ = "products" +class MarketplaceProduct(Base, TimestampMixin): + __tablename__ = "marketplace_products" 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) description = Column(String) link = Column(String) @@ -56,16 +57,11 @@ class Product(Base): ) # Index for marketplace filtering shop_name = Column(String, index=True, nullable=True) # Index for shop filtering - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) - updated_at = Column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False - ) - # Relationship to stock (one-to-many via GTIN) stock_entries = relationship( "Stock", foreign_keys="Stock.gtin", - primaryjoin="Product.gtin == Stock.gtin", + primaryjoin="MarketplaceProduct.gtin == Stock.gtin", viewonly=True, ) shop_products = relationship("ShopProduct", back_populates="product") @@ -82,6 +78,6 @@ class Product(Base): def __repr__(self): return ( - f"" ) diff --git a/models/database/shop.py b/models/database/shop.py index 577f576c..5e85ec0d 100644 --- a/models/database/shop.py +++ b/models/database/shop.py @@ -6,9 +6,10 @@ from sqlalchemy.orm import relationship # Import Base from the central database module instead of creating a new one from app.core.database import Base +from models.database.base import TimestampMixin -class Shop(Base): +class Shop(Base, TimestampMixin): __tablename__ = "shops" id = Column(Integer, primary_key=True, index=True) @@ -32,10 +33,6 @@ class Shop(Base): is_active = Column(Boolean, default=True) is_verified = Column(Boolean, default=False) - # Timestamps - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - # Relationships owner = relationship("User", back_populates="owned_shops") shop_products = relationship("ShopProduct", back_populates="shop") @@ -49,7 +46,7 @@ class ShopProduct(Base): id = Column(Integer, primary_key=True, index=True) shop_id = Column(Integer, ForeignKey("shops.id"), nullable=False) - product_id = Column(Integer, ForeignKey("products.id"), nullable=False) + marketplace_product_id = Column(Integer, ForeignKey("marketplace_products.id"), nullable=False) # Shop-specific overrides (can override the main product data) shop_product_id = Column(String) # Shop's internal product ID @@ -74,11 +71,11 @@ class ShopProduct(Base): # Relationships shop = relationship("Shop", back_populates="shop_products") - product = relationship("Product", back_populates="shop_products") + product = relationship("MarketplaceProduct", back_populates="shop_products") # Constraints __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_featured", "shop_id", "is_featured"), ) diff --git a/models/database/stock.py b/models/database/stock.py index ef66010d..e9a15d0f 100644 --- a/models/database/stock.py +++ b/models/database/stock.py @@ -6,9 +6,9 @@ from sqlalchemy.orm import relationship # Import Base from the central database module instead of creating a new one from app.core.database import Base +from models.database.base import TimestampMixin - -class Stock(Base): +class Stock(Base, TimestampMixin): __tablename__ = "stock" 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 shop_id = Column(Integer, ForeignKey("shops.id")) # Optional: shop-specific stock - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) - updated_at = Column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False - ) - # Relationships shop = relationship("Shop") diff --git a/models/database/user.py b/models/database/user.py index bb1ddf62..8e2f528e 100644 --- a/models/database/user.py +++ b/models/database/user.py @@ -6,9 +6,9 @@ from sqlalchemy.orm import relationship # Import Base from the central database module instead of creating a new one from app.core.database import Base +from models.database.base import TimestampMixin - -class User(Base): +class User(Base, TimestampMixin): __tablename__ = "users" 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 is_active = Column(Boolean, default=True, nullable=False) last_login = Column(DateTime, nullable=True) - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) - updated_at = Column( - DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False - ) # Relationships marketplace_import_jobs = relationship( diff --git a/models/schemas/__init__.py b/models/schemas/__init__.py index 7ac8a6c3..200c7064 100644 --- a/models/schemas/__init__.py +++ b/models/schemas/__init__.py @@ -1,13 +1,13 @@ -# models/api/__init__.py +# models/schemas/__init__.py """API models package - Pydantic models for request/response validation.""" # Import API model modules from . import base from . import auth -from . import product +from . import marketplace_product from . import stock from . import shop -from . import marketplace +from . import marketplace_import_job from . import stats # Common imports for convenience @@ -16,9 +16,9 @@ from .base import * # Base Pydantic models __all__ = [ "base", "auth", - "product", + "marketplace_product", "stock", "shop", - "marketplace", + "marketplace_import_job", "stats", ] diff --git a/models/schemas/marketplace.py b/models/schemas/marketplace_import_job.py similarity index 90% rename from models/schemas/marketplace.py rename to models/schemas/marketplace_import_job.py index 90fe14ed..5bb73ff6 100644 --- a/models/schemas/marketplace.py +++ b/models/schemas/marketplace_import_job.py @@ -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 typing import Optional from pydantic import BaseModel, Field, field_validator -class MarketplaceImportRequest(BaseModel): +class MarketplaceImportJobRequest(BaseModel): url: str = Field(..., description="URL to CSV file from marketplace") marketplace: str = Field(default="Letzshop", description="Marketplace name") shop_code: str = Field(..., description="Shop code to associate products with") diff --git a/models/schemas/product.py b/models/schemas/marketplace_product.py similarity index 74% rename from models/schemas/product.py rename to models/schemas/marketplace_product.py index 110171ad..40331444 100644 --- a/models/schemas/product.py +++ b/models/schemas/marketplace_product.py @@ -1,11 +1,11 @@ -# product.py - Simplified validation +# models/schemas/marketplace_products.py - Simplified validation from datetime import datetime from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field from models.schemas.stock import StockSummaryResponse -class ProductBase(BaseModel): - product_id: Optional[str] = None +class MarketplaceProductBase(BaseModel): + marketplace_product_id: Optional[str] = None title: Optional[str] = None description: Optional[str] = None link: Optional[str] = None @@ -45,27 +45,27 @@ class ProductBase(BaseModel): marketplace: Optional[str] = None shop_name: Optional[str] = None -class ProductCreate(ProductBase): - product_id: str = Field(..., description="Product identifier") - title: str = Field(..., description="Product title") +class MarketplaceProductCreate(MarketplaceProductBase): + marketplace_product_id: str = Field(..., description="MarketplaceProduct identifier") + title: str = Field(..., description="MarketplaceProduct title") # Removed: min_length constraints and custom validators # Service will handle empty string validation with proper domain exceptions -class ProductUpdate(ProductBase): +class MarketplaceProductUpdate(MarketplaceProductBase): pass -class ProductResponse(ProductBase): +class MarketplaceProductResponse(MarketplaceProductBase): model_config = ConfigDict(from_attributes=True) id: int created_at: datetime updated_at: datetime -class ProductListResponse(BaseModel): - products: List[ProductResponse] +class MarketplaceProductListResponse(BaseModel): + products: List[MarketplaceProductResponse] total: int skip: int limit: int -class ProductDetailResponse(BaseModel): - product: ProductResponse +class MarketplaceProductDetailResponse(BaseModel): + product: MarketplaceProductResponse stock_info: Optional[StockSummaryResponse] = None diff --git a/models/schemas/shop.py b/models/schemas/shop.py index 7c8142f9..c0c7c91d 100644 --- a/models/schemas/shop.py +++ b/models/schemas/shop.py @@ -3,7 +3,7 @@ import re from datetime import datetime from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator -from models.schemas.product import ProductResponse +from models.schemas.marketplace_product import MarketplaceProductResponse class ShopCreate(BaseModel): shop_code: str = Field(..., description="Unique shop identifier") @@ -64,7 +64,7 @@ class ShopListResponse(BaseModel): limit: int 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_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) id: int shop_id: int - product: ProductResponse + product: MarketplaceProductResponse shop_product_id: Optional[str] shop_price: Optional[float] shop_sale_price: Optional[float] diff --git a/scripts/verify_setup.py b/scripts/verify_setup.py index 87a2b6e8..c0b9aefd 100644 --- a/scripts/verify_setup.py +++ b/scripts/verify_setup.py @@ -131,10 +131,10 @@ def verify_model_structure(): # Import specific models 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.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") @@ -171,7 +171,7 @@ def check_project_structure(): critical_paths = [ "models/database/base.py", "models/database/user.py", - "models/database/product.py", + "models/database/marketplace_products.py", "models/database/stock.py", "app/core/config.py", "alembic/env.py", diff --git a/tests/.coverage b/tests/.coverage index 4c3e319ba3837b7a311f24d7427b8d523161c38f..1d081b77e25c121ae3ec4466afff135816cb13f9 100644 GIT binary patch delta 4049 zcma)8eQXow89)2(Y@hGW=al$jCr+FI3AW>}B#b};#jGTdGz>AdjZLmGhvOt(;@FD~ zDO4!-qMmvOFL1iNJt&s=pt=Q)fCY7N7r^6{L`(Y+B8k;_Q&X8NE@QnviE&` zw$HJhEmCx^?|Giz^Z7o%%f3No-ypB8`8B;i3h2y>ZxX`C!rMYl7!^DMZThY0s_C1i zF_Vk`oc|SnftUFn?w{PxxmUSKu9q{g@3R-#8P;X|xAB+8*No#vzfr^7U|wV*jNkCc zaLe$z;h4du|GWMt`tRyPdZ+F)-CMeox)Gg~zDr-Hm*^1fp*7Sk>P;g5_s%OVl(w@U zbPZaplAKCOCo=Kbv@{`S(wSs*1|fV21cxl(>Ojk)Yy1CUC1=vms=J3IW{Q5x z(@DqXPsXFMw1o1ED8HbMvzsJFYuKQm7Ex5t1i^RJhA-7VlA2FKKk0ngz8s*498&|o z7*j%?pNVBsvvM?s%qLDnGldZMLdQG(B(YEfLn&Mp1XK~95Bz4((j+C~6R_nAGHmHY z40cw|BDs z&dMh;(-5Vn9VdMEV2jqbHD57wp^7=Ek`8P^6>C;k6jn19nT;pn8GNa{o59+C3s@Sm zTKBfW3Xzq$fz_cVbEGU0Pb;{+-(r&z$%z=e#p&pDEFtHo6$a>aAWVOSRJktwEhKTK zx*NVQP!Pq_jC_FQ;Rdb8j*qMG7-+&XaBsM2f3Ok8OIHFF{1$dg?S6P|IQj7>ty-TI zRT5ly(x%U|7{y*dJWR#&5E_eUyvX3qBv&5CNjD(k{4AWZ-I?ORxJ{if5iXO~x^gk^h6o3NsGVaBHjT zf}4je4egq$Nq~EY#l_2n@O!~)`j((G{m%5Z;kH56{X=(%{)}E0ejtQ|Z_r*^YjShH z;D613LEYva@GtV)`QuE$@VVi8hDP1@bvF8^bXb2^e_el8AJ#2Umrau1#T?@sWYO=HBUWnMA{iXR3J3Iv`J8}R6rm`S-H{lNUJ*s^{sOCJS& zZZZrsPjAY9RIK|9*wsmnA)$!v^THDG?*v_=A{czyK`Iy)tfq`WVe)rit*bS)@?^dp z>^my9Vf*49UdP=%E^-tFQ(xvqA!}p~PCnB@JwfFdq!lQ~Qd7!0&V{WH>>?MC(Ka=7 z6%%wb{C+Zubm7J-U9K{k^?^D`1jD0t)E2Nf+JGwn`4#?Pd<oc_oa5TSt*1n5VXv>FxK@N=h#i2DWGyHN^3!oVEL#OKUd)sRr=mh zJGGC>(a55+09m-$waKdyCow)|E94C>j5U&b$VG(X+z_Y4UJVGO$m2z)3VDfTSr(f~h3$FW|hR9h%i8=|VKZNgQ) z@oc`@_u&&(+kxjy#eb?mS!Jji)K^2{9b*T11SMO#y$aJX)s{zrS`0XZPsXieg}K5T zy0ZO+c8@9`x)=~j9q+FqHBicKv*CoWtk58MQAkA!Zbn40ojg+IqkiRHL~Pac0)5sq zbjlPq4VpYAi;3s&@IT{o{C?id{h0&YSuV$&;=al~&TZyIj$$9N_u0R(AG7bVKW5)z zFR?GP-)7ISDfSq9h<$?XU8ys_YXn=)P7g8Nl!mDn-FGfoj9u_3THSZ{oTAJDc;0Mt zuk=rS`p$#&~iDTr&~i%JLtX^AApcv6mnoCsxj%|9bGh@||OA literal 53248 zcmeI5eQ+Dcb-?#NfCF%cTaXkM=wE5%y&>`T_V(?2``fp-y8sV-=%WYYSzVh+WfIY>Hi!a<xWO-3V&Yd<$bK@bg$U z*V;@f(VtCe3-KgvX3~gfcC=%14Onpu+CZ+)S6N`y7=^W6=8P2AhG@W#_DYLN^_st#pve4e5 z?zE7T9?fYjCgVpI^?c_CwSs1lHY1q|YNQbm0qwH8eyoN=5S98dJ*o zpz|YPcu}6N@<{cqt=uz5XrK^0k*Rpr$S1r-L6gsR2pw|WIc~@(DGm}ciWU~M)C?Um zL|e;s4n(tk%=B9@q8T#Kw3Jy8JpGME8*vtV(54nrQ^m1HBgW8SfWW=LDmJs2jFF*Y zkT%3pNA*l}R_`;f=57nW=|Fd#RO--ZF($ z6hc-{-r+WCT}tRtV=-ghNXp$LEserm(i$V?6=+VcWuHI30z;(W%s8mc}nRrQu2D zo`S!*3JT{IdnnaKi^K4KFl88Vm{!TOWsF<7TE|?}Aa^07Lz_ya7W8N`H#dOB0RwEt z4UPJ6`>b?tp$_9XsbltN2ldICT*z(yVHkAQ_<}?t5lv2;^A(wfbG5>~pA1f7CwCJ8 zMXdt{r2#{H4i{lA?dlN9g#qcUNSM5mU9yKP7y(A$?js;@JSUO;KaZ~<{4Tt(gArf^7y(9r5nu!u0Y-ok zU<4QeMt~7u1b!MzvqsL!cSsJm5<|1JM%f5t!I$I4sEi^?%&pAwLNEdRNDRK8!9eHVN$`m(+WpX&XQ z_eHPa-Q$%#|L8gGNqKfjH>LB^=OtYVxo^79yFcfiboaZiyT0f8tZUj86mN=eieCn^ z>|g{Kfe#0PzFh)}?H`{!sAo?YbE$OifkZl$$@U%%Pe#+}$;E7Z!I&hEfK5dW9UkQx z7#GmIb9wz(Oi$BCI49@enX&~v13JMm0Y#j3;JIpjav?Q43lCRA>E2NRMK>xPpV5tU zGzrCrM+9Uz7dQ0G(KtN)J!xc9vH6I3N}L)l>r^xyp9BU}i&z<|)JimK*sbiW%nE3M zm7zfa9jT-(&`~xcJp%%I%vsH93Mdg9;j%Ug9Ue(%QUK1#^~z{SKugXx@|ZzsdNG!@ zdDqupHo{ErOg(Tg+$W&;oh>Ee(-2Zi@R0N5GP>2xOziyfHZyjm^(yQj5vy2+bgNy35+h>Dlbu(qzL1PA59D|_5wZgFLZ?4oWvq$^4RH|T3 z9fM8I{ZfqB)|n%X0$MCf%bJ`fi!p~9HlmhSB=a6n3vYG|HIrIFgG?r%GFZP+lkzGB z(xJMFJ%evYp}|>Ov4+Wd=OzKAD#K=j-BaT@qD=y}!l7zMr^v{(tpuWEOaraqD#sC1 zqEXzH{Q<{FE73E1GK4F2&6?i_RL2ohVxrKG{r*bbvPO(jse(0PWXJF-@ygaOJwE47 zEX<##OPyW;J$mOjAr{DVF|DV`92@jhvXZg7x?d{WRC5}#dEM#W(2+!xMp4kUp(D0> zdqhVXq=9tp314k76Q@g0VdJHzHJ|}WFTZhdn=74M*(TablF$Eli3}um;4vSuP7J&hCkrn>s37e|=a-UKqEAltw{qmChw(l+9qVKf#v~REX$KI?D ze>gTitd)!aBY;r95aXODfWmgroRW483iD;k+h+a3{(rnzh?LPO+5eCA3DFHp7xw?7 zBZ5(;cz%OWy8j<3@02-1S?dFMm!U!{#ryxE3a#X}6g!87BNer!c>g~b6do(1W;G?Z z${i?gqtM~jW;wV25042;Wo+azo695H4lYza!c6aM%c=n(?qJEbeJk4k_w5uC4k~1l z&$X#^|KD5QuiN(j!SW&Cxc~1dZ|AoCfA?^lpC?y|G~A~wzSnb2ufTl47-+!-7ey1IUmW-UO{2nM zd0N(jyk!61xFNN?qPc5qa1J$-T0z6y|8L%~NqLn5>H5k&Gxz^>m1~%+YlnnXh1hH( zXOr`YHVN1Y*El;xMy72gU~LMkokvWGM)8QLa*nhTJ+mhR6}x85@7Q_7l$a>=qgt_B z=7{lEtYG${;v8NjUfKFZF4Kv(?f-pU!lMpxLM)K!!rcFRD_Y4|UF|8~RC5}#c`a@1 zh`IlFZ|sPzUY9cs(!k>VzgWT2(;Cn)ZweI@x4FW13Xgi7m)f?H?EC-N&WsUY1Q-EE zfDvE>7y(9r5nu!u0Y-okxT^^W@J9lE^7%i9KS1!q4n}|xU<4QeMt~7u1Q-EEfDvE> z7y(9r5x5Hp2!h{BKK~z=w;}vK{yBaJU&24ce~Vwn&*6{b8T>K48xP@j+=yN34fU$} zef4kDKUPnwlj;GrU)`(*{Qu%V>tFWY=l3ZWl|N9PRIg6J}8gL{a}R7*G{gFU0=Gs{;Qi`TVGd4AgiV2>^dB$UVrsx@-Uol-g^0` zS3X!5JVQitOZUfzm7S1kY(D$b^eeymHuemX{HC8g!1)FsRae)2Y5frA3zPKb?n{5S z?b$#G(rpdhm!2OSeD?XT4G!W!Kgp?wl5o5^{_16qzYlV@Grb`AG*C=!R6=Ht}q530Q0JpUQn*kW<$uj{YlB8ap99>=V!SKX~S^ zzwy=kpEz~mPyX;`2ljck21Yc{NDqpuuJ_K6>f!i7D|w|3Y#}N1!YL|5Bx+h<8Eu{L z1ommWT0!wxNW5Nm{mdgPOQD7}PSX&&nR~ibc+=e}Q}X%U^UIl3MB^YfsfV^dB6kqL(EE zWy$5k&FC<9m}{(q>%-&XYR{L}kGK(1h&OA{S(=A$t%Yk7ium^@B0pPwch4bdRb6dC zhno*KublvE%dWsCDA49V7hGNA=h30VD??2+kTY6+ZrMdlt*QtVP~EbNSleAy4Hvh| zB0qsR-h;Ra#CxhBt9neFKqj>o@pR1039BQUJRL=1o5vP@u(^!TSRfwL%cRN zHtTi!;auhU8inW$ix(gN#cNXQ$@gBn_lne7Ekovxo#Mq;$IumR?U&~{B-Qvx!2xmZ zoA0A_v?g8W&@G|f3z?yQ@#60D5Go%)XY6scq9?Bibso6T*C}3n@;nK^Rq2-!Qr}&{ ziUj$MJD`?^m!IWo-EbE4h^sfxg(#C!Mgx|p`KCK)aTV_)i0VkSg{gi5`m+^kQ2i`##!QaCn?8k0( zUA?ZpgTITr)Su(CxD9^`H{rj;oA77xr|}#?%??I@5nu!u0Y-okU<4QeMt~7u1Q>z4 zj{w=Qa;@W3w2e`rjZ)DvLdDi$Dz*$!(Y%w2ra>y|2B_E^rlKK4MSVXNjeS(q_ENDa zNJUK#72CS0=7y(9r5nu!u0Y-okU<7~w{bL*JPiz7y(9r5nu!u0Y-ok sxGM>e=l|K~|955AvPLlii~u9R2rvSS03*N%FanGKBftnS0(k=e3(7hRNB{r; diff --git a/tests/conftest.py b/tests/conftest.py index b86c5c3b..95c7c912 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,8 +8,8 @@ from sqlalchemy.pool import StaticPool from app.core.database import Base, get_db from main import app # Import all models to ensure they're registered with Base metadata -from models.database.marketplace import MarketplaceImportJob -from models.database.product import Product +from models.database.marketplace_import_job import MarketplaceImportJob +from models.database.marketplace_product import MarketplaceProduct from models.database.shop import Shop, ShopProduct from models.database.stock import Stock from models.database.user import User @@ -87,8 +87,8 @@ def cleanup(): # Import fixtures from fixture modules pytest_plugins = [ "tests.fixtures.auth_fixtures", - "tests.fixtures.product_fixtures", + "tests.fixtures.marketplace_product_fixtures", "tests.fixtures.shop_fixtures", - "tests.fixtures.marketplace_fixtures", + "tests.fixtures.marketplace_import_job_fixtures", "tests.fixtures.testing_fixtures", ] diff --git a/tests/fixtures/marketplace_fixtures.py b/tests/fixtures/marketplace_import_job_fixtures.py similarity index 81% rename from tests/fixtures/marketplace_fixtures.py rename to tests/fixtures/marketplace_import_job_fixtures.py index ffe256be..e59ea1f7 100644 --- a/tests/fixtures/marketplace_fixtures.py +++ b/tests/fixtures/marketplace_import_job_fixtures.py @@ -1,11 +1,11 @@ -# tests/fixtures/marketplace_fixtures.py +# tests/fixtures/marketplace_import_job_fixtures.py import pytest -from models.database.marketplace import MarketplaceImportJob +from models.database.marketplace_import_job import MarketplaceImportJob @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""" job = MarketplaceImportJob( marketplace="amazon", @@ -26,7 +26,7 @@ def test_marketplace_job(db, test_shop, test_user): 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""" defaults = { "marketplace": "test", diff --git a/tests/fixtures/product_fixtures.py b/tests/fixtures/marketplace_product_fixtures.py similarity index 53% rename from tests/fixtures/product_fixtures.py rename to tests/fixtures/marketplace_product_fixtures.py index a1b7469d..5f468e50 100644 --- a/tests/fixtures/product_fixtures.py +++ b/tests/fixtures/marketplace_product_fixtures.py @@ -1,17 +1,17 @@ -# tests/fixtures/product_fixtures.py +# tests/fixtures/marketplace_product_fixtures.py import uuid import pytest -from models.database.product import Product +from models.database.marketplace_product import MarketplaceProduct @pytest.fixture -def test_product(db): +def test_marketplace_product(db): """Create a test product""" - product = Product( - product_id="TEST001", - title="Test Product", + marketplace_product = MarketplaceProduct( + marketplace_product_id="TEST001", + title="Test MarketplaceProduct", description="A test product", price="10.99", currency="EUR", @@ -21,19 +21,19 @@ def test_product(db): marketplace="Letzshop", shop_name="TestShop", ) - db.add(product) + db.add(marketplace_product) db.commit() - db.refresh(product) - return product + db.refresh(marketplace_product) + return marketplace_product @pytest.fixture def unique_product(db): """Create a unique product for tests that need isolated product data""" unique_id = str(uuid.uuid4())[:8] - product = Product( - product_id=f"UNIQUE_{unique_id}", - title=f"Unique Product {unique_id}", + marketplace_product = MarketplaceProduct( + marketplace_product_id=f"UNIQUE_{unique_id}", + title=f"Unique MarketplaceProduct {unique_id}", description=f"A unique test product {unique_id}", price="19.99", currency="EUR", @@ -44,22 +44,22 @@ def unique_product(db): shop_name=f"UniqueShop_{unique_id}", google_product_category=f"UniqueCategory_{unique_id}", ) - db.add(product) + db.add(marketplace_product) db.commit() - db.refresh(product) - return product + db.refresh(marketplace_product) + return marketplace_product @pytest.fixture def multiple_products(db): """Create multiple products for testing statistics and pagination""" unique_id = str(uuid.uuid4())[:8] - products = [] + marketplace_products = [] for i in range(5): - product = Product( - product_id=f"MULTI_{unique_id}_{i}", - title=f"Multi Product {i} {unique_id}", + marketplace_product = MarketplaceProduct( + marketplace_product_id=f"MULTI_{unique_id}_{i}", + title=f"Multi MarketplaceProduct {i} {unique_id}", description=f"Multi test product {i}", price=f"{10 + i}.99", currency="EUR", @@ -69,23 +69,23 @@ def multiple_products(db): google_product_category=f"MultiCategory_{i % 2}", # Create 2 different categories 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() - for product in products: + for product in marketplace_products: 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""" - def _create_product(db, **kwargs): + def _marketplace_create_product(db, **kwargs): unique_id = str(uuid.uuid4())[:8] defaults = { - "product_id": f"FACTORY_{unique_id}", - "title": f"Factory Product {unique_id}", + "marketplace_product_id": f"FACTORY_{unique_id}", + "title": f"Factory MarketplaceProduct {unique_id}", "price": "15.99", "currency": "EUR", "marketplace": "TestMarket", @@ -93,31 +93,31 @@ def create_unique_product_factory(): } defaults.update(kwargs) - product = Product(**defaults) - db.add(product) + marketplace_product = MarketplaceProduct(**defaults) + db.add(marketplace_product) db.commit() - db.refresh(product) - return product + db.refresh(marketplace_product) + return marketplace_product - return _create_product + return _marketplace_create_product @pytest.fixture -def product_factory(): +def marketplace_product_factory(): """Fixture that provides a product factory function""" - return create_unique_product_factory() + return create_unique_marketplace_product_factory() @pytest.fixture -def test_product_with_stock(db, test_product, test_stock): - """Product with associated stock record.""" +def test_marketplace_product_with_stock(db, test_marketplace_product, test_stock): + """MarketplaceProduct with associated stock record.""" # Ensure they're linked by GTIN - if test_product.gtin != test_stock.gtin: - test_stock.gtin = test_product.gtin + if test_marketplace_product.gtin != test_stock.gtin: + test_stock.gtin = test_marketplace_product.gtin db.commit() db.refresh(test_stock) return { - 'product': test_product, + 'marketplace_product': test_marketplace_product, 'stock': test_stock } diff --git a/tests/fixtures/shop_fixtures.py b/tests/fixtures/shop_fixtures.py index 43d775f3..866976fa 100644 --- a/tests/fixtures/shop_fixtures.py +++ b/tests/fixtures/shop_fixtures.py @@ -80,7 +80,7 @@ def verified_shop(db, other_user): def shop_product(db, test_shop, unique_product): """Create a shop product relationship""" 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 if hasattr(ShopProduct, "shop_price"): @@ -97,11 +97,11 @@ def shop_product(db, test_shop, unique_product): @pytest.fixture -def test_stock(db, test_product, test_shop): +def test_stock(db, test_marketplace_product, test_shop): """Create test stock entry""" unique_id = str(uuid.uuid4())[:8].upper() # Short unique identifier 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}", quantity=10, reserved_quantity=0, diff --git a/tests/integration/api/v1/test_admin_endpoints.py b/tests/integration/api/v1/test_admin_endpoints.py index 232f9aa2..e83cb753 100644 --- a/tests/integration/api/v1/test_admin_endpoints.py +++ b/tests/integration/api/v1/test_admin_endpoints.py @@ -137,7 +137,7 @@ class TestAdminAPI: assert data["error_code"] == "SHOP_NOT_FOUND" 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""" response = client.get( @@ -148,17 +148,17 @@ class TestAdminAPI: data = response.json() 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] - 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, admin_headers, test_marketplace_job + self, client, admin_headers, test_marketplace_import_job ): """Test admin getting marketplace import jobs with filters""" response = client.get( "/api/v1/admin/marketplace-import-jobs", - params={"marketplace": test_marketplace_job.marketplace}, + params={"marketplace": test_marketplace_import_job.marketplace}, headers=admin_headers, ) @@ -166,7 +166,7 @@ class TestAdminAPI: data = response.json() assert len(data) >= 1 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): diff --git a/tests/integration/api/v1/test_filtering.py b/tests/integration/api/v1/test_filtering.py index f6919afa..b28d3862 100644 --- a/tests/integration/api/v1/test_filtering.py +++ b/tests/integration/api/v1/test_filtering.py @@ -1,12 +1,13 @@ # tests/integration/api/v1/test_filtering.py import pytest -from models.database.product import Product +from models.database.marketplace_product import MarketplaceProduct @pytest.mark.integration @pytest.mark.api @pytest.mark.products +@pytest.mark.marketplace class TestFiltering: def test_product_brand_filter_success(self, client, auth_headers, db): @@ -16,27 +17,27 @@ class TestFiltering: unique_suffix = str(uuid.uuid4())[:8] products = [ - Product(product_id=f"BRAND1_{unique_suffix}", title="Product 1", brand="BrandA"), - Product(product_id=f"BRAND2_{unique_suffix}", title="Product 2", brand="BrandB"), - Product(product_id=f"BRAND3_{unique_suffix}", title="Product 3", brand="BrandA"), + MarketplaceProduct(marketplace_product_id=f"BRAND1_{unique_suffix}", title="MarketplaceProduct 1", brand="BrandA"), + MarketplaceProduct(marketplace_product_id=f"BRAND2_{unique_suffix}", title="MarketplaceProduct 2", brand="BrandB"), + MarketplaceProduct(marketplace_product_id=f"BRAND3_{unique_suffix}", title="MarketplaceProduct 3", brand="BrandA"), ] db.add_all(products) db.commit() # 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 data = response.json() assert data["total"] >= 2 # At least our test products # Verify all returned products have BrandA for product in data["products"]: - if product["product_id"].endswith(unique_suffix): + if product["marketplace_product_id"].endswith(unique_suffix): assert product["brand"] == "BrandA" # 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 data = response.json() assert data["total"] >= 1 # At least our test product @@ -47,21 +48,21 @@ class TestFiltering: unique_suffix = str(uuid.uuid4())[:8] products = [ - Product(product_id=f"MKT1_{unique_suffix}", title="Product 1", marketplace="Amazon"), - Product(product_id=f"MKT2_{unique_suffix}", title="Product 2", marketplace="eBay"), - Product(product_id=f"MKT3_{unique_suffix}", title="Product 3", marketplace="Amazon"), + MarketplaceProduct(marketplace_product_id=f"MKT1_{unique_suffix}", title="MarketplaceProduct 1", marketplace="Amazon"), + MarketplaceProduct(marketplace_product_id=f"MKT2_{unique_suffix}", title="MarketplaceProduct 2", marketplace="eBay"), + MarketplaceProduct(marketplace_product_id=f"MKT3_{unique_suffix}", title="MarketplaceProduct 3", marketplace="Amazon"), ] db.add_all(products) 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 data = response.json() assert data["total"] >= 2 # At least our test products # 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: assert product["marketplace"] == "Amazon" @@ -71,18 +72,18 @@ class TestFiltering: unique_suffix = str(uuid.uuid4())[:8] products = [ - Product( - product_id=f"SEARCH1_{unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"SEARCH1_{unique_suffix}", title=f"Apple iPhone {unique_suffix}", description="Smartphone" ), - Product( - product_id=f"SEARCH2_{unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"SEARCH2_{unique_suffix}", title=f"Samsung Galaxy {unique_suffix}", description="Android phone", ), - Product( - product_id=f"SEARCH3_{unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"SEARCH3_{unique_suffix}", title=f"iPad Tablet {unique_suffix}", description="Apple tablet" ), @@ -92,13 +93,13 @@ class TestFiltering: db.commit() # 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 data = response.json() assert data["total"] >= 2 # iPhone and iPad # 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 data = response.json() assert data["total"] >= 2 # iPhone and Galaxy @@ -109,20 +110,20 @@ class TestFiltering: unique_suffix = str(uuid.uuid4())[:8] products = [ - Product( - product_id=f"COMBO1_{unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"COMBO1_{unique_suffix}", title=f"Apple iPhone {unique_suffix}", brand="Apple", marketplace="Amazon", ), - Product( - product_id=f"COMBO2_{unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"COMBO2_{unique_suffix}", title=f"Apple iPad {unique_suffix}", brand="Apple", marketplace="eBay", ), - Product( - product_id=f"COMBO3_{unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"COMBO3_{unique_suffix}", title=f"Samsung Phone {unique_suffix}", brand="Samsung", marketplace="Amazon", @@ -134,14 +135,14 @@ class TestFiltering: # Filter by brand AND marketplace 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 data = response.json() assert data["total"] >= 1 # At least iPhone matches both # 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: assert product["brand"] == "Apple" assert product["marketplace"] == "Amazon" @@ -149,7 +150,7 @@ class TestFiltering: def test_filter_with_no_results(self, client, auth_headers): """Test filtering with criteria that returns no results""" 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 @@ -162,9 +163,9 @@ class TestFiltering: import uuid unique_suffix = str(uuid.uuid4())[:8] - product = Product( - product_id=f"CASE_{unique_suffix}", - title="Test Product", + product = MarketplaceProduct( + marketplace_product_id=f"CASE_{unique_suffix}", + title="Test MarketplaceProduct", brand="TestBrand", marketplace="TestMarket", ) @@ -173,7 +174,7 @@ class TestFiltering: # Test different case variations 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 data = response.json() assert data["total"] >= 1 @@ -182,9 +183,9 @@ class TestFiltering: """Test behavior with invalid filter parameters""" # Test with very long filter values 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 # Test with special characters - response = client.get("/api/v1/product?brand=", headers=auth_headers) + response = client.get("/api/v1/marketplace/product?brand=", headers=auth_headers) assert response.status_code == 200 # Should handle gracefully diff --git a/tests/integration/api/v1/test_marketplace_endpoints.py b/tests/integration/api/v1/test_marketplace_import_job_endpoints.py similarity index 90% rename from tests/integration/api/v1/test_marketplace_endpoints.py rename to tests/integration/api/v1/test_marketplace_import_job_endpoints.py index 54860199..8c92b333 100644 --- a/tests/integration/api/v1/test_marketplace_endpoints.py +++ b/tests/integration/api/v1/test_marketplace_import_job_endpoints.py @@ -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 import pytest @@ -7,7 +7,7 @@ import pytest @pytest.mark.integration @pytest.mark.api @pytest.mark.marketplace -class TestMarketplaceAPI: +class TestMarketplaceImportJobAPI: def test_import_from_marketplace(self, client, auth_headers, test_shop, test_user): """Test marketplace import endpoint - just test job creation""" # Ensure user owns the shop @@ -102,18 +102,18 @@ class TestMarketplaceAPI: assert data["marketplace"] == "AdminMarket" 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""" 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 ) assert response.status_code == 200 data = response.json() - assert data["job_id"] == test_marketplace_job.id - assert data["status"] == test_marketplace_job.status - assert data["marketplace"] == test_marketplace_job.marketplace + assert data["job_id"] == test_marketplace_import_job.id + assert data["status"] == test_marketplace_import_job.status + assert data["marketplace"] == test_marketplace_import_job.marketplace def test_get_marketplace_import_status_not_found(self, client, auth_headers): """Test getting status of non-existent import job""" @@ -127,13 +127,13 @@ class TestMarketplaceAPI: assert data["error_code"] == "IMPORT_JOB_NOT_FOUND" 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""" # 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( - f"/api/v1/marketplace/import-status/{test_marketplace_job.id}", + f"/api/v1/marketplace/import-status/{test_marketplace_import_job.id}", headers=auth_headers ) @@ -141,7 +141,7 @@ class TestMarketplaceAPI: data = response.json() 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""" response = client.get("/api/v1/marketplace/import-jobs", headers=auth_headers) @@ -152,12 +152,12 @@ class TestMarketplaceAPI: # Find our test job in the results 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""" 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 ) @@ -167,7 +167,7 @@ class TestMarketplaceAPI: assert len(data) >= 1 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): """Test import jobs pagination""" @@ -181,7 +181,7 @@ class TestMarketplaceAPI: assert isinstance(data, list) 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""" 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): """Test cancelling a marketplace import job""" # Create a pending job that can be cancelled - from models.database.marketplace import MarketplaceImportJob + from models.database.marketplace_import_job import MarketplaceImportJob import uuid unique_id = str(uuid.uuid4())[:8] @@ -240,14 +240,14 @@ class TestMarketplaceAPI: data = response.json() 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""" # Set job to completed status - test_marketplace_job.status = "completed" + test_marketplace_import_job.status = "completed" db.commit() 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 ) @@ -259,7 +259,7 @@ class TestMarketplaceAPI: def test_delete_marketplace_import_job(self, client, auth_headers, test_user, test_shop, db): """Test deleting a marketplace import job""" # Create a completed job that can be deleted - from models.database.marketplace import MarketplaceImportJob + from models.database.marketplace_import_job import MarketplaceImportJob import uuid 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): """Test deleting a 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 unique_id = str(uuid.uuid4())[:8] @@ -352,7 +352,7 @@ class TestMarketplaceAPI: data = response.json() 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""" response = client.get("/api/v1/marketplace/import-jobs", headers=admin_headers) @@ -361,23 +361,23 @@ class TestMarketplaceAPI: assert isinstance(data, list) # Admin should see all jobs, including the test job 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""" 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 ) assert response.status_code == 200 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): """Test that admin can cancel any job""" # Create a pending job owned by different user - from models.database.marketplace import MarketplaceImportJob + from models.database.marketplace_import_job import MarketplaceImportJob import uuid unique_id = str(uuid.uuid4())[:8] diff --git a/tests/integration/api/v1/test_product_export.py b/tests/integration/api/v1/test_marketplace_product_export.py similarity index 74% rename from tests/integration/api/v1/test_product_export.py rename to tests/integration/api/v1/test_marketplace_product_export.py index 6557cb19..49a9ee17 100644 --- a/tests/integration/api/v1/test_product_export.py +++ b/tests/integration/api/v1/test_marketplace_product_export.py @@ -5,7 +5,7 @@ import uuid import pytest -from models.database.product import Product +from models.database.marketplace_product import MarketplaceProduct @pytest.mark.integration @@ -13,9 +13,9 @@ from models.database.product import Product @pytest.mark.performance # for the performance test 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""" - 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.headers["content-type"] == "text/csv; charset=utf-8" @@ -26,13 +26,13 @@ class TestExportFunctionality: # Check header row 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: assert field in header # Verify test product appears in export 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" 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 unique_suffix = str(uuid.uuid4())[:8] products = [ - Product( - product_id=f"EXP1_{unique_suffix}", - title=f"Amazon Product {unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"EXP1_{unique_suffix}", + title=f"Amazon MarketplaceProduct {unique_suffix}", marketplace="Amazon" ), - Product( - product_id=f"EXP2_{unique_suffix}", - title=f"eBay Product {unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"EXP2_{unique_suffix}", + title=f"eBay MarketplaceProduct {unique_suffix}", marketplace="eBay" ), ] @@ -56,7 +56,7 @@ class TestExportFunctionality: db.commit() 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.headers["content-type"] == "text/csv; charset=utf-8" @@ -69,14 +69,14 @@ class TestExportFunctionality: """Test CSV export with shop name filtering successfully""" unique_suffix = str(uuid.uuid4())[:8] products = [ - Product( - product_id=f"SHOP1_{unique_suffix}", - title=f"Shop1 Product {unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"SHOP1_{unique_suffix}", + title=f"Shop1 MarketplaceProduct {unique_suffix}", shop_name="TestShop1" ), - Product( - product_id=f"SHOP2_{unique_suffix}", - title=f"Shop2 Product {unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"SHOP2_{unique_suffix}", + title=f"Shop2 MarketplaceProduct {unique_suffix}", shop_name="TestShop2" ), ] @@ -85,7 +85,7 @@ class TestExportFunctionality: db.commit() 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 @@ -97,21 +97,21 @@ class TestExportFunctionality: """Test CSV export with combined marketplace and shop filters successfully""" unique_suffix = str(uuid.uuid4())[:8] products = [ - Product( - product_id=f"COMBO1_{unique_suffix}", - title=f"Combo Product 1 {unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"COMBO1_{unique_suffix}", + title=f"Combo MarketplaceProduct 1 {unique_suffix}", marketplace="Amazon", shop_name="TestShop" ), - Product( - product_id=f"COMBO2_{unique_suffix}", - title=f"Combo Product 2 {unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"COMBO2_{unique_suffix}", + title=f"Combo MarketplaceProduct 2 {unique_suffix}", marketplace="eBay", shop_name="TestShop" ), - Product( - product_id=f"COMBO3_{unique_suffix}", - title=f"Combo Product 3 {unique_suffix}", + MarketplaceProduct( + marketplace_product_id=f"COMBO3_{unique_suffix}", + title=f"Combo MarketplaceProduct 3 {unique_suffix}", marketplace="Amazon", shop_name="OtherShop" ), @@ -121,7 +121,7 @@ class TestExportFunctionality: db.commit() 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 ) assert response.status_code == 200 @@ -134,7 +134,7 @@ class TestExportFunctionality: def test_csv_export_no_results(self, client, auth_headers): """Test CSV export with filters that return no results""" response = client.get( - "/api/v1/product/export-csv?marketplace=NonexistentMarketplace12345", + "/api/v1/marketplace/product/export-csv?marketplace=NonexistentMarketplace12345", headers=auth_headers ) @@ -146,7 +146,7 @@ class TestExportFunctionality: # Should have header row even with no data assert len(csv_lines) >= 1 # 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): """Test CSV export performance with many products""" @@ -156,9 +156,9 @@ class TestExportFunctionality: products = [] batch_size = 100 # Reduced from 1000 for faster test execution for i in range(batch_size): - product = Product( - product_id=f"PERF{i:04d}_{unique_suffix}", - title=f"Performance Product {i}", + product = MarketplaceProduct( + marketplace_product_id=f"PERF{i:04d}_{unique_suffix}", + title=f"Performance MarketplaceProduct {i}", marketplace="Performance", description=f"Performance test product {i}", price="10.99" @@ -174,7 +174,7 @@ class TestExportFunctionality: import 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() execution_time = end_time - start_time @@ -186,20 +186,20 @@ class TestExportFunctionality: # Verify content contains our test data csv_content = response.content.decode("utf-8") 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): """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 data = response.json() assert data["error_code"] == "INVALID_TOKEN" 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""" - 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.headers["content-type"] == "text/csv; charset=utf-8" @@ -215,9 +215,9 @@ class TestExportFunctionality: unique_suffix = str(uuid.uuid4())[:8] # Create product with special characters that might break CSV - product = Product( - product_id=f"SPECIAL_{unique_suffix}", - title=f'Product with quotes and commas {unique_suffix}', # Simplified to avoid CSV escaping issues + product = MarketplaceProduct( + marketplace_product_id=f"SPECIAL_{unique_suffix}", + title=f'MarketplaceProduct with quotes and commas {unique_suffix}', # Simplified to avoid CSV escaping issues description=f"Description with special chars {unique_suffix}", marketplace="Test Market", price="19.99" @@ -226,14 +226,14 @@ class TestExportFunctionality: db.add(product) 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 csv_content = response.content.decode("utf-8") # Verify our test product appears in the 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 "19.99" in csv_content @@ -243,7 +243,7 @@ class TestExportFunctionality: header = next(csv_reader) # 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: assert field in header @@ -274,7 +274,7 @@ class TestExportFunctionality: # This would require access to your service instance to mock properly # 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 assert response.status_code in [200, 400, 500] @@ -293,7 +293,7 @@ class TestExportFunctionality: def test_csv_export_filename_generation(self, client, auth_headers): """Test CSV export generates appropriate filenames based on filters""" # 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 content_disposition = response.headers.get("content-disposition", "") @@ -301,7 +301,7 @@ class TestExportFunctionality: # Test with marketplace filter response = client.get( - "/api/v1/product/export-csv?marketplace=Amazon", + "/api/v1/marketplace/product/export-csv?marketplace=Amazon", headers=auth_headers ) assert response.status_code == 200 diff --git a/tests/integration/api/v1/test_product_endpoints.py b/tests/integration/api/v1/test_marketplace_products_endpoints.py similarity index 66% rename from tests/integration/api/v1/test_product_endpoints.py rename to tests/integration/api/v1/test_marketplace_products_endpoints.py index bd8c1e73..8ce73961 100644 --- a/tests/integration/api/v1/test_product_endpoints.py +++ b/tests/integration/api/v1/test_marketplace_products_endpoints.py @@ -1,49 +1,49 @@ -# tests/integration/api/v1/test_product_endpoints.py +# tests/integration/api/v1/test_marketplace_products_endpoints.py import pytest @pytest.mark.integration @pytest.mark.api @pytest.mark.products -class TestProductsAPI: +class TestMarketplaceProductsAPI: def test_get_products_empty(self, client, auth_headers): """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 data = response.json() assert data["products"] == [] 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""" - response = client.get("/api/v1/product", headers=auth_headers) + response = client.get("/api/v1/marketplace/product", headers=auth_headers) assert response.status_code == 200 data = response.json() assert len(data["products"]) >= 1 assert data["total"] >= 1 # 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 - 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 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 data = response.json() assert data["total"] >= 1 # 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 data = response.json() assert data["total"] >= 1 # 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 data = response.json() assert data["total"] >= 1 @@ -51,8 +51,8 @@ class TestProductsAPI: def test_create_product_success(self, client, auth_headers): """Test creating a new product successfully""" product_data = { - "product_id": "NEW001", - "title": "New Product", + "marketplace_product_id": "NEW001", + "title": "New MarketplaceProduct", "description": "A new product", "price": "15.99", "brand": "NewBrand", @@ -62,19 +62,19 @@ class TestProductsAPI: } 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 data = response.json() - assert data["product_id"] == "NEW001" - assert data["title"] == "New Product" + assert data["marketplace_product_id"] == "NEW001" + assert data["title"] == "New MarketplaceProduct" assert data["marketplace"] == "Amazon" - def test_create_product_duplicate_id_returns_conflict(self, client, auth_headers, test_product): - """Test creating product with duplicate ID returns ProductAlreadyExistsException""" + def test_create_product_duplicate_id_returns_conflict(self, client, auth_headers, test_marketplace_product): + """Test creating product with duplicate ID returns MarketplaceProductAlreadyExistsException""" product_data = { - "product_id": test_product.product_id, + "marketplace_product_id": test_marketplace_product.marketplace_product_id, "title": "Different Title", "description": "A new product", "price": "15.99", @@ -85,65 +85,65 @@ class TestProductsAPI: } 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 data = response.json() assert data["error_code"] == "PRODUCT_ALREADY_EXISTS" assert data["status_code"] == 409 - assert test_product.product_id in data["message"] - assert data["details"]["product_id"] == test_product.product_id + assert test_marketplace_product.marketplace_product_id in data["message"] + assert data["details"]["marketplace_product_id"] == test_marketplace_product.marketplace_product_id def test_create_product_missing_title_validation_error(self, client, auth_headers): """Test creating product without title returns ValidationException""" product_data = { - "product_id": "VALID001", + "marketplace_product_id": "VALID001", "title": "", # Empty title "price": "15.99", } 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 data = response.json() assert data["error_code"] == "PRODUCT_VALIDATION_FAILED" 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" 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_id": "", # Empty product ID + "marketplace_product_id": "", # Empty product ID "title": "Valid Title", "price": "15.99", } 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 data = response.json() assert data["error_code"] == "PRODUCT_VALIDATION_FAILED" assert data["status_code"] == 422 - assert "Product ID is required" in data["message"] - assert data["details"]["field"] == "product_id" + assert "MarketplaceProduct ID is required" in data["message"] + assert data["details"]["field"] == "marketplace_product_id" 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_id": "GTIN001", - "title": "GTIN Test Product", + "marketplace_product_id": "GTIN001", + "title": "GTIN Test MarketplaceProduct", "price": "15.99", "gtin": "invalid_gtin", } 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 @@ -154,15 +154,15 @@ class TestProductsAPI: assert data["details"]["field"] == "gtin" 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_id": "PRICE001", - "title": "Price Test Product", + "marketplace_product_id": "PRICE001", + "title": "Price Test MarketplaceProduct", "price": "invalid_price", } 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 @@ -181,7 +181,7 @@ class TestProductsAPI: } 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 @@ -191,50 +191,50 @@ class TestProductsAPI: assert "Request validation failed" in data["message"] 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""" 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 data = response.json() - assert data["product"]["product_id"] == test_product.product_id - assert data["product"]["title"] == test_product.title + assert data["product"]["marketplace_product_id"] == test_marketplace_product.marketplace_product_id + assert data["product"]["title"] == test_marketplace_product.title def test_get_nonexistent_product_returns_not_found(self, client, auth_headers): - """Test getting nonexistent product returns ProductNotFoundException""" - response = client.get("/api/v1/product/NONEXISTENT", headers=auth_headers) + """Test getting nonexistent product returns MarketplaceProductNotFoundException""" + response = client.get("/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers) assert response.status_code == 404 data = response.json() assert data["error_code"] == "PRODUCT_NOT_FOUND" assert data["status_code"] == 404 assert "NONEXISTENT" in data["message"] - assert data["details"]["resource_type"] == "Product" + assert data["details"]["resource_type"] == "MarketplaceProduct" 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""" - update_data = {"title": "Updated Product Title", "price": "25.99"} + update_data = {"title": "Updated MarketplaceProduct Title", "price": "25.99"} 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, json=update_data, ) assert response.status_code == 200 data = response.json() - assert data["title"] == "Updated Product Title" + assert data["title"] == "Updated MarketplaceProduct Title" assert data["price"] == "25.99" def test_update_nonexistent_product_returns_not_found(self, client, auth_headers): - """Test updating nonexistent product returns ProductNotFoundException""" - update_data = {"title": "Updated Product Title"} + """Test updating nonexistent product returns MarketplaceProductNotFoundException""" + update_data = {"title": "Updated MarketplaceProduct Title"} response = client.put( - "/api/v1/product/NONEXISTENT", + "/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers, json=update_data, ) @@ -244,15 +244,15 @@ class TestProductsAPI: assert data["error_code"] == "PRODUCT_NOT_FOUND" assert data["status_code"] == 404 assert "NONEXISTENT" in data["message"] - assert data["details"]["resource_type"] == "Product" + assert data["details"]["resource_type"] == "MarketplaceProduct" assert data["details"]["identifier"] == "NONEXISTENT" - def test_update_product_empty_title_validation_error(self, client, auth_headers, test_product): - """Test updating product with empty title returns ProductValidationException""" + def test_update_product_empty_title_validation_error(self, client, auth_headers, test_marketplace_product): + """Test updating product with empty title returns MarketplaceProductValidationException""" update_data = {"title": ""} 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, json=update_data, ) @@ -261,15 +261,15 @@ class TestProductsAPI: data = response.json() assert data["error_code"] == "PRODUCT_VALIDATION_FAILED" 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" - def test_update_product_invalid_gtin_data_error(self, client, auth_headers, test_product): - """Test updating product with invalid GTIN returns InvalidProductDataException""" + def test_update_product_invalid_gtin_data_error(self, client, auth_headers, test_marketplace_product): + """Test updating product with invalid GTIN returns InvalidMarketplaceProductDataException""" update_data = {"gtin": "invalid_gtin"} 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, json=update_data, ) @@ -281,12 +281,12 @@ class TestProductsAPI: assert "Invalid GTIN format" in data["message"] assert data["details"]["field"] == "gtin" - def test_update_product_invalid_price_data_error(self, client, auth_headers, test_product): - """Test updating product with invalid price returns InvalidProductDataException""" + def test_update_product_invalid_price_data_error(self, client, auth_headers, test_marketplace_product): + """Test updating product with invalid price returns InvalidMarketplaceProductDataException""" update_data = {"price": "invalid_price"} 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, json=update_data, ) @@ -298,30 +298,30 @@ class TestProductsAPI: assert "Invalid price format" in data["message"] 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""" 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 "deleted successfully" in response.json()["message"] def test_delete_nonexistent_product_returns_not_found(self, client, auth_headers): - """Test deleting nonexistent product returns ProductNotFoundException""" - response = client.delete("/api/v1/product/NONEXISTENT", headers=auth_headers) + """Test deleting nonexistent product returns MarketplaceProductNotFoundException""" + response = client.delete("/api/v1/marketplace/product/NONEXISTENT", headers=auth_headers) assert response.status_code == 404 data = response.json() assert data["error_code"] == "PRODUCT_NOT_FOUND" assert data["status_code"] == 404 assert "NONEXISTENT" in data["message"] - assert data["details"]["resource_type"] == "Product" + assert data["details"]["resource_type"] == "MarketplaceProduct" assert data["details"]["identifier"] == "NONEXISTENT" def test_get_product_without_auth_returns_invalid_token(self, client): """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 data = response.json() @@ -331,7 +331,7 @@ class TestProductsAPI: def test_exception_structure_consistency(self, client, auth_headers): """Test that all exceptions follow the consistent LetzShopException structure""" # 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 data = response.json() diff --git a/tests/integration/api/v1/test_pagination.py b/tests/integration/api/v1/test_pagination.py index 9ee325a9..de735099 100644 --- a/tests/integration/api/v1/test_pagination.py +++ b/tests/integration/api/v1/test_pagination.py @@ -1,7 +1,7 @@ # tests/integration/api/v1/test_pagination.py import pytest -from models.database.product import Product +from models.database.marketplace_product import MarketplaceProduct from models.database.shop import Shop @pytest.mark.integration @@ -18,9 +18,9 @@ class TestPagination: # Create multiple products products = [] for i in range(25): - product = Product( - product_id=f"PAGE{i:03d}_{unique_suffix}", - title=f"Pagination Test Product {i}", + product = MarketplaceProduct( + marketplace_product_id=f"PAGE{i:03d}_{unique_suffix}", + title=f"Pagination Test MarketplaceProduct {i}", marketplace="PaginationTest", ) products.append(product) @@ -29,7 +29,7 @@ class TestPagination: db.commit() # 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 data = response.json() assert len(data["products"]) == 10 @@ -38,21 +38,21 @@ class TestPagination: assert data["limit"] == 10 # 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 data = response.json() assert len(data["products"]) == 10 assert data["skip"] == 10 # 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 data = response.json() 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): """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 data = response.json() @@ -63,7 +63,7 @@ class TestPagination: def test_pagination_boundary_zero_limit_validation_error(self, client, auth_headers): """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 data = response.json() @@ -73,7 +73,7 @@ class TestPagination: def test_pagination_boundary_excessive_limit_validation_error(self, client, auth_headers): """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 data = response.json() @@ -84,7 +84,7 @@ class TestPagination: def test_pagination_beyond_available_records(self, client, auth_headers, db): """Test pagination beyond available records returns empty results""" # 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 data = response.json() @@ -101,9 +101,9 @@ class TestPagination: # Create products with same brand for filtering products = [] for i in range(15): - product = Product( - product_id=f"FILTPAGE{i:03d}_{unique_suffix}", - title=f"Filter Page Product {i}", + product = MarketplaceProduct( + marketplace_product_id=f"FILTPAGE{i:03d}_{unique_suffix}", + title=f"Filter Page MarketplaceProduct {i}", brand="FilterBrand", marketplace="FilterMarket", ) @@ -114,7 +114,7 @@ class TestPagination: # Test first page with filter 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 ) @@ -124,13 +124,13 @@ class TestPagination: assert data["total"] >= 15 # At least our test products # 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: assert product["brand"] == "FilterBrand" # Test second page with same filter 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 ) @@ -141,7 +141,7 @@ class TestPagination: def test_pagination_default_values(self, client, auth_headers): """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 data = response.json() @@ -157,9 +157,9 @@ class TestPagination: # Create products with predictable ordering products = [] for i in range(10): - product = Product( - product_id=f"CONSIST{i:03d}_{unique_suffix}", - title=f"Consistent Product {i:03d}", + product = MarketplaceProduct( + marketplace_product_id=f"CONSIST{i:03d}_{unique_suffix}", + title=f"Consistent MarketplaceProduct {i:03d}", marketplace="ConsistentMarket", ) products.append(product) @@ -168,14 +168,14 @@ class TestPagination: db.commit() # 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 - 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 - 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 - 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 overlap = set(first_page_ids) & set(second_page_ids) @@ -249,7 +249,7 @@ class TestPagination: import 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() assert response.status_code == 200 @@ -262,26 +262,26 @@ class TestPagination: def test_pagination_with_invalid_parameters_types(self, client, auth_headers): """Test pagination with invalid parameter types returns ValidationException""" # 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 data = response.json() assert data["error_code"] == "VALIDATION_ERROR" # 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 data = response.json() assert data["error_code"] == "VALIDATION_ERROR" # 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 def test_empty_dataset_pagination(self, client, auth_headers): """Test pagination behavior with empty dataset""" # Use a filter that should return no results 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 ) @@ -294,7 +294,7 @@ class TestPagination: def test_exception_structure_in_pagination_errors(self, client, auth_headers): """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 data = response.json() diff --git a/tests/integration/api/v1/test_shop_endpoints.py b/tests/integration/api/v1/test_shop_endpoints.py index b89ee563..a2b0fffb 100644 --- a/tests/integration/api/v1/test_shop_endpoints.py +++ b/tests/integration/api/v1/test_shop_endpoints.py @@ -184,7 +184,7 @@ class TestShopsAPI: def test_add_product_to_shop_success(self, client, auth_headers, test_shop, unique_product): """Test adding product to shop successfully""" 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, "is_active": True, "is_featured": False, @@ -205,18 +205,18 @@ class TestShopsAPI: assert data["is_active"] is True 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 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 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""" - # 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 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, } @@ -231,12 +231,12 @@ class TestShopsAPI: assert data["error_code"] == "SHOP_PRODUCT_ALREADY_EXISTS" assert data["status_code"] == 409 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): - """Test adding nonexistent product to shop returns ProductNotFoundException""" + """Test adding nonexistent product to shop returns MarketplaceProductNotFoundException""" 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, } @@ -321,7 +321,7 @@ class TestShopsAPI: # Test adding products (might require verification) product_data = { - "product_id": 1, + "marketplace_product_id": 1, "shop_price": 29.99, } diff --git a/tests/integration/api/v1/test_stats_endpoints.py b/tests/integration/api/v1/test_stats_endpoints.py index 032faabf..25cba339 100644 --- a/tests/integration/api/v1/test_stats_endpoints.py +++ b/tests/integration/api/v1/test_stats_endpoints.py @@ -1,9 +1,11 @@ # tests/integration/api/v1/test_stats_endpoints.py import pytest - +@pytest.mark.integration +@pytest.mark.api +@pytest.mark.stats 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""" response = client.get("/api/v1/stats", headers=auth_headers) @@ -16,7 +18,7 @@ class TestStatsAPI: assert "unique_shops" in data 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""" response = client.get("/api/v1/stats/marketplace", headers=auth_headers) diff --git a/tests/integration/security/test_authentication.py b/tests/integration/security/test_authentication.py index 1ad4cb33..6ae0cd3e 100644 --- a/tests/integration/security/test_authentication.py +++ b/tests/integration/security/test_authentication.py @@ -12,7 +12,7 @@ class TestAuthentication: "/api/v1/admin/users", "/api/v1/admin/shops", "/api/v1/marketplace/import-jobs", - "/api/v1/product", + "/api/v1/marketplace/product", "/api/v1/shop", "/api/v1/stats", "/api/v1/stock", @@ -26,7 +26,7 @@ class TestAuthentication: """Test protected endpoints with invalid token""" 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 def test_debug_direct_bearer(self, client): @@ -49,7 +49,7 @@ class TestAuthentication: # Test 2: Try a regular endpoint that uses get_current_user response2 = client.get( - "/api/v1/product" + "/api/v1/marketplace/product" ) # or any endpoint with get_current_user print(f"Regular endpoint - Status: {response2.status_code}") try: @@ -64,12 +64,12 @@ class TestAuthentication: if hasattr(route, "path") and hasattr(route, "methods"): print(f"{list(route.methods)} {route.path}") - print("\n=== Testing Product Endpoint Variations ===") + print("\n=== Testing MarketplaceProduct Endpoint Variations ===") variations = [ - "/api/v1/product", # Your current attempt - "/api/v1/product/", # With trailing slash - "/api/v1/product/list", # With list endpoint - "/api/v1/product/all", # With all endpoint + "/api/v1/marketplace/product", # Your current attempt + "/api/v1/marketplace/product/", # With trailing slash + "/api/v1/marketplace/product/list", # With list endpoint + "/api/v1/marketplace/product/all", # With all endpoint ] for path in variations: diff --git a/tests/integration/security/test_authorization.py b/tests/integration/security/test_authorization.py index 6a7b392e..4d0cedb2 100644 --- a/tests/integration/security/test_authorization.py +++ b/tests/integration/security/test_authorization.py @@ -27,7 +27,7 @@ class TestAuthorization: def test_regular_endpoints_with_user_access(self, client, auth_headers): """Test that regular users can access non-admin endpoints""" user_endpoints = [ - "/api/v1/product", + "/api/v1/marketplace/product", "/api/v1/stats", "/api/v1/stock", ] diff --git a/tests/integration/security/test_input_validation.py b/tests/integration/security/test_input_validation.py index dfaaec38..1b44136f 100644 --- a/tests/integration/security/test_input_validation.py +++ b/tests/integration/security/test_input_validation.py @@ -11,7 +11,7 @@ class TestInputValidation: malicious_search = "'; DROP TABLE products; --" 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 @@ -25,12 +25,12 @@ class TestInputValidation: # xss_payload = "" # # product_data = { - # "product_id": "XSS_TEST", + # "marketplace_product_id": "XSS_TEST", # "title": 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 # data = response.json() @@ -40,24 +40,24 @@ class TestInputValidation: def test_parameter_validation(self, client, auth_headers): """Test parameter validation for API endpoints""" # 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 - 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 def test_json_validation(self, client, auth_headers): """Test JSON validation for POST requests""" # Test invalid JSON structure 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 # Test missing required fields response = client.post( - "/api/v1/product", + "/api/v1/marketplace/product", 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 diff --git a/tests/integration/tasks/test_background_tasks.py b/tests/integration/tasks/test_background_tasks.py index 31478f10..37fe0547 100644 --- a/tests/integration/tasks/test_background_tasks.py +++ b/tests/integration/tasks/test_background_tasks.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch import pytest 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 diff --git a/tests/integration/workflows/test_integration.py b/tests/integration/workflows/test_integration.py index faab5054..bef1b630 100644 --- a/tests/integration/workflows/test_integration.py +++ b/tests/integration/workflows/test_integration.py @@ -10,8 +10,8 @@ class TestIntegrationFlows: """Test complete product creation and management workflow""" # 1. Create a product product_data = { - "product_id": "FLOW001", - "title": "Integration Test Product", + "marketplace_product_id": "FLOW001", + "title": "Integration Test MarketplaceProduct", "description": "Testing full workflow", "price": "29.99", "brand": "FlowBrand", @@ -21,7 +21,7 @@ class TestIntegrationFlows: } 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 product = response.json() @@ -38,16 +38,16 @@ class TestIntegrationFlows: # 3. Get product with stock info 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 product_detail = response.json() assert product_detail["stock_info"]["total_quantity"] == 50 # 4. Update product - update_data = {"title": "Updated Integration Test Product"} + update_data = {"title": "Updated Integration Test MarketplaceProduct"} response = client.put( - f"/api/v1/product/{product['product_id']}", + f"/api/v1/marketplace/product/{product['marketplace_product_id']}", headers=auth_headers, json=update_data, ) @@ -55,7 +55,7 @@ class TestIntegrationFlows: # 5. Search for product 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.json()["total"] == 1 @@ -75,14 +75,14 @@ class TestIntegrationFlows: # 2. Create a product product_data = { - "product_id": "SHOPFLOW001", - "title": "Shop Flow Product", + "marketplace_product_id": "SHOPFLOW001", + "title": "Shop Flow MarketplaceProduct", "price": "15.99", "marketplace": "ShopFlow", } 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 product = response.json() diff --git a/tests/performance/test_api_performance.py b/tests/performance/test_api_performance.py index 17c9230b..e60f7888 100644 --- a/tests/performance/test_api_performance.py +++ b/tests/performance/test_api_performance.py @@ -3,7 +3,7 @@ import time import pytest -from models.database.product import Product +from models.database.marketplace_product import MarketplaceProduct @pytest.mark.performance @@ -15,9 +15,9 @@ class TestPerformance: # Create multiple products products = [] for i in range(100): - product = Product( - product_id=f"PERF{i:03d}", - title=f"Performance Test Product {i}", + product = MarketplaceProduct( + marketplace_product_id=f"PERF{i:03d}", + title=f"Performance Test MarketplaceProduct {i}", price=f"{i}.99", marketplace="Performance", ) @@ -28,7 +28,7 @@ class TestPerformance: # Time the request 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() assert response.status_code == 200 @@ -40,9 +40,9 @@ class TestPerformance: # Create products with searchable content products = [] for i in range(50): - product = Product( - product_id=f"SEARCH{i:03d}", - title=f"Searchable Product {i}", + product = MarketplaceProduct( + marketplace_product_id=f"SEARCH{i:03d}", + title=f"Searchable MarketplaceProduct {i}", description=f"This is a searchable product number {i}", brand="SearchBrand", marketplace="SearchMarket", @@ -54,7 +54,7 @@ class TestPerformance: # Time search request 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() assert response.status_code == 200 @@ -69,9 +69,9 @@ class TestPerformance: marketplaces = ["Market1", "Market2"] for i in range(200): - product = Product( - product_id=f"COMPLEX{i:03d}", - title=f"Complex Product {i}", + product = MarketplaceProduct( + marketplace_product_id=f"COMPLEX{i:03d}", + title=f"Complex MarketplaceProduct {i}", brand=brands[i % 3], marketplace=marketplaces[i % 2], price=f"{10 + (i % 50)}.99", @@ -85,7 +85,7 @@ class TestPerformance: # Test complex filtering performance start_time = time.time() 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, ) end_time = time.time() @@ -100,9 +100,9 @@ class TestPerformance: # Create a large dataset products = [] for i in range(500): - product = Product( - product_id=f"LARGE{i:04d}", - title=f"Large Dataset Product {i}", + product = MarketplaceProduct( + marketplace_product_id=f"LARGE{i:04d}", + title=f"Large Dataset MarketplaceProduct {i}", marketplace="LargeTest", ) products.append(product) @@ -115,7 +115,7 @@ class TestPerformance: for offset in offsets: start_time = time.time() 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() diff --git a/tests/system/test_error_handling.py b/tests/system/test_error_handling.py index c0633cb0..8cb07bf9 100644 --- a/tests/system/test_error_handling.py +++ b/tests/system/test_error_handling.py @@ -104,13 +104,13 @@ class TestErrorHandling: def test_product_not_found(self, client, auth_headers): """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 data = response.json() assert data["error_code"] == "PRODUCT_NOT_FOUND" assert data["status_code"] == 404 - assert data["details"]["resource_type"] == "Product" + assert data["details"]["resource_type"] == "MarketplaceProduct" assert data["details"]["identifier"] == "NONEXISTENT" def test_duplicate_shop_creation(self, client, auth_headers, test_shop): @@ -128,21 +128,21 @@ class TestErrorHandling: assert data["status_code"] == 409 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""" product_data = { - "product_id": test_product.product_id, - "title": "Duplicate Product", + "marketplace_product_id": test_marketplace_product.marketplace_product_id, + "title": "Duplicate MarketplaceProduct", "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 data = response.json() assert data["error_code"] == "PRODUCT_ALREADY_EXISTS" 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): """Test accessing shop without proper permissions""" @@ -191,12 +191,12 @@ class TestErrorHandling: def test_validation_error_invalid_gtin(self, client, auth_headers): """Test validation error for invalid GTIN format""" product_data = { - "product_id": "TESTPROD001", - "title": "Test Product", + "marketplace_product_id": "TESTPROD001", + "title": "Test MarketplaceProduct", "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 data = response.json() @@ -204,11 +204,11 @@ class TestErrorHandling: assert data["status_code"] == 422 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""" # First create some stock stock_data = { - "gtin": test_product.gtin, + "gtin": test_marketplace_product.gtin, "location": "WAREHOUSE_A", "quantity": 5 } @@ -216,7 +216,7 @@ class TestErrorHandling: # Try to remove more than available using your remove endpoint remove_data = { - "gtin": test_product.gtin, + "gtin": test_marketplace_product.gtin, "location": "WAREHOUSE_A", "quantity": 10 # More than the 5 we added } @@ -345,7 +345,7 @@ class TestErrorHandling: """Test that all error responses follow consistent structure""" test_cases = [ ("/api/v1/shop/NONEXISTENT", 404), - ("/api/v1/product/NONEXISTENT", 404), + ("/api/v1/marketplace/product/NONEXISTENT", 404), ] for endpoint, expected_status in test_cases: diff --git a/tests/test_data/csv/sample_products.csv b/tests/test_data/csv/sample_products.csv index bd8d6733..3d943c17 100644 --- a/tests/test_data/csv/sample_products.csv +++ b/tests/test_data/csv/sample_products.csv @@ -1,4 +1,4 @@ product_id,title,price,currency,brand,marketplace -TEST001,Sample Product 1,19.99,EUR,TestBrand,TestMarket -TEST002,Sample Product 2,29.99,EUR,TestBrand,TestMarket -TEST003,Sample Product 3,39.99,USD,AnotherBrand,TestMarket +TEST001,Sample MarketplaceProduct 1,19.99,EUR,TestBrand,TestMarket +TEST002,Sample MarketplaceProduct 2,29.99,EUR,TestBrand,TestMarket +TEST003,Sample MarketplaceProduct 3,39.99,USD,AnotherBrand,TestMarket diff --git a/tests/unit/middleware/test_middleware.py b/tests/unit/middleware/test_middleware.py index 27c0616d..aa7f5753 100644 --- a/tests/unit/middleware/test_middleware.py +++ b/tests/unit/middleware/test_middleware.py @@ -41,7 +41,8 @@ class TestRateLimiter: # Next request should be blocked assert limiter.allow_request(client_id, max_requests, 3600) is False - +@pytest.mark.unit +@pytest.mark.auth # for auth manager tests class TestAuthManager: def test_password_hashing_and_verification(self): """Test password hashing and verification""" diff --git a/tests/unit/models/test_database_models.py b/tests/unit/models/test_database_models.py index d59b9572..13e77d71 100644 --- a/tests/unit/models/test_database_models.py +++ b/tests/unit/models/test_database_models.py @@ -1,7 +1,7 @@ # tests/unit/models/test_database_models.py import pytest -from models.database.product import Product +from models.database.marketplace_product import MarketplaceProduct from models.database.shop import Shop from models.database.stock import Stock from models.database.user import User @@ -30,10 +30,10 @@ class TestDatabaseModels: assert user.updated_at is not None def test_product_model(self, db): - """Test Product model creation""" - product = Product( - product_id="DB_TEST_001", - title="Database Test Product", + """Test MarketplaceProduct model creation""" + marketplace_product = MarketplaceProduct( + marketplace_product_id="DB_TEST_001", + title="Database Test MarketplaceProduct", description="Testing product model", price="25.99", currency="USD", @@ -44,13 +44,13 @@ class TestDatabaseModels: shop_name="DBTestShop", ) - db.add(product) + db.add(marketplace_product) db.commit() - db.refresh(product) + db.refresh(marketplace_product) - assert product.id is not None - assert product.product_id == "DB_TEST_001" - assert product.created_at is not None + assert marketplace_product.id is not None + assert marketplace_product.marketplace_product_id == "DB_TEST_001" + assert marketplace_product.created_at is not None def test_stock_model(self, db): """Test Stock model creation""" @@ -87,13 +87,13 @@ class TestDatabaseModels: def test_database_constraints(self, db): """Test database constraints and unique indexes""" - # Test unique product_id constraint - product1 = Product(product_id="UNIQUE_001", title="Product 1") + # Test unique marketplace_product_id constraint + product1 = MarketplaceProduct(marketplace_product_id="UNIQUE_001", title="MarketplaceProduct 1") db.add(product1) db.commit() # This should raise an integrity error 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.commit() diff --git a/tests/unit/services/test_admin_service.py b/tests/unit/services/test_admin_service.py index 50bab673..ebaa6861 100644 --- a/tests/unit/services/test_admin_service.py +++ b/tests/unit/services/test_admin_service.py @@ -10,7 +10,7 @@ from app.exceptions import ( AdminOperationException, ) 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 @@ -169,51 +169,51 @@ class TestAdminService: assert exception.error_code == "SHOP_NOT_FOUND" # 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""" result = self.service.get_marketplace_import_jobs(db, skip=0, limit=10) assert len(result) >= 1 # Find our test job in the results 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.marketplace == test_marketplace_job.marketplace - assert test_job.shop_name == test_marketplace_job.shop_name - assert test_job.status == test_marketplace_job.status + assert test_job.marketplace == test_marketplace_import_job.marketplace + assert test_job.shop_name == test_marketplace_import_job.shop_name + 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""" 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 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""" 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 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""" 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 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""" 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) diff --git a/tests/unit/services/test_marketplace_service.py b/tests/unit/services/test_marketplace_service.py index 24a36e96..16c9fb2d 100644 --- a/tests/unit/services/test_marketplace_service.py +++ b/tests/unit/services/test_marketplace_service.py @@ -4,7 +4,7 @@ from datetime import datetime import pytest -from app.exceptions.marketplace import ( +from app.exceptions.marketplace_import_job import ( ImportJobNotFoundException, ImportJobNotOwnedException, ImportJobCannotBeCancelledException, @@ -12,9 +12,9 @@ from app.exceptions.marketplace import ( ) from app.exceptions.shop import ShopNotFoundException, UnauthorizedShopAccessException from app.exceptions.base import ValidationException -from app.services.marketplace_service import MarketplaceService -from models.schemas.marketplace import MarketplaceImportRequest -from models.database.marketplace import MarketplaceImportJob +from app.services.marketplace_import_job_service import MarketplaceImportJobService +from models.schemas.marketplace_import_job import MarketplaceImportJobRequest +from models.database.marketplace_import_job import MarketplaceImportJob from models.database.shop import Shop from models.database.user import User @@ -23,7 +23,7 @@ from models.database.user import User @pytest.mark.marketplace class TestMarketplaceService: def setup_method(self): - self.service = MarketplaceService() + self.service = MarketplaceImportJobService() def test_validate_shop_access_success(self, db, test_shop, test_user): """Test successful shop access validation""" @@ -76,7 +76,7 @@ class TestMarketplaceService: test_shop.owner_id = test_user.id db.commit() - request = MarketplaceImportRequest( + request = MarketplaceImportJobRequest( url="https://example.com/products.csv", marketplace="Amazon", shop_code=test_shop.shop_code, @@ -94,7 +94,7 @@ class TestMarketplaceService: def test_create_import_job_invalid_shop(self, db, test_user): """Test import job creation with invalid shop""" - request = MarketplaceImportRequest( + request = MarketplaceImportJobRequest( url="https://example.com/products.csv", marketplace="Amazon", shop_code="INVALID_SHOP", @@ -114,7 +114,7 @@ class TestMarketplaceService: test_shop.owner_id = other_user.id db.commit() - request = MarketplaceImportRequest( + request = MarketplaceImportJobRequest( url="https://example.com/products.csv", marketplace="Amazon", shop_code=test_shop.shop_code, @@ -127,24 +127,24 @@ class TestMarketplaceService: exception = exc_info.value 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""" 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 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""" 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): """Test getting non-existent import job""" @@ -157,42 +157,42 @@ class TestMarketplaceService: assert "99999" in exception.message 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""" 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 assert exception.error_code == "IMPORT_JOB_NOT_OWNED" 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""" jobs = self.service.get_import_jobs(db, test_user) assert len(jobs) >= 1 - assert any(job.id == test_marketplace_job.id for job in jobs) - assert test_marketplace_job.user_id == test_user.id + assert any(job.id == test_marketplace_import_job.id for job in jobs) + 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""" jobs = self.service.get_import_jobs(db, test_admin) 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( - self, db, test_marketplace_job, test_user + self, db, test_marketplace_import_job, test_user ): """Test getting import jobs with marketplace filter""" 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 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): """Test getting import jobs with pagination""" @@ -228,11 +228,11 @@ class TestMarketplaceService: assert exception.error_code == "VALIDATION_ERROR" 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""" result = self.service.update_job_status( db, - test_marketplace_job.id, + test_marketplace_import_job.id, "completed", imported_count=100, total_processed=100, @@ -260,7 +260,7 @@ class TestMarketplaceService: assert exception.error_code == "VALIDATION_ERROR" 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""" stats = self.service.get_job_stats(db, test_user) @@ -271,7 +271,7 @@ class TestMarketplaceService: assert "failed_jobs" in stats 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""" stats = self.service.get_job_stats(db, test_admin) @@ -287,14 +287,14 @@ class TestMarketplaceService: assert exception.error_code == "VALIDATION_ERROR" 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""" - 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.status == test_marketplace_job.status - assert response.marketplace == test_marketplace_job.marketplace - assert response.imported == (test_marketplace_job.imported_count or 0) + assert response.job_id == test_marketplace_import_job.id + assert response.status == test_marketplace_import_job.status + assert response.marketplace == test_marketplace_import_job.marketplace + assert response.imported == (test_marketplace_import_job.imported_count or 0) def test_cancel_import_job_success(self, db, test_user, test_shop): """Test cancelling a pending import job""" @@ -330,24 +330,24 @@ class TestMarketplaceService: exception = exc_info.value 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""" 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 assert exception.error_code == "IMPORT_JOB_NOT_OWNED" 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""" # Set job status to completed - test_marketplace_job.status = "completed" + test_marketplace_import_job.status = "completed" db.commit() 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 assert exception.error_code == "IMPORT_JOB_CANNOT_BE_CANCELLED" @@ -396,10 +396,10 @@ class TestMarketplaceService: exception = exc_info.value 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""" 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 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): """Test import job creation handles database errors""" - request = MarketplaceImportRequest( + request = MarketplaceImportJobRequest( url="https://example.com/products.csv", marketplace="Amazon", shop_code="TEST_SHOP", diff --git a/tests/unit/services/test_product_service.py b/tests/unit/services/test_product_service.py index 2437a336..7c625688 100644 --- a/tests/unit/services/test_product_service.py +++ b/tests/unit/services/test_product_service.py @@ -1,29 +1,29 @@ # tests/test_product_service.py import pytest -from app.services.product_service import ProductService +from app.services.marketplace_product_service import MarketplaceProductService from app.exceptions import ( - ProductNotFoundException, - ProductAlreadyExistsException, - InvalidProductDataException, - ProductValidationException, + MarketplaceProductNotFoundException, + MarketplaceProductAlreadyExistsException, + InvalidMarketplaceProductDataException, + MarketplaceProductValidationException, ValidationException, ) -from models.schemas.product import ProductCreate, ProductUpdate -from models.database.product import Product +from models.schemas.marketplace_product import MarketplaceProductCreate, MarketplaceProductUpdate +from models.database.marketplace_product import MarketplaceProduct @pytest.mark.unit @pytest.mark.products class TestProductService: def setup_method(self): - self.service = ProductService() + self.service = MarketplaceProductService() def test_create_product_success(self, db): """Test successful product creation with valid data""" - product_data = ProductCreate( - product_id="SVC001", - title="Service Test Product", + product_data = MarketplaceProductCreate( + marketplace_product_id="SVC001", + title="Service Test MarketplaceProduct", gtin="1234567890123", price="19.99", marketplace="TestMarket", @@ -31,22 +31,22 @@ class TestProductService: product = self.service.create_product(db, product_data) - assert product.product_id == "SVC001" - assert product.title == "Service Test Product" + assert product.marketplace_product_id == "SVC001" + assert product.title == "Service Test MarketplaceProduct" assert product.gtin == "1234567890123" assert product.marketplace == "TestMarket" assert product.price == "19.99" # Price is stored as string after processing def test_create_product_invalid_gtin(self, db): - """Test product creation with invalid GTIN raises InvalidProductDataException""" - product_data = ProductCreate( - product_id="SVC002", - title="Service Test Product", + """Test product creation with invalid GTIN raises InvalidMarketplaceProductDataException""" + product_data = MarketplaceProductCreate( + marketplace_product_id="SVC002", + title="Service Test MarketplaceProduct", gtin="invalid_gtin", price="19.99", ) - with pytest.raises(InvalidProductDataException) as exc_info: + with pytest.raises(InvalidMarketplaceProductDataException) as exc_info: self.service.create_product(db, 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" def test_create_product_missing_product_id(self, db): - """Test product creation without product_id raises ProductValidationException""" - product_data = ProductCreate( - product_id="", # Empty product ID - title="Service Test Product", + """Test product creation without marketplace_product_id raises MarketplaceProductValidationException""" + product_data = MarketplaceProductCreate( + marketplace_product_id="", # Empty product ID + title="Service Test MarketplaceProduct", price="19.99", ) - with pytest.raises(ProductValidationException) as exc_info: + with pytest.raises(MarketplaceProductValidationException) as exc_info: self.service.create_product(db, product_data) assert exc_info.value.error_code == "PRODUCT_VALIDATION_FAILED" - assert "Product ID is required" in str(exc_info.value) - assert exc_info.value.details.get("field") == "product_id" + assert "MarketplaceProduct ID is required" in str(exc_info.value) + assert exc_info.value.details.get("field") == "marketplace_product_id" def test_create_product_missing_title(self, db): - """Test product creation without title raises ProductValidationException""" - product_data = ProductCreate( - product_id="SVC003", + """Test product creation without title raises MarketplaceProductValidationException""" + product_data = MarketplaceProductCreate( + marketplace_product_id="SVC003", title="", # Empty title price="19.99", ) - with pytest.raises(ProductValidationException) as exc_info: + with pytest.raises(MarketplaceProductValidationException) as exc_info: self.service.create_product(db, product_data) 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" - def test_create_product_already_exists(self, db, test_product): - """Test creating product with existing ID raises ProductAlreadyExistsException""" - product_data = ProductCreate( - product_id=test_product.product_id, # Use existing product ID - title="Duplicate Product", + def test_create_product_already_exists(self, db, test_marketplace_product): + """Test creating product with existing ID raises MarketplaceProductAlreadyExistsException""" + product_data = MarketplaceProductCreate( + marketplace_product_id=test_marketplace_product.marketplace_product_id, # Use existing product ID + title="Duplicate MarketplaceProduct", price="29.99", ) - with pytest.raises(ProductAlreadyExistsException) as exc_info: + with pytest.raises(MarketplaceProductAlreadyExistsException) as exc_info: self.service.create_product(db, product_data) 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.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): - """Test product creation with invalid price raises InvalidProductDataException""" - product_data = ProductCreate( - product_id="SVC004", - title="Service Test Product", + """Test product creation with invalid price raises InvalidMarketplaceProductDataException""" + product_data = MarketplaceProductCreate( + marketplace_product_id="SVC004", + title="Service Test MarketplaceProduct", price="invalid_price", ) - with pytest.raises(InvalidProductDataException) as exc_info: + with pytest.raises(InvalidMarketplaceProductDataException) as exc_info: self.service.create_product(db, product_data) assert exc_info.value.error_code == "INVALID_PRODUCT_DATA" assert "Invalid price format" in str(exc_info.value) 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""" - 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.title == test_product.title + assert product.marketplace_product_id == test_marketplace_product.marketplace_product_id + assert product.title == test_marketplace_product.title def test_get_product_by_id_or_raise_not_found(self, db): - """Test product retrieval with non-existent ID raises ProductNotFoundException""" - with pytest.raises(ProductNotFoundException) as exc_info: + """Test product retrieval with non-existent ID raises MarketplaceProductNotFoundException""" + with pytest.raises(MarketplaceProductNotFoundException) as exc_info: self.service.get_product_by_id_or_raise(db, "NONEXISTENT") assert exc_info.value.error_code == "PRODUCT_NOT_FOUND" assert "NONEXISTENT" in str(exc_info.value) 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" - 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""" products, total = self.service.get_products_with_filters( - db, brand=test_product.brand + db, brand=test_marketplace_product.brand ) assert total == 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""" products, total = self.service.get_products_with_filters( - db, search="Test Product" + db, search="Test MarketplaceProduct" ) assert total >= 1 assert len(products) >= 1 # 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 - def test_update_product_success(self, db, test_product): + def test_update_product_success(self, db, test_marketplace_product): """Test successful product update""" - update_data = ProductUpdate( - title="Updated Product Title", + update_data = MarketplaceProductUpdate( + title="Updated MarketplaceProduct Title", 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.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): - """Test updating non-existent product raises ProductNotFoundException""" - update_data = ProductUpdate(title="Updated Title") + """Test updating non-existent product raises MarketplaceProductNotFoundException""" + 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) assert exc_info.value.error_code == "PRODUCT_NOT_FOUND" assert "NONEXISTENT" in str(exc_info.value) - def test_update_product_invalid_gtin(self, db, test_product): - """Test updating product with invalid GTIN raises InvalidProductDataException""" - update_data = ProductUpdate(gtin="invalid_gtin") + def test_update_product_invalid_gtin(self, db, test_marketplace_product): + """Test updating product with invalid GTIN raises InvalidMarketplaceProductDataException""" + update_data = MarketplaceProductUpdate(gtin="invalid_gtin") - with pytest.raises(InvalidProductDataException) as exc_info: - self.service.update_product(db, test_product.product_id, update_data) + with pytest.raises(InvalidMarketplaceProductDataException) as exc_info: + self.service.update_product(db, test_marketplace_product.marketplace_product_id, update_data) assert exc_info.value.error_code == "INVALID_PRODUCT_DATA" assert "Invalid GTIN format" in str(exc_info.value) assert exc_info.value.details.get("field") == "gtin" - def test_update_product_empty_title(self, db, test_product): - """Test updating product with empty title raises ProductValidationException""" - update_data = ProductUpdate(title="") + def test_update_product_empty_title(self, db, test_marketplace_product): + """Test updating product with empty title raises MarketplaceProductValidationException""" + update_data = MarketplaceProductUpdate(title="") - with pytest.raises(ProductValidationException) as exc_info: - self.service.update_product(db, test_product.product_id, update_data) + with pytest.raises(MarketplaceProductValidationException) as exc_info: + self.service.update_product(db, test_marketplace_product.marketplace_product_id, update_data) 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" - def test_update_product_invalid_price(self, db, test_product): - """Test updating product with invalid price raises InvalidProductDataException""" - update_data = ProductUpdate(price="invalid_price") + def test_update_product_invalid_price(self, db, test_marketplace_product): + """Test updating product with invalid price raises InvalidMarketplaceProductDataException""" + update_data = MarketplaceProductUpdate(price="invalid_price") - with pytest.raises(InvalidProductDataException) as exc_info: - self.service.update_product(db, test_product.product_id, update_data) + with pytest.raises(InvalidMarketplaceProductDataException) as exc_info: + self.service.update_product(db, test_marketplace_product.marketplace_product_id, update_data) assert exc_info.value.error_code == "INVALID_PRODUCT_DATA" assert "Invalid price format" in str(exc_info.value) 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""" - 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 # 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 def test_delete_product_not_found(self, db): - """Test deleting non-existent product raises ProductNotFoundException""" - with pytest.raises(ProductNotFoundException) as exc_info: + """Test deleting non-existent product raises MarketplaceProductNotFoundException""" + with pytest.raises(MarketplaceProductNotFoundException) as exc_info: self.service.delete_product(db, "NONEXISTENT") assert exc_info.value.error_code == "PRODUCT_NOT_FOUND" 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""" # 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.gtin == product.gtin + assert stock_info.gtin == marketplace_product.gtin assert stock_info.total_quantity > 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""" - 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 - 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""" - 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 def test_product_exists_false(self, db): @@ -257,7 +257,7 @@ class TestProductService: exists = self.service.product_exists(db, "NONEXISTENT") 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""" csv_generator = self.service.generate_csv_export(db) @@ -265,17 +265,17 @@ class TestProductService: csv_lines = list(csv_generator) 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 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""" csv_generator = self.service.generate_csv_export( db, - marketplace=test_product.marketplace + marketplace=test_marketplace_product.marketplace ) csv_lines = list(csv_generator) @@ -283,4 +283,4 @@ class TestProductService: if len(csv_lines) > 1: # If there's data csv_content = "".join(csv_lines) - assert test_product.marketplace in csv_content + assert test_marketplace_product.marketplace in csv_content diff --git a/tests/unit/services/test_shop_service.py b/tests/unit/services/test_shop_service.py index 1fe09d68..11c7f2fc 100644 --- a/tests/unit/services/test_shop_service.py +++ b/tests/unit/services/test_shop_service.py @@ -7,7 +7,7 @@ from app.exceptions import ( ShopAlreadyExistsException, UnauthorizedShopAccessException, InvalidShopDataException, - ProductNotFoundException, + MarketplaceProductNotFoundException, ShopProductAlreadyExistsException, MaxShopsReachedException, ValidationException, @@ -179,7 +179,7 @@ class TestShopService: def test_add_product_to_shop_success(self, db, test_shop, unique_product): """Test successfully adding product to shop""" shop_product_data = ShopProductCreate( - product_id=unique_product.product_id, + marketplace_product_id=unique_product.marketplace_product_id, price="15.99", is_featured=True, stock_quantity=5, @@ -191,25 +191,25 @@ class TestShopService: assert shop_product is not None 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): """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) exception = exc_info.value assert exception.status_code == 404 assert exception.error_code == "PRODUCT_NOT_FOUND" - assert exception.details["resource_type"] == "Product" + assert exception.details["resource_type"] == "MarketplaceProduct" assert exception.details["identifier"] == "NONEXISTENT" def test_add_product_to_shop_already_exists(self, db, test_shop, shop_product): """Test adding product that's already in shop fails""" 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: @@ -219,7 +219,7 @@ class TestShopService: assert exception.status_code == 409 assert exception.error_code == "SHOP_PRODUCT_ALREADY_EXISTS" 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( self, db, test_user, test_shop, shop_product @@ -229,8 +229,8 @@ class TestShopService: assert total >= 1 assert len(products) >= 1 - product_ids = [p.product_id for p in products] - assert shop_product.product_id in product_ids + product_ids = [p.marketplace_product_id for p in products] + assert shop_product.marketplace_product_id in product_ids def test_get_shop_products_access_denied(self, db, test_user, inactive_shop): """Test non-owner cannot access unverified shop products""" @@ -300,7 +300,7 @@ class TestShopService: monkeypatch.setattr(db, "commit", mock_commit) 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: diff --git a/tests/unit/services/test_stats_service.py b/tests/unit/services/test_stats_service.py index 0deeb822..9be98b8e 100644 --- a/tests/unit/services/test_stats_service.py +++ b/tests/unit/services/test_stats_service.py @@ -2,7 +2,7 @@ import pytest 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 @@ -15,7 +15,7 @@ class TestStatsService: """Setup method following the same pattern as other service tests""" 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""" stats = self.service.get_comprehensive_stats(db) @@ -31,13 +31,13 @@ class TestStatsService: assert stats["total_stock_entries"] >= 1 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""" # Create products with different brands, categories, marketplaces additional_products = [ - Product( - product_id="PROD002", - title="Product 2", + MarketplaceProduct( + marketplace_product_id="PROD002", + title="MarketplaceProduct 2", brand="DifferentBrand", google_product_category="Different Category", marketplace="Amazon", @@ -45,9 +45,9 @@ class TestStatsService: price="15.99", currency="EUR", ), - Product( - product_id="PROD003", - title="Product 3", + MarketplaceProduct( + marketplace_product_id="PROD003", + title="MarketplaceProduct 3", brand="ThirdBrand", google_product_category="Third Category", marketplace="eBay", @@ -55,12 +55,12 @@ class TestStatsService: price="25.99", currency="USD", ), - Product( - product_id="PROD004", - title="Product 4", - brand="TestBrand", # Same as test_product + MarketplaceProduct( + marketplace_product_id="PROD004", + title="MarketplaceProduct 4", + brand="TestBrand", # Same as test_marketplace_product google_product_category="Different Category", - marketplace="Letzshop", # Same as test_product + marketplace="Letzshop", # Same as test_marketplace_product shop_name="DifferentShop", price="35.99", currency="EUR", @@ -71,7 +71,7 @@ class TestStatsService: 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_categories"] >= 2 # At least 2 different categories assert stats["unique_marketplaces"] >= 3 # Letzshop, Amazon, eBay @@ -81,9 +81,9 @@ class TestStatsService: """Test comprehensive stats handles null/empty values correctly""" # Create products with null/empty values products_with_nulls = [ - Product( - product_id="NULL001", - title="Product with Nulls", + MarketplaceProduct( + marketplace_product_id="NULL001", + title="MarketplaceProduct with Nulls", brand=None, # Null brand google_product_category=None, # Null category marketplace=None, # Null marketplace @@ -91,9 +91,9 @@ class TestStatsService: price="10.00", currency="EUR", ), - Product( - product_id="EMPTY001", - title="Product with Empty Values", + MarketplaceProduct( + marketplace_product_id="EMPTY001", + title="MarketplaceProduct with Empty Values", brand="", # Empty brand google_product_category="", # Empty category marketplace="", # Empty marketplace @@ -115,7 +115,7 @@ class TestStatsService: assert isinstance(stats["unique_marketplaces"], 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""" stats = self.service.get_marketplace_breakdown_stats(db) @@ -124,7 +124,7 @@ class TestStatsService: # Find our test marketplace in the results 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, ) assert test_marketplace_stat is not None @@ -133,32 +133,32 @@ class TestStatsService: assert test_marketplace_stat["unique_brands"] >= 1 def test_get_marketplace_breakdown_stats_multiple_marketplaces( - self, db, test_product + self, db, test_marketplace_product ): """Test marketplace breakdown with multiple marketplaces""" # Create products for different marketplaces marketplace_products = [ - Product( - product_id="AMAZON001", - title="Amazon Product 1", + MarketplaceProduct( + marketplace_product_id="AMAZON001", + title="Amazon MarketplaceProduct 1", brand="AmazonBrand1", marketplace="Amazon", shop_name="AmazonShop1", price="20.00", currency="EUR", ), - Product( - product_id="AMAZON002", - title="Amazon Product 2", + MarketplaceProduct( + marketplace_product_id="AMAZON002", + title="Amazon MarketplaceProduct 2", brand="AmazonBrand2", marketplace="Amazon", shop_name="AmazonShop2", price="25.00", currency="EUR", ), - Product( - product_id="EBAY001", - title="eBay Product", + MarketplaceProduct( + marketplace_product_id="EBAY001", + title="eBay MarketplaceProduct", brand="eBayBrand", marketplace="eBay", shop_name="eBayShop", @@ -171,11 +171,11 @@ class TestStatsService: 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] assert "Amazon" 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 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): """Test marketplace breakdown excludes products with null marketplaces""" # Create product with null marketplace - null_marketplace_product = Product( - product_id="NULLMARKET001", - title="Product without marketplace", + null_marketplace_product = MarketplaceProduct( + marketplace_product_id="NULLMARKET001", + title="MarketplaceProduct without marketplace", marketplace=None, shop_name="SomeShop", brand="SomeBrand", @@ -212,29 +212,29 @@ class TestStatsService: ] 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""" count = self.service._get_product_count(db) assert count >= 1 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""" # Add products with different brands brand_products = [ - Product( - product_id="BRAND001", - title="Brand Product 1", + MarketplaceProduct( + marketplace_product_id="BRAND001", + title="Brand MarketplaceProduct 1", brand="BrandA", marketplace="Test", shop_name="TestShop", price="10.00", currency="EUR", ), - Product( - product_id="BRAND002", - title="Brand Product 2", + MarketplaceProduct( + marketplace_product_id="BRAND002", + title="Brand MarketplaceProduct 2", brand="BrandB", marketplace="Test", shop_name="TestShop", @@ -249,25 +249,25 @@ class TestStatsService: assert ( 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) - 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""" # Add products with different categories category_products = [ - Product( - product_id="CAT001", - title="Category Product 1", + MarketplaceProduct( + marketplace_product_id="CAT001", + title="Category MarketplaceProduct 1", google_product_category="Electronics", marketplace="Test", shop_name="TestShop", price="10.00", currency="EUR", ), - Product( - product_id="CAT002", - title="Category Product 2", + MarketplaceProduct( + marketplace_product_id="CAT002", + title="Category MarketplaceProduct 2", google_product_category="Books", marketplace="Test", shop_name="TestShop", @@ -283,21 +283,21 @@ class TestStatsService: assert count >= 2 # At least Electronics and Books 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""" # Add products with different marketplaces marketplace_products = [ - Product( - product_id="MARKET001", - title="Marketplace Product 1", + MarketplaceProduct( + marketplace_product_id="MARKET001", + title="Marketplace MarketplaceProduct 1", marketplace="Amazon", shop_name="AmazonShop", price="10.00", currency="EUR", ), - Product( - product_id="MARKET002", - title="Marketplace Product 2", + MarketplaceProduct( + marketplace_product_id="MARKET002", + title="Marketplace MarketplaceProduct 2", marketplace="eBay", shop_name="eBayShop", price="15.00", @@ -309,24 +309,24 @@ class TestStatsService: 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) - 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""" # Add products with different shop names shop_products = [ - Product( - product_id="SHOP001", - title="Shop Product 1", + MarketplaceProduct( + marketplace_product_id="SHOP001", + title="Shop MarketplaceProduct 1", marketplace="Test", shop_name="ShopA", price="10.00", currency="EUR", ), - Product( - product_id="SHOP002", - title="Shop Product 2", + MarketplaceProduct( + marketplace_product_id="SHOP002", + title="Shop MarketplaceProduct 2", marketplace="Test", shop_name="ShopB", price="15.00", @@ -338,7 +338,7 @@ class TestStatsService: 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) def test_get_stock_statistics(self, db, test_stock): @@ -374,27 +374,27 @@ class TestStatsService: """Test getting brands for a specific marketplace""" # Create products for specific marketplace marketplace_products = [ - Product( - product_id="SPECIFIC001", - title="Specific Product 1", + MarketplaceProduct( + marketplace_product_id="SPECIFIC001", + title="Specific MarketplaceProduct 1", brand="SpecificBrand1", marketplace="SpecificMarket", shop_name="SpecificShop1", price="10.00", currency="EUR", ), - Product( - product_id="SPECIFIC002", - title="Specific Product 2", + MarketplaceProduct( + marketplace_product_id="SPECIFIC002", + title="Specific MarketplaceProduct 2", brand="SpecificBrand2", marketplace="SpecificMarket", shop_name="SpecificShop2", price="15.00", currency="EUR", ), - Product( - product_id="OTHER001", - title="Other Product", + MarketplaceProduct( + marketplace_product_id="OTHER001", + title="Other MarketplaceProduct", brand="OtherBrand", marketplace="OtherMarket", shop_name="OtherShop", @@ -416,18 +416,18 @@ class TestStatsService: """Test getting shops for a specific marketplace""" # Create products for specific marketplace marketplace_products = [ - Product( - product_id="SHOPTEST001", - title="Shop Test Product 1", + MarketplaceProduct( + marketplace_product_id="SHOPTEST001", + title="Shop Test MarketplaceProduct 1", brand="TestBrand", marketplace="TestMarketplace", shop_name="TestShop1", price="10.00", currency="EUR", ), - Product( - product_id="SHOPTEST002", - title="Shop Test Product 2", + MarketplaceProduct( + marketplace_product_id="SHOPTEST002", + title="Shop Test MarketplaceProduct 2", brand="TestBrand", marketplace="TestMarketplace", shop_name="TestShop2", @@ -448,25 +448,25 @@ class TestStatsService: """Test getting product count for a specific marketplace""" # Create products for specific marketplace marketplace_products = [ - Product( - product_id="COUNT001", - title="Count Product 1", + MarketplaceProduct( + marketplace_product_id="COUNT001", + title="Count MarketplaceProduct 1", marketplace="CountMarketplace", shop_name="CountShop", price="10.00", currency="EUR", ), - Product( - product_id="COUNT002", - title="Count Product 2", + MarketplaceProduct( + marketplace_product_id="COUNT002", + title="Count MarketplaceProduct 2", marketplace="CountMarketplace", shop_name="CountShop", price="15.00", currency="EUR", ), - Product( - product_id="COUNT003", - title="Count Product 3", + MarketplaceProduct( + marketplace_product_id="COUNT003", + title="Count MarketplaceProduct 3", marketplace="CountMarketplace", shop_name="CountShop", price="20.00", diff --git a/tests/unit/services/test_stock_service.py b/tests/unit/services/test_stock_service.py index c99f3ccb..380a0875 100644 --- a/tests/unit/services/test_stock_service.py +++ b/tests/unit/services/test_stock_service.py @@ -14,7 +14,7 @@ from app.exceptions import ( ValidationException, ) 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 @@ -254,7 +254,7 @@ class TestStockService: # The service prevents negative stock through InsufficientStockException 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.""" result = self.service.get_stock_by_gtin(db, test_stock.gtin) @@ -263,11 +263,11 @@ class TestStockService: assert len(result.locations) == 1 assert result.locations[0].location == test_stock.location 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.""" - unique_gtin = test_product.gtin + unique_gtin = test_marketplace_product.gtin unique_id = str(uuid.uuid4())[:8] # 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 "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.""" result = self.service.get_total_stock(db, test_stock.gtin) assert result["gtin"] == test_stock.gtin 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 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 "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.""" # Set stock to a low value test_stock.quantity = 5 @@ -428,7 +428,7 @@ class TestStockService: assert low_stock_item is not None assert low_stock_item["current_quantity"] == 5 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): """Test getting low stock items with invalid threshold returns InvalidQuantityException.""" @@ -491,9 +491,9 @@ class TestStockService: @pytest.fixture def test_product_with_stock(db, test_stock): """Create a test product that corresponds to the test stock.""" - product = Product( - product_id="STOCK_TEST_001", - title="Stock Test Product", + product = MarketplaceProduct( + marketplace_product_id="STOCK_TEST_001", + title="Stock Test MarketplaceProduct", gtin=test_stock.gtin, price="29.99", brand="TestBrand", diff --git a/tests/unit/utils/test_csv_processor.py b/tests/unit/utils/test_csv_processor.py index 39af417d..5dc728e9 100644 --- a/tests/unit/utils/test_csv_processor.py +++ b/tests/unit/utils/test_csv_processor.py @@ -18,7 +18,7 @@ class TestCSVProcessor: def test_download_csv_encoding_fallback(self, mock_get): """Test CSV download with encoding fallback""" # 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.status_code = 200 @@ -31,7 +31,7 @@ class TestCSVProcessor: mock_get.assert_called_once_with("http://example.com/test.csv", timeout=30) assert isinstance(csv_content, str) - assert "Café Product" in csv_content + assert "Café MarketplaceProduct" in csv_content @patch("requests.get") def test_download_csv_encoding_ignore_fallback(self, mock_get): @@ -41,7 +41,7 @@ class TestCSVProcessor: mock_response.status_code = 200 # Create bytes that will fail most encodings 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_get.return_value = mock_response @@ -51,7 +51,7 @@ class TestCSVProcessor: mock_get.assert_called_once_with("http://example.com/test.csv", timeout=30) assert isinstance(csv_content, str) # 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 @patch("requests.get") @@ -91,15 +91,15 @@ class TestCSVProcessor: def test_parse_csv_content(self): """Test CSV content parsing""" - csv_content = """product_id,title,price,marketplace -TEST001,Test Product 1,10.99,TestMarket -TEST002,Test Product 2,15.99,TestMarket""" + csv_content = """marketplace_product_id,title,price,marketplace +TEST001,Test MarketplaceProduct 1,10.99,TestMarket +TEST002,Test MarketplaceProduct 2,15.99,TestMarket""" df = self.processor.parse_csv(csv_content) assert len(df) == 2 - assert "product_id" in df.columns - assert df.iloc[0]["product_id"] == "TEST001" + assert "marketplace_product_id" in df.columns + assert df.iloc[0]["marketplace_product_id"] == "TEST001" assert df.iloc[1]["price"] == 15.99 @pytest.mark.asyncio @@ -112,8 +112,8 @@ TEST002,Test Product 2,15.99,TestMarket""" mock_download.return_value = "csv_content" mock_df = pd.DataFrame( { - "product_id": ["TEST001", "TEST002"], - "title": ["Product 1", "Product 2"], + "marketplace_product_id": ["TEST001", "TEST002"], + "title": ["MarketplaceProduct 1", "MarketplaceProduct 2"], "price": ["10.99", "15.99"], "marketplace": ["TestMarket", "TestMarket"], "shop_name": ["TestShop", "TestShop"],