From e0b69f5a7d26f75c0fc16364ffab9cfdb126e56a Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Fri, 30 Jan 2026 23:24:10 +0100 Subject: [PATCH] refactor(customers): migrate routes to module with auto-discovery - Move customer route implementations to app/modules/customers/routes/ - Convert legacy app/api/v1/{admin,vendor}/customers.py to re-exports - Update router registrations to use module routers with access control - Fix CustomerListResponse pagination (page/per_page/total_pages) - Update URL routing docs to use storefront consistently - Fix mkdocs.yml nav references (shop -> storefront) - Fix broken doc links in logging.md and cdn-fallback-strategy.md Co-Authored-By: Claude Opus 4.5 --- app/api/v1/admin/__init__.py | 10 +- app/api/v1/admin/customers.py | 129 +++--------- app/api/v1/vendor/__init__.py | 11 +- app/api/v1/vendor/customers.py | 244 +++------------------- app/modules/customers/routes/admin.py | 125 +++++++++-- app/modules/customers/routes/vendor.py | 228 +++++++++++++++++++- docs/architecture/url-routing/overview.md | 212 +++++++++++-------- docs/frontend/cdn-fallback-strategy.md | 2 +- docs/frontend/shared/logging.md | 2 +- mkdocs.yml | 14 +- 10 files changed, 533 insertions(+), 444 deletions(-) diff --git a/app/api/v1/admin/__init__.py b/app/api/v1/admin/__init__.py index cb397790..0cf40d08 100644 --- a/app/api/v1/admin/__init__.py +++ b/app/api/v1/admin/__init__.py @@ -49,7 +49,7 @@ from . import ( code_quality, companies, # content_pages - moved to app.modules.cms.routes.api.admin - customers, + # customers - moved to app.modules.customers.routes.admin dashboard, email_templates, features, @@ -92,6 +92,9 @@ from app.modules.marketplace.routes.api.admin import admin_letzshop_router as le # CMS module router from app.modules.cms.routes.api.admin import router as cms_admin_router +# Customers module router +from app.modules.customers.routes.admin import admin_router as customers_admin_router + # Create admin router router = APIRouter() @@ -149,8 +152,9 @@ router.include_router(users.router, tags=["admin-users"]) # Include admin user management endpoints (super admin only) router.include_router(admin_users.router, tags=["admin-admin-users"]) -# Include customer management endpoints -router.include_router(customers.router, tags=["admin-customers"]) +# Include customers module router (with module access control) +router.include_router(customers_admin_router, tags=["admin-customers"]) +# Legacy: router.include_router(customers.router, tags=["admin-customers"]) # ============================================================================ diff --git a/app/api/v1/admin/customers.py b/app/api/v1/admin/customers.py index 8291fcf2..ab2533c5 100644 --- a/app/api/v1/admin/customers.py +++ b/app/api/v1/admin/customers.py @@ -1,112 +1,31 @@ # app/api/v1/admin/customers.py """ -Customer management endpoints for admin. +LEGACY LOCATION - Re-exports from module for backwards compatibility. -Provides admin-level access to customer data across all vendors. +The canonical implementation is now in: + app/modules/customers/routes/admin.py + +This file exists to maintain backwards compatibility with code that +imports from the old location. All new code should import directly +from the module: + + from app.modules.customers.routes.admin import admin_router """ -from fastapi import APIRouter, Depends, Query -from sqlalchemy.orm import Session - -from app.api.deps import get_current_admin_api -from app.core.database import get_db -from app.services.admin_customer_service import admin_customer_service -from models.schema.auth import UserContext -from app.modules.customers.schemas import ( - CustomerDetailResponse, - CustomerListResponse, - CustomerMessageResponse, - CustomerStatisticsResponse, +from app.modules.customers.routes.admin import ( + admin_router, + router, + list_customers, + get_customer_stats, + get_customer, + toggle_customer_status, ) -router = APIRouter(prefix="/customers") - - -# ============================================================================ -# List Customers -# ============================================================================ - - -@router.get("", response_model=CustomerListResponse) -def list_customers( - vendor_id: int | None = Query(None, description="Filter by vendor ID"), - search: str = Query("", description="Search by email, name, or customer number"), - is_active: bool | None = Query(None, description="Filter by active status"), - skip: int = Query(0, ge=0), - limit: int = Query(20, ge=1, le=100), - db: Session = Depends(get_db), - current_admin: UserContext = Depends(get_current_admin_api), -) -> CustomerListResponse: - """ - Get paginated list of customers across all vendors. - - Admin can filter by vendor, search, and active status. - """ - customers, total = admin_customer_service.list_customers( - db=db, - vendor_id=vendor_id, - search=search if search else None, - is_active=is_active, - skip=skip, - limit=limit, - ) - - return CustomerListResponse( - customers=customers, - total=total, - skip=skip, - limit=limit, - ) - - -# ============================================================================ -# Customer Statistics -# ============================================================================ - - -@router.get("/stats", response_model=CustomerStatisticsResponse) -def get_customer_stats( - vendor_id: int | None = Query(None, description="Filter by vendor ID"), - db: Session = Depends(get_db), - current_admin: UserContext = Depends(get_current_admin_api), -) -> CustomerStatisticsResponse: - """Get customer statistics.""" - stats = admin_customer_service.get_customer_stats(db=db, vendor_id=vendor_id) - return CustomerStatisticsResponse(**stats) - - -# ============================================================================ -# Get Single Customer -# ============================================================================ - - -@router.get("/{customer_id}", response_model=CustomerDetailResponse) -def get_customer( - customer_id: int, - db: Session = Depends(get_db), - current_admin: UserContext = Depends(get_current_admin_api), -) -> CustomerDetailResponse: - """Get customer details by ID.""" - customer = admin_customer_service.get_customer(db=db, customer_id=customer_id) - return CustomerDetailResponse(**customer) - - -# ============================================================================ -# Toggle Customer Status -# ============================================================================ - - -@router.patch("/{customer_id}/toggle-status", response_model=CustomerMessageResponse) -def toggle_customer_status( - customer_id: int, - db: Session = Depends(get_db), - current_admin: UserContext = Depends(get_current_admin_api), -) -> CustomerMessageResponse: - """Toggle customer active status.""" - result = admin_customer_service.toggle_customer_status( - db=db, - customer_id=customer_id, - admin_email=current_admin.email, - ) - db.commit() - return CustomerMessageResponse(message=result["message"]) +__all__ = [ + "admin_router", + "router", + "list_customers", + "get_customer_stats", + "get_customer", + "toggle_customer_status", +] diff --git a/app/api/v1/vendor/__init__.py b/app/api/v1/vendor/__init__.py index f2f46ce5..baad0c40 100644 --- a/app/api/v1/vendor/__init__.py +++ b/app/api/v1/vendor/__init__.py @@ -35,7 +35,7 @@ from . import ( auth, billing, # content_pages - moved to app.modules.cms.routes.api.vendor - customers, + # customers - moved to app.modules.customers.routes.vendor dashboard, email_settings, email_templates, @@ -71,6 +71,9 @@ from app.modules.marketplace.routes.api.vendor import vendor_letzshop_router as # CMS module router from app.modules.cms.routes.api.vendor import router as cms_vendor_router +# Customers module router +from app.modules.customers.routes.vendor import vendor_router as customers_vendor_router + # Create vendor router router = APIRouter() @@ -104,7 +107,11 @@ router.include_router(orders_exceptions_router, tags=["vendor-order-exceptions"] # Legacy: router.include_router(order_item_exceptions.router, tags=["vendor-order-exceptions"]) router.include_router(invoices.router, tags=["vendor-invoices"]) -router.include_router(customers.router, tags=["vendor-customers"]) + +# Include customers module router (with module access control) +router.include_router(customers_vendor_router, tags=["vendor-customers"]) +# Legacy: router.include_router(customers.router, tags=["vendor-customers"]) + router.include_router(team.router, tags=["vendor-team"]) # Include inventory module router (with module access control) diff --git a/app/api/v1/vendor/customers.py b/app/api/v1/vendor/customers.py index 85c236a0..3d7f26ff 100644 --- a/app/api/v1/vendor/customers.py +++ b/app/api/v1/vendor/customers.py @@ -1,223 +1,35 @@ # app/api/v1/vendor/customers.py """ -Vendor customer management endpoints. +LEGACY LOCATION - Re-exports from module for backwards compatibility. -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. +The canonical implementation is now in: + app/modules/customers/routes/vendor.py + +This file exists to maintain backwards compatibility with code that +imports from the old location. All new code should import directly +from the module: + + from app.modules.customers.routes.vendor import vendor_router """ -import logging - -from fastapi import APIRouter, Depends, Query -from sqlalchemy.orm import Session - -from app.api.deps import get_current_vendor_api -from app.core.database import get_db -from app.services.customer_service import customer_service -from models.schema.auth import UserContext -from app.modules.customers.schemas import ( - CustomerDetailResponse, - CustomerMessageResponse, - CustomerOrdersResponse, - CustomerResponse, - CustomerStatisticsResponse, - CustomerUpdate, - VendorCustomerListResponse, +from app.modules.customers.routes.vendor import ( + vendor_router, + router, + get_vendor_customers, + get_customer_details, + get_customer_orders, + update_customer, + toggle_customer_status, + get_customer_statistics, ) -router = APIRouter(prefix="/customers") -logger = logging.getLogger(__name__) - - -@router.get("", response_model=VendorCustomerListResponse) -def get_vendor_customers( - skip: int = Query(0, ge=0), - limit: int = Query(100, ge=1, le=1000), - search: str | None = Query(None), - is_active: bool | None = Query(None), - current_user: UserContext = Depends(get_current_vendor_api), - db: Session = Depends(get_db), -): - """ - Get all customers for this vendor. - - - Query customers filtered by vendor_id - - Support search by name/email - - Support filtering by active status - - Return paginated results - """ - customers, total = customer_service.get_vendor_customers( - db=db, - vendor_id=current_user.token_vendor_id, - skip=skip, - limit=limit, - search=search, - is_active=is_active, - ) - - return VendorCustomerListResponse( - customers=[CustomerResponse.model_validate(c) for c in customers], - total=total, - skip=skip, - limit=limit, - ) - - -@router.get("/{customer_id}", response_model=CustomerDetailResponse) -def get_customer_details( - customer_id: int, - current_user: UserContext = Depends(get_current_vendor_api), - db: Session = Depends(get_db), -): - """ - Get detailed customer information. - - - Get customer by ID - - Verify customer belongs to vendor - - Include order statistics - """ - # Service will raise CustomerNotFoundException if not found - customer = customer_service.get_customer( - db=db, - vendor_id=current_user.token_vendor_id, - customer_id=customer_id, - ) - - # Get statistics - stats = customer_service.get_customer_statistics( - db=db, - vendor_id=current_user.token_vendor_id, - customer_id=customer_id, - ) - - return CustomerDetailResponse( - id=customer.id, - email=customer.email, - first_name=customer.first_name, - last_name=customer.last_name, - phone=customer.phone, - customer_number=customer.customer_number, - is_active=customer.is_active, - marketing_consent=customer.marketing_consent, - total_orders=stats["total_orders"], - total_spent=stats["total_spent"], - average_order_value=stats["average_order_value"], - last_order_date=stats["last_order_date"], - created_at=customer.created_at, - ) - - -@router.get("/{customer_id}/orders", response_model=CustomerOrdersResponse) -def get_customer_orders( - customer_id: int, - skip: int = Query(0, ge=0), - limit: int = Query(50, ge=1, le=100), - current_user: UserContext = Depends(get_current_vendor_api), - db: Session = Depends(get_db), -): - """ - Get order history for a specific customer. - - - Get all orders for customer - - Filter by vendor_id - - Return order details - """ - # Service will raise CustomerNotFoundException if not found - orders, total = customer_service.get_customer_orders( - db=db, - vendor_id=current_user.token_vendor_id, - customer_id=customer_id, - skip=skip, - limit=limit, - ) - - return CustomerOrdersResponse( - orders=[ - { - "id": o.id, - "order_number": o.order_number, - "status": o.status, - "total": o.total_cents / 100 if o.total_cents else 0, - "created_at": o.created_at, - } - for o in orders - ], - total=total, - skip=skip, - limit=limit, - ) - - -@router.put("/{customer_id}", response_model=CustomerMessageResponse) -def update_customer( - customer_id: int, - customer_data: CustomerUpdate, - current_user: UserContext = Depends(get_current_vendor_api), - db: Session = Depends(get_db), -): - """ - Update customer information. - - - Update customer details - - Verify customer belongs to vendor - """ - # Service will raise CustomerNotFoundException if not found - customer_service.update_customer( - db=db, - vendor_id=current_user.token_vendor_id, - customer_id=customer_id, - customer_data=customer_data, - ) - - db.commit() - - return CustomerMessageResponse(message="Customer updated successfully") - - -@router.put("/{customer_id}/status", response_model=CustomerMessageResponse) -def toggle_customer_status( - customer_id: int, - current_user: UserContext = Depends(get_current_vendor_api), - db: Session = Depends(get_db), -): - """ - Activate/deactivate customer account. - - - Toggle customer is_active status - - Verify customer belongs to vendor - """ - # Service will raise CustomerNotFoundException if not found - customer = customer_service.toggle_customer_status( - db=db, - vendor_id=current_user.token_vendor_id, - customer_id=customer_id, - ) - - db.commit() - - status = "activated" if customer.is_active else "deactivated" - return CustomerMessageResponse(message=f"Customer {status} successfully") - - -@router.get("/{customer_id}/stats", response_model=CustomerStatisticsResponse) -def get_customer_statistics( - customer_id: int, - current_user: UserContext = Depends(get_current_vendor_api), - db: Session = Depends(get_db), -): - """ - Get customer statistics and metrics. - - - Total orders - - Total spent - - Average order value - - Last order date - """ - # Service will raise CustomerNotFoundException if not found - stats = customer_service.get_customer_statistics( - db=db, - vendor_id=current_user.token_vendor_id, - customer_id=customer_id, - ) - - return CustomerStatisticsResponse(**stats) +__all__ = [ + "vendor_router", + "router", + "get_vendor_customers", + "get_customer_details", + "get_customer_orders", + "update_customer", + "toggle_customer_status", + "get_customer_statistics", +] diff --git a/app/modules/customers/routes/admin.py b/app/modules/customers/routes/admin.py index ed3accfe..545a3f90 100644 --- a/app/modules/customers/routes/admin.py +++ b/app/modules/customers/routes/admin.py @@ -1,18 +1,23 @@ # app/modules/customers/routes/admin.py """ -Customers module admin routes. +Customer management endpoints for admin. -This module wraps the existing admin customers routes and adds -module-based access control. Routes are re-exported from the -original location with the module access dependency. +Provides admin-level access to customer data across all vendors. """ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session -from app.api.deps import require_module_access - -# Import original router (direct import to avoid circular dependency) -from app.api.v1.admin.customers import router as original_router +from app.api.deps import get_current_admin_api, require_module_access +from app.core.database import get_db +from app.modules.customers.services import admin_customer_service +from models.schema.auth import UserContext +from app.modules.customers.schemas import ( + CustomerDetailResponse, + CustomerListResponse, + CustomerMessageResponse, + CustomerStatisticsResponse, +) # Create module-aware router admin_router = APIRouter( @@ -20,6 +25,102 @@ admin_router = APIRouter( dependencies=[Depends(require_module_access("customers"))], ) -# Re-export all routes from the original module with module access control -for route in original_router.routes: - admin_router.routes.append(route) + +# ============================================================================ +# List Customers +# ============================================================================ + + +@admin_router.get("", response_model=CustomerListResponse) +def list_customers( + vendor_id: int | None = Query(None, description="Filter by vendor ID"), + search: str = Query("", description="Search by email, name, or customer number"), + is_active: bool | None = Query(None, description="Filter by active status"), + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + db: Session = Depends(get_db), + current_admin: UserContext = Depends(get_current_admin_api), +) -> CustomerListResponse: + """ + Get paginated list of customers across all vendors. + + Admin can filter by vendor, search, and active status. + """ + customers, total = admin_customer_service.list_customers( + db=db, + vendor_id=vendor_id, + search=search if search else None, + is_active=is_active, + skip=skip, + limit=limit, + ) + + # Calculate pagination values + page = (skip // limit) + 1 if limit > 0 else 1 + per_page = limit + total_pages = (total + limit - 1) // limit if limit > 0 else 1 + + return CustomerListResponse( + customers=customers, + total=total, + page=page, + per_page=per_page, + total_pages=total_pages, + ) + + +# ============================================================================ +# Customer Statistics +# ============================================================================ + + +@admin_router.get("/stats", response_model=CustomerStatisticsResponse) +def get_customer_stats( + vendor_id: int | None = Query(None, description="Filter by vendor ID"), + db: Session = Depends(get_db), + current_admin: UserContext = Depends(get_current_admin_api), +) -> CustomerStatisticsResponse: + """Get customer statistics.""" + stats = admin_customer_service.get_customer_stats(db=db, vendor_id=vendor_id) + return CustomerStatisticsResponse(**stats) + + +# ============================================================================ +# Get Single Customer +# ============================================================================ + + +@admin_router.get("/{customer_id}", response_model=CustomerDetailResponse) +def get_customer( + customer_id: int, + db: Session = Depends(get_db), + current_admin: UserContext = Depends(get_current_admin_api), +) -> CustomerDetailResponse: + """Get customer details by ID.""" + customer = admin_customer_service.get_customer(db=db, customer_id=customer_id) + return CustomerDetailResponse(**customer) + + +# ============================================================================ +# Toggle Customer Status +# ============================================================================ + + +@admin_router.patch("/{customer_id}/toggle-status", response_model=CustomerMessageResponse) +def toggle_customer_status( + customer_id: int, + db: Session = Depends(get_db), + current_admin: UserContext = Depends(get_current_admin_api), +) -> CustomerMessageResponse: + """Toggle customer active status.""" + result = admin_customer_service.toggle_customer_status( + db=db, + customer_id=customer_id, + admin_email=current_admin.email, + ) + db.commit() + return CustomerMessageResponse(message=result["message"]) + + +# Legacy alias for backwards compatibility +router = admin_router diff --git a/app/modules/customers/routes/vendor.py b/app/modules/customers/routes/vendor.py index a7a28bf6..5b306db9 100644 --- a/app/modules/customers/routes/vendor.py +++ b/app/modules/customers/routes/vendor.py @@ -1,25 +1,231 @@ # app/modules/customers/routes/vendor.py """ -Customers module vendor routes. +Vendor customer management endpoints. -This module wraps the existing vendor customers routes and adds -module-based access control. Routes are re-exported from the -original location with the module access dependency. +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. """ -from fastapi import APIRouter, Depends +import logging -from app.api.deps import require_module_access +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session -# Import original router (direct import to avoid circular dependency) -from app.api.v1.vendor.customers import router as original_router +from app.api.deps import get_current_vendor_api, require_module_access +from app.core.database import get_db +from app.modules.customers.services import customer_service +from models.schema.auth import UserContext +from app.modules.customers.schemas import ( + CustomerDetailResponse, + CustomerMessageResponse, + CustomerOrdersResponse, + CustomerResponse, + CustomerStatisticsResponse, + CustomerUpdate, + VendorCustomerListResponse, +) # Create module-aware router vendor_router = APIRouter( prefix="/customers", dependencies=[Depends(require_module_access("customers"))], ) +logger = logging.getLogger(__name__) -# Re-export all routes from the original module with module access control -for route in original_router.routes: - vendor_router.routes.append(route) + +@vendor_router.get("", response_model=VendorCustomerListResponse) +def get_vendor_customers( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + search: str | None = Query(None), + is_active: bool | None = Query(None), + current_user: UserContext = Depends(get_current_vendor_api), + db: Session = Depends(get_db), +): + """ + Get all customers for this vendor. + + - Query customers filtered by vendor_id + - Support search by name/email + - Support filtering by active status + - Return paginated results + """ + customers, total = customer_service.get_vendor_customers( + db=db, + vendor_id=current_user.token_vendor_id, + skip=skip, + limit=limit, + search=search, + is_active=is_active, + ) + + return VendorCustomerListResponse( + customers=[CustomerResponse.model_validate(c) for c in customers], + total=total, + skip=skip, + limit=limit, + ) + + +@vendor_router.get("/{customer_id}", response_model=CustomerDetailResponse) +def get_customer_details( + customer_id: int, + current_user: UserContext = Depends(get_current_vendor_api), + db: Session = Depends(get_db), +): + """ + Get detailed customer information. + + - Get customer by ID + - Verify customer belongs to vendor + - Include order statistics + """ + # Service will raise CustomerNotFoundException if not found + customer = customer_service.get_customer( + db=db, + vendor_id=current_user.token_vendor_id, + customer_id=customer_id, + ) + + # Get statistics + stats = customer_service.get_customer_statistics( + db=db, + vendor_id=current_user.token_vendor_id, + customer_id=customer_id, + ) + + return CustomerDetailResponse( + id=customer.id, + email=customer.email, + first_name=customer.first_name, + last_name=customer.last_name, + phone=customer.phone, + customer_number=customer.customer_number, + is_active=customer.is_active, + marketing_consent=customer.marketing_consent, + total_orders=stats["total_orders"], + total_spent=stats["total_spent"], + average_order_value=stats["average_order_value"], + last_order_date=stats["last_order_date"], + created_at=customer.created_at, + ) + + +@vendor_router.get("/{customer_id}/orders", response_model=CustomerOrdersResponse) +def get_customer_orders( + customer_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(50, ge=1, le=100), + current_user: UserContext = Depends(get_current_vendor_api), + db: Session = Depends(get_db), +): + """ + Get order history for a specific customer. + + - Get all orders for customer + - Filter by vendor_id + - Return order details + """ + # Service will raise CustomerNotFoundException if not found + orders, total = customer_service.get_customer_orders( + db=db, + vendor_id=current_user.token_vendor_id, + customer_id=customer_id, + skip=skip, + limit=limit, + ) + + return CustomerOrdersResponse( + orders=[ + { + "id": o.id, + "order_number": o.order_number, + "status": o.status, + "total": o.total_cents / 100 if o.total_cents else 0, + "created_at": o.created_at, + } + for o in orders + ], + total=total, + skip=skip, + limit=limit, + ) + + +@vendor_router.put("/{customer_id}", response_model=CustomerMessageResponse) +def update_customer( + customer_id: int, + customer_data: CustomerUpdate, + current_user: UserContext = Depends(get_current_vendor_api), + db: Session = Depends(get_db), +): + """ + Update customer information. + + - Update customer details + - Verify customer belongs to vendor + """ + # Service will raise CustomerNotFoundException if not found + customer_service.update_customer( + db=db, + vendor_id=current_user.token_vendor_id, + customer_id=customer_id, + customer_data=customer_data, + ) + + db.commit() + + return CustomerMessageResponse(message="Customer updated successfully") + + +@vendor_router.put("/{customer_id}/status", response_model=CustomerMessageResponse) +def toggle_customer_status( + customer_id: int, + current_user: UserContext = Depends(get_current_vendor_api), + db: Session = Depends(get_db), +): + """ + Activate/deactivate customer account. + + - Toggle customer is_active status + - Verify customer belongs to vendor + """ + # Service will raise CustomerNotFoundException if not found + customer = customer_service.toggle_customer_status( + db=db, + vendor_id=current_user.token_vendor_id, + customer_id=customer_id, + ) + + db.commit() + + status = "activated" if customer.is_active else "deactivated" + return CustomerMessageResponse(message=f"Customer {status} successfully") + + +@vendor_router.get("/{customer_id}/stats", response_model=CustomerStatisticsResponse) +def get_customer_statistics( + customer_id: int, + current_user: UserContext = Depends(get_current_vendor_api), + db: Session = Depends(get_db), +): + """ + Get customer statistics and metrics. + + - Total orders + - Total spent + - Average order value + - Last order date + """ + # Service will raise CustomerNotFoundException if not found + stats = customer_service.get_customer_statistics( + db=db, + vendor_id=current_user.token_vendor_id, + customer_id=customer_id, + ) + + return CustomerStatisticsResponse(**stats) + + +# Legacy alias for backwards compatibility +router = vendor_router diff --git a/docs/architecture/url-routing/overview.md b/docs/architecture/url-routing/overview.md index ade01d06..366960f9 100644 --- a/docs/architecture/url-routing/overview.md +++ b/docs/architecture/url-routing/overview.md @@ -2,37 +2,37 @@ ## Quick Answer -**How do customers access a vendor's shop in Wizamart?** +**How do customers access a vendor's storefront in Wizamart?** There are three ways depending on the deployment mode: -**⚠️ Important:** This guide describes **customer-facing shop routes**. For vendor dashboard/management routes, see [Vendor Frontend Architecture](../../frontend/vendor/architecture.md). The shop uses `/vendors/{code}/shop/*` (plural) in path-based mode, while the vendor dashboard uses `/vendor/{code}/*` (singular). +**⚠️ Important:** This guide describes **customer-facing storefront routes**. For vendor dashboard/management routes, see [Vendor Frontend Architecture](../../frontend/vendor/architecture.md). The storefront uses `/vendors/{code}/storefront/*` (plural) in path-based mode, while the vendor dashboard uses `/vendor/{code}/*` (singular). ### 1. **SUBDOMAIN MODE** (Production - Recommended) ``` -https://VENDOR_SUBDOMAIN.platform.com/shop/products +https://VENDOR_SUBDOMAIN.platform.com/storefront/products Example: -https://acme.wizamart.com/shop/products -https://techpro.wizamart.com/shop/categories/electronics +https://acme.wizamart.com/storefront/products +https://techpro.wizamart.com/storefront/categories/electronics ``` ### 2. **CUSTOM DOMAIN MODE** (Production - Premium) ``` -https://VENDOR_CUSTOM_DOMAIN/shop/products +https://VENDOR_CUSTOM_DOMAIN/storefront/products Example: -https://store.acmecorp.com/shop/products -https://shop.techpro.io/shop/cart +https://store.acmecorp.com/storefront/products +https://shop.techpro.io/storefront/cart ``` ### 3. **PATH-BASED MODE** (Development Only) ``` -http://localhost:PORT/vendors/VENDOR_CODE/shop/products +http://localhost:PORT/platforms/PLATFORM_CODE/vendors/VENDOR_CODE/storefront/products Example: -http://localhost:8000/vendors/acme/shop/products -http://localhost:8000/vendors/techpro/shop/checkout +http://localhost:8000/platforms/oms/vendors/acme/storefront/products +http://localhost:8000/platforms/loyalty/vendors/techpro/storefront/checkout ``` --- @@ -51,7 +51,9 @@ Wizamart supports multiple platforms (OMS, Loyalty, Site Builder), each with its | `/about` | Main marketing site about page | | `/platforms/oms/` | OMS platform homepage | | `/platforms/oms/pricing` | OMS platform pricing page | -| `/platforms/oms/vendors/{code}/` | Vendor storefront on OMS | +| `/platforms/oms/vendors/{code}/storefront/` | Vendor storefront on OMS | +| `/platforms/oms/admin/` | Admin panel for OMS platform | +| `/platforms/oms/vendor/{code}/` | Vendor dashboard on OMS | | `/platforms/loyalty/` | Loyalty platform homepage | | `/platforms/loyalty/features` | Loyalty platform features page | @@ -63,9 +65,45 @@ Wizamart supports multiple platforms (OMS, Loyalty, Site Builder), each with its | `wizamart.lu/about` | Main marketing site about page | | `oms.lu/` | OMS platform homepage | | `oms.lu/pricing` | OMS platform pricing page | -| `oms.lu/vendors/{code}/` | Vendor storefront on OMS | +| `oms.lu/admin/` | Admin panel for OMS platform | +| `oms.lu/vendor/{code}/` | Vendor dashboard on OMS | +| `https://mybakery.lu/storefront/` | Vendor storefront (vendor's custom domain) | | `loyalty.lu/` | Loyalty platform homepage | +**Note:** In production, vendors configure their own custom domains for storefronts. The platform domain (e.g., `oms.lu`) is used for admin and vendor dashboards, while storefronts use vendor-owned domains. + +### Quick Reference by Platform + +#### For "oms" Platform +``` +Dev: + Platform: http://localhost:8000/platforms/oms/ + Admin: http://localhost:8000/platforms/oms/admin/ + Vendor: http://localhost:8000/platforms/oms/vendor/{vendor_code}/ + Storefront: http://localhost:8000/platforms/oms/vendors/{vendor_code}/storefront/ + +Prod: + Platform: https://oms.lu/ + Admin: https://oms.lu/admin/ + Vendor: https://oms.lu/vendor/{vendor_code}/ + Storefront: https://mybakery.lu/storefront/ (vendor's custom domain) +``` + +#### For "loyalty" Platform +``` +Dev: + Platform: http://localhost:8000/platforms/loyalty/ + Admin: http://localhost:8000/platforms/loyalty/admin/ + Vendor: http://localhost:8000/platforms/loyalty/vendor/{vendor_code}/ + Storefront: http://localhost:8000/platforms/loyalty/vendors/{vendor_code}/storefront/ + +Prod: + Platform: https://loyalty.lu/ + Admin: https://loyalty.lu/admin/ + Vendor: https://loyalty.lu/vendor/{vendor_code}/ + Storefront: https://myrewards.lu/storefront/ (vendor's custom domain) +``` + ### Platform Routing Logic ``` @@ -100,10 +138,10 @@ Request arrives | Platform | Code | Dev URL | Prod Domain | |----------|------|---------|-------------| -| Main Marketing | `main` | `localhost:9999/` | `wizamart.lu` | -| OMS | `oms` | `localhost:9999/platforms/oms/` | `oms.lu` | -| Loyalty | `loyalty` | `localhost:9999/platforms/loyalty/` | `loyalty.lu` | -| Site Builder | `site-builder` | `localhost:9999/platforms/site-builder/` | `sitebuilder.lu` | +| Main Marketing | `main` | `localhost:8000/` | `wizamart.lu` | +| OMS | `oms` | `localhost:8000/platforms/oms/` | `oms.lu` | +| Loyalty | `loyalty` | `localhost:8000/platforms/loyalty/` | `loyalty.lu` | +| Site Builder | `site-builder` | `localhost:8000/platforms/site-builder/` | `sitebuilder.lu` | **See:** [Multi-Platform CMS Architecture](../multi-platform-cms.md) for content management details. @@ -113,23 +151,23 @@ Request arrives ### 1. SUBDOMAIN MODE (Production - Recommended) -**URL Pattern:** `https://VENDOR_SUBDOMAIN.platform.com/shop/...` +**URL Pattern:** `https://VENDOR_SUBDOMAIN.platform.com/storefront/...` **Example:** - Vendor subdomain: `acme` - Platform domain: `wizamart.com` -- Customer Shop URL: `https://acme.wizamart.com/shop/products` -- Product Detail: `https://acme.wizamart.com/shop/products/123` +- Customer Storefront URL: `https://acme.wizamart.com/storefront/products` +- Product Detail: `https://acme.wizamart.com/storefront/products/123` **How It Works:** -1. Customer visits `https://acme.wizamart.com/shop/products` +1. Customer visits `https://acme.wizamart.com/storefront/products` 2. `vendor_context_middleware` detects subdomain `"acme"` 3. Queries: `SELECT * FROM vendors WHERE subdomain = 'acme'` 4. Finds Vendor with ID=1 (ACME Store) 5. Sets `request.state.vendor = Vendor(ACME Store)` -6. `context_middleware` detects it's a SHOP request +6. `context_middleware` detects it's a STOREFRONT request 7. `theme_context_middleware` loads ACME's theme -8. Routes to `shop_pages.py` → `shop_products_page()` +8. Routes to `storefront_pages.py` → `storefront_products_page()` 9. Renders template with ACME's colors, logo, and products **Advantages:** @@ -141,12 +179,12 @@ Request arrives ### 2. CUSTOM DOMAIN MODE (Production - Premium) -**URL Pattern:** `https://CUSTOM_DOMAIN/shop/...` +**URL Pattern:** `https://CUSTOM_DOMAIN/storefront/...` **Example:** - Vendor name: "ACME Store" - Custom domain: `store.acme-corp.com` -- Customer Shop URL: `https://store.acme-corp.com/shop/products` +- Customer Storefront URL: `https://store.acme-corp.com/storefront/products` **Database Setup:** ```sql @@ -160,7 +198,7 @@ id | vendor_id | domain | is_active | is_verified ``` **How It Works:** -1. Customer visits `https://store.acme-corp.com/shop/products` +1. Customer visits `https://store.acme-corp.com/storefront/products` 2. `vendor_context_middleware` detects custom domain (not *.wizamart.com, not localhost) 3. Normalizes domain to `"store.acme-corp.com"` 4. Queries: `SELECT * FROM vendor_domains WHERE domain = 'store.acme-corp.com'` @@ -181,24 +219,25 @@ id | vendor_id | domain | is_active | is_verified ### 3. PATH-BASED MODE (Development Only) -**URL Pattern:** `http://localhost:PORT/vendors/VENDOR_CODE/shop/...` +**URL Pattern:** `http://localhost:PORT/platforms/PLATFORM_CODE/vendors/VENDOR_CODE/storefront/...` **Example:** -- Development: `http://localhost:8000/vendors/acme/shop/products` -- With port: `http://localhost:8000/vendors/acme/shop/products/123` +- Development: `http://localhost:8000/platforms/oms/vendors/acme/storefront/products` +- With port: `http://localhost:8000/platforms/loyalty/vendors/acme/storefront/products/123` **How It Works:** -1. Developer visits `http://localhost:8000/vendors/acme/shop/products` -2. `vendor_context_middleware` detects path-based routing pattern `/vendors/acme/...` -3. Extracts vendor code `"acme"` from the path -4. Looks up Vendor: `SELECT * FROM vendors WHERE subdomain = 'acme'` -5. Sets `request.state.vendor = Vendor(acme)` -6. Routes to shop pages +1. Developer visits `http://localhost:8000/platforms/oms/vendors/acme/storefront/products` +2. Platform middleware detects `/platforms/oms/` prefix, sets platform context +3. `vendor_context_middleware` detects path-based routing pattern `/vendors/acme/...` +4. Extracts vendor code `"acme"` from the path +5. Looks up Vendor: `SELECT * FROM vendors WHERE subdomain = 'acme'` +6. Sets `request.state.vendor = Vendor(acme)` +7. Routes to storefront pages **Advantages:** - Perfect for local development - No need to configure DNS/domains -- Test multiple vendors easily without domain setup +- Test multiple vendors and platforms easily without domain setup **Limitations:** - Only for development (not production-ready) @@ -210,34 +249,34 @@ id | vendor_id | domain | is_active | is_verified ### Subdomain/Custom Domain (PRODUCTION) ``` -https://acme.wizamart.com/shop/ → Homepage -https://acme.wizamart.com/shop/products → Product Catalog -https://acme.wizamart.com/shop/products/123 → Product Detail -https://acme.wizamart.com/shop/categories/electronics → Category Page -https://acme.wizamart.com/shop/cart → Shopping Cart -https://acme.wizamart.com/shop/checkout → Checkout -https://acme.wizamart.com/shop/search?q=laptop → Search Results -https://acme.wizamart.com/shop/account/login → Customer Login -https://acme.wizamart.com/shop/account/dashboard → Account Dashboard (Auth Required) -https://acme.wizamart.com/shop/account/orders → Order History (Auth Required) -https://acme.wizamart.com/shop/account/profile → Profile (Auth Required) +https://acme.wizamart.com/storefront/ → Homepage +https://acme.wizamart.com/storefront/products → Product Catalog +https://acme.wizamart.com/storefront/products/123 → Product Detail +https://acme.wizamart.com/storefront/categories/electronics → Category Page +https://acme.wizamart.com/storefront/cart → Shopping Cart +https://acme.wizamart.com/storefront/checkout → Checkout +https://acme.wizamart.com/storefront/search?q=laptop → Search Results +https://acme.wizamart.com/storefront/account/login → Customer Login +https://acme.wizamart.com/storefront/account/dashboard → Account Dashboard (Auth Required) +https://acme.wizamart.com/storefront/account/orders → Order History (Auth Required) +https://acme.wizamart.com/storefront/account/profile → Profile (Auth Required) ``` ### Path-Based (DEVELOPMENT) ``` -http://localhost:8000/vendors/acme/shop/ → Homepage -http://localhost:8000/vendors/acme/shop/products → Products -http://localhost:8000/vendors/acme/shop/products/123 → Product Detail -http://localhost:8000/vendors/acme/shop/cart → Cart -http://localhost:8000/vendors/acme/shop/checkout → Checkout -http://localhost:8000/vendors/acme/shop/account/login → Login +http://localhost:8000/platforms/oms/vendors/acme/storefront/ → Homepage +http://localhost:8000/platforms/oms/vendors/acme/storefront/products → Products +http://localhost:8000/platforms/oms/vendors/acme/storefront/products/123 → Product Detail +http://localhost:8000/platforms/oms/vendors/acme/storefront/cart → Cart +http://localhost:8000/platforms/oms/vendors/acme/storefront/checkout → Checkout +http://localhost:8000/platforms/oms/vendors/acme/storefront/account/login → Login ``` ### API Endpoints (Same for All Modes) ``` -GET /api/v1/public/vendors/1/products → Get vendor products -GET /api/v1/public/vendors/1/products/123 → Get product details -POST /api/v1/public/vendors/1/products/{id}/reviews → Add product review +GET /api/v1/storefront/vendors/1/products → Get vendor products +GET /api/v1/storefront/vendors/1/products/123 → Get product details +POST /api/v1/storefront/vendors/1/products/{id}/reviews → Add product review ``` --- @@ -266,19 +305,19 @@ POST /api/v1/public/vendors/1/products/{id}/reviews → Add product review ### Example: No Cross-Vendor Leakage ```python # Customer on acme.wizamart.com tries to access TechPro's products -# They make API call to /api/v1/public/vendors/2/products +# They make API call to /api/v1/storefront/vendors/2/products # Backend checks: vendor = get_vendor_from_request(request) # Returns Vendor(id=1, name="ACME") if vendor.id != requested_vendor_id: # if 1 != 2 - raise UnauthorizedShopAccessException() + raise UnauthorizedStorefrontAccessException() ``` --- ## Request Lifecycle: Complete Flow -### Scenario: Customer visits `https://acme.wizamart.com/shop/products` +### Scenario: Customer visits `https://acme.wizamart.com/storefront/products` ``` ┌─────────────────────────────────────────────────────────────────┐ @@ -286,7 +325,7 @@ if vendor.id != requested_vendor_id: # if 1 != 2 └─────────────────────────────────────────────────────────────────┘ method: GET host: acme.wizamart.com - path: /shop/products + path: /storefront/products ┌─────────────────────────────────────────────────────────────────┐ │ 2. MIDDLEWARE CHAIN │ @@ -299,9 +338,9 @@ if vendor.id != requested_vendor_id: # if 1 != 2 └─ Sets: request.state.vendor = Vendor(ACME Store) B) context_middleware - ├─ Checks path: "/shop/products" + ├─ Checks path: "/storefront/products" ├─ Has request.state.vendor? YES - └─ Sets: request.state.context_type = RequestContext.SHOP + └─ Sets: request.state.context_type = RequestContext.STOREFRONT C) theme_context_middleware ├─ Queries: SELECT * FROM vendor_themes WHERE vendor_id = 1 @@ -310,17 +349,17 @@ if vendor.id != requested_vendor_id: # if 1 != 2 ┌─────────────────────────────────────────────────────────────────┐ │ 3. ROUTE MATCHING │ └─────────────────────────────────────────────────────────────────┘ - Path: /shop/products - Matches: @router.get("/shop/products") - Handler: shop_products_page(request) + Path: /storefront/products + Matches: @router.get("/storefront/products") + Handler: storefront_products_page(request) ┌─────────────────────────────────────────────────────────────────┐ │ 4. HANDLER EXECUTES │ └─────────────────────────────────────────────────────────────────┘ - @router.get("/shop/products", response_class=HTMLResponse) - async def shop_products_page(request: Request): + @router.get("/storefront/products", response_class=HTMLResponse) + async def storefront_products_page(request: Request): return templates.TemplateResponse( - "shop/products.html", + "storefront/products.html", {"request": request} ) @@ -336,7 +375,7 @@ if vendor.id != requested_vendor_id: # if 1 != 2 ┌─────────────────────────────────────────────────────────────────┐ │ 6. JAVASCRIPT LOADS PRODUCTS (Client-Side) │ └─────────────────────────────────────────────────────────────────┘ - fetch(`/api/v1/public/vendors/1/products`) + fetch(`/api/v1/storefront/vendors/1/products`) .then(data => renderProducts(data.products, {theme})) ┌─────────────────────────────────────────────────────────────────┐ @@ -349,7 +388,7 @@ if vendor.id != requested_vendor_id: # if 1 != 2 ## Theme Integration -Each vendor's shop is fully branded with their custom theme: +Each vendor's storefront is fully branded with their custom theme: ```python # Theme loaded for https://acme.wizamart.com @@ -421,25 +460,26 @@ In Jinja2 template: **Current Solution: Double Router Mounting** -The application handles path-based routing by registering shop routes **twice** with different prefixes: +The application handles path-based routing by registering storefront routes **twice** with different prefixes: ```python # In main.py -app.include_router(shop_pages.router, prefix="/shop") -app.include_router(shop_pages.router, prefix="/vendors/{vendor_code}/shop") +app.include_router(storefront_pages.router, prefix="/storefront") +app.include_router(storefront_pages.router, prefix="/vendors/{vendor_code}/storefront") ``` **How This Works:** 1. **For Subdomain/Custom Domain Mode:** - - URL: `https://acme.wizamart.com/shop/products` - - Matches: First router with `/shop` prefix - - Route: `@router.get("/products")` → Full path: `/shop/products` + - URL: `https://acme.wizamart.com/storefront/products` + - Matches: First router with `/storefront` prefix + - Route: `@router.get("/products")` → Full path: `/storefront/products` 2. **For Path-Based Development Mode:** - - URL: `http://localhost:8000/vendors/acme/shop/products` - - Matches: Second router with `/vendors/{vendor_code}/shop` prefix - - Route: `@router.get("/products")` → Full path: `/vendors/{vendor_code}/shop/products` + - URL: `http://localhost:8000/platforms/oms/vendors/acme/storefront/products` + - Platform middleware strips `/platforms/oms/` prefix, sets platform context + - Matches: Second router with `/vendors/{vendor_code}/storefront` prefix + - Route: `@router.get("/products")` → Full path: `/vendors/{vendor_code}/storefront/products` - Bonus: `vendor_code` available as path parameter! **Benefits:** @@ -451,13 +491,13 @@ app.include_router(shop_pages.router, prefix="/vendors/{vendor_code}/shop") --- -## Authentication in Multi-Tenant Shop +## Authentication in Multi-Tenant Storefront Customer authentication uses vendor-scoped cookies: ```python -# Login sets cookie scoped to vendor's shop -Set-Cookie: customer_token=eyJ...; Path=/shop; HttpOnly; SameSite=Lax +# Login sets cookie scoped to vendor's storefront +Set-Cookie: customer_token=eyJ...; Path=/storefront; HttpOnly; SameSite=Lax # This prevents: # - Tokens leaking across vendors @@ -471,9 +511,9 @@ Set-Cookie: customer_token=eyJ...; Path=/shop; HttpOnly; SameSite=Lax | Mode | URL | Use Case | SSL | DNS | |------|-----|----------|-----|-----| -| Subdomain | `vendor.platform.com/shop` | Production (standard) | *.platform.com | Add subdomains | -| Custom Domain | `vendor-domain.com/shop` | Production (premium) | Per vendor | Vendor configures | -| Path-Based | `localhost:8000/vendors/v/shop` | Development only | None | None | +| Subdomain | `vendor.platform.com/storefront` | Production (standard) | *.platform.com | Add subdomains | +| Custom Domain | `vendor-domain.com/storefront` | Production (premium) | Per vendor | Vendor configures | +| Path-Based | `localhost:8000/platforms/{p}/vendors/{v}/storefront` | Development only | None | None | --- @@ -487,5 +527,5 @@ Set-Cookie: customer_token=eyJ...; Path=/shop; HttpOnly; SameSite=Lax --- -Generated: November 7, 2025 +Generated: January 30, 2026 Wizamart Version: Current Development diff --git a/docs/frontend/cdn-fallback-strategy.md b/docs/frontend/cdn-fallback-strategy.md index 8ec90338..bfaef79b 100644 --- a/docs/frontend/cdn-fallback-strategy.md +++ b/docs/frontend/cdn-fallback-strategy.md @@ -493,7 +493,7 @@ Content-Security-Policy: ## Related Documentation -- [Shop Frontend Architecture](shop/architecture.md) +- [Storefront Architecture](storefront/architecture.md) - [Vendor Frontend Architecture](vendor/architecture.md) - [Admin Frontend Architecture](admin/architecture.md) - [Production Deployment](../deployment/production.md) diff --git a/docs/frontend/shared/logging.md b/docs/frontend/shared/logging.md index 6b7a7f6d..567a8df5 100644 --- a/docs/frontend/shared/logging.md +++ b/docs/frontend/shared/logging.md @@ -395,7 +395,7 @@ window.LogConfig = { - [Admin Page Templates](../admin/page-templates.md) - [Vendor Page Templates](../vendor/page-templates.md) -- [Shop Page Templates](../shop/page-templates.md) +- [Storefront Page Templates](../storefront/page-templates.md) --- diff --git a/mkdocs.yml b/mkdocs.yml index d4609f62..3cd7daab 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,7 +64,7 @@ nav: # --- API Reference --- - API Reference: - Overview: api/index.md - - Shop API Reference: api/shop-api-reference.md + - Storefront API Reference: api/storefront-api-reference.md - Authentication: - Guide: api/authentication.md - Quick Reference: api/authentication-quick-reference.md @@ -106,12 +106,12 @@ nav: - Vendor Frontend: - Architecture: frontend/vendor/architecture.md - Page Templates: frontend/vendor/page-templates.md - - Shop Frontend: - - Architecture: frontend/shop/architecture.md - - Page Templates: frontend/shop/page-templates.md - - E-commerce Components Proposal: frontend/shop/ecommerce-components-proposal.md - - Authentication Pages: frontend/shop/authentication-pages.md - - Navigation Flow: frontend/shop/navigation-flow.md + - Storefront: + - Architecture: frontend/storefront/architecture.md + - Page Templates: frontend/storefront/page-templates.md + - E-commerce Components Proposal: frontend/storefront/ecommerce-components-proposal.md + - Authentication Pages: frontend/storefront/authentication-pages.md + - Navigation Flow: frontend/storefront/navigation-flow.md # --- Development --- - Development: