From 6f278131a3d719dbc0ba8a0dfbf33bbb88afe4c9 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sat, 31 Jan 2026 12:49:11 +0100 Subject: [PATCH] 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 --- app/api/v1/admin/__init__.py | 16 +------ app/api/v1/vendor/__init__.py | 5 +- app/modules/catalog/routes/api/__init__.py | 18 ++++++- .../catalog/routes/api/admin.py} | 35 +++++++------- .../catalog/routes/api/vendor.py} | 31 ++++++------ app/modules/marketplace/routes/api/admin.py | 48 ++++++++----------- .../marketplace/routes/api/admin_products.py} | 29 ++++++----- 7 files changed, 92 insertions(+), 90 deletions(-) rename app/{api/v1/admin/vendor_products.py => modules/catalog/routes/api/admin.py} (82%) rename app/{api/v1/vendor/products.py => modules/catalog/routes/api/vendor.py} (89%) rename app/{api/v1/admin/products.py => modules/marketplace/routes/api/admin_products.py} (88%) diff --git a/app/api/v1/admin/__init__.py b/app/api/v1/admin/__init__.py index abb7a651..e2c9426a 100644 --- a/app/api/v1/admin/__init__.py +++ b/app/api/v1/admin/__init__.py @@ -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 # ============================================================================ diff --git a/app/api/v1/vendor/__init__.py b/app/api/v1/vendor/__init__.py index 96a6fadb..30f31584 100644 --- a/app/api/v1/vendor/__init__.py +++ b/app/api/v1/vendor/__init__.py @@ -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"]) diff --git a/app/modules/catalog/routes/api/__init__.py b/app/modules/catalog/routes/api/__init__.py index 115cd87f..a94bc14c 100644 --- a/app/modules/catalog/routes/api/__init__.py +++ b/app/modules/catalog/routes/api/__init__.py @@ -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}") diff --git a/app/api/v1/admin/vendor_products.py b/app/modules/catalog/routes/api/admin.py similarity index 82% rename from app/api/v1/admin/vendor_products.py rename to app/modules/catalog/routes/api/admin.py index c12a17fd..0894f273 100644 --- a/app/api/v1/admin/vendor_products.py +++ b/app/modules/catalog/routes/api/admin.py @@ -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) diff --git a/app/api/v1/vendor/products.py b/app/modules/catalog/routes/api/vendor.py similarity index 89% rename from app/api/v1/vendor/products.py rename to app/modules/catalog/routes/api/vendor.py index 30e4165f..4dd79ad6 100644 --- a/app/api/v1/vendor/products.py +++ b/app/modules/catalog/routes/api/vendor.py @@ -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), diff --git a/app/modules/marketplace/routes/api/admin.py b/app/modules/marketplace/routes/api/admin.py index 2c11165b..369da1c0 100644 --- a/app/modules/marketplace/routes/api/admin.py +++ b/app/modules/marketplace/routes/api/admin.py @@ -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"] diff --git a/app/api/v1/admin/products.py b/app/modules/marketplace/routes/api/admin_products.py similarity index 88% rename from app/api/v1/admin/products.py rename to app/modules/marketplace/routes/api/admin_products.py index 03171e87..99534c3b 100644 --- a/app/api/v1/admin/products.py +++ b/app/modules/marketplace/routes/api/admin_products.py @@ -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),