shop product refactoring

This commit is contained in:
2025-10-04 23:38:53 +02:00
parent 4d2866af5e
commit 0114b6c46e
68 changed files with 2234 additions and 2236 deletions

View File

@@ -14,9 +14,9 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from middleware.auth import AuthManager
from middleware.rate_limiter import RateLimiter
from models.database.shop import Shop
from models.database.vendor import Vendor
from models.database.user import User
from app.exceptions import (AdminRequiredException,ShopNotFoundException, UnauthorizedShopAccessException)
from app.exceptions import (AdminRequiredException, VendorNotFoundException, UnauthorizedVendorAccessException)
# Set auto_error=False to prevent automatic 403 responses
security = HTTPBearer(auto_error=False)
@@ -43,18 +43,18 @@ def get_current_admin_user(current_user: User = Depends(get_current_user)):
return auth_manager.require_admin(current_user)
def get_user_shop(
shop_code: str,
def get_user_vendor(
vendor_code: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Get shop and verify user ownership."""
shop = db.query(Shop).filter(Shop.shop_code == shop_code.upper()).first()
if not shop:
raise ShopNotFoundException(shop_code)
"""Get vendor and verify user ownership."""
vendor = db.query(Vendor).filter(Vendor.vendor_code == vendor_code.upper()).first()
if not vendor:
raise VendorNotFoundException(vendor_code)
if current_user.role != "admin" and shop.owner_id != current_user.id:
raise UnauthorizedShopAccessException(shop_code, current_user.id)
if current_user.role != "admin" and vendor.owner_id != current_user.id:
raise UnauthorizedVendorAccessException(vendor_code, current_user.id)
return shop
return vendor

View File

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

View File

@@ -4,7 +4,7 @@ Admin endpoints - simplified with service-level exception handling.
This module provides classes and functions for:
- User management (view, toggle status)
- Shop management (view, verify, toggle status)
- Vendor management (view, verify, toggle status)
- Marketplace import job monitoring
- Admin dashboard statistics
"""
@@ -20,7 +20,7 @@ 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_job import MarketplaceImportJobResponse
from models.schemas.shop import ShopListResponse
from models.schemas.vendor import VendorListResponse
from models.database.user import User
router = APIRouter()
@@ -50,37 +50,37 @@ def toggle_user_status(
return {"message": message}
@router.get("/admin/shops", response_model=ShopListResponse)
def get_all_shops_admin(
@router.get("/admin/vendors", response_model=VendorListResponse)
def get_all_vendors_admin(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user),
):
"""Get all shops with admin view (Admin only)."""
shops, total = admin_service.get_all_shops(db=db, skip=skip, limit=limit)
return ShopListResponse(shops=shops, total=total, skip=skip, limit=limit)
"""Get all vendors with admin view (Admin only)."""
vendors, total = admin_service.get_all_vendors(db=db, skip=skip, limit=limit)
return VendorListResponse(vendors=vendors, total=total, skip=skip, limit=limit)
@router.put("/admin/shops/{shop_id}/verify")
def verify_shop(
shop_id: int,
@router.put("/admin/vendors/{vendor_id}/verify")
def verify_vendor(
vendor_id: int,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user),
):
"""Verify/unverify shop (Admin only)."""
shop, message = admin_service.verify_shop(db, shop_id)
"""Verify/unverify vendor (Admin only)."""
vendor, message = admin_service.verify_vendor(db, vendor_id)
return {"message": message}
@router.put("/admin/shops/{shop_id}/status")
def toggle_shop_status(
shop_id: int,
@router.put("/admin/vendors/{vendor_id}/status")
def toggle_vendor_status(
vendor_id: int,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user),
):
"""Toggle shop active status (Admin only)."""
shop, message = admin_service.toggle_shop_status(db, shop_id)
"""Toggle vendor active status (Admin only)."""
vendor, message = admin_service.toggle_vendor_status(db, vendor_id)
return {"message": message}
@@ -89,7 +89,7 @@ def toggle_shop_status(
)
def get_all_marketplace_import_jobs(
marketplace: Optional[str] = Query(None),
shop_name: Optional[str] = Query(None),
vendor_name: Optional[str] = Query(None),
status: Optional[str] = Query(None),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100),
@@ -100,7 +100,7 @@ def get_all_marketplace_import_jobs(
return admin_service.get_marketplace_import_jobs(
db=db,
marketplace=marketplace,
shop_name=shop_name,
vendor_name=vendor_name,
status=status,
skip=skip,
limit=limit,
@@ -116,10 +116,10 @@ def get_user_statistics(
return admin_service.get_user_statistics(db)
@router.get("/admin/stats/shops")
def get_shop_statistics(
@router.get("/admin/stats/vendors")
def get_vendor_statistics(
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_user),
):
"""Get shop statistics for admin dashboard (Admin only)."""
return admin_service.get_shop_statistics(db)
"""Get vendor statistics for admin dashboard (Admin only)."""
return admin_service.get_vendor_statistics(db)

View File

@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
@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"),
vendor_name: Optional[str] = Query(None, description="Filter by vendor name"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
@@ -50,14 +50,14 @@ async def export_csv(
def generate_csv():
return marketplace_product_service.generate_csv_export(
db=db, marketplace=marketplace, shop_name=shop_name
db=db, marketplace=marketplace, vendor_name=vendor_name
)
filename = "marketplace_products_export"
if marketplace:
filename += f"_{marketplace}"
if shop_name:
filename += f"_{shop_name}"
if vendor_name:
filename += f"_{vendor_name}"
filename += ".csv"
return StreamingResponse(
@@ -75,12 +75,12 @@ def get_products(
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"),
vendor_name: Optional[str] = Query(None, description="Filter by vendor 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)."""
"""Get products with advanced filtering including marketplace and vendor (Protected)."""
products, total = marketplace_product_service.get_products_with_filters(
db=db,
skip=skip,
@@ -89,7 +89,7 @@ def get_products(
category=category,
availability=availability,
marketplace=marketplace,
shop_name=shop_name,
vendor_name=vendor_name,
search=search,
)
@@ -166,7 +166,7 @@ async def import_products_from_marketplace(
):
"""Import products from marketplace CSV with background processing (Protected)."""
logger.info(
f"Starting marketplace import: {request.marketplace} -> {request.shop_code} by user {current_user.username}"
f"Starting marketplace import: {request.marketplace} -> {request.vendor_code} by user {current_user.username}"
)
# Create import job through service
@@ -178,7 +178,7 @@ async def import_products_from_marketplace(
import_job.id,
request.url,
request.marketplace,
request.shop_code,
request.vendor_code,
request.batch_size or 1000,
)
@@ -186,9 +186,9 @@ async def import_products_from_marketplace(
job_id=import_job.id,
status="pending",
marketplace=request.marketplace,
shop_code=request.shop_code,
shop_id=import_job.shop_id,
shop_name=import_job.shop_name,
vendor_code=request.vendor_code,
vendor_id=import_job.vendor_id,
vendor_name=import_job.vendor_name,
message=f"Marketplace import started from {request.marketplace}. Check status with "
f"/import-status/{import_job.id}",
)
@@ -212,7 +212,7 @@ def get_marketplace_import_status(
)
def get_marketplace_import_jobs(
marketplace: Optional[str] = Query(None, description="Filter by marketplace"),
shop_name: Optional[str] = Query(None, description="Filter by shop name"),
vendor_name: Optional[str] = Query(None, description="Filter by vendor name"),
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
db: Session = Depends(get_db),
@@ -223,7 +223,7 @@ def get_marketplace_import_jobs(
db=db,
user=current_user,
marketplace=marketplace,
shop_name=shop_name,
vendor_name=vendor_name,
skip=skip,
limit=limit,
)

View File

@@ -1,137 +0,0 @@
# app/api/v1/shop.py
"""
Shop endpoints - simplified with service-level exception handling.
This module provides classes and functions for:
- Shop CRUD operations and management
- Shop product catalog management
- Shop filtering and verification
"""
import logging
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_user, get_user_shop
from app.core.database import get_db
from app.services.shop_service import shop_service
from models.schemas.shop import (ShopCreate, ShopListResponse,ShopResponse)
from models.schemas.product import (ProductCreate,ProductResponse)
from models.database.user import User
router = APIRouter()
logger = logging.getLogger(__name__)
@router.post("/shop", response_model=ShopResponse)
def create_shop(
shop_data: ShopCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Create a new shop (Protected)."""
shop = shop_service.create_shop(
db=db, shop_data=shop_data, current_user=current_user
)
return ShopResponse.model_validate(shop)
@router.get("/shop", response_model=ShopListResponse)
def get_shops(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
active_only: bool = Query(True),
verified_only: bool = Query(False),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get shops with filtering (Protected)."""
shops, total = shop_service.get_shops(
db=db,
current_user=current_user,
skip=skip,
limit=limit,
active_only=active_only,
verified_only=verified_only,
)
return ShopListResponse(shops=shops, total=total, skip=skip, limit=limit)
@router.get("/shop/{shop_code}", response_model=ShopResponse)
def get_shop(
shop_code: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get shop details (Protected)."""
shop = shop_service.get_shop_by_code(
db=db, shop_code=shop_code, current_user=current_user
)
return ShopResponse.model_validate(shop)
@router.post("/shop/{shop_code}/products", response_model=ProductResponse)
def add_product_to_shop(
shop_code: str,
product: ProductCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Add existing product to shop catalog with shop-specific settings (Protected)."""
# Get and verify shop (using existing dependency)
shop = get_user_shop(shop_code, current_user, db)
# Add product to shop
new_product = shop_service.add_product_to_shop(
db=db, shop=shop, product=product
)
# Return with product details
response = ProductResponse.model_validate(new_product)
response.marketplace_product = new_product.marketplace_product
return response
@router.get("/shop/{shop_code}/products")
def get_products(
shop_code: str,
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
active_only: bool = Query(True),
featured_only: bool = Query(False),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get products in shop catalog (Protected)."""
# Get shop
shop = shop_service.get_shop_by_code(
db=db, shop_code=shop_code, current_user=current_user
)
# Get shop products
vendor_products, total = shop_service.get_products(
db=db,
shop=shop,
current_user=current_user,
skip=skip,
limit=limit,
active_only=active_only,
featured_only=featured_only,
)
# Format response
products = []
for vp in vendor_products:
product_response = ProductResponse.model_validate(vp)
product_response.marketplace_product = vp.marketplace_product
products.append(product_response)
return {
"products": products,
"total": total,
"skip": skip,
"limit": limit,
"shop": ShopResponse.model_validate(shop),
}

View File

@@ -36,7 +36,7 @@ def get_stats(
unique_brands=stats_data["unique_brands"],
unique_categories=stats_data["unique_categories"],
unique_marketplaces=stats_data["unique_marketplaces"],
unique_shops=stats_data["unique_shops"],
unique_vendors=stats_data["unique_vendors"],
total_stock_entries=stats_data["total_stock_entries"],
total_inventory_quantity=stats_data["total_inventory_quantity"],
)
@@ -87,7 +87,7 @@ def get_stats(
unique_brands=stats_data["unique_brands"],
unique_categories=stats_data["unique_categories"],
unique_marketplaces=stats_data["unique_marketplaces"],
unique_shops=stats_data["unique_shops"],
unique_vendors=stats_data["unique_vendors"],
total_stock_entries=stats_data["total_stock_entries"],
total_inventory_quantity=stats_data["total_inventory_quantity"],
)
@@ -104,7 +104,7 @@ def get_marketplace_stats(
MarketplaceStatsResponse(
marketplace=stat["marketplace"],
total_products=stat["total_products"],
unique_shops=stat["unique_shops"],
unique_vendors=stat["unique_vendors"],
unique_brands=stat["unique_brands"],
)
for stat in marketplace_stats

137
app/api/v1/vendor.py Normal file
View File

@@ -0,0 +1,137 @@
# app/api/v1/vendor.py
"""
Vendor endpoints - simplified with service-level exception handling.
This module provides classes and functions for:
- Vendor CRUD operations and management
- Vendor product catalog management
- Vendor filtering and verification
"""
import logging
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_user, get_user_vendor
from app.core.database import get_db
from app.services.vendor_service import vendor_service
from models.schemas.vendor import (VendorCreate, VendorListResponse, VendorResponse)
from models.schemas.product import (ProductCreate,ProductResponse)
from models.database.user import User
router = APIRouter()
logger = logging.getLogger(__name__)
@router.post("/vendor", response_model=VendorResponse)
def create_vendor(
vendor_data: VendorCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Create a new vendor (Protected)."""
vendor = vendor_service.create_vendor(
db=db, vendor_data=vendor_data, current_user=current_user
)
return VendorResponse.model_validate(vendor)
@router.get("/vendor", response_model=VendorListResponse)
def get_vendors(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
active_only: bool = Query(True),
verified_only: bool = Query(False),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get vendors with filtering (Protected)."""
vendors, total = vendor_service.get_vendors(
db=db,
current_user=current_user,
skip=skip,
limit=limit,
active_only=active_only,
verified_only=verified_only,
)
return VendorListResponse(vendors=vendors, total=total, skip=skip, limit=limit)
@router.get("/vendor/{vendor_code}", response_model=VendorResponse)
def get_vendor(
vendor_code: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get vendor details (Protected)."""
vendor = vendor_service.get_vendor_by_code(
db=db, vendor_code=vendor_code, current_user=current_user
)
return VendorResponse.model_validate(vendor)
@router.post("/vendor/{vendor_code}/products", response_model=ProductResponse)
def add_product_to_catalog(
vendor_code: str,
product: ProductCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Add existing product to vendor catalog with vendor -specific settings (Protected)."""
# Get and verify vendor (using existing dependency)
vendor = get_user_vendor(vendor_code, current_user, db)
# Add product to vendor
new_product = vendor_service.add_product_to_catalog(
db=db, vendor=vendor, product=product
)
# Return with product details
response = ProductResponse.model_validate(new_product)
response.marketplace_product = new_product.marketplace_product
return response
@router.get("/vendor/{vendor_code}/products")
def get_products(
vendor_code: str,
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
active_only: bool = Query(True),
featured_only: bool = Query(False),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get products in vendor catalog (Protected)."""
# Get vendor
vendor = vendor_service.get_vendor_by_code(
db=db, vendor_code=vendor_code, current_user=current_user
)
# Get vendor products
vendor_products, total = vendor_service.get_products(
db=db,
vendor=vendor,
current_user=current_user,
skip=skip,
limit=limit,
active_only=active_only,
featured_only=featured_only,
)
# Format response
products = []
for vp in vendor_products:
product_response = ProductResponse.model_validate(vp)
product_response.marketplace_product = vp.marketplace_product
products.append(product_response)
return {
"products": products,
"total": total,
"skip": skip,
"limit": limit,
"vendor": VendorResponse.model_validate(vendor),
}

View File

@@ -23,13 +23,13 @@ class Settings(BaseSettings):
# Clean description without HTML
description: str = """
Marketplace product import and management system with multi-shop support.
Marketplace product import and management system with multi-vendor support.
**Features:**
- JWT Authentication with role-based access
- Multi-marketplace product import (CSV processing)
- Inventory management across multiple locations
- Shop management with individual configurations
- Vendor management with individual configurations
**Documentation:** Visit /documentation for complete guides
**API Testing:** Use /docs for interactive API exploration

View File

@@ -1,6 +1,6 @@
# app/exceptions/__init__.py
"""
Custom exception classes for the LetzShop API.
Custom exception classes for the LetzVendor API.
This module provides frontend-friendly exceptions with consistent error codes,
messages, and HTTP status mappings.
@@ -48,15 +48,15 @@ from .stock import (
LocationNotFoundException
)
from .shop import (
ShopNotFoundException,
ShopAlreadyExistsException,
ShopNotActiveException,
ShopNotVerifiedException,
UnauthorizedShopAccessException,
InvalidShopDataException,
MaxShopsReachedException,
ShopValidationException,
from .vendor import (
VendorNotFoundException,
VendorAlreadyExistsException,
VendorNotActiveException,
VendorNotVerifiedException,
UnauthorizedVendorAccessException,
InvalidVendorDataException,
MaxVendorsReachedException,
VendorValidationException,
)
from .product import (
@@ -81,7 +81,7 @@ from .marketplace_import_job import (
from .admin import (
UserNotFoundException,
UserStatusChangeException,
ShopVerificationException,
VendorVerificationException,
AdminOperationException,
CannotModifyAdminException,
CannotModifySelfException,
@@ -127,15 +127,15 @@ __all__ = [
"InvalidQuantityException",
"LocationNotFoundException",
# Shop exceptions
"ShopNotFoundException",
"ShopAlreadyExistsException",
"ShopNotActiveException",
"ShopNotVerifiedException",
"UnauthorizedShopAccessException",
"InvalidShopDataException",
"MaxShopsReachedException",
"ShopValidationException",
# Vendor exceptions
"VendorNotFoundException",
"VendorAlreadyExistsException",
"VendorNotActiveException",
"VendorNotVerifiedException",
"UnauthorizedVendorAccessException",
"InvalidVendorDataException",
"MaxVendorsReachedException",
"VendorValidationException",
# Product exceptions
"ProductAlreadyExistsException",
@@ -157,7 +157,7 @@ __all__ = [
# Admin exceptions
"UserNotFoundException",
"UserStatusChangeException",
"ShopVerificationException",
"VendorVerificationException",
"AdminOperationException",
"CannotModifyAdminException",
"CannotModifySelfException",

View File

@@ -57,17 +57,17 @@ class UserStatusChangeException(BusinessLogicException):
)
class ShopVerificationException(BusinessLogicException):
"""Raised when shop verification fails."""
class VendorVerificationException(BusinessLogicException):
"""Raised when vendor verification fails."""
def __init__(
self,
shop_id: int,
vendor_id: int,
reason: str,
current_verification_status: Optional[bool] = None,
):
details = {
"shop_id": shop_id,
"vendor_id": vendor_id,
"reason": reason,
}
@@ -75,8 +75,8 @@ class ShopVerificationException(BusinessLogicException):
details["current_verification_status"] = current_verification_status
super().__init__(
message=f"Shop verification failed for shop {shop_id}: {reason}",
error_code="SHOP_VERIFICATION_FAILED",
message=f"Vendor verification failed for vendor {vendor_id}: {reason}",
error_code="VENDOR_VERIFICATION_FAILED",
details=details,
)

View File

@@ -1,6 +1,6 @@
# app/exceptions/base.py
"""
Base exception classes for the LetzShop application.
Base exception classes for the LetzVendor application.
This module provides classes and functions for:
- Base exception class with consistent error formatting
@@ -12,7 +12,7 @@ from typing import Any, Dict, Optional
class LetzShopException(Exception):
"""Base exception class for all LetzShop custom exceptions."""
"""Base exception class for all LetzVendor custom exceptions."""
def __init__(
self,
@@ -206,6 +206,6 @@ class ServiceUnavailableException(LetzShopException):
status_code=503,
)
# Note: Domain-specific exceptions like ShopNotFoundException, UserNotFoundException, etc.
# are defined in their respective domain modules (shop.py, admin.py, etc.)
# Note: Domain-specific exceptions like VendorNotFoundException, UserNotFoundException, etc.
# are defined in their respective domain modules (vendor.py, admin.py, etc.)
# to keep domain-specific logic separate from base exceptions.

View File

@@ -26,7 +26,7 @@ def setup_exception_handlers(app):
@app.exception_handler(LetzShopException)
async def custom_exception_handler(request: Request, exc: LetzShopException):
"""Handle custom LetzShop exceptions."""
"""Handle custom LetzVendor exceptions."""
logger.error(
f"Custom exception in {request.method} {request.url}: "

View File

@@ -189,12 +189,12 @@ class InvalidMarketplaceException(ValidationException):
class ImportJobAlreadyProcessingException(BusinessLogicException):
"""Raised when trying to start import while another is already processing."""
def __init__(self, shop_code: str, existing_job_id: int):
def __init__(self, vendor_code: str, existing_job_id: int):
super().__init__(
message=f"Import already in progress for shop '{shop_code}'",
message=f"Import already in progress for vendor '{vendor_code}'",
error_code="IMPORT_JOB_ALREADY_PROCESSING",
details={
"shop_code": shop_code,
"vendor_code": vendor_code,
"existing_job_id": existing_job_id,
},
)

View File

@@ -1,6 +1,6 @@
# app/exceptions/shop.py
# app/exceptions/vendor.py
"""
Shop management specific exceptions.
Vendor management specific exceptions.
"""
from .base import (
@@ -9,26 +9,26 @@ from .base import (
)
class ProductAlreadyExistsException(ConflictException):
"""Raised when trying to add a product that already exists in shop."""
"""Raised when trying to add a product that already exists in vendor."""
def __init__(self, shop_code: str, marketplace_product_id: str):
def __init__(self, vendor_code: str, marketplace_product_id: str):
super().__init__(
message=f"MarketplaceProduct '{marketplace_product_id}' already exists in shop '{shop_code}'",
message=f"MarketplaceProduct '{marketplace_product_id}' already exists in vendor '{vendor_code}'",
error_code="PRODUCT_ALREADY_EXISTS",
details={
"shop_code": shop_code,
"vendor_code": vendor_code,
"marketplace_product_id": marketplace_product_id,
},
)
class ProductNotFoundException(ResourceNotFoundException):
"""Raised when a shop product relationship is not found."""
"""Raised when a vendor product relationship is not found."""
def __init__(self, shop_code: str, marketplace_product_id: str):
def __init__(self, vendor_code: str, marketplace_product_id: str):
super().__init__(
resource_type="ShopProduct",
identifier=f"{shop_code}/{marketplace_product_id}",
message=f"MarketplaceProduct '{marketplace_product_id}' not found in shop '{shop_code}'",
resource_type="Product",
identifier=f"{vendor_code}/{marketplace_product_id}",
message=f"MarketplaceProduct '{marketplace_product_id}' not found in vendor '{vendor_code}'",
error_code="PRODUCT_NOT_FOUND",
)

View File

@@ -1,131 +0,0 @@
# app/exceptions/shop.py
"""
Shop management specific exceptions.
"""
from typing import Any, Dict, Optional
from .base import (
ResourceNotFoundException,
ConflictException,
ValidationException,
AuthorizationException,
BusinessLogicException
)
class ShopNotFoundException(ResourceNotFoundException):
"""Raised when a shop is not found."""
def __init__(self, shop_identifier: str, identifier_type: str = "code"):
if identifier_type.lower() == "id":
message = f"Shop with ID '{shop_identifier}' not found"
else:
message = f"Shop with code '{shop_identifier}' not found"
super().__init__(
resource_type="Shop",
identifier=shop_identifier,
message=message,
error_code="SHOP_NOT_FOUND",
)
class ShopAlreadyExistsException(ConflictException):
"""Raised when trying to create a shop that already exists."""
def __init__(self, shop_code: str):
super().__init__(
message=f"Shop with code '{shop_code}' already exists",
error_code="SHOP_ALREADY_EXISTS",
details={"shop_code": shop_code},
)
class ShopNotActiveException(BusinessLogicException):
"""Raised when trying to perform operations on inactive shop."""
def __init__(self, shop_code: str):
super().__init__(
message=f"Shop '{shop_code}' is not active",
error_code="SHOP_NOT_ACTIVE",
details={"shop_code": shop_code},
)
class ShopNotVerifiedException(BusinessLogicException):
"""Raised when trying to perform operations requiring verified shop."""
def __init__(self, shop_code: str):
super().__init__(
message=f"Shop '{shop_code}' is not verified",
error_code="SHOP_NOT_VERIFIED",
details={"shop_code": shop_code},
)
class UnauthorizedShopAccessException(AuthorizationException):
"""Raised when user tries to access shop they don't own."""
def __init__(self, shop_code: str, user_id: Optional[int] = None):
details = {"shop_code": shop_code}
if user_id:
details["user_id"] = user_id
super().__init__(
message=f"Unauthorized access to shop '{shop_code}'",
error_code="UNAUTHORIZED_SHOP_ACCESS",
details=details,
)
class InvalidShopDataException(ValidationException):
"""Raised when shop data is invalid."""
def __init__(
self,
message: str = "Invalid shop data",
field: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_SHOP_DATA"
class MaxShopsReachedException(BusinessLogicException):
"""Raised when user tries to create more shops than allowed."""
def __init__(self, max_shops: int, user_id: Optional[int] = None):
details = {"max_shops": max_shops}
if user_id:
details["user_id"] = user_id
super().__init__(
message=f"Maximum number of shops reached ({max_shops})",
error_code="MAX_SHOPS_REACHED",
details=details,
)
class ShopValidationException(ValidationException):
"""Raised when shop validation fails."""
def __init__(
self,
message: str = "Shop validation failed",
field: Optional[str] = None,
validation_errors: Optional[Dict[str, str]] = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "SHOP_VALIDATION_FAILED"

131
app/exceptions/vendor.py Normal file
View File

@@ -0,0 +1,131 @@
# app/exceptions/vendor.py
"""
Vendor management specific exceptions.
"""
from typing import Any, Dict, Optional
from .base import (
ResourceNotFoundException,
ConflictException,
ValidationException,
AuthorizationException,
BusinessLogicException
)
class VendorNotFoundException(ResourceNotFoundException):
"""Raised when a vendor is not found."""
def __init__(self, vendor_identifier: str, identifier_type: str = "code"):
if identifier_type.lower() == "id":
message = f"Vendor with ID '{vendor_identifier}' not found"
else:
message = f"Vendor with code '{vendor_identifier}' not found"
super().__init__(
resource_type="Vendor",
identifier=vendor_identifier,
message=message,
error_code="VENDOR_NOT_FOUND",
)
class VendorAlreadyExistsException(ConflictException):
"""Raised when trying to create a vendor that already exists."""
def __init__(self, vendor_code: str):
super().__init__(
message=f"Vendor with code '{vendor_code}' already exists",
error_code="VENDOR_ALREADY_EXISTS",
details={"vendor_code": vendor_code},
)
class VendorNotActiveException(BusinessLogicException):
"""Raised when trying to perform operations on inactive vendor."""
def __init__(self, vendor_code: str):
super().__init__(
message=f"Vendor '{vendor_code}' is not active",
error_code="VENDOR_NOT_ACTIVE",
details={"vendor_code": vendor_code},
)
class VendorNotVerifiedException(BusinessLogicException):
"""Raised when trying to perform operations requiring verified vendor."""
def __init__(self, vendor_code: str):
super().__init__(
message=f"Vendor '{vendor_code}' is not verified",
error_code="VENDOR_NOT_VERIFIED",
details={"vendor_code": vendor_code},
)
class UnauthorizedVendorAccessException(AuthorizationException):
"""Raised when user tries to access vendor they don't own."""
def __init__(self, vendor_code: str, user_id: Optional[int] = None):
details = {"vendor_code": vendor_code}
if user_id:
details["user_id"] = user_id
super().__init__(
message=f"Unauthorized access to vendor '{vendor_code}'",
error_code="UNAUTHORIZED_VENDOR_ACCESS",
details=details,
)
class InvalidVendorDataException(ValidationException):
"""Raised when vendor data is invalid."""
def __init__(
self,
message: str = "Invalid vendor data",
field: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_VENDOR_DATA"
class MaxVendorsReachedException(BusinessLogicException):
"""Raised when user tries to create more vendors than allowed."""
def __init__(self, max_vendors: int, user_id: Optional[int] = None):
details = {"max_vendors": max_vendors}
if user_id:
details["user_id"] = user_id
super().__init__(
message=f"Maximum number of vendors reached ({max_vendors})",
error_code="MAX_VENDORS_REACHED",
details=details,
)
class VendorValidationException(ValidationException):
"""Raised when vendor validation fails."""
def __init__(
self,
message: str = "Vendor validation failed",
field: Optional[str] = None,
validation_errors: Optional[Dict[str, str]] = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "VENDOR_VALIDATION_FAILED"

View File

@@ -1,10 +1,10 @@
# app/services/admin_service.py
"""
Admin service for managing users, shops, and import jobs.
Admin service for managing users, vendors, and import jobs.
This module provides classes and functions for:
- User management and status control
- Shop verification and activation
- Vendor verification and activation
- Marketplace import job monitoring
"""
@@ -18,13 +18,13 @@ from app.exceptions import (
UserNotFoundException,
UserStatusChangeException,
CannotModifySelfException,
ShopNotFoundException,
ShopVerificationException,
VendorNotFoundException,
VendorVerificationException,
AdminOperationException,
)
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.vendor import Vendor
from models.database.user import User
logger = logging.getLogger(__name__)
@@ -101,11 +101,11 @@ class AdminService:
reason="Database update failed"
)
def get_all_shops(
def get_all_vendors(
self, db: Session, skip: int = 0, limit: int = 100
) -> Tuple[List[Shop], int]:
) -> Tuple[List[Vendor], int]:
"""
Get paginated list of all shops with total count.
Get paginated list of all vendors with total count.
Args:
db: Database session
@@ -113,108 +113,108 @@ class AdminService:
limit: Maximum number of records to return
Returns:
Tuple of (shops_list, total_count)
Tuple of (vendors_list, total_count)
"""
try:
total = db.query(Shop).count()
shops = db.query(Shop).offset(skip).limit(limit).all()
return shops, total
total = db.query(Vendor).count()
vendors =db.query(Vendor).offset(skip).limit(limit).all()
return vendors, total
except Exception as e:
logger.error(f"Failed to retrieve shops: {str(e)}")
logger.error(f"Failed to retrieve vendors: {str(e)}")
raise AdminOperationException(
operation="get_all_shops",
operation="get_all_vendors",
reason="Database query failed"
)
def verify_shop(self, db: Session, shop_id: int) -> Tuple[Shop, str]:
def verify_vendor(self, db: Session, vendor_id: int) -> Tuple[Vendor, str]:
"""
Toggle shop verification status.
Toggle vendor verification status.
Args:
db: Database session
shop_id: ID of shop to verify/unverify
vendor_id: ID of vendor to verify/unverify
Returns:
Tuple of (updated_shop, status_message)
Tuple of (updated_vendor, status_message)
Raises:
ShopNotFoundException: If shop not found
ShopVerificationException: If verification fails
VendorNotFoundException: If vendor not found
VendorVerificationException: If verification fails
"""
shop = self._get_shop_by_id_or_raise(db, shop_id)
vendor = self._get_vendor_by_id_or_raise(db, vendor_id)
try:
original_status = shop.is_verified
shop.is_verified = not shop.is_verified
shop.updated_at = datetime.now(timezone.utc)
original_status = vendor.is_verified
vendor.is_verified = not vendor.is_verified
vendor.updated_at = datetime.now(timezone.utc)
# Add verification timestamp if implementing audit trail
if shop.is_verified:
shop.verified_at = datetime.now(timezone.utc)
if vendor.is_verified:
vendor.verified_at = datetime.now(timezone.utc)
db.commit()
db.refresh(shop)
db.refresh(vendor)
status_action = "verified" if shop.is_verified else "unverified"
message = f"Shop {shop.shop_code} has been {status_action}"
status_action = "verified" if vendor.is_verified else "unverified"
message = f"Vendor {vendor.vendor_code} has been {status_action}"
logger.info(message)
return shop, message
return vendor, message
except Exception as e:
db.rollback()
logger.error(f"Failed to verify shop {shop_id}: {str(e)}")
raise ShopVerificationException(
shop_id=shop_id,
logger.error(f"Failed to verify vendor {vendor_id}: {str(e)}")
raise VendorVerificationException(
vendor_id=vendor_id,
reason="Database update failed",
current_verification_status=original_status
)
def toggle_shop_status(self, db: Session, shop_id: int) -> Tuple[Shop, str]:
def toggle_vendor_status(self, db: Session, vendor_id: int) -> Tuple[Vendor, str]:
"""
Toggle shop active status.
Toggle vendor active status.
Args:
db: Database session
shop_id: ID of shop to activate/deactivate
vendor_id: ID of vendor to activate/deactivate
Returns:
Tuple of (updated_shop, status_message)
Tuple of (updated_vendor, status_message)
Raises:
ShopNotFoundException: If shop not found
VendorNotFoundException: If vendor not found
AdminOperationException: If status change fails
"""
shop = self._get_shop_by_id_or_raise(db, shop_id)
vendor = self._get_vendor_by_id_or_raise(db, vendor_id)
try:
original_status = shop.is_active
shop.is_active = not shop.is_active
shop.updated_at = datetime.now(timezone.utc)
original_status = vendor.is_active
vendor.is_active = not vendor.is_active
vendor.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(shop)
db.refresh(vendor)
status_action = "activated" if shop.is_active else "deactivated"
message = f"Shop {shop.shop_code} has been {status_action}"
status_action = "activated" if vendor.is_active else "deactivated"
message = f"Vendor {vendor.vendor_code} has been {status_action}"
logger.info(message)
return shop, message
return vendor , message
except Exception as e:
db.rollback()
logger.error(f"Failed to toggle shop {shop_id} status: {str(e)}")
logger.error(f"Failed to toggle vendor {vendor_id} status: {str(e)}")
raise AdminOperationException(
operation="toggle_shop_status",
operation="toggle_vendor_status",
reason="Database update failed",
target_type="shop",
target_id=str(shop_id)
target_type="vendor ",
target_id=str(vendor_id)
)
def get_marketplace_import_jobs(
self,
db: Session,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None,
vendor_name: Optional[str] = None,
status: Optional[str] = None,
skip: int = 0,
limit: int = 100,
@@ -225,7 +225,7 @@ class AdminService:
Args:
db: Database session
marketplace: Filter by marketplace name (case-insensitive partial match)
shop_name: Filter by shop name (case-insensitive partial match)
vendor_name: Filter by vendor name (case-insensitive partial match)
status: Filter by exact status
skip: Number of records to skip
limit: Maximum number of records to return
@@ -241,8 +241,8 @@ class AdminService:
query = query.filter(
MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%")
)
if shop_name:
query = query.filter(MarketplaceImportJob.shop_name.ilike(f"%{shop_name}%"))
if vendor_name:
query = query.filter(MarketplaceImportJob.vendor_name.ilike(f"%{vendor_name}%"))
if status:
query = query.filter(MarketplaceImportJob.status == status)
@@ -283,23 +283,23 @@ class AdminService:
reason="Database query failed"
)
def get_shop_statistics(self, db: Session) -> dict:
"""Get shop statistics for admin dashboard."""
def get_vendor_statistics(self, db: Session) -> dict:
"""Get vendor statistics for admin dashboard."""
try:
total_shops = db.query(Shop).count()
active_shops = db.query(Shop).filter(Shop.is_active == True).count()
verified_shops = db.query(Shop).filter(Shop.is_verified == True).count()
total_vendors = db.query(Vendor).count()
active_vendors = db.query(Vendor).filter(Vendor.is_active == True).count()
verified_vendors = db.query(Vendor).filter(Vendor.is_verified == True).count()
return {
"total_shops": total_shops,
"active_shops": active_shops,
"verified_shops": verified_shops,
"verification_rate": (verified_shops / total_shops * 100) if total_shops > 0 else 0
"total_vendors": total_vendors,
"active_vendors": active_vendors,
"verified_vendors": verified_vendors,
"verification_rate": (verified_vendors / total_vendors * 100) if total_vendors > 0 else 0
}
except Exception as e:
logger.error(f"Failed to get shop statistics: {str(e)}")
logger.error(f"Failed to get vendor statistics: {str(e)}")
raise AdminOperationException(
operation="get_shop_statistics",
operation="get_vendor_statistics",
reason="Database query failed"
)
@@ -311,12 +311,12 @@ class AdminService:
raise UserNotFoundException(str(user_id))
return user
def _get_shop_by_id_or_raise(self, db: Session, shop_id: int) -> Shop:
"""Get shop by ID or raise ShopNotFoundException."""
shop = db.query(Shop).filter(Shop.id == shop_id).first()
if not shop:
raise ShopNotFoundException(str(shop_id), identifier_type="id")
return shop
def _get_vendor_by_id_or_raise(self, db: Session, vendor_id: int) -> Vendor:
"""Get vendor by ID or raise VendorNotFoundException."""
vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first()
if not vendor :
raise VendorNotFoundException(str(vendor_id), identifier_type="id")
return vendor
def _convert_job_to_response(self, job: MarketplaceImportJob) -> MarketplaceImportJobResponse:
"""Convert database model to response schema."""
@@ -324,9 +324,9 @@ class AdminService:
job_id=job.id,
status=job.status,
marketplace=job.marketplace,
shop_id=job.shop.id if job.shop else None,
shop_code=job.shop.shop_code if job.shop else None,
shop_name=job.shop_name,
vendor_id=job.vendor.id if job.vendor else None,
vendor_code=job.vendor.vendor_code if job.vendor else None,
vendor_name=job.vendor_name,
imported=job.imported_count or 0,
updated=job.updated_count or 0,
total_processed=job.total_processed or 0,

View File

@@ -4,7 +4,7 @@ Marketplace service for managing import jobs and marketplace integrations.
This module provides classes and functions for:
- Import job creation and management
- Shop access validation
- Vendor access validation
- Import job status tracking and updates
"""
@@ -16,8 +16,8 @@ from sqlalchemy import func
from sqlalchemy.orm import Session
from app.exceptions import (
ShopNotFoundException,
UnauthorizedShopAccessException,
VendorNotFoundException,
UnauthorizedVendorAccessException,
ImportJobNotFoundException,
ImportJobNotOwnedException,
ImportJobCannotBeCancelledException,
@@ -27,7 +27,7 @@ from app.exceptions import (
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.vendor import Vendor
from models.database.user import User
logger = logging.getLogger(__name__)
@@ -36,44 +36,44 @@ logger = logging.getLogger(__name__)
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:
def validate_vendor_access(self, db: Session, vendor_code: str, user: User) -> Vendor:
"""
Validate that the shop exists and user has access to it.
Validate that the vendor exists and user has access to it.
Args:
db: Database session
shop_code: Shop code to validate
vendor_code: Vendor code to validate
user: User requesting access
Returns:
Shop object if access is valid
Vendor object if access is valid
Raises:
ShopNotFoundException: If shop doesn't exist
UnauthorizedShopAccessException: If user lacks access
VendorNotFoundException: If vendor doesn't exist
UnauthorizedVendorAccessException: If user lacks access
"""
try:
# Use case-insensitive query to handle both uppercase and lowercase codes
shop = (
db.query(Shop)
.filter(func.upper(Shop.shop_code) == shop_code.upper())
vendor = (
db.query(Vendor)
.filter(func.upper(Vendor.vendor_code) == vendor_code.upper())
.first()
)
if not shop:
raise ShopNotFoundException(shop_code)
if not vendor :
raise VendorNotFoundException(vendor_code)
# Check permissions: admin can import for any shop, others only for their own
if user.role != "admin" and shop.owner_id != user.id:
raise UnauthorizedShopAccessException(shop_code, user.id)
# Check permissions: admin can import for any vendor, others only for their own
if user.role != "admin" and vendor.owner_id != user.id:
raise UnauthorizedVendorAccessException(vendor_code, user.id)
return shop
return vendor
except (ShopNotFoundException, UnauthorizedShopAccessException):
except (VendorNotFoundException, UnauthorizedVendorAccessException):
raise # Re-raise custom exceptions
except Exception as e:
logger.error(f"Error validating shop access: {str(e)}")
raise ValidationException("Failed to validate shop access")
logger.error(f"Error validating vendor access: {str(e)}")
raise ValidationException("Failed to validate vendor access")
def create_import_job(
self, db: Session, request: MarketplaceImportJobRequest, user: User
@@ -90,21 +90,21 @@ class MarketplaceImportJobService:
Created MarketplaceImportJob object
Raises:
ShopNotFoundException: If shop doesn't exist
UnauthorizedShopAccessException: If user lacks shop access
VendorNotFoundException: If vendor doesn't exist
UnauthorizedVendorAccessException: If user lacks vendor access
ValidationException: If job creation fails
"""
try:
# Validate shop access first
shop = self.validate_shop_access(db, request.shop_code, user)
# Validate vendor access first
vendor = self.validate_vendor_access(db, request.vendor_code, user)
# Create marketplace import job record
import_job = MarketplaceImportJob(
status="pending",
source_url=request.url,
marketplace=request.marketplace,
shop_id=shop.id, # Foreign key to shops table
shop_name=shop.shop_name, # Use shop.shop_name (the display name)
vendor_id=vendor.id, # Foreign key to vendors table
vendor_name=vendor.vendor_name, # Use vendor.vendor_name (the display name)
user_id=user.id,
created_at=datetime.now(timezone.utc),
)
@@ -115,12 +115,12 @@ class MarketplaceImportJobService:
logger.info(
f"Created marketplace import job {import_job.id}: "
f"{request.marketplace} -> {shop.shop_name} (shop_code: {shop.shop_code}) by user {user.username}"
f"{request.marketplace} -> {vendor.vendor_name} (vendor_code: {vendor.vendor_code}) by user {user.username}"
)
return import_job
except (ShopNotFoundException, UnauthorizedShopAccessException):
except (VendorNotFoundException, UnauthorizedVendorAccessException):
raise # Re-raise custom exceptions
except Exception as e:
db.rollback()
@@ -172,7 +172,7 @@ class MarketplaceImportJobService:
db: Session,
user: User,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None,
vendor_name: Optional[str] = None,
skip: int = 0,
limit: int = 50,
) -> List[MarketplaceImportJob]:
@@ -183,7 +183,7 @@ class MarketplaceImportJobService:
db: Database session
user: User requesting jobs
marketplace: Optional marketplace filter
shop_name: Optional shop name filter
vendor_name: Optional vendor name filter
skip: Number of records to skip
limit: Maximum records to return
@@ -202,8 +202,8 @@ class MarketplaceImportJobService:
query = query.filter(
MarketplaceImportJob.marketplace.ilike(f"%{marketplace}%")
)
if shop_name:
query = query.filter(MarketplaceImportJob.shop_name.ilike(f"%{shop_name}%"))
if vendor_name:
query = query.filter(MarketplaceImportJob.vendor_name.ilike(f"%{vendor_name}%"))
# Order by creation date (newest first) and apply pagination
jobs = (
@@ -319,11 +319,11 @@ class MarketplaceImportJobService:
job_id=job.id,
status=job.status,
marketplace=job.marketplace,
shop_id=job.shop_id,
shop_code=(
job.shop.shop_code if job.shop else None
vendor_id=job.vendor_id,
vendor_code=(
job.vendor.vendor_code if job.vendor else None
), # Add this optional field via relationship
shop_name=job.shop_name,
vendor_name=job.vendor_name,
imported=job.imported_count or 0,
updated=job.updated_count or 0,
total_processed=job.total_processed or 0,

View File

@@ -135,7 +135,7 @@ class MarketplaceProductService:
category: Optional[str] = None,
availability: Optional[str] = None,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None,
vendor_name: Optional[str] = None,
search: Optional[str] = None,
) -> Tuple[List[MarketplaceProduct], int]:
"""
@@ -149,7 +149,7 @@ class MarketplaceProductService:
category: Category filter
availability: Availability filter
marketplace: Marketplace filter
shop_name: Shop name filter
vendor_name: Vendor name filter
search: Search term
Returns:
@@ -167,16 +167,16 @@ class MarketplaceProductService:
query = query.filter(MarketplaceProduct.availability == availability)
if marketplace:
query = query.filter(MarketplaceProduct.marketplace.ilike(f"%{marketplace}%"))
if shop_name:
query = query.filter(MarketplaceProduct.shop_name.ilike(f"%{shop_name}%"))
if vendor_name:
query = query.filter(MarketplaceProduct.vendor_name.ilike(f"%{vendor_name}%"))
if search:
# Search in title, description, marketplace, and shop_name
# Search in title, description, marketplace, and vendor_name
search_term = f"%{search}%"
query = query.filter(
(MarketplaceProduct.title.ilike(search_term))
| (MarketplaceProduct.description.ilike(search_term))
| (MarketplaceProduct.marketplace.ilike(search_term))
| (MarketplaceProduct.shop_name.ilike(search_term))
| (MarketplaceProduct.vendor_name.ilike(search_term))
)
total = query.count()
@@ -311,7 +311,7 @@ class MarketplaceProductService:
self,
db: Session,
marketplace: Optional[str] = None,
shop_name: Optional[str] = None,
vendor_name: Optional[str] = None,
) -> Generator[str, None, None]:
"""
Generate CSV export with streaming for memory efficiency and proper CSV escaping.
@@ -319,7 +319,7 @@ class MarketplaceProductService:
Args:
db: Database session
marketplace: Optional marketplace filter
shop_name: Optional shop name filter
vendor_name: Optional vendor name filter
Yields:
CSV content as strings with proper escaping
@@ -333,7 +333,7 @@ class MarketplaceProductService:
headers = [
"marketplace_product_id", "title", "description", "link", "image_link",
"availability", "price", "currency", "brand", "gtin",
"marketplace", "shop_name"
"marketplace", "vendor_name"
]
writer.writerow(headers)
yield output.getvalue()
@@ -351,8 +351,8 @@ class MarketplaceProductService:
# Apply marketplace filters
if marketplace:
query = query.filter(MarketplaceProduct.marketplace.ilike(f"%{marketplace}%"))
if shop_name:
query = query.filter(MarketplaceProduct.shop_name.ilike(f"%{shop_name}%"))
if vendor_name:
query = query.filter(MarketplaceProduct.vendor_name.ilike(f"%{vendor_name}%"))
products = query.offset(offset).limit(batch_size).all()
if not products:
@@ -372,7 +372,7 @@ class MarketplaceProductService:
product.brand or "",
product.gtin or "",
product.marketplace or "",
product.shop_name or "",
product.vendor_name or "",
]
writer.writerow(row_data)
@@ -413,7 +413,7 @@ class MarketplaceProductService:
normalized = product_data.copy()
# Trim whitespace from string fields
string_fields = ['marketplace_product_id', 'title', 'description', 'brand', 'marketplace', 'shop_name']
string_fields = ['marketplace_product_id', 'title', 'description', 'brand', 'marketplace', 'vendor_name']
for field in string_fields:
if field in normalized and normalized[field]:
normalized[field] = normalized[field].strip()

View File

@@ -1,359 +0,0 @@
# app/services/shop_service.py
"""
Shop service for managing shop operations and product catalog.
This module provides classes and functions for:
- Shop creation and management
- Shop access control and validation
- Shop product catalog operations
- Shop filtering and search
"""
import logging
from typing import List, Optional, Tuple
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.exceptions import (
ShopNotFoundException,
ShopAlreadyExistsException,
UnauthorizedShopAccessException,
InvalidShopDataException,
MarketplaceProductNotFoundException,
ProductAlreadyExistsException,
MaxShopsReachedException,
ValidationException,
)
from models.schemas.shop import ShopCreate
from models.schemas.product import ProductCreate
from models.database.marketplace_product import MarketplaceProduct
from models.database.shop import Shop
from models.database.product import Product
from models.database.user import User
logger = logging.getLogger(__name__)
class ShopService:
"""Service class for shop operations following the application's service pattern."""
def create_shop(
self, db: Session, shop_data: ShopCreate, current_user: User
) -> Shop:
"""
Create a new shop.
Args:
db: Database session
shop_data: Shop creation data
current_user: User creating the shop
Returns:
Created shop object
Raises:
ShopAlreadyExistsException: If shop code already exists
MaxShopsReachedException: If user has reached maximum shops
InvalidShopDataException: If shop data is invalid
"""
try:
# Validate shop data
self._validate_shop_data(shop_data)
# Check user's shop limit (if applicable)
self._check_shop_limit(db, current_user)
# Normalize shop code to uppercase
normalized_shop_code = shop_data.shop_code.upper()
# Check if shop code already exists (case-insensitive check)
if self._shop_code_exists(db, normalized_shop_code):
raise ShopAlreadyExistsException(normalized_shop_code)
# Create shop with uppercase code
shop_dict = shop_data.model_dump()
shop_dict["shop_code"] = normalized_shop_code # Store as uppercase
new_shop = Shop(
**shop_dict,
owner_id=current_user.id,
is_active=True,
is_verified=(current_user.role == "admin"),
)
db.add(new_shop)
db.commit()
db.refresh(new_shop)
logger.info(
f"New shop created: {new_shop.shop_code} by {current_user.username}"
)
return new_shop
except (ShopAlreadyExistsException, MaxShopsReachedException, InvalidShopDataException):
db.rollback()
raise # Re-raise custom exceptions
except Exception as e:
db.rollback()
logger.error(f"Error creating shop: {str(e)}")
raise ValidationException("Failed to create shop")
def get_shops(
self,
db: Session,
current_user: User,
skip: int = 0,
limit: int = 100,
active_only: bool = True,
verified_only: bool = False,
) -> Tuple[List[Shop], int]:
"""
Get shops with filtering.
Args:
db: Database session
current_user: Current user requesting shops
skip: Number of records to skip
limit: Maximum number of records to return
active_only: Filter for active shops only
verified_only: Filter for verified shops only
Returns:
Tuple of (shops_list, total_count)
"""
try:
query = db.query(Shop)
# Non-admin users can only see active and verified shops, plus their own
if current_user.role != "admin":
query = query.filter(
(Shop.is_active == True)
& ((Shop.is_verified == True) | (Shop.owner_id == current_user.id))
)
else:
# Admin can apply filters
if active_only:
query = query.filter(Shop.is_active == True)
if verified_only:
query = query.filter(Shop.is_verified == True)
total = query.count()
shops = query.offset(skip).limit(limit).all()
return shops, total
except Exception as e:
logger.error(f"Error getting shops: {str(e)}")
raise ValidationException("Failed to retrieve shops")
def get_shop_by_code(self, db: Session, shop_code: str, current_user: User) -> Shop:
"""
Get shop by shop code with access control.
Args:
db: Database session
shop_code: Shop code to find
current_user: Current user requesting the shop
Returns:
Shop object
Raises:
ShopNotFoundException: If shop not found
UnauthorizedShopAccessException: If access denied
"""
try:
shop = (
db.query(Shop)
.filter(func.upper(Shop.shop_code) == shop_code.upper())
.first()
)
if not shop:
raise ShopNotFoundException(shop_code)
# Check access permissions
if not self._can_access_shop(shop, current_user):
raise UnauthorizedShopAccessException(shop_code, current_user.id)
return shop
except (ShopNotFoundException, UnauthorizedShopAccessException):
raise # Re-raise custom exceptions
except Exception as e:
logger.error(f"Error getting shop {shop_code}: {str(e)}")
raise ValidationException("Failed to retrieve shop")
def add_product_to_shop(
self, db: Session, shop: Shop, product: ProductCreate
) -> Product:
"""
Add existing product to shop catalog with shop-specific settings.
Args:
db: Database session
shop: Shop to add product to
product: Shop product data
Returns:
Created ShopProduct object
Raises:
MarketplaceProductNotFoundException: If product not found
ProductAlreadyExistsException: If product already in shop
"""
try:
# Check if product exists
marketplace_product = self._get_product_by_id_or_raise(db, product.marketplace_product_id)
# Check if product already in shop
if self._product_in_shop(db, shop.id, marketplace_product.id):
raise ProductAlreadyExistsException(shop.shop_code, product.marketplace_product_id)
# Create shop-product association
new_product = Product(
shop_id=shop.id,
marketplace_product_id=marketplace_product.id,
**product.model_dump(exclude={"marketplace_product_id"}),
)
db.add(new_product)
db.commit()
db.refresh(new_product)
# Load the product relationship
db.refresh(new_product)
logger.info(f"MarketplaceProduct {product.marketplace_product_id} added to shop {shop.shop_code}")
return new_product
except (MarketplaceProductNotFoundException, ProductAlreadyExistsException):
db.rollback()
raise # Re-raise custom exceptions
except Exception as e:
db.rollback()
logger.error(f"Error adding product to shop: {str(e)}")
raise ValidationException("Failed to add product to shop")
def get_products(
self,
db: Session,
shop: Shop,
current_user: User,
skip: int = 0,
limit: int = 100,
active_only: bool = True,
featured_only: bool = False,
) -> Tuple[List[Product], int]:
"""
Get products in shop catalog with filtering.
Args:
db: Database session
shop: Shop to get products from
current_user: Current user requesting products
skip: Number of records to skip
limit: Maximum number of records to return
active_only: Filter for active products only
featured_only: Filter for featured products only
Returns:
Tuple of (products_list, total_count)
Raises:
UnauthorizedShopAccessException: If shop access denied
"""
try:
# Check access permissions
if not self._can_access_shop(shop, current_user):
raise UnauthorizedShopAccessException(shop.shop_code, current_user.id)
# Query shop products
query = db.query(Product).filter(Product.shop_id == shop.id)
if active_only:
query = query.filter(Product.is_active == True)
if featured_only:
query = query.filter(Product.is_featured == True)
total = query.count()
products = query.offset(skip).limit(limit).all()
return products, total
except UnauthorizedShopAccessException:
raise # Re-raise custom exceptions
except Exception as e:
logger.error(f"Error getting shop products: {str(e)}")
raise ValidationException("Failed to retrieve shop products")
# Private helper methods
def _validate_shop_data(self, shop_data: ShopCreate) -> None:
"""Validate shop creation data."""
if not shop_data.shop_code or not shop_data.shop_code.strip():
raise InvalidShopDataException("Shop code is required", field="shop_code")
if not shop_data.shop_name or not shop_data.shop_name.strip():
raise InvalidShopDataException("Shop name is required", field="shop_name")
# Validate shop code format (alphanumeric, underscores, hyphens)
import re
if not re.match(r'^[A-Za-z0-9_-]+$', shop_data.shop_code):
raise InvalidShopDataException(
"Shop code can only contain letters, numbers, underscores, and hyphens",
field="shop_code"
)
def _check_shop_limit(self, db: Session, user: User) -> None:
"""Check if user has reached maximum shop limit."""
if user.role == "admin":
return # Admins have no limit
user_shop_count = db.query(Shop).filter(Shop.owner_id == user.id).count()
max_shops = 5 # Configure this as needed
if user_shop_count >= max_shops:
raise MaxShopsReachedException(max_shops, user.id)
def _shop_code_exists(self, db: Session, shop_code: str) -> bool:
"""Check if shop code already exists (case-insensitive)."""
return (
db.query(Shop)
.filter(func.upper(Shop.shop_code) == shop_code.upper())
.first() is not None
)
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(MarketplaceProduct).filter(MarketplaceProduct.marketplace_product_id == marketplace_product_id).first()
if not product:
raise MarketplaceProductNotFoundException(marketplace_product_id)
return product
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(Product)
.filter(
Product.shop_id == shop_id,
Product.marketplace_product_id == marketplace_product_id
)
.first() is not None
)
def _can_access_shop(self, shop: Shop, user: User) -> bool:
"""Check if user can access shop."""
# Admins and owners can always access
if user.role == "admin" or shop.owner_id == user.id:
return True
# Others can only access active and verified shops
return shop.is_active and shop.is_verified
def _is_shop_owner(self, shop: Shop, user: User) -> bool:
"""Check if user is shop owner."""
return shop.owner_id == user.id
# Create service instance following the same pattern as other services
shop_service = ShopService()

View File

@@ -44,7 +44,7 @@ class StatsService:
unique_brands = self._get_unique_brands_count(db)
unique_categories = self._get_unique_categories_count(db)
unique_marketplaces = self._get_unique_marketplaces_count(db)
unique_shops = self._get_unique_shops_count(db)
unique_vendors = self._get_unique_vendors_count(db)
# Stock statistics
stock_stats = self._get_stock_statistics(db)
@@ -54,7 +54,7 @@ class StatsService:
"unique_brands": unique_brands,
"unique_categories": unique_categories,
"unique_marketplaces": unique_marketplaces,
"unique_shops": unique_shops,
"unique_vendors": unique_vendors,
"total_stock_entries": stock_stats["total_stock_entries"],
"total_inventory_quantity": stock_stats["total_inventory_quantity"],
}
@@ -87,7 +87,7 @@ class StatsService:
db.query(
MarketplaceProduct.marketplace,
func.count(MarketplaceProduct.id).label("total_products"),
func.count(func.distinct(MarketplaceProduct.shop_name)).label("unique_shops"),
func.count(func.distinct(MarketplaceProduct.vendor_name)).label("unique_vendors"),
func.count(func.distinct(MarketplaceProduct.brand)).label("unique_brands"),
)
.filter(MarketplaceProduct.marketplace.isnot(None))
@@ -99,7 +99,7 @@ class StatsService:
{
"marketplace": stat.marketplace,
"total_products": stat.total_products,
"unique_shops": stat.unique_shops,
"unique_vendors": stat.unique_vendors,
"unique_brands": stat.unique_brands,
}
for stat in marketplace_stats
@@ -130,7 +130,7 @@ class StatsService:
"unique_brands": self._get_unique_brands_count(db),
"unique_categories": self._get_unique_categories_count(db),
"unique_marketplaces": self._get_unique_marketplaces_count(db),
"unique_shops": self._get_unique_shops_count(db),
"unique_vendors": self._get_unique_vendors_count(db),
"products_with_gtin": self._get_products_with_gtin_count(db),
"products_with_images": self._get_products_with_images_count(db),
}
@@ -175,15 +175,15 @@ class StatsService:
product_count = self._get_products_by_marketplace_count(db, marketplace)
brands = self._get_brands_by_marketplace(db, marketplace)
shops = self._get_shops_by_marketplace(db, marketplace)
vendors =self._get_vendors_by_marketplace(db, marketplace)
return {
"marketplace": marketplace,
"total_products": product_count,
"unique_brands": len(brands),
"unique_shops": len(shops),
"unique_vendors": len(vendors),
"brands": brands,
"shops": shops,
"vendors": vendors,
}
except ValidationException:
@@ -227,11 +227,11 @@ class StatsService:
.count()
)
def _get_unique_shops_count(self, db: Session) -> int:
"""Get count of unique shops."""
def _get_unique_vendors_count(self, db: Session) -> int:
"""Get count of unique vendors."""
return (
db.query(MarketplaceProduct.shop_name)
.filter(MarketplaceProduct.shop_name.isnot(None), MarketplaceProduct.shop_name != "")
db.query(MarketplaceProduct.vendor_name)
.filter(MarketplaceProduct.vendor_name.isnot(None), MarketplaceProduct.vendor_name != "")
.distinct()
.count()
)
@@ -276,19 +276,19 @@ class StatsService:
)
return [brand[0] for brand in brands]
def _get_shops_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
"""Get unique shops for a specific marketplace."""
shops = (
db.query(MarketplaceProduct.shop_name)
def _get_vendors_by_marketplace(self, db: Session, marketplace: str) -> List[str]:
"""Get unique vendors for a specific marketplace."""
vendors =(
db.query(MarketplaceProduct.vendor_name)
.filter(
MarketplaceProduct.marketplace == marketplace,
MarketplaceProduct.shop_name.isnot(None),
MarketplaceProduct.shop_name != "",
MarketplaceProduct.vendor_name.isnot(None),
MarketplaceProduct.vendor_name != "",
)
.distinct()
.all()
)
return [shop[0] for shop in shops]
return [vendor [0] for vendor in vendors]
def _get_products_by_marketplace_count(self, db: Session, marketplace: str) -> int:
"""Get product count for a specific marketplace."""

View File

@@ -0,0 +1,359 @@
# app/services/vendor_service.py
"""
Vendor service for managing vendor operations and product catalog.
This module provides classes and functions for:
- Vendor creation and management
- Vendor access control and validation
- Vendor product catalog operations
- Vendor filtering and search
"""
import logging
from typing import List, Optional, Tuple
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.exceptions import (
VendorNotFoundException,
VendorAlreadyExistsException,
UnauthorizedVendorAccessException,
InvalidVendorDataException,
MarketplaceProductNotFoundException,
ProductAlreadyExistsException,
MaxVendorsReachedException,
ValidationException,
)
from models.schemas.vendor import VendorCreate
from models.schemas.product import ProductCreate
from models.database.marketplace_product import MarketplaceProduct
from models.database.vendor import Vendor
from models.database.product import Product
from models.database.user import User
logger = logging.getLogger(__name__)
class VendorService:
"""Service class for vendor operations following the application's service pattern."""
def create_vendor(
self, db: Session, vendor_data: VendorCreate, current_user: User
) -> Vendor:
"""
Create a new vendor.
Args:
db: Database session
vendor_data: Vendor creation data
current_user: User creating the vendor
Returns:
Created vendor object
Raises:
VendorAlreadyExistsException: If vendor code already exists
MaxVendorsReachedException: If user has reached maximum vendors
InvalidVendorDataException: If vendor data is invalid
"""
try:
# Validate vendor data
self._validate_vendor_data(vendor_data)
# Check user's vendor limit (if applicable)
self._check_vendor_limit(db, current_user)
# Normalize vendor code to uppercase
normalized_vendor_code = vendor_data.vendor_code.upper()
# Check if vendor code already exists (case-insensitive check)
if self._vendor_code_exists(db, normalized_vendor_code):
raise VendorAlreadyExistsException(normalized_vendor_code)
# Create vendor with uppercase code
vendor_dict = vendor_data.model_dump()
vendor_dict["vendor_code"] = normalized_vendor_code # Store as uppercase
new_vendor = Vendor(
**vendor_dict,
owner_id=current_user.id,
is_active=True,
is_verified=(current_user.role == "admin"),
)
db.add(new_vendor)
db.commit()
db.refresh(new_vendor)
logger.info(
f"New vendor created: {new_vendor.vendor_code} by {current_user.username}"
)
return new_vendor
except (VendorAlreadyExistsException, MaxVendorsReachedException, InvalidVendorDataException):
db.rollback()
raise # Re-raise custom exceptions
except Exception as e:
db.rollback()
logger.error(f"Error creating vendor : {str(e)}")
raise ValidationException("Failed to create vendor ")
def get_vendors(
self,
db: Session,
current_user: User,
skip: int = 0,
limit: int = 100,
active_only: bool = True,
verified_only: bool = False,
) -> Tuple[List[Vendor], int]:
"""
Get vendors with filtering.
Args:
db: Database session
current_user: Current user requesting vendors
skip: Number of records to skip
limit: Maximum number of records to return
active_only: Filter for active vendors only
verified_only: Filter for verified vendors only
Returns:
Tuple of (vendors_list, total_count)
"""
try:
query = db.query(Vendor)
# Non-admin users can only see active and verified vendors, plus their own
if current_user.role != "admin":
query = query.filter(
(Vendor.is_active == True)
& ((Vendor.is_verified == True) | (Vendor.owner_id == current_user.id))
)
else:
# Admin can apply filters
if active_only:
query = query.filter(Vendor.is_active == True)
if verified_only:
query = query.filter(Vendor.is_verified == True)
total = query.count()
vendors = query.offset(skip).limit(limit).all()
return vendors, total
except Exception as e:
logger.error(f"Error getting vendors: {str(e)}")
raise ValidationException("Failed to retrieve vendors")
def get_vendor_by_code(self, db: Session, vendor_code: str, current_user: User) -> Vendor:
"""
Get vendor by vendor code with access control.
Args:
db: Database session
vendor_code: Vendor code to find
current_user: Current user requesting the vendor
Returns:
Vendor object
Raises:
VendorNotFoundException: If vendor not found
UnauthorizedVendorAccessException: If access denied
"""
try:
vendor = (
db.query(Vendor)
.filter(func.upper(Vendor.vendor_code) == vendor_code.upper())
.first()
)
if not vendor :
raise VendorNotFoundException(vendor_code)
# Check access permissions
if not self._can_access_vendor(vendor, current_user):
raise UnauthorizedVendorAccessException(vendor_code, current_user.id)
return vendor
except (VendorNotFoundException, UnauthorizedVendorAccessException):
raise # Re-raise custom exceptions
except Exception as e:
logger.error(f"Error getting vendor {vendor_code}: {str(e)}")
raise ValidationException("Failed to retrieve vendor ")
def add_product_to_catalog(
self, db: Session, vendor : Vendor, product: ProductCreate
) -> Product:
"""
Add existing product to vendor catalog with vendor -specific settings.
Args:
db: Database session
vendor : Vendor to add product to
product: Vendor product data
Returns:
Created Product object
Raises:
MarketplaceProductNotFoundException: If product not found
ProductAlreadyExistsException: If product already in vendor
"""
try:
# Check if product exists
marketplace_product = self._get_product_by_id_or_raise(db, product.marketplace_product_id)
# Check if product already in vendor
if self._product_in_catalog(db, vendor.id, marketplace_product.id):
raise ProductAlreadyExistsException(vendor.vendor_code, product.marketplace_product_id)
# Create vendor -product association
new_product = Product(
vendor_id=vendor.id,
marketplace_product_id=marketplace_product.id,
**product.model_dump(exclude={"marketplace_product_id"}),
)
db.add(new_product)
db.commit()
db.refresh(new_product)
# Load the product relationship
db.refresh(new_product)
logger.info(f"MarketplaceProduct {product.marketplace_product_id} added to vendor {vendor.vendor_code}")
return new_product
except (MarketplaceProductNotFoundException, ProductAlreadyExistsException):
db.rollback()
raise # Re-raise custom exceptions
except Exception as e:
db.rollback()
logger.error(f"Error adding product to vendor : {str(e)}")
raise ValidationException("Failed to add product to vendor ")
def get_products(
self,
db: Session,
vendor : Vendor,
current_user: User,
skip: int = 0,
limit: int = 100,
active_only: bool = True,
featured_only: bool = False,
) -> Tuple[List[Product], int]:
"""
Get products in vendor catalog with filtering.
Args:
db: Database session
vendor : Vendor to get products from
current_user: Current user requesting products
skip: Number of records to skip
limit: Maximum number of records to return
active_only: Filter for active products only
featured_only: Filter for featured products only
Returns:
Tuple of (products_list, total_count)
Raises:
UnauthorizedVendorAccessException: If vendor access denied
"""
try:
# Check access permissions
if not self._can_access_vendor(vendor, current_user):
raise UnauthorizedVendorAccessException(vendor.vendor_code, current_user.id)
# Query vendor products
query = db.query(Product).filter(Product.vendor_id == vendor.id)
if active_only:
query = query.filter(Product.is_active == True)
if featured_only:
query = query.filter(Product.is_featured == True)
total = query.count()
products = query.offset(skip).limit(limit).all()
return products, total
except UnauthorizedVendorAccessException:
raise # Re-raise custom exceptions
except Exception as e:
logger.error(f"Error getting vendor products: {str(e)}")
raise ValidationException("Failed to retrieve vendor products")
# Private helper methods
def _validate_vendor_data(self, vendor_data: VendorCreate) -> None:
"""Validate vendor creation data."""
if not vendor_data.vendor_code or not vendor_data.vendor_code.strip():
raise InvalidVendorDataException("Vendor code is required", field="vendor_code")
if not vendor_data.vendor_name or not vendor_data.vendor_name.strip():
raise InvalidVendorDataException("Vendor name is required", field="vendor_name")
# Validate vendor code format (alphanumeric, underscores, hyphens)
import re
if not re.match(r'^[A-Za-z0-9_-]+$', vendor_data.vendor_code):
raise InvalidVendorDataException(
"Vendor code can only contain letters, numbers, underscores, and hyphens",
field="vendor_code"
)
def _check_vendor_limit(self, db: Session, user: User) -> None:
"""Check if user has reached maximum vendor limit."""
if user.role == "admin":
return # Admins have no limit
user_vendor_count = db.query(Vendor).filter(Vendor.owner_id == user.id).count()
max_vendors = 5 # Configure this as needed
if user_vendor_count >= max_vendors:
raise MaxVendorsReachedException(max_vendors, user.id)
def _vendor_code_exists(self, db: Session, vendor_code: str) -> bool:
"""Check if vendor code already exists (case-insensitive)."""
return (
db.query(Vendor)
.filter(func.upper(Vendor.vendor_code) == vendor_code.upper())
.first() is not None
)
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(MarketplaceProduct).filter(MarketplaceProduct.marketplace_product_id == marketplace_product_id).first()
if not product:
raise MarketplaceProductNotFoundException(marketplace_product_id)
return product
def _product_in_catalog(self, db: Session, vendor_id: int, marketplace_product_id: int) -> bool:
"""Check if product is already in vendor."""
return (
db.query(Product)
.filter(
Product.vendor_id == vendor_id,
Product.marketplace_product_id == marketplace_product_id
)
.first() is not None
)
def _can_access_vendor(self, vendor : Vendor, user: User) -> bool:
"""Check if user can access vendor."""
# Admins and owners can always access
if user.role == "admin" or vendor.owner_id == user.id:
return True
# Others can only access active and verified vendors
return vendor.is_active and vendor.is_verified
def _is_vendor_owner(self, vendor : Vendor, user: User) -> bool:
"""Check if user is vendor owner."""
return vendor.owner_id == user.id
# Create service instance following the same pattern as other services
vendor_service = VendorService()

View File

@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
async def process_marketplace_import(
job_id: int, url: str, marketplace: str, shop_name: str, batch_size: int = 1000
job_id: int, url: str, marketplace: str, vendor_name: str, batch_size: int = 1000
):
"""Background task to process marketplace CSV import."""
db = SessionLocal()
@@ -44,7 +44,7 @@ async def process_marketplace_import(
# Process CSV
result = await csv_processor.process_marketplace_csv_from_url(
url, marketplace, shop_name, batch_size, db
url, marketplace, vendor_name, batch_size, db
)
# Update job with results

View File

@@ -187,15 +187,15 @@ class CSVProcessor:
return processed_data
async def process_marketplace_csv_from_url(
self, url: str, marketplace: str, shop_name: str, batch_size: int, db: Session
self, url: str, marketplace: str, vendor_name: str, batch_size: int, db: Session
) -> Dict[str, Any]:
"""
Process CSV from URL with marketplace and shop information.
Process CSV from URL with marketplace and vendor information.
Args:
url: URL to the CSV file
marketplace: Name of the marketplace (e.g., 'Letzshop', 'Amazon')
shop_name: Name of the shop
vendor_name: Name of the vendor
batch_size: Number of rows to process in each batch
db: Database session
@@ -203,7 +203,7 @@ class CSVProcessor:
Dictionary with processing results
"""
logger.info(
f"Starting marketplace CSV import from {url} for {marketplace} -> {shop_name}"
f"Starting marketplace CSV import from {url} for {marketplace} -> {vendor_name}"
)
# Download and parse CSV
csv_content = self.download_csv(url)
@@ -220,7 +220,7 @@ class CSVProcessor:
for i in range(0, len(df), batch_size):
batch_df = df.iloc[i : i + batch_size]
batch_result = await self._process_marketplace_batch(
batch_df, marketplace, shop_name, db, i // batch_size + 1
batch_df, marketplace, vendor_name, db, i // batch_size + 1
)
imported += batch_result["imported"]
@@ -235,14 +235,14 @@ class CSVProcessor:
"updated": updated,
"errors": errors,
"marketplace": marketplace,
"shop_name": shop_name,
"vendor_name": vendor_name,
}
async def _process_marketplace_batch(
self,
batch_df: pd.DataFrame,
marketplace: str,
shop_name: str,
vendor_name: str,
db: Session,
batch_num: int,
) -> Dict[str, int]:
@@ -253,7 +253,7 @@ class CSVProcessor:
logger.info(
f"Processing batch {batch_num} with {len(batch_df)} rows for "
f"{marketplace} -> {shop_name}"
f"{marketplace} -> {vendor_name}"
)
for index, row in batch_df.iterrows():
@@ -261,9 +261,9 @@ class CSVProcessor:
# Convert row to dictionary and clean up
product_data = self._clean_row_data(row.to_dict())
# Add marketplace and shop information
# Add marketplace and vendor information
product_data["marketplace"] = marketplace
product_data["shop_name"] = shop_name
product_data["vendor_name"] = vendor_name
# Validate required fields
if not product_data.get("marketplace_product_id"):
@@ -294,7 +294,7 @@ class CSVProcessor:
updated += 1
logger.debug(
f"Updated product {product_data['marketplace_product_id']} for "
f"{marketplace} and shop {shop_name}"
f"{marketplace} and vendor {vendor_name}"
)
else:
# Create new product
@@ -309,7 +309,7 @@ class CSVProcessor:
imported += 1
logger.debug(
f"Imported new product {product_data['marketplace_product_id']} "
f"for {marketplace} and shop {shop_name}"
f"for {marketplace} and vendor {vendor_name}"
)
except Exception as e: