refactor: migrate products and vendor_products to module auto-discovery
- Move admin/products.py to marketplace module as admin_products.py (marketplace product catalog browsing) - Move admin/vendor_products.py to catalog module as admin.py (vendor catalog management) - Move vendor/products.py to catalog module as vendor.py (vendor's own product catalog) - Update marketplace admin router to include products routes - Update catalog module routes/api/__init__.py with lazy imports - Remove legacy imports from admin and vendor API init files All product routes now auto-discovered via module system. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,8 @@ Self-contained modules (auto-discovered from app/modules/{module}/routes/api/adm
|
||||
- billing: Subscription tiers, vendor billing, invoices
|
||||
- inventory: Stock management, inventory tracking
|
||||
- orders: Order management, fulfillment, exceptions
|
||||
- marketplace: Letzshop integration, product sync
|
||||
- marketplace: Letzshop integration, product sync, marketplace products
|
||||
- catalog: Vendor product catalog management
|
||||
- cms: Content pages management
|
||||
- customers: Customer management
|
||||
"""
|
||||
@@ -58,13 +59,11 @@ from . import (
|
||||
notifications,
|
||||
platform_health,
|
||||
platforms,
|
||||
products,
|
||||
settings,
|
||||
subscriptions, # Legacy - will be replaced by billing module router
|
||||
tests,
|
||||
users,
|
||||
vendor_domains,
|
||||
vendor_products,
|
||||
vendor_themes,
|
||||
vendors,
|
||||
)
|
||||
@@ -129,17 +128,6 @@ router.include_router(admin_users.router, tags=["admin-admin-users"])
|
||||
router.include_router(dashboard.router, tags=["admin-dashboard"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Vendor Operations (Product Catalog)
|
||||
# ============================================================================
|
||||
|
||||
# Include marketplace product catalog management endpoints
|
||||
router.include_router(products.router, tags=["admin-marketplace-products"])
|
||||
|
||||
# Include vendor product catalog management endpoints
|
||||
router.include_router(vendor_products.router, tags=["admin-vendor-products"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Platform Administration
|
||||
# ============================================================================
|
||||
|
||||
5
app/api/v1/vendor/__init__.py
vendored
5
app/api/v1/vendor/__init__.py
vendored
@@ -19,6 +19,7 @@ Self-contained modules (auto-discovered from app/modules/{module}/routes/api/ven
|
||||
- inventory: Stock management, inventory tracking
|
||||
- orders: Order management, fulfillment, exceptions
|
||||
- marketplace: Letzshop integration, product sync
|
||||
- catalog: Vendor product catalog management
|
||||
- cms: Content pages management
|
||||
- customers: Customer management
|
||||
"""
|
||||
@@ -41,7 +42,6 @@ from . import (
|
||||
notifications,
|
||||
onboarding,
|
||||
payments,
|
||||
products,
|
||||
profile,
|
||||
settings,
|
||||
team,
|
||||
@@ -71,8 +71,7 @@ router.include_router(email_templates.router, tags=["vendor-email-templates"])
|
||||
router.include_router(email_settings.router, tags=["vendor-email-settings"])
|
||||
router.include_router(onboarding.router, tags=["vendor-onboarding"])
|
||||
|
||||
# Business operations (with prefixes: /products/*, etc.)
|
||||
router.include_router(products.router, tags=["vendor-products"])
|
||||
# Business operations (with prefixes: /invoices/*, /team/*)
|
||||
router.include_router(invoices.router, tags=["vendor-invoices"])
|
||||
router.include_router(team.router, tags=["vendor-team"])
|
||||
|
||||
|
||||
@@ -6,4 +6,20 @@ from app.modules.catalog.routes.api.storefront import router as storefront_route
|
||||
# Tag for OpenAPI documentation
|
||||
STOREFRONT_TAG = "Catalog (Storefront)"
|
||||
|
||||
__all__ = ["storefront_router", "STOREFRONT_TAG"]
|
||||
__all__ = [
|
||||
"storefront_router",
|
||||
"STOREFRONT_TAG",
|
||||
"admin_router",
|
||||
"vendor_router",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy import routers to avoid circular dependencies."""
|
||||
if name == "admin_router":
|
||||
from app.modules.catalog.routes.api.admin import admin_router
|
||||
return admin_router
|
||||
elif name == "vendor_router":
|
||||
from app.modules.catalog.routes.api.vendor import vendor_router
|
||||
return vendor_router
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
# app/api/v1/admin/vendor_products.py
|
||||
# app/modules/catalog/routes/api/admin.py
|
||||
"""
|
||||
Admin vendor product catalog endpoints.
|
||||
|
||||
Provides management of vendor-specific product catalogs:
|
||||
- Browse products in vendor catalogs
|
||||
- View product details with override info
|
||||
- Remove products from catalog
|
||||
- Create/update/remove products from catalog
|
||||
|
||||
Architecture Notes:
|
||||
- All Pydantic schemas are defined in models/schema/vendor_product.py
|
||||
- Business logic is delegated to vendor_product_service
|
||||
All routes require module access control for the 'catalog' module.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -17,7 +15,7 @@ import logging
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.api.deps import get_current_admin_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.services.subscription_service import subscription_service
|
||||
from app.services.vendor_product_service import vendor_product_service
|
||||
@@ -35,7 +33,10 @@ from app.modules.catalog.schemas import (
|
||||
VendorProductUpdate,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/vendor-products")
|
||||
admin_router = APIRouter(
|
||||
prefix="/vendor-products",
|
||||
dependencies=[Depends(require_module_access("catalog"))],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -44,7 +45,7 @@ logger = logging.getLogger(__name__)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("", response_model=VendorProductListResponse)
|
||||
@admin_router.get("", response_model=VendorProductListResponse)
|
||||
def get_vendor_products(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
@@ -81,7 +82,7 @@ def get_vendor_products(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats", response_model=VendorProductStats)
|
||||
@admin_router.get("/stats", response_model=VendorProductStats)
|
||||
def get_vendor_product_stats(
|
||||
vendor_id: int | None = Query(None, description="Filter stats by vendor ID"),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -92,7 +93,7 @@ def get_vendor_product_stats(
|
||||
return VendorProductStats(**stats)
|
||||
|
||||
|
||||
@router.get("/vendors", response_model=CatalogVendorsResponse)
|
||||
@admin_router.get("/vendors", response_model=CatalogVendorsResponse)
|
||||
def get_catalog_vendors(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
@@ -102,7 +103,7 @@ def get_catalog_vendors(
|
||||
return CatalogVendorsResponse(vendors=[CatalogVendor(**v) for v in vendors])
|
||||
|
||||
|
||||
@router.get("/{product_id}", response_model=VendorProductDetail)
|
||||
@admin_router.get("/{product_id}", response_model=VendorProductDetail)
|
||||
def get_vendor_product_detail(
|
||||
product_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -113,7 +114,7 @@ def get_vendor_product_detail(
|
||||
return VendorProductDetail(**product)
|
||||
|
||||
|
||||
@router.post("", response_model=VendorProductCreateResponse)
|
||||
@admin_router.post("", response_model=VendorProductCreateResponse)
|
||||
def create_vendor_product(
|
||||
data: VendorProductCreate,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -124,13 +125,13 @@ def create_vendor_product(
|
||||
subscription_service.check_product_limit(db, data.vendor_id)
|
||||
|
||||
product = vendor_product_service.create_product(db, data.model_dump())
|
||||
db.commit() # ✅ ARCH: Commit at API level for transaction control
|
||||
db.commit()
|
||||
return VendorProductCreateResponse(
|
||||
id=product.id, message="Product created successfully"
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/{product_id}", response_model=VendorProductDetail)
|
||||
@admin_router.patch("/{product_id}", response_model=VendorProductDetail)
|
||||
def update_vendor_product(
|
||||
product_id: int,
|
||||
data: VendorProductUpdate,
|
||||
@@ -141,13 +142,13 @@ def update_vendor_product(
|
||||
# Only include fields that were explicitly set
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
vendor_product_service.update_product(db, product_id, update_data)
|
||||
db.commit() # ✅ ARCH: Commit at API level for transaction control
|
||||
db.commit()
|
||||
# Return the updated product detail
|
||||
product = vendor_product_service.get_product_detail(db, product_id)
|
||||
return VendorProductDetail(**product)
|
||||
|
||||
|
||||
@router.delete("/{product_id}", response_model=RemoveProductResponse)
|
||||
@admin_router.delete("/{product_id}", response_model=RemoveProductResponse)
|
||||
def remove_vendor_product(
|
||||
product_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -155,5 +156,5 @@ def remove_vendor_product(
|
||||
):
|
||||
"""Remove a product from vendor catalog."""
|
||||
result = vendor_product_service.remove_product(db, product_id)
|
||||
db.commit() # ✅ ARCH: Commit at API level for transaction control
|
||||
db.commit()
|
||||
return RemoveProductResponse(**result)
|
||||
@@ -1,9 +1,11 @@
|
||||
# app/api/v1/vendor/products.py
|
||||
# app/modules/catalog/routes/api/vendor.py
|
||||
"""
|
||||
Vendor product catalog management endpoints.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
|
||||
The get_current_vendor_api dependency guarantees token_vendor_id is present.
|
||||
|
||||
All routes require module access control for the 'catalog' module.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -11,7 +13,7 @@ import logging
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.api.deps import get_current_vendor_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.services.product_service import product_service
|
||||
from app.services.subscription_service import subscription_service
|
||||
@@ -25,17 +27,18 @@ from app.modules.catalog.schemas import (
|
||||
ProductResponse,
|
||||
ProductToggleResponse,
|
||||
ProductUpdate,
|
||||
)
|
||||
from app.modules.catalog.schemas import (
|
||||
VendorDirectProductCreate,
|
||||
VendorProductCreateResponse,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/products")
|
||||
vendor_router = APIRouter(
|
||||
prefix="/products",
|
||||
dependencies=[Depends(require_module_access("catalog"))],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("", response_model=ProductListResponse)
|
||||
@vendor_router.get("", response_model=ProductListResponse)
|
||||
def get_vendor_products(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
@@ -70,7 +73,7 @@ def get_vendor_products(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{product_id}", response_model=ProductDetailResponse)
|
||||
@vendor_router.get("/{product_id}", response_model=ProductDetailResponse)
|
||||
def get_product_details(
|
||||
product_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
@@ -84,7 +87,7 @@ def get_product_details(
|
||||
return ProductDetailResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.post("", response_model=ProductResponse)
|
||||
@vendor_router.post("", response_model=ProductResponse)
|
||||
def add_product_to_catalog(
|
||||
product_data: ProductCreate,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
@@ -111,7 +114,7 @@ def add_product_to_catalog(
|
||||
return ProductResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.post("/create", response_model=VendorProductCreateResponse)
|
||||
@vendor_router.post("/create", response_model=VendorProductCreateResponse)
|
||||
def create_product_direct(
|
||||
product_data: VendorDirectProductCreate,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
@@ -155,7 +158,7 @@ def create_product_direct(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{product_id}", response_model=ProductResponse)
|
||||
@vendor_router.put("/{product_id}", response_model=ProductResponse)
|
||||
def update_product(
|
||||
product_id: int,
|
||||
product_data: ProductUpdate,
|
||||
@@ -179,7 +182,7 @@ def update_product(
|
||||
return ProductResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.delete("/{product_id}", response_model=ProductDeleteResponse)
|
||||
@vendor_router.delete("/{product_id}", response_model=ProductDeleteResponse)
|
||||
def remove_product_from_catalog(
|
||||
product_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
@@ -199,7 +202,7 @@ def remove_product_from_catalog(
|
||||
return ProductDeleteResponse(message=f"Product {product_id} removed from catalog")
|
||||
|
||||
|
||||
@router.post("/from-import/{marketplace_product_id}", response_model=ProductResponse)
|
||||
@vendor_router.post("/from-import/{marketplace_product_id}", response_model=ProductResponse)
|
||||
def publish_from_marketplace(
|
||||
marketplace_product_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
@@ -230,7 +233,7 @@ def publish_from_marketplace(
|
||||
return ProductResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.put("/{product_id}/toggle-active", response_model=ProductToggleResponse)
|
||||
@vendor_router.put("/{product_id}/toggle-active", response_model=ProductToggleResponse)
|
||||
def toggle_product_active(
|
||||
product_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
@@ -253,7 +256,7 @@ def toggle_product_active(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{product_id}/toggle-featured", response_model=ProductToggleResponse)
|
||||
@vendor_router.put("/{product_id}/toggle-featured", response_model=ProductToggleResponse)
|
||||
def toggle_product_featured(
|
||||
product_id: int,
|
||||
current_user: UserContext = Depends(get_current_vendor_api),
|
||||
@@ -2,43 +2,33 @@
|
||||
"""
|
||||
Marketplace module admin routes.
|
||||
|
||||
This module wraps the existing admin marketplace routes and adds
|
||||
module-based access control. Routes are re-exported from the
|
||||
original location with the module access dependency.
|
||||
This module aggregates all marketplace admin routers into a single router
|
||||
for auto-discovery. Routes are defined in dedicated files with module-based
|
||||
access control.
|
||||
|
||||
Includes:
|
||||
- /marketplace/* - Marketplace monitoring
|
||||
- /products/* - Marketplace product catalog browsing
|
||||
- /marketplace-import-jobs/* - Marketplace import job monitoring
|
||||
- /letzshop/* - Letzshop integration
|
||||
"""
|
||||
|
||||
import importlib
|
||||
from fastapi import APIRouter
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from .admin_products import admin_products_router
|
||||
from .admin_marketplace import admin_marketplace_router
|
||||
from .admin_letzshop import admin_letzshop_router
|
||||
|
||||
from app.api.deps import require_module_access
|
||||
# Create aggregate router for auto-discovery
|
||||
# The router is named 'admin_router' for auto-discovery compatibility
|
||||
admin_router = APIRouter()
|
||||
|
||||
# Import original routers using importlib to avoid circular imports
|
||||
# (direct import triggers app.api.v1.admin.__init__.py which imports us)
|
||||
_marketplace_module = importlib.import_module("app.api.v1.admin.marketplace")
|
||||
_letzshop_module = importlib.import_module("app.api.v1.admin.letzshop")
|
||||
marketplace_original_router = _marketplace_module.router
|
||||
letzshop_original_router = _letzshop_module.router
|
||||
# Include marketplace product catalog routes
|
||||
admin_router.include_router(admin_products_router)
|
||||
|
||||
# Create module-aware router for marketplace
|
||||
admin_router = APIRouter(
|
||||
prefix="/marketplace",
|
||||
dependencies=[Depends(require_module_access("marketplace"))],
|
||||
)
|
||||
# Include marketplace import jobs routes
|
||||
admin_router.include_router(admin_marketplace_router)
|
||||
|
||||
# Re-export all routes from the original marketplace module
|
||||
for route in marketplace_original_router.routes:
|
||||
admin_router.routes.append(route)
|
||||
# Include letzshop routes
|
||||
admin_router.include_router(admin_letzshop_router)
|
||||
|
||||
# Create separate router for letzshop integration
|
||||
admin_letzshop_router = APIRouter(
|
||||
prefix="/letzshop",
|
||||
dependencies=[Depends(require_module_access("marketplace"))],
|
||||
)
|
||||
|
||||
for route in letzshop_original_router.routes:
|
||||
admin_letzshop_router.routes.append(route)
|
||||
__all__ = ["admin_router"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# app/api/v1/admin/products.py
|
||||
# app/modules/marketplace/routes/api/admin_products.py
|
||||
"""
|
||||
Admin product catalog endpoints.
|
||||
Admin marketplace product catalog endpoints.
|
||||
|
||||
Provides platform-wide product search and management capabilities:
|
||||
- Browse all marketplace products across vendors
|
||||
@@ -8,6 +8,8 @@ Provides platform-wide product search and management capabilities:
|
||||
- Filter by marketplace, vendor, availability, product type
|
||||
- View product details and translations
|
||||
- Copy products to vendor catalogs
|
||||
|
||||
All routes require module access control for the 'marketplace' module.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -16,12 +18,15 @@ from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.api.deps import get_current_admin_api, require_module_access
|
||||
from app.core.database import get_db
|
||||
from app.services.marketplace_product_service import marketplace_product_service
|
||||
from app.modules.marketplace.services.marketplace_product_service import marketplace_product_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
router = APIRouter(prefix="/products")
|
||||
admin_products_router = APIRouter(
|
||||
prefix="/products",
|
||||
dependencies=[Depends(require_module_access("marketplace"))],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -147,7 +152,7 @@ class AdminProductDetail(BaseModel):
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("", response_model=AdminProductListResponse)
|
||||
@admin_products_router.get("", response_model=AdminProductListResponse)
|
||||
def get_products(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
@@ -190,7 +195,7 @@ def get_products(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats", response_model=AdminProductStats)
|
||||
@admin_products_router.get("/stats", response_model=AdminProductStats)
|
||||
def get_product_stats(
|
||||
marketplace: str | None = Query(None, description="Filter by marketplace"),
|
||||
vendor_name: str | None = Query(None, description="Filter by vendor name"),
|
||||
@@ -204,7 +209,7 @@ def get_product_stats(
|
||||
return AdminProductStats(**stats)
|
||||
|
||||
|
||||
@router.get("/marketplaces", response_model=MarketplacesResponse)
|
||||
@admin_products_router.get("/marketplaces", response_model=MarketplacesResponse)
|
||||
def get_marketplaces(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
@@ -214,7 +219,7 @@ def get_marketplaces(
|
||||
return MarketplacesResponse(marketplaces=marketplaces)
|
||||
|
||||
|
||||
@router.get("/vendors", response_model=VendorsResponse)
|
||||
@admin_products_router.get("/vendors", response_model=VendorsResponse)
|
||||
def get_product_vendors(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
@@ -224,7 +229,7 @@ def get_product_vendors(
|
||||
return VendorsResponse(vendors=vendors)
|
||||
|
||||
|
||||
@router.post("/copy-to-vendor", response_model=CopyToVendorResponse)
|
||||
@admin_products_router.post("/copy-to-vendor", response_model=CopyToVendorResponse)
|
||||
def copy_products_to_vendor(
|
||||
request: CopyToVendorRequest,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -245,11 +250,11 @@ def copy_products_to_vendor(
|
||||
vendor_id=request.vendor_id,
|
||||
skip_existing=request.skip_existing,
|
||||
)
|
||||
db.commit() # ✅ ARCH: Commit at API level for transaction control
|
||||
db.commit()
|
||||
return CopyToVendorResponse(**result)
|
||||
|
||||
|
||||
@router.get("/{product_id}", response_model=AdminProductDetail)
|
||||
@admin_products_router.get("/{product_id}", response_model=AdminProductDetail)
|
||||
def get_product_detail(
|
||||
product_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
Reference in New Issue
Block a user