# app/routes/shop_pages.py """ Shop/Customer HTML page routes using Jinja2 templates. These routes serve the public-facing shop interface for customers. Authentication required only for account pages. AUTHENTICATION: - Public pages (catalog, products): No auth required - Account pages (dashboard, orders): Requires customer authentication - Customer authentication accepts: * customer_token cookie (path=/shop) - for page navigation * Authorization header - for API calls - Customers CANNOT access admin or vendor routes Routes (all mounted at /shop/* or /vendors/{code}/shop/* prefix): - GET / → Shop homepage / product catalog - GET /products → Product catalog - GET /products/{id} → Product detail page - GET /categories/{slug} → Category products - GET /cart → Shopping cart - GET /checkout → Checkout process - GET /account/register → Customer registration - GET /account/login → Customer login - GET /account/dashboard → Customer dashboard (auth required) - GET /account/orders → Order history (auth required) - GET /account/orders/{id} → Order detail (auth required) - GET /account/profile → Customer profile (auth required) - GET /account/addresses → Address management (auth required) - GET /{slug} → Dynamic content pages (CMS): /about, /faq, /contact, etc. """ import logging from fastapi import APIRouter, Depends, Path, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy.orm import Session from app.api.deps import get_current_customer_from_cookie_or_header, get_db from app.services.content_page_service import content_page_service from models.database.customer import Customer router = APIRouter() templates = Jinja2Templates(directory="app/templates") logger = logging.getLogger(__name__) # ============================================================================ # HELPER: Build Shop Template Context # ============================================================================ def get_shop_context(request: Request, db: Session = None, **extra_context) -> dict: """ Build template context for shop pages. Automatically includes vendor and theme from middleware request.state. Additional context can be passed as keyword arguments. Args: request: FastAPI request object with vendor/theme in state db: Optional database session for loading navigation pages **extra_context: Additional variables for template (user, product_id, etc.) Returns: Dictionary with request, vendor, theme, navigation pages, and extra context Example: # Simple usage get_shop_context(request) # With database session for navigation get_shop_context(request, db=db) # With extra data get_shop_context(request, db=db, user=current_user, product_id=123) """ # Extract from middleware state vendor = getattr(request.state, "vendor", None) theme = getattr(request.state, "theme", None) clean_path = getattr(request.state, "clean_path", request.url.path) vendor_context = getattr(request.state, "vendor_context", None) # Get detection method from vendor_context access_method = ( vendor_context.get("detection_method", "unknown") if vendor_context else "unknown" ) if vendor is None: logger.warning( "[SHOP_CONTEXT] Vendor not found in request.state", extra={ "path": request.url.path, "host": request.headers.get("host", ""), "has_vendor": False, }, ) # Calculate base URL for links # - Domain/subdomain access: base_url = "/" # - Path-based access: base_url = "/vendor/{vendor_code}/" or "/vendors/{vendor_code}/" base_url = "/" if access_method == "path" and vendor: # Use the full_prefix from vendor_context to determine which pattern was used full_prefix = ( vendor_context.get("full_prefix", "/vendor/") if vendor_context else "/vendor/" ) base_url = f"{full_prefix}{vendor.subdomain}/" # Load footer navigation pages from CMS if db session provided footer_pages = [] header_pages = [] if db and vendor: try: vendor_id = vendor.id # Get pages configured to show in footer footer_pages = content_page_service.list_pages_for_vendor( db, vendor_id=vendor_id, footer_only=True, include_unpublished=False ) # Get pages configured to show in header header_pages = content_page_service.list_pages_for_vendor( db, vendor_id=vendor_id, header_only=True, include_unpublished=False ) except Exception as e: logger.error( "[SHOP_CONTEXT] Failed to load navigation pages", extra={"error": str(e), "vendor_id": vendor.id if vendor else None}, ) context = { "request": request, "vendor": vendor, "theme": theme, "clean_path": clean_path, "access_method": access_method, "base_url": base_url, "footer_pages": footer_pages, "header_pages": header_pages, } # Add any extra context (user, product_id, category_slug, etc.) if extra_context: context.update(extra_context) logger.debug( "[SHOP_CONTEXT] Context built", extra={ "vendor_id": vendor.id if vendor else None, "vendor_name": vendor.name if vendor else None, "vendor_subdomain": vendor.subdomain if vendor else None, "has_theme": theme is not None, "access_method": access_method, "base_url": base_url, "footer_pages_count": len(footer_pages), "header_pages_count": len(header_pages), "extra_keys": list(extra_context.keys()) if extra_context else [], }, ) return context # ============================================================================ # PUBLIC SHOP ROUTES (No Authentication Required) # ============================================================================ @router.get("/", response_class=HTMLResponse, include_in_schema=False) @router.get("/products", response_class=HTMLResponse, include_in_schema=False) async def shop_products_page(request: Request, db: Session = Depends(get_db)): """ Render shop homepage / product catalog. Shows featured products and categories. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/products.html", get_shop_context(request, db=db) ) @router.get( "/products/{product_id}", response_class=HTMLResponse, include_in_schema=False ) async def shop_product_detail_page( request: Request, product_id: int = Path(..., description="Product ID") ): """ Render product detail page. Shows product information, images, reviews, and buy options. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/product.html", get_shop_context(request, product_id=product_id) ) @router.get( "/categories/{category_slug}", response_class=HTMLResponse, include_in_schema=False ) async def shop_category_page( request: Request, category_slug: str = Path(..., description="Category slug") ): """ Render category products page. Shows all products in a specific category. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/category.html", get_shop_context(request, category_slug=category_slug) ) @router.get("/cart", response_class=HTMLResponse, include_in_schema=False) async def shop_cart_page(request: Request): """ Render shopping cart page. Shows cart items and allows quantity updates. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse("shop/cart.html", get_shop_context(request)) @router.get("/checkout", response_class=HTMLResponse, include_in_schema=False) async def shop_checkout_page(request: Request): """ Render checkout page. Handles shipping, payment, and order confirmation. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse("shop/checkout.html", get_shop_context(request)) @router.get("/search", response_class=HTMLResponse, include_in_schema=False) async def shop_search_page(request: Request): """ Render search results page. Shows products matching search query. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse("shop/search.html", get_shop_context(request)) # ============================================================================ # CUSTOMER ACCOUNT - PUBLIC ROUTES (No Authentication) # ============================================================================ @router.get("/account/register", response_class=HTMLResponse, include_in_schema=False) async def shop_register_page(request: Request): """ Render customer registration page. No authentication required. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/account/register.html", get_shop_context(request) ) @router.get("/account/login", response_class=HTMLResponse, include_in_schema=False) async def shop_login_page(request: Request): """ Render customer login page. No authentication required. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/account/login.html", get_shop_context(request) ) @router.get( "/account/forgot-password", response_class=HTMLResponse, include_in_schema=False ) async def shop_forgot_password_page(request: Request): """ Render forgot password page. Allows customers to reset their password. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/account/forgot-password.html", get_shop_context(request) ) # ============================================================================ # CUSTOMER ACCOUNT - AUTHENTICATED ROUTES # ============================================================================ @router.get("/account", response_class=RedirectResponse, include_in_schema=False) @router.get("/account/", response_class=RedirectResponse, include_in_schema=False) async def shop_account_root(request: Request): """ Redirect /shop/account or /shop/account/ to dashboard. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) # Get base_url from context for proper redirect vendor = getattr(request.state, "vendor", None) vendor_context = getattr(request.state, "vendor_context", None) access_method = ( vendor_context.get("detection_method", "unknown") if vendor_context else "unknown" ) base_url = "/" if access_method == "path" and vendor: full_prefix = ( vendor_context.get("full_prefix", "/vendor/") if vendor_context else "/vendor/" ) base_url = f"{full_prefix}{vendor.subdomain}/" return RedirectResponse(url=f"{base_url}shop/account/dashboard", status_code=302) @router.get("/account/dashboard", response_class=HTMLResponse, include_in_schema=False) async def shop_account_dashboard_page( request: Request, current_customer: Customer = Depends(get_current_customer_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render customer account dashboard. Shows account overview, recent orders, and quick links. Requires customer authentication. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/account/dashboard.html", get_shop_context(request, user=current_customer) ) @router.get("/account/orders", response_class=HTMLResponse, include_in_schema=False) async def shop_orders_page( request: Request, current_customer: Customer = Depends(get_current_customer_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render customer orders history page. Shows all past and current orders. Requires customer authentication. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/account/orders.html", get_shop_context(request, user=current_customer) ) @router.get( "/account/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False ) async def shop_order_detail_page( request: Request, order_id: int = Path(..., description="Order ID"), current_customer: Customer = Depends(get_current_customer_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render customer order detail page. Shows detailed order information and tracking. Requires customer authentication. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/account/order-detail.html", get_shop_context(request, user=current_customer, order_id=order_id), ) @router.get("/account/profile", response_class=HTMLResponse, include_in_schema=False) async def shop_profile_page( request: Request, current_customer: Customer = Depends(get_current_customer_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render customer profile page. Edit personal information and preferences. Requires customer authentication. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/account/profile.html", get_shop_context(request, user=current_customer) ) @router.get("/account/addresses", response_class=HTMLResponse, include_in_schema=False) async def shop_addresses_page( request: Request, current_customer: Customer = Depends(get_current_customer_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render customer addresses management page. Manage shipping and billing addresses. Requires customer authentication. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/account/addresses.html", get_shop_context(request, user=current_customer) ) @router.get("/account/wishlist", response_class=HTMLResponse, include_in_schema=False) async def shop_wishlist_page( request: Request, current_customer: Customer = Depends(get_current_customer_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render customer wishlist page. View and manage saved products. Requires customer authentication. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/account/wishlist.html", get_shop_context(request, user=current_customer) ) @router.get("/account/settings", response_class=HTMLResponse, include_in_schema=False) async def shop_settings_page( request: Request, current_customer: Customer = Depends(get_current_customer_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render customer account settings page. Configure notifications, privacy, and preferences. Requires customer authentication. """ logger.debug( "[SHOP_HANDLER] shop_products_page REACHED", extra={ "path": request.url.path, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) return templates.TemplateResponse( "shop/account/settings.html", get_shop_context(request, user=current_customer) ) # ============================================================================ # DYNAMIC CONTENT PAGES (CMS) # ============================================================================ @router.get("/{slug}", response_class=HTMLResponse, include_in_schema=False) async def generic_content_page( request: Request, slug: str = Path(..., description="Content page slug"), db: Session = Depends(get_db), ): """ Generic content page handler (CMS). Handles dynamic content pages like: - /about, /faq, /contact, /shipping, /returns, /privacy, /terms, etc. Features: - Two-tier system: Vendor overrides take priority, fallback to platform defaults - Only shows published pages - Returns 404 if page not found This route MUST be defined last in the router to avoid conflicts with specific routes (like /products, /cart, /account, etc.) """ from fastapi import HTTPException logger.debug( "[SHOP_HANDLER] generic_content_page REACHED", extra={ "path": request.url.path, "slug": slug, "vendor": getattr(request.state, "vendor", "NOT SET"), "context": getattr(request.state, "context_type", "NOT SET"), }, ) vendor = getattr(request.state, "vendor", None) vendor_id = vendor.id if vendor else None # Load content page from database (vendor override → platform default) page = content_page_service.get_page_for_vendor( db, slug=slug, vendor_id=vendor_id, include_unpublished=False ) if not page: logger.warning( "[SHOP_HANDLER] Content page not found", extra={ "slug": slug, "vendor_id": vendor_id, "vendor_name": vendor.name if vendor else None, }, ) raise HTTPException(status_code=404, detail=f"Page not found: {slug}") logger.info( "[SHOP_HANDLER] Content page found", extra={ "slug": slug, "page_id": page.id, "page_title": page.title, "is_vendor_override": page.vendor_id is not None, "vendor_id": vendor_id, }, ) return templates.TemplateResponse( "shop/content-page.html", get_shop_context(request, page=page) ) # ============================================================================ # DEBUG ENDPOINTS - For troubleshooting context issues # ============================================================================ @router.get("/debug/context", response_class=HTMLResponse, include_in_schema=False) async def debug_context(request: Request): """ DEBUG ENDPOINT: Display request context. Shows what's available in request.state. Useful for troubleshooting template variable issues. URL: /shop/debug/context """ vendor = getattr(request.state, "vendor", None) theme = getattr(request.state, "theme", None) debug_info = { "path": request.url.path, "host": request.headers.get("host", ""), "vendor": { "found": vendor is not None, "id": vendor.id if vendor else None, "name": vendor.name if vendor else None, "subdomain": vendor.subdomain if vendor else None, "is_active": vendor.is_active if vendor else None, }, "theme": { "found": theme is not None, "name": theme.get("theme_name") if theme else None, }, "clean_path": getattr(request.state, "clean_path", "NOT SET"), "context_type": str(getattr(request.state, "context_type", "NOT SET")), } # Return as JSON-like HTML for easy reading import json html_content = f"""
{json.dumps(debug_info, indent=2)}
Vendor: {"✓ Found" if vendor else "✗ Not Found"}
Theme: {"✓ Found" if theme else "✗ Not Found"}
Context Type: {str(getattr(request.state, "context_type", "NOT SET"))}
""" return HTMLResponse(content=html_content)