# app/routes/vendor_pages.py """ Vendor HTML page routes using Jinja2 templates. These routes serve HTML pages for vendor-facing interfaces. Follows the same minimal server-side rendering pattern as admin routes. All routes except /login require vendor authentication. Authentication failures redirect to /vendor/{vendor_code}/login. Routes: - GET /vendor/{vendor_code}/ → Redirect to login or dashboard - GET /vendor/{vendor_code}/login → Vendor login page - GET /vendor/{vendor_code}/onboarding → Vendor onboarding wizard - GET /vendor/{vendor_code}/dashboard → Vendor dashboard (requires onboarding) - GET /vendor/{vendor_code}/products → Product management - GET /vendor/{vendor_code}/orders → Order management - GET /vendor/{vendor_code}/customers → Customer management - GET /vendor/{vendor_code}/inventory → Inventory management - GET /vendor/{vendor_code}/marketplace → Marketplace imports - GET /vendor/{vendor_code}/team → Team management - GET /vendor/{vendor_code}/settings → Vendor settings """ import logging from fastapi import APIRouter, Depends, HTTPException, Path, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy.orm import Session from app.api.deps import ( get_current_vendor_from_cookie_or_header, get_current_vendor_optional, get_db, ) from app.services.onboarding_service import OnboardingService from app.services.platform_settings_service import platform_settings_service from app.templates_config import templates from models.database.user import User from models.database.vendor import Vendor logger = logging.getLogger(__name__) router = APIRouter() # ============================================================================ # HELPER: Build Vendor Dashboard Context # ============================================================================ def get_vendor_context( request: Request, db: Session, current_user: User, vendor_code: str, **extra_context, ) -> dict: """ Build template context for vendor dashboard pages. Resolves locale/currency using the platform settings service with vendor override support: 1. Vendor's storefront_locale (if set) 2. Platform's default from PlatformSettingsService 3. Environment variable 4. Hardcoded fallback Args: request: FastAPI request object db: Database session current_user: Authenticated vendor user vendor_code: Vendor subdomain/code **extra_context: Additional variables for template Returns: Dictionary with request, user, vendor, resolved locale/currency, and extra context """ # Load vendor from database vendor = db.query(Vendor).filter(Vendor.subdomain == vendor_code).first() # Get platform defaults platform_config = platform_settings_service.get_storefront_config(db) # Resolve with vendor override storefront_locale = platform_config["locale"] storefront_currency = platform_config["currency"] if vendor and vendor.storefront_locale: storefront_locale = vendor.storefront_locale context = { "request": request, "user": current_user, "vendor": vendor, "vendor_code": vendor_code, "storefront_locale": storefront_locale, "storefront_currency": storefront_currency, "dashboard_language": vendor.dashboard_language if vendor else "en", } # Add any extra context if extra_context: context.update(extra_context) logger.debug( "[VENDOR_CONTEXT] Context built", extra={ "vendor_id": vendor.id if vendor else None, "vendor_code": vendor_code, "storefront_locale": storefront_locale, "storefront_currency": storefront_currency, "extra_keys": list(extra_context.keys()) if extra_context else [], }, ) return context # ============================================================================ # PUBLIC ROUTES (No Authentication Required) # ============================================================================ @router.get("/{vendor_code}", response_class=RedirectResponse, include_in_schema=False) async def vendor_root_no_slash(vendor_code: str = Path(..., description="Vendor code")): """ Redirect /vendor/{code} (no trailing slash) to login page. Handles requests without trailing slash. """ return RedirectResponse(url=f"/vendor/{vendor_code}/login", status_code=302) @router.get("/{vendor_code}/", response_class=RedirectResponse, include_in_schema=False) async def vendor_root( vendor_code: str = Path(..., description="Vendor code"), current_user: User | None = Depends(get_current_vendor_optional), ): """ Redirect /vendor/{code}/ based on authentication status. - Authenticated vendor users → /vendor/{code}/dashboard - Unauthenticated users → /vendor/{code}/login """ if current_user: # User is already logged in as vendor, redirect to dashboard return RedirectResponse(url=f"/vendor/{vendor_code}/dashboard", status_code=302) return RedirectResponse(url=f"/vendor/{vendor_code}/login", status_code=302) @router.get( "/{vendor_code}/login", response_class=HTMLResponse, include_in_schema=False ) async def vendor_login_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User | None = Depends(get_current_vendor_optional), ): """ Render vendor login page. If user is already authenticated as vendor, redirect to dashboard. Otherwise, show login form. JavaScript will: - Load vendor info via API - Handle login form submission - Redirect to dashboard on success """ if current_user: # User is already logged in as vendor, redirect to dashboard return RedirectResponse(url=f"/vendor/{vendor_code}/dashboard", status_code=302) return templates.TemplateResponse( "vendor/login.html", { "request": request, "vendor_code": vendor_code, }, ) # ============================================================================ # AUTHENTICATED ROUTES (Vendor Users Only) # ============================================================================ @router.get( "/{vendor_code}/onboarding", response_class=HTMLResponse, include_in_schema=False ) async def vendor_onboarding_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render vendor onboarding wizard. Mandatory 4-step wizard that must be completed before accessing dashboard: 1. Company Profile Setup 2. Letzshop API Configuration 3. Product & Order Import Configuration 4. Order Sync (historical import) If onboarding is already completed, redirects to dashboard. """ # Check if onboarding is completed onboarding_service = OnboardingService(db) if onboarding_service.is_completed(current_user.token_vendor_id): return RedirectResponse( url=f"/vendor/{vendor_code}/dashboard", status_code=302, ) return templates.TemplateResponse( "vendor/onboarding.html", get_vendor_context(request, db, current_user, vendor_code), ) @router.get( "/{vendor_code}/dashboard", response_class=HTMLResponse, include_in_schema=False ) async def vendor_dashboard_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render vendor dashboard. Redirects to onboarding if not completed. JavaScript will: - Load vendor info via API - Load dashboard stats via API - Load recent orders via API - Handle all interactivity """ # Check if onboarding is completed onboarding_service = OnboardingService(db) if not onboarding_service.is_completed(current_user.token_vendor_id): return RedirectResponse( url=f"/vendor/{vendor_code}/onboarding", status_code=302, ) return templates.TemplateResponse( "vendor/dashboard.html", get_vendor_context(request, db, current_user, vendor_code), ) # ============================================================================ # PRODUCT MANAGEMENT # ============================================================================ @router.get( "/{vendor_code}/products", response_class=HTMLResponse, include_in_schema=False ) async def vendor_products_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render products management page. JavaScript loads product list via API. """ return templates.TemplateResponse( "vendor/products.html", get_vendor_context(request, db, current_user, vendor_code), ) @router.get( "/{vendor_code}/products/create", response_class=HTMLResponse, include_in_schema=False ) async def vendor_product_create_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render product creation page. JavaScript handles form submission via API. """ return templates.TemplateResponse( "vendor/product-create.html", get_vendor_context(request, db, current_user, vendor_code), ) # ============================================================================ # ORDER MANAGEMENT # ============================================================================ @router.get( "/{vendor_code}/orders", response_class=HTMLResponse, include_in_schema=False ) async def vendor_orders_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render orders management page. JavaScript loads order list via API. """ return templates.TemplateResponse( "vendor/orders.html", get_vendor_context(request, db, current_user, vendor_code), ) @router.get( "/{vendor_code}/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False, ) async def vendor_order_detail_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), order_id: int = Path(..., description="Order ID"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render order detail page. Shows comprehensive order information including: - Order header and status - Customer and shipping details - Order items with shipment status - Invoice creation/viewing - Partial shipment controls JavaScript loads order details via API. """ return templates.TemplateResponse( "vendor/order-detail.html", get_vendor_context(request, db, current_user, vendor_code, order_id=order_id), ) # ============================================================================ # CUSTOMER MANAGEMENT # ============================================================================ @router.get( "/{vendor_code}/customers", response_class=HTMLResponse, include_in_schema=False ) async def vendor_customers_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render customers management page. JavaScript loads customer list via API. """ return templates.TemplateResponse( "vendor/customers.html", get_vendor_context(request, db, current_user, vendor_code), ) # ============================================================================ # MEDIA LIBRARY # ============================================================================ @router.get( "/{vendor_code}/media", response_class=HTMLResponse, include_in_schema=False ) async def vendor_media_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render media library page. JavaScript loads media files via API. """ return templates.TemplateResponse( "vendor/media.html", get_vendor_context(request, db, current_user, vendor_code), ) # ============================================================================ # MESSAGING # ============================================================================ @router.get( "/{vendor_code}/messages", response_class=HTMLResponse, include_in_schema=False ) async def vendor_messages_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render messages page. JavaScript loads conversations and messages via API. """ return templates.TemplateResponse( "vendor/messages.html", get_vendor_context(request, db, current_user, vendor_code), ) @router.get( "/{vendor_code}/messages/{conversation_id}", response_class=HTMLResponse, include_in_schema=False, ) async def vendor_message_detail_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), conversation_id: int = Path(..., description="Conversation ID"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render message detail page. Shows the full conversation thread. """ return templates.TemplateResponse( "vendor/messages.html", get_vendor_context( request, db, current_user, vendor_code, conversation_id=conversation_id ), ) # ============================================================================ # INVENTORY MANAGEMENT # ============================================================================ @router.get( "/{vendor_code}/inventory", response_class=HTMLResponse, include_in_schema=False ) async def vendor_inventory_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render inventory management page. JavaScript loads inventory data via API. """ return templates.TemplateResponse( "vendor/inventory.html", get_vendor_context(request, db, current_user, vendor_code), ) # ============================================================================ # MARKETPLACE IMPORTS # ============================================================================ @router.get( "/{vendor_code}/marketplace", response_class=HTMLResponse, include_in_schema=False ) async def vendor_marketplace_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render marketplace import page. JavaScript loads import jobs and products via API. """ return templates.TemplateResponse( "vendor/marketplace.html", get_vendor_context(request, db, current_user, vendor_code), ) # ============================================================================ # LETZSHOP INTEGRATION # ============================================================================ @router.get( "/{vendor_code}/letzshop", response_class=HTMLResponse, include_in_schema=False ) async def vendor_letzshop_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render Letzshop integration page. JavaScript loads orders, credentials status, and handles fulfillment operations. """ return templates.TemplateResponse( "vendor/letzshop.html", get_vendor_context(request, db, current_user, vendor_code), ) # ============================================================================ # INVOICES # ============================================================================ @router.get( "/{vendor_code}/invoices", response_class=HTMLResponse, include_in_schema=False ) async def vendor_invoices_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render invoices management page. JavaScript loads invoices via API. """ return templates.TemplateResponse( "vendor/invoices.html", get_vendor_context(request, db, current_user, vendor_code), ) # ============================================================================ # TEAM MANAGEMENT # ============================================================================ @router.get("/{vendor_code}/team", response_class=HTMLResponse, include_in_schema=False) async def vendor_team_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render team management page. JavaScript loads team members via API. """ return templates.TemplateResponse( "vendor/team.html", get_vendor_context(request, db, current_user, vendor_code), ) # ============================================================================ # PROFILE & SETTINGS # ============================================================================ @router.get( "/{vendor_code}/profile", response_class=HTMLResponse, include_in_schema=False ) async def vendor_profile_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render vendor profile page. User can manage their personal profile information. """ return templates.TemplateResponse( "vendor/profile.html", get_vendor_context(request, db, current_user, vendor_code), ) @router.get( "/{vendor_code}/settings", response_class=HTMLResponse, include_in_schema=False ) async def vendor_settings_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render vendor settings page. JavaScript loads settings via API. """ return templates.TemplateResponse( "vendor/settings.html", get_vendor_context(request, db, current_user, vendor_code), ) @router.get( "/{vendor_code}/email-templates", response_class=HTMLResponse, include_in_schema=False ) async def vendor_email_templates_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render vendor email templates customization page. Allows vendors to override platform email templates. """ return templates.TemplateResponse( "vendor/email-templates.html", get_vendor_context(request, db, current_user, vendor_code), ) @router.get( "/{vendor_code}/billing", response_class=HTMLResponse, include_in_schema=False ) async def vendor_billing_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render billing and subscription management page. JavaScript loads subscription status, tiers, and invoices via API. """ return templates.TemplateResponse( "vendor/billing.html", get_vendor_context(request, db, current_user, vendor_code), ) # ============================================================================ # NOTIFICATIONS # ============================================================================ @router.get( "/{vendor_code}/notifications", response_class=HTMLResponse, include_in_schema=False ) async def vendor_notifications_page( request: Request, vendor_code: str = Path(..., description="Vendor code"), current_user: User = Depends(get_current_vendor_from_cookie_or_header), db: Session = Depends(get_db), ): """ Render notifications center page. JavaScript loads notifications via API. """ return templates.TemplateResponse( "vendor/notifications.html", get_vendor_context(request, db, current_user, vendor_code), ) # ============================================================================ # ANALYTICS # ============================================================================ # NOTE: Analytics routes moved to self-contained module: app.modules.analytics.routes.pages.vendor # Routes are registered directly in main.py from the Analytics module # This includes: # - /{vendor_code}/analytics (dashboard) # ============================================================================ # CONTENT PAGES MANAGEMENT & CMS # ============================================================================ # NOTE: CMS routes moved to self-contained module: app.modules.cms.routes.pages.vendor # Routes are registered directly in main.py from the CMS module # This includes: # - /{vendor_code}/content-pages (list) # - /{vendor_code}/content-pages/create # - /{vendor_code}/content-pages/{page_id}/edit # - /{vendor_code}/{slug} (catch-all CMS page viewer)