From 843703258fe578b5690468e219d6f31da2c52de6 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sat, 31 Jan 2026 21:23:26 +0100 Subject: [PATCH] chore: delete empty app/routes directory The legacy route files were migrated to modules. The directory only contained an empty __init__.py. Updated docs to reflect new location. Co-Authored-By: Claude Opus 4.5 --- app/routes/__init__.py | 0 app/routes/admin_pages.py | 1585 ----------------------------- app/routes/platform_pages.py | 435 -------- app/routes/storefront_pages.py | 865 ---------------- app/routes/vendor_pages.py | 691 ------------- docs/architecture/request-flow.md | 4 +- 6 files changed, 2 insertions(+), 3578 deletions(-) delete mode 100644 app/routes/__init__.py delete mode 100644 app/routes/admin_pages.py delete mode 100644 app/routes/platform_pages.py delete mode 100644 app/routes/storefront_pages.py delete mode 100644 app/routes/vendor_pages.py diff --git a/app/routes/__init__.py b/app/routes/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/app/routes/admin_pages.py b/app/routes/admin_pages.py deleted file mode 100644 index d3486bb8..00000000 --- a/app/routes/admin_pages.py +++ /dev/null @@ -1,1585 +0,0 @@ -# app/routes/admin_pages.py -""" -Admin HTML page routes using Jinja2 templates. - -These routes return rendered HTML pages (response_class=HTMLResponse). -Separate from admin API routes which return JSON data. - -All routes require admin authentication except /login. -Authentication failures redirect to /admin/login. - -Routes: -- GET / → Redirect to /admin/login -- GET /login → Admin login page (no auth) -- GET /dashboard → Admin dashboard (auth required) -- GET /companies → Company list page (auth required) -- GET /companies/create → Create company form (auth required) -- GET /companies/{company_id}/edit → Edit company form (auth required) -- GET /vendors → Vendor list page (auth required) -- GET /vendors/create → Create vendor form (auth required) -- GET /vendors/{vendor_code} → Vendor details (auth required) -- GET /vendors/{vendor_code}/edit → Edit vendor form (auth required) -- GET /vendors/{vendor_code}/domains → Vendor domains management (auth required) -- GET /vendor-themes → Vendor themes selection page (auth required) -- GET /vendors/{vendor_code}/theme → Vendor theme editor (auth required) -- GET /admin-users → Admin users management (super admin only) -- GET /admin-users/create → Create admin user (super admin only) -- GET /admin-users/{id} → Admin user detail (super admin only) -- GET /users → Redirects to /admin/admin-users -- GET /customers → Customer management page (auth required) -- GET /inventory → Inventory management page (auth required) -- GET /orders → Orders management page (auth required) -- GET /imports → Import history page (auth required) -- GET /marketplace-products → Marketplace products catalog (auth required) -- GET /vendor-products → Vendor products catalog (auth required) -- GET /settings → Settings page (auth required) -- GET /platform-homepage → Platform homepage manager (auth required) -- GET /content-pages → Content pages list (auth required) -- GET /content-pages/create → Create content page (auth required) -- GET /content-pages/{page_id}/edit → Edit content page (auth required) -- GET /code-quality → Code quality dashboard (auth required) -- GET /code-quality/violations → Violations list (auth required) -- GET /code-quality/violations/{violation_id} → Violation details (auth required) -""" - -from fastapi import APIRouter, Depends, Path, Request -from fastapi.responses import HTMLResponse, RedirectResponse -from sqlalchemy.orm import Session - -from app.api.deps import ( - get_current_admin_optional, - get_db, - require_menu_access, -) -from app.core.config import settings -from app.templates_config import templates -from models.database.admin_menu_config import FrontendType -from models.database.user import User - -router = APIRouter() - - -# ============================================================================ -# PUBLIC ROUTES (No Authentication Required) -# ============================================================================ - - -@router.get("/", response_class=RedirectResponse, include_in_schema=False) -async def admin_root( - current_user: User | None = Depends(get_current_admin_optional), -): - """ - Redirect /admin/ based on authentication status. - - - Authenticated admin users → /admin/dashboard - - Unauthenticated users → /admin/login - """ - if current_user: - # User is already logged in as admin, redirect to dashboard - return RedirectResponse(url="/admin/dashboard", status_code=302) - - return RedirectResponse(url="/admin/login", status_code=302) - - -@router.get("/login", response_class=HTMLResponse, include_in_schema=False) -async def admin_login_page( - request: Request, current_user: User | None = Depends(get_current_admin_optional) -): - """ - Render admin login page. - - If user is already authenticated as admin, redirect to dashboard. - Otherwise, show login form. - """ - if current_user: - # User is already logged in as admin, redirect to dashboard - return RedirectResponse(url="/admin/dashboard", status_code=302) - - return templates.TemplateResponse("admin/login.html", {"request": request}) - - -@router.get("/select-platform", response_class=HTMLResponse, include_in_schema=False) -async def admin_select_platform_page( - request: Request, - current_user: User | None = Depends(get_current_admin_optional), -): - """ - Render platform selection page for platform admins. - - Platform admins with access to multiple platforms must select - which platform they want to manage before accessing the dashboard. - Super admins are redirected to dashboard (they have global access). - """ - if not current_user: - # Not logged in, redirect to login - return RedirectResponse(url="/admin/login", status_code=302) - - if current_user.is_super_admin: - # Super admins don't need platform selection - return RedirectResponse(url="/admin/dashboard", status_code=302) - - return templates.TemplateResponse( - "admin/select-platform.html", - {"request": request, "user": current_user}, - ) - - -# ============================================================================ -# AUTHENTICATED ROUTES (Admin Only) -# ============================================================================ - - -@router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False) -async def admin_dashboard_page( - request: Request, - current_user: User = Depends(require_menu_access("dashboard", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render admin dashboard page. - Shows platform statistics and recent activity. - """ - return templates.TemplateResponse( - "admin/dashboard.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# COMPANY MANAGEMENT ROUTES -# ============================================================================ - - -@router.get("/companies", response_class=HTMLResponse, include_in_schema=False) -async def admin_companies_list_page( - request: Request, - current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render companies management page. - Shows list of all companies with stats. - """ - return templates.TemplateResponse( - "admin/companies.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/companies/create", response_class=HTMLResponse, include_in_schema=False) -async def admin_company_create_page( - request: Request, - current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render company creation form. - """ - return templates.TemplateResponse( - "admin/company-create.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/companies/{company_id}", response_class=HTMLResponse, include_in_schema=False -) -async def admin_company_detail_page( - request: Request, - company_id: int = Path(..., description="Company ID"), - current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render company detail view. - """ - return templates.TemplateResponse( - "admin/company-detail.html", - { - "request": request, - "user": current_user, - "company_id": company_id, - }, - ) - - -@router.get( - "/companies/{company_id}/edit", response_class=HTMLResponse, include_in_schema=False -) -async def admin_company_edit_page( - request: Request, - company_id: int = Path(..., description="Company ID"), - current_user: User = Depends(require_menu_access("companies", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render company edit form. - """ - return templates.TemplateResponse( - "admin/company-edit.html", - { - "request": request, - "user": current_user, - "company_id": company_id, - }, - ) - - -# ============================================================================ -# VENDOR MANAGEMENT ROUTES -# ============================================================================ - - -@router.get("/vendors", response_class=HTMLResponse, include_in_schema=False) -async def admin_vendors_list_page( - request: Request, - current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendors management page. - Shows list of all vendors with stats. - """ - return templates.TemplateResponse( - "admin/vendors.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/vendors/create", response_class=HTMLResponse, include_in_schema=False) -async def admin_vendor_create_page( - request: Request, - current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendor creation form. - """ - return templates.TemplateResponse( - "admin/vendor-create.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/vendors/{vendor_code}", response_class=HTMLResponse, include_in_schema=False -) -async def admin_vendor_detail_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendor detail page. - Shows full vendor information. - """ - return templates.TemplateResponse( - "admin/vendor-detail.html", - { - "request": request, - "user": current_user, - "vendor_code": vendor_code, - }, - ) - - -@router.get( - "/vendors/{vendor_code}/edit", response_class=HTMLResponse, include_in_schema=False -) -async def admin_vendor_edit_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendor edit form. - """ - return templates.TemplateResponse( - "admin/vendor-edit.html", - { - "request": request, - "user": current_user, - "vendor_code": vendor_code, - }, - ) - - -# ============================================================================ -# VENDOR DOMAINS ROUTES -# ============================================================================ - - -@router.get( - "/vendors/{vendor_code}/domains", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_vendor_domains_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(require_menu_access("vendors", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendor domains management page. - Shows custom domains, verification status, and DNS configuration. - """ - return templates.TemplateResponse( - "admin/vendor-domains.html", - { - "request": request, - "user": current_user, - "vendor_code": vendor_code, - }, - ) - - -# ============================================================================ -# VENDOR THEMES ROUTES -# ============================================================================ - - -@router.get("/vendor-themes", response_class=HTMLResponse, include_in_schema=False) -async def admin_vendor_themes_page( - request: Request, - current_user: User = Depends(require_menu_access("vendor-themes", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendor themes selection page. - Allows admins to select a vendor to customize their theme. - """ - return templates.TemplateResponse( - "admin/vendor-themes.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/vendors/{vendor_code}/theme", response_class=HTMLResponse, include_in_schema=False -) -async def admin_vendor_theme_page( - request: Request, - vendor_code: str = Path(..., description="Vendor code"), - current_user: User = Depends(require_menu_access("vendor-themes", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendor theme customization page. - Allows admins to customize colors, fonts, layout, and branding. - """ - return templates.TemplateResponse( - "admin/vendor-theme.html", - { - "request": request, - "user": current_user, - "vendor_code": vendor_code, - }, - ) - - -# ============================================================================ -# ADMIN USER MANAGEMENT ROUTES (Super Admin Only) -# ============================================================================ - - -@router.get("/admin-users", response_class=HTMLResponse, include_in_schema=False) -async def admin_users_list_page( - request: Request, - current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render admin users management page. - Shows list of all admin users (super admins and platform admins). - Super admin only (menu is in super_admin_only section). - """ - - return templates.TemplateResponse( - "admin/admin-users.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/admin-users/create", response_class=HTMLResponse, include_in_schema=False) -async def admin_user_create_page( - request: Request, - current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render admin user creation form. - Super admin only (menu is in super_admin_only section). - """ - - return templates.TemplateResponse( - "admin/user-create.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/admin-users/{user_id}", response_class=HTMLResponse, include_in_schema=False -) -async def admin_user_detail_page( - request: Request, - user_id: int = Path(..., description="User ID"), - current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render admin user detail view. - Super admin only (menu is in super_admin_only section). - """ - - return templates.TemplateResponse( - "admin/admin-user-detail.html", - { - "request": request, - "user": current_user, - "user_id": user_id, - }, - ) - - -@router.get( - "/admin-users/{user_id}/edit", response_class=HTMLResponse, include_in_schema=False -) -async def admin_user_edit_page( - request: Request, - user_id: int = Path(..., description="User ID"), - current_user: User = Depends(require_menu_access("admin-users", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render admin user edit form. - Super admin only (menu is in super_admin_only section). - """ - - return templates.TemplateResponse( - "admin/admin-user-edit.html", - { - "request": request, - "user": current_user, - "user_id": user_id, - }, - ) - - -# ============================================================================ -# USER MANAGEMENT ROUTES (Legacy - Redirects) -# ============================================================================ - - -@router.get("/users", response_class=RedirectResponse, include_in_schema=False) -async def admin_users_page_redirect(): - """ - Redirect old /admin/users to /admin/admin-users. - """ - return RedirectResponse(url="/admin/admin-users", status_code=302) - - -@router.get("/users/create", response_class=RedirectResponse, include_in_schema=False) -async def admin_user_create_page_redirect(): - """ - Redirect old /admin/users/create to /admin/admin-users/create. - """ - return RedirectResponse(url="/admin/admin-users/create", status_code=302) - - -@router.get( - "/users/{user_id}", response_class=RedirectResponse, include_in_schema=False -) -async def admin_user_detail_page_redirect(user_id: int = Path(..., description="User ID")): - """ - Redirect old /admin/users/{id} to /admin/admin-users/{id}. - """ - return RedirectResponse(url=f"/admin/admin-users/{user_id}", status_code=302) - - -@router.get( - "/users/{user_id}/edit", response_class=RedirectResponse, include_in_schema=False -) -async def admin_user_edit_page_redirect(user_id: int = Path(..., description="User ID")): - """ - Redirect old /admin/users/{id}/edit to /admin/admin-users/{id}/edit. - """ - return RedirectResponse(url=f"/admin/admin-users/{user_id}/edit", status_code=302) - - -# ============================================================================ -# CUSTOMER MANAGEMENT ROUTES -# ============================================================================ - - -@router.get("/customers", response_class=HTMLResponse, include_in_schema=False) -async def admin_customers_page( - request: Request, - current_user: User = Depends(require_menu_access("customers", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render customers management page. - Shows list of all platform customers. - """ - return templates.TemplateResponse( - "admin/customers.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# NOTIFICATIONS ROUTES -# ============================================================================ - - -@router.get("/notifications", response_class=HTMLResponse, include_in_schema=False) -async def admin_notifications_page( - request: Request, - current_user: User = Depends(require_menu_access("notifications", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render notifications management page. - Shows all admin notifications and platform alerts. - """ - return templates.TemplateResponse( - "admin/notifications.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# EMAIL TEMPLATES ROUTES -# ============================================================================ - - -@router.get("/email-templates", response_class=HTMLResponse, include_in_schema=False) -async def admin_email_templates_page( - request: Request, - current_user: User = Depends(require_menu_access("email-templates", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render email templates management page. - Shows all platform email templates with edit capabilities. - """ - return templates.TemplateResponse( - "admin/email-templates.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# MESSAGING ROUTES -# ============================================================================ - - -@router.get("/messages", response_class=HTMLResponse, include_in_schema=False) -async def admin_messages_page( - request: Request, - current_user: User = Depends(require_menu_access("messages", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render messaging page. - Shows all conversations (admin_vendor and admin_customer channels). - """ - return templates.TemplateResponse( - "admin/messages.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/messages/{conversation_id}", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_conversation_detail_page( - request: Request, - conversation_id: int = Path(..., description="Conversation ID"), - current_user: User = Depends(require_menu_access("messages", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render conversation detail page. - Shows the full conversation thread with messages. - """ - return templates.TemplateResponse( - "admin/messages.html", - { - "request": request, - "user": current_user, - "conversation_id": conversation_id, - }, - ) - - -# ============================================================================ -# INVENTORY MANAGEMENT ROUTES -# ============================================================================ - - -@router.get("/inventory", response_class=HTMLResponse, include_in_schema=False) -async def admin_inventory_page( - request: Request, - current_user: User = Depends(require_menu_access("inventory", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render inventory management page. - Shows stock levels across all vendors with filtering and adjustment capabilities. - """ - return templates.TemplateResponse( - "admin/inventory.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# ORDER MANAGEMENT ROUTES -# ============================================================================ - - -@router.get("/orders", response_class=HTMLResponse, include_in_schema=False) -async def admin_orders_page( - request: Request, - current_user: User = Depends(require_menu_access("orders", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render orders management page. - Shows orders across all vendors with filtering and status management. - """ - return templates.TemplateResponse( - "admin/orders.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# IMPORT MANAGEMENT ROUTES -# ============================================================================ - - -@router.get("/imports", response_class=HTMLResponse, include_in_schema=False) -async def admin_imports_page( - request: Request, - current_user: User = Depends(require_menu_access("imports", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render imports management page. - Shows import history and status. - """ - return templates.TemplateResponse( - "admin/imports.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/background-tasks", response_class=HTMLResponse, include_in_schema=False) -async def admin_background_tasks_page( - request: Request, - current_user: User = Depends(require_menu_access("background-tasks", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render background tasks monitoring page. - Shows running and completed background tasks across the system. - """ - return templates.TemplateResponse( - "admin/background-tasks.html", - { - "request": request, - "user": current_user, - "flower_url": settings.flower_url, - }, - ) - - -@router.get("/marketplace", response_class=HTMLResponse, include_in_schema=False) -async def admin_marketplace_page( - request: Request, - current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render marketplace import management page. - Allows admins to import products for any vendor and monitor all imports. - """ - return templates.TemplateResponse( - "admin/marketplace.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# MARKETPLACE INTEGRATION ROUTES -# ============================================================================ - - -@router.get( - "/marketplace/letzshop", response_class=HTMLResponse, include_in_schema=False -) -async def admin_marketplace_letzshop_page( - request: Request, - current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render unified Letzshop management page. - Combines products (import/export), orders, and settings management. - Admin can select a vendor and manage their Letzshop integration. - """ - return templates.TemplateResponse( - "admin/marketplace-letzshop.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/letzshop/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False -) -async def admin_letzshop_order_detail_page( - request: Request, - order_id: int = Path(..., description="Letzshop order ID"), - current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render detailed Letzshop order page. - Shows full order information with shipping address, billing address, - product details, and order history. - """ - return templates.TemplateResponse( - "admin/letzshop-order-detail.html", - { - "request": request, - "user": current_user, - "order_id": order_id, - }, - ) - - -@router.get( - "/letzshop/products/{product_id}", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_letzshop_product_detail_page( - request: Request, - product_id: int = Path(..., description="Marketplace Product ID"), - current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render Letzshop product detail page. - Shows full product information from the marketplace. - """ - return templates.TemplateResponse( - "admin/marketplace-product-detail.html", - { - "request": request, - "user": current_user, - "product_id": product_id, - "back_url": "/admin/marketplace/letzshop", - }, - ) - - -# ============================================================================ -# LETZSHOP VENDOR DIRECTORY -# ============================================================================ - - -@router.get( - "/letzshop/vendor-directory", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_letzshop_vendor_directory_page( - request: Request, - current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render Letzshop vendor directory management page. - - Allows admins to: - - View cached Letzshop vendors - - Trigger manual sync from Letzshop API - - Create platform vendors from cached Letzshop vendors - """ - return templates.TemplateResponse( - "admin/letzshop-vendor-directory.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# PRODUCT CATALOG ROUTES -# ============================================================================ - - -@router.get( - "/marketplace-products", response_class=HTMLResponse, include_in_schema=False -) -async def admin_marketplace_products_page( - request: Request, - current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render marketplace products page. - Browse the master product repository imported from external sources. - """ - return templates.TemplateResponse( - "admin/marketplace-products.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/marketplace-products/{product_id}", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_marketplace_product_detail_page( - request: Request, - product_id: int = Path(..., description="Marketplace Product ID"), - current_user: User = Depends(require_menu_access("marketplace-letzshop", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render marketplace product detail page. - Shows full product information from the master repository. - """ - return templates.TemplateResponse( - "admin/marketplace-product-detail.html", - { - "request": request, - "user": current_user, - "product_id": product_id, - "back_url": "/admin/marketplace-products", - }, - ) - - -@router.get("/vendor-products", response_class=HTMLResponse, include_in_schema=False) -async def admin_vendor_products_page( - request: Request, - current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendor products catalog page. - Browse vendor-specific product catalogs with override capability. - """ - return templates.TemplateResponse( - "admin/vendor-products.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/vendor-products/create", response_class=HTMLResponse, include_in_schema=False) -async def admin_vendor_product_create_page( - request: Request, - current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendor product create page. - Create a new vendor product entry. - """ - return templates.TemplateResponse( - "admin/vendor-product-create.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/vendor-products/{product_id}", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_vendor_product_detail_page( - request: Request, - product_id: int = Path(..., description="Vendor Product ID"), - current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendor product detail page. - Shows full product information with vendor-specific overrides. - """ - return templates.TemplateResponse( - "admin/vendor-product-detail.html", - { - "request": request, - "user": current_user, - "product_id": product_id, - }, - ) - - -@router.get( - "/vendor-products/{product_id}/edit", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_vendor_product_edit_page( - request: Request, - product_id: int = Path(..., description="Vendor Product ID"), - current_user: User = Depends(require_menu_access("vendor-products", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendor product edit page. - Edit vendor product information and overrides. - """ - return templates.TemplateResponse( - "admin/vendor-product-edit.html", - { - "request": request, - "user": current_user, - "product_id": product_id, - }, - ) - - -# ============================================================================ -# BILLING & SUBSCRIPTIONS ROUTES -# ============================================================================ - - -@router.get("/subscription-tiers", response_class=HTMLResponse, include_in_schema=False) -async def admin_subscription_tiers_page( - request: Request, - current_user: User = Depends(require_menu_access("subscription-tiers", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render subscription tiers management page. - Shows all subscription tiers with their limits and pricing. - """ - return templates.TemplateResponse( - "admin/subscription-tiers.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/subscriptions", response_class=HTMLResponse, include_in_schema=False) -async def admin_subscriptions_page( - request: Request, - current_user: User = Depends(require_menu_access("subscriptions", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendor subscriptions management page. - Shows all vendor subscriptions with status and usage. - """ - return templates.TemplateResponse( - "admin/subscriptions.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/billing-history", response_class=HTMLResponse, include_in_schema=False) -async def admin_billing_history_page( - request: Request, - current_user: User = Depends(require_menu_access("billing-history", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render billing history page. - Shows invoices and payments across all vendors. - """ - return templates.TemplateResponse( - "admin/billing-history.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# SETTINGS ROUTES -# ============================================================================ - - -@router.get("/settings", response_class=HTMLResponse, include_in_schema=False) -async def admin_settings_page( - request: Request, - current_user: User = Depends(require_menu_access("settings", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render admin settings page. - Platform configuration and preferences. - """ - return templates.TemplateResponse( - "admin/settings.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/my-menu", response_class=HTMLResponse, include_in_schema=False) -async def admin_my_menu_config( - request: Request, - current_user: User = Depends(require_menu_access("my-menu", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render personal menu configuration page for super admins. - Allows super admins to customize their own sidebar menu. - """ - # Only super admins can configure their own menu - if not current_user.is_super_admin: - return RedirectResponse(url="/admin/settings", status_code=302) - - return templates.TemplateResponse( - "admin/my-menu-config.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/logs", response_class=HTMLResponse, include_in_schema=False) -async def admin_logs_page( - request: Request, - current_user: User = Depends(require_menu_access("logs", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render admin logs viewer page. - View database and file logs with filtering and search. - """ - return templates.TemplateResponse( - "admin/logs.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# PLATFORM MANAGEMENT ROUTES (Multi-Platform Support) -# ============================================================================ - - -@router.get("/platforms", response_class=HTMLResponse, include_in_schema=False) -async def admin_platforms_list( - request: Request, - current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render platforms management page. - Shows all platforms (OMS, Loyalty, etc.) with their configuration. - """ - return templates.TemplateResponse( - "admin/platforms.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/platforms/{platform_code}", response_class=HTMLResponse, include_in_schema=False -) -async def admin_platform_detail( - request: Request, - platform_code: str = Path(..., description="Platform code (oms, loyalty, etc.)"), - current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render platform detail page. - Shows platform configuration, marketing pages, and vendor defaults. - """ - return templates.TemplateResponse( - "admin/platform-detail.html", - { - "request": request, - "user": current_user, - "platform_code": platform_code, - }, - ) - - -@router.get( - "/platforms/{platform_code}/edit", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_platform_edit( - request: Request, - platform_code: str = Path(..., description="Platform code"), - current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render platform edit form. - Allows editing platform settings, branding, and configuration. - """ - return templates.TemplateResponse( - "admin/platform-edit.html", - { - "request": request, - "user": current_user, - "platform_code": platform_code, - }, - ) - - -@router.get( - "/platforms/{platform_code}/menu-config", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_platform_menu_config( - request: Request, - platform_code: str = Path(..., description="Platform code"), - current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render platform menu configuration page. - Super admin only - allows configuring which menu items are visible - for the platform's admin and vendor frontends. - """ - # Only super admins can access menu configuration - if not current_user.is_super_admin: - return RedirectResponse(url=f"/admin/platforms/{platform_code}", status_code=302) - - return templates.TemplateResponse( - "admin/platform-menu-config.html", - { - "request": request, - "user": current_user, - "platform_code": platform_code, - }, - ) - - -@router.get( - "/platforms/{platform_code}/modules", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_platform_modules( - request: Request, - platform_code: str = Path(..., description="Platform code"), - current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render platform module configuration page. - Super admin only - allows enabling/disabling feature modules - for the platform. - """ - # Only super admins can access module configuration - if not current_user.is_super_admin: - return RedirectResponse(url=f"/admin/platforms/{platform_code}", status_code=302) - - return templates.TemplateResponse( - "admin/platform-modules.html", - { - "request": request, - "user": current_user, - "platform_code": platform_code, - }, - ) - - -@router.get( - "/platforms/{platform_code}/modules/{module_code}", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_module_info( - request: Request, - platform_code: str = Path(..., description="Platform code"), - module_code: str = Path(..., description="Module code"), - current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render module info/detail page. - Shows module details including features, menu items, dependencies, - and self-contained module information. - """ - # Only super admins can access module details - if not current_user.is_super_admin: - return RedirectResponse(url=f"/admin/platforms/{platform_code}", status_code=302) - - return templates.TemplateResponse( - "admin/module-info.html", - { - "request": request, - "user": current_user, - "platform_code": platform_code, - "module_code": module_code, - }, - ) - - -@router.get( - "/platforms/{platform_code}/modules/{module_code}/config", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_module_config( - request: Request, - platform_code: str = Path(..., description="Platform code"), - module_code: str = Path(..., description="Module code"), - current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render module configuration page. - Allows configuring module-specific settings. - """ - # Only super admins can access module configuration - if not current_user.is_super_admin: - return RedirectResponse(url=f"/admin/platforms/{platform_code}", status_code=302) - - return templates.TemplateResponse( - "admin/module-config.html", - { - "request": request, - "user": current_user, - "platform_code": platform_code, - "module_code": module_code, - }, - ) - - -# ============================================================================ -# CONTENT MANAGEMENT SYSTEM (CMS) ROUTES -# ============================================================================ -# NOTE: CMS routes moved to self-contained module: app.modules.cms.routes.pages.admin -# Routes are registered directly in main.py from the CMS module - - -# ============================================================================ -# DEVELOPER TOOLS - COMPONENTS & TESTING -# ============================================================================ - - -@router.get("/components", response_class=HTMLResponse, include_in_schema=False) -async def admin_components_page( - request: Request, - current_user: User = Depends(require_menu_access("components", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render UI components library page. - Reference for all available UI components. - """ - return templates.TemplateResponse( - "admin/components.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/icons", response_class=HTMLResponse, include_in_schema=False) -async def admin_icons_page( - request: Request, - current_user: User = Depends(require_menu_access("icons", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render icons browser page. - Browse and search all available icons. - """ - return templates.TemplateResponse( - "admin/icons.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/testing", response_class=HTMLResponse, include_in_schema=False) -async def admin_testing_dashboard( - request: Request, - current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render testing dashboard page. - pytest results and test coverage overview. - """ - return templates.TemplateResponse( - "admin/testing-dashboard.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/testing-hub", response_class=HTMLResponse, include_in_schema=False) -async def admin_testing_hub( - request: Request, - current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render manual testing hub page. - Central hub for all manual test suites and QA tools. - """ - return templates.TemplateResponse( - "admin/testing-hub.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get("/test/auth-flow", response_class=HTMLResponse, include_in_schema=False) -async def admin_test_auth_flow( - request: Request, - current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render authentication flow testing page. - Tests login, logout, token expiration, and protected routes. - """ - return templates.TemplateResponse( - "admin/test-auth-flow.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/test/vendors-users-migration", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_test_vendors_users_migration( - request: Request, - current_user: User = Depends(require_menu_access("testing", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render vendors and users migration testing page. - Tests CRUD operations, data migration, and form validation. - """ - return templates.TemplateResponse( - "admin/test-vendors-users-migration.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# CODE QUALITY & ARCHITECTURE ROUTES -# ============================================================================ - - -@router.get("/code-quality", response_class=HTMLResponse, include_in_schema=False) -async def admin_code_quality_dashboard( - request: Request, - current_user: User = Depends(require_menu_access("code-quality", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render code quality dashboard. - Shows architecture violations, trends, and technical debt score. - """ - return templates.TemplateResponse( - "admin/code-quality-dashboard.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/code-quality/violations", response_class=HTMLResponse, include_in_schema=False -) -async def admin_code_quality_violations( - request: Request, - current_user: User = Depends(require_menu_access("code-quality", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render violations list page. - Shows all violations with filtering and sorting options. - """ - return templates.TemplateResponse( - "admin/code-quality-violations.html", - { - "request": request, - "user": current_user, - }, - ) - - -@router.get( - "/code-quality/violations/{violation_id}", - response_class=HTMLResponse, - include_in_schema=False, -) -async def admin_code_quality_violation_detail( - request: Request, - violation_id: int = Path(..., description="Violation ID"), - current_user: User = Depends(require_menu_access("code-quality", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render violation detail page. - Shows violation details, code context, assignments, and comments. - """ - return templates.TemplateResponse( - "admin/code-quality-violation-detail.html", - { - "request": request, - "user": current_user, - "violation_id": violation_id, - }, - ) - - -# ============================================================================ -# PLATFORM HEALTH & MONITORING ROUTES -# ============================================================================ - - -@router.get("/platform-health", response_class=HTMLResponse, include_in_schema=False) -async def admin_platform_health( - request: Request, - current_user: User = Depends(require_menu_access("platform-health", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render platform health monitoring page. - Shows system metrics, capacity thresholds, and scaling recommendations. - """ - return templates.TemplateResponse( - "admin/platform-health.html", - { - "request": request, - "user": current_user, - }, - ) - - -# ============================================================================ -# FEATURE MANAGEMENT ROUTES -# ============================================================================ - - -@router.get("/features", response_class=HTMLResponse, include_in_schema=False) -async def admin_features_page( - request: Request, - current_user: User = Depends(require_menu_access("subscription-tiers", FrontendType.ADMIN)), - db: Session = Depends(get_db), -): - """ - Render feature management page. - Shows all features with tier assignments and allows editing. - """ - return templates.TemplateResponse( - "admin/features.html", - { - "request": request, - "user": current_user, - }, - ) diff --git a/app/routes/platform_pages.py b/app/routes/platform_pages.py deleted file mode 100644 index 491efc16..00000000 --- a/app/routes/platform_pages.py +++ /dev/null @@ -1,435 +0,0 @@ -# app/routes/platform_pages.py -""" -Platform public page routes. - -These routes serve the marketing homepage, pricing page, -Letzshop vendor finder, and signup wizard. -""" - -import logging -from pathlib import Path - -from fastapi import APIRouter, Depends, HTTPException, Request -from fastapi.responses import HTMLResponse -from app.templates_config import templates -from sqlalchemy.orm import Session - -from app.core.config import settings -from app.core.database import get_db -from app.modules.cms.services import content_page_service -from app.utils.i18n import get_jinja2_globals - -router = APIRouter() -logger = logging.getLogger(__name__) - -# Get the templates directory -BASE_DIR = Path(__file__).resolve().parent.parent.parent -# TEMPLATES_DIR moved to app.templates_config -# templates imported from app.templates_config - - -def get_platform_context(request: Request, db: Session) -> dict: - """Build context for platform pages.""" - # Get language from request state (set by middleware) - language = getattr(request.state, "language", "fr") - - # Get platform from middleware (default to OMS platform_id=1) - platform = getattr(request.state, "platform", None) - platform_id = platform.id if platform else 1 - - # Get translation function - i18n_globals = get_jinja2_globals(language) - - context = { - "request": request, - "platform_name": "Wizamart", - "platform_domain": settings.platform_domain, - "stripe_publishable_key": settings.stripe_publishable_key, - "trial_days": settings.stripe_trial_days, - } - - # Add i18n globals (_, t, current_language, SUPPORTED_LANGUAGES, etc.) - context.update(i18n_globals) - - # Load CMS pages for header, footer, and legal navigation - header_pages = [] - footer_pages = [] - legal_pages = [] - try: - # Platform marketing pages (is_platform_page=True) - header_pages = content_page_service.list_platform_pages( - db, platform_id=platform_id, header_only=True, include_unpublished=False - ) - footer_pages = content_page_service.list_platform_pages( - db, platform_id=platform_id, footer_only=True, include_unpublished=False - ) - # For legal pages, we need to add footer support or use a different approach - # For now, legal pages come from footer pages with show_in_legal flag - legal_pages = [] # Will be handled separately if needed - logger.debug( - f"Loaded CMS pages: {len(header_pages)} header, {len(footer_pages)} footer, {len(legal_pages)} legal" - ) - except Exception as e: - logger.error(f"Failed to load CMS navigation pages: {e}") - - context["header_pages"] = header_pages - context["footer_pages"] = footer_pages - context["legal_pages"] = legal_pages - - return context - - -# ============================================================================= -# Homepage -# ============================================================================= - - -@router.get("/", response_class=HTMLResponse, name="platform_homepage") -async def homepage( - request: Request, - db: Session = Depends(get_db), -): - """ - Homepage handler. - - Handles two scenarios: - 1. Vendor on custom domain (vendor.com) → Show vendor landing page or redirect to shop - 2. Platform marketing site → Show platform homepage from CMS or default template - - URL routing: - - localhost:9999/ → Main marketing site ('main' platform) - - localhost:9999/platforms/oms/ → OMS platform (middleware rewrites to /) - - oms.lu/ → OMS platform (domain-based) - - shop.mycompany.com/ → Vendor landing page (custom domain) - """ - from fastapi.responses import RedirectResponse - - # Get platform and vendor from middleware - platform = getattr(request.state, "platform", None) - vendor = getattr(request.state, "vendor", None) - - # Scenario 1: Vendor detected (custom domain like vendor.com) - if vendor: - logger.debug(f"[HOMEPAGE] Vendor detected: {vendor.subdomain}") - - # Get platform_id (use platform from context or default to 1 for OMS) - platform_id = platform.id if platform else 1 - - # Try to find vendor landing page (slug='landing' or 'home') - landing_page = content_page_service.get_page_for_vendor( - db, platform_id=platform_id, slug="landing", vendor_id=vendor.id, include_unpublished=False - ) - - if not landing_page: - landing_page = content_page_service.get_page_for_vendor( - db, platform_id=platform_id, slug="home", vendor_id=vendor.id, include_unpublished=False - ) - - if landing_page: - # Render landing page with selected template - from app.routes.storefront_pages import get_storefront_context - - template_name = landing_page.template or "default" - template_path = f"vendor/landing-{template_name}.html" - - logger.info(f"[HOMEPAGE] Rendering vendor landing page: {template_path}") - return templates.TemplateResponse( - template_path, get_storefront_context(request, db=db, page=landing_page) - ) - - # No landing page - redirect to shop - vendor_context = getattr(request.state, "vendor_context", None) - access_method = ( - vendor_context.get("detection_method", "unknown") - if vendor_context - else "unknown" - ) - - if access_method == "path": - full_prefix = ( - vendor_context.get("full_prefix", "/vendor/") - if vendor_context - else "/vendor/" - ) - return RedirectResponse( - url=f"{full_prefix}{vendor.subdomain}/shop/", status_code=302 - ) - # Domain/subdomain - redirect to /shop/ - return RedirectResponse(url="/shop/", status_code=302) - - # Scenario 2: Platform marketing site (no vendor) - # Load platform homepage from CMS (slug='home') - platform_id = platform.id if platform else 1 - - cms_homepage = content_page_service.get_platform_page( - db, platform_id=platform_id, slug="home", include_unpublished=False - ) - - if cms_homepage: - # Use CMS-based homepage with template selection - context = get_platform_context(request, db) - context["page"] = cms_homepage - context["platform"] = platform - - # Include subscription tiers for pricing section - from app.modules.billing.models import TIER_LIMITS, TierCode - - tiers = [] - for tier_code, limits in TIER_LIMITS.items(): - tiers.append({ - "code": tier_code.value, - "name": limits["name"], - "price_monthly": limits["price_monthly_cents"] / 100, - "price_annual": (limits["price_annual_cents"] / 100) if limits.get("price_annual_cents") else None, - "orders_per_month": limits.get("orders_per_month"), - "products_limit": limits.get("products_limit"), - "team_members": limits.get("team_members"), - "features": limits.get("features", []), - "is_popular": tier_code == TierCode.PROFESSIONAL, - "is_enterprise": tier_code == TierCode.ENTERPRISE, - }) - context["tiers"] = tiers - - template_name = cms_homepage.template or "default" - template_path = f"platform/homepage-{template_name}.html" - - logger.info(f"[HOMEPAGE] Rendering CMS homepage with template: {template_path}") - return templates.TemplateResponse(template_path, context) - - # Fallback: Default wizamart homepage (no CMS content) - logger.info("[HOMEPAGE] No CMS homepage found, using default wizamart template") - context = get_platform_context(request, db) - context["platform"] = platform - - # Fetch tiers for display (use API service internally) - from app.modules.billing.models import TIER_LIMITS, TierCode - - tiers = [] - for tier_code, limits in TIER_LIMITS.items(): - tiers.append({ - "code": tier_code.value, - "name": limits["name"], - "price_monthly": limits["price_monthly_cents"] / 100, - "price_annual": (limits["price_annual_cents"] / 100) if limits.get("price_annual_cents") else None, - "orders_per_month": limits.get("orders_per_month"), - "products_limit": limits.get("products_limit"), - "team_members": limits.get("team_members"), - "features": limits.get("features", []), - "is_popular": tier_code == TierCode.PROFESSIONAL, - "is_enterprise": tier_code == TierCode.ENTERPRISE, - }) - - context["tiers"] = tiers - - # Add-ons (hardcoded for now, will come from DB) - context["addons"] = [ - { - "code": "domain", - "name": "Custom Domain", - "description": "Use your own domain (mydomain.com)", - "price": 15, - "billing_period": "year", - "icon": "globe", - }, - { - "code": "ssl_premium", - "name": "Premium SSL", - "description": "EV certificate for trust badges", - "price": 49, - "billing_period": "year", - "icon": "shield-check", - }, - { - "code": "email", - "name": "Email Package", - "description": "Professional email addresses", - "price": 5, - "billing_period": "month", - "icon": "mail", - "options": [ - {"quantity": 5, "price": 5}, - {"quantity": 10, "price": 9}, - {"quantity": 25, "price": 19}, - ], - }, - ] - - return templates.TemplateResponse( - "platform/homepage-wizamart.html", - context, - ) - - -# ============================================================================= -# Pricing Page -# ============================================================================= - - -@router.get("/pricing", response_class=HTMLResponse, name="platform_pricing") -async def pricing_page( - request: Request, - db: Session = Depends(get_db), -): - """ - Standalone pricing page with detailed tier comparison. - """ - context = get_platform_context(request, db) - - # Reuse tier data from homepage - from app.modules.billing.models import TIER_LIMITS, TierCode - - tiers = [] - for tier_code, limits in TIER_LIMITS.items(): - tiers.append({ - "code": tier_code.value, - "name": limits["name"], - "price_monthly": limits["price_monthly_cents"] / 100, - "price_annual": (limits["price_annual_cents"] / 100) if limits.get("price_annual_cents") else None, - "orders_per_month": limits.get("orders_per_month"), - "products_limit": limits.get("products_limit"), - "team_members": limits.get("team_members"), - "order_history_months": limits.get("order_history_months"), - "features": limits.get("features", []), - "is_popular": tier_code == TierCode.PROFESSIONAL, - "is_enterprise": tier_code == TierCode.ENTERPRISE, - }) - - context["tiers"] = tiers - context["page_title"] = "Pricing" - - return templates.TemplateResponse( - "platform/pricing.html", - context, - ) - - -# ============================================================================= -# Find Your Shop (Letzshop Vendor Browser) -# ============================================================================= - - -@router.get("/find-shop", response_class=HTMLResponse, name="platform_find_shop") -async def find_shop_page( - request: Request, - db: Session = Depends(get_db), -): - """ - Letzshop vendor browser page. - - Allows vendors to search for and claim their Letzshop shop. - """ - context = get_platform_context(request, db) - context["page_title"] = "Find Your Letzshop Shop" - - return templates.TemplateResponse( - "platform/find-shop.html", - context, - ) - - -# ============================================================================= -# Signup Wizard -# ============================================================================= - - -@router.get("/signup", response_class=HTMLResponse, name="platform_signup") -async def signup_page( - request: Request, - tier: str | None = None, - annual: bool = False, - db: Session = Depends(get_db), -): - """ - Multi-step signup wizard. - - Query params: - - tier: Pre-selected tier code - - annual: Pre-select annual billing - """ - context = get_platform_context(request, db) - context["page_title"] = "Start Your Free Trial" - context["selected_tier"] = tier - context["is_annual"] = annual - - # Get tiers for tier selection step - from app.modules.billing.models import TIER_LIMITS, TierCode - - tiers = [] - for tier_code, limits in TIER_LIMITS.items(): - tiers.append({ - "code": tier_code.value, - "name": limits["name"], - "price_monthly": limits["price_monthly_cents"] / 100, - "price_annual": (limits["price_annual_cents"] / 100) if limits.get("price_annual_cents") else None, - "orders_per_month": limits.get("orders_per_month"), - "team_members": limits.get("team_members"), - "is_enterprise": tier_code == TierCode.ENTERPRISE, - }) - - context["tiers"] = tiers - - return templates.TemplateResponse( - "platform/signup.html", - context, - ) - - -@router.get("/signup/success", response_class=HTMLResponse, name="platform_signup_success") -async def signup_success_page( - request: Request, - vendor_code: str | None = None, - db: Session = Depends(get_db), -): - """ - Signup success page. - - Shown after successful account creation. - """ - context = get_platform_context(request, db) - context["page_title"] = "Welcome to Wizamart!" - context["vendor_code"] = vendor_code - - return templates.TemplateResponse( - "platform/signup-success.html", - context, - ) - - -# ============================================================================= -# Generic Content Pages (CMS) -# ============================================================================= -# IMPORTANT: This route must be LAST as it catches all /{slug} URLs - - -@router.get("/{slug}", response_class=HTMLResponse, name="platform_content_page") -async def content_page( - request: Request, - slug: str, - db: Session = Depends(get_db), -): - """ - Serve CMS content pages (about, contact, faq, privacy, terms, etc.). - - This is a catch-all route for dynamic content pages managed via the admin CMS. - Platform pages have vendor_id=None and is_platform_page=True. - """ - # Get platform from middleware (default to OMS platform_id=1) - platform = getattr(request.state, "platform", None) - platform_id = platform.id if platform else 1 - - # Load platform marketing page from database - page = content_page_service.get_platform_page( - db, platform_id=platform_id, slug=slug, include_unpublished=False - ) - - if not page: - raise HTTPException(status_code=404, detail=f"Page not found: {slug}") - - context = get_platform_context(request, db) - context["page"] = page - context["page_title"] = page.title - - return templates.TemplateResponse( - "platform/content-page.html", - context, - ) diff --git a/app/routes/storefront_pages.py b/app/routes/storefront_pages.py deleted file mode 100644 index 05c20261..00000000 --- a/app/routes/storefront_pages.py +++ /dev/null @@ -1,865 +0,0 @@ -# app/routes/storefront_pages.py -""" -Storefront/Customer HTML page routes using Jinja2 templates. - -These routes serve the public-facing storefront interface for customers. -Authentication required only for account pages. - -Note: Previously named "shop_pages.py", renamed to "storefront" as not all -platforms sell items - storefront is a more accurate term. - -AUTHENTICATION: -- Public pages (catalog, products): No auth required -- Account pages (dashboard, orders): Requires customer authentication -- Customer authentication accepts: - * customer_token cookie (path=/storefront) - for page navigation - * Authorization header - for API calls -- Customers CANNOT access admin or vendor routes - -Routes (all mounted at /storefront/* or /vendors/{code}/storefront/* 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 sqlalchemy.orm import Session - -from app.api.deps import get_current_customer_from_cookie_or_header, get_db -from app.modules.cms.services import content_page_service -from app.services.platform_settings_service import platform_settings_service -from app.templates_config import templates -from app.modules.customers.models import Customer - -router = APIRouter() - -logger = logging.getLogger(__name__) - - -# ============================================================================ -# HELPER: Resolve Storefront Locale -# ============================================================================ - - -def get_resolved_storefront_config(db: Session, vendor) -> dict: - """ - Resolve storefront locale and currency with priority: - 1. Vendor's storefront_locale (if set) - 2. Platform's default_storefront_locale (from AdminSetting) - 3. Environment variable (from config) - 4. Hardcoded fallback: 'fr-LU' - - Args: - db: Database session - vendor: Vendor model instance - - Returns: - dict with 'locale' and 'currency' keys - """ - # Get platform defaults from service (handles resolution chain 2-4) - platform_config = platform_settings_service.get_storefront_config(db) - - # Check for vendor override (step 1) - locale = platform_config["locale"] - if vendor and vendor.storefront_locale: - locale = vendor.storefront_locale - - return { - "locale": locale, - "currency": platform_config["currency"], - } - - -# ============================================================================ -# HELPER: Build Shop Template Context -# ============================================================================ - - -def get_storefront_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_storefront_context(request) - - # With database session for navigation - get_storefront_context(request, db=db) - - # With extra data - get_storefront_context(request, db=db, user=current_user, product_id=123) - """ - # Extract from middleware state - vendor = getattr(request.state, "vendor", None) - platform = getattr(request.state, "platform", 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 platform_id (default to 1 for OMS if not set) - platform_id = platform.id if platform else 1 - - # 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, platform_id=platform_id, 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, platform_id=platform_id, 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}, - ) - - # Resolve storefront locale and currency - storefront_config = {"locale": "fr-LU", "currency": "EUR"} # defaults - if db and vendor: - storefront_config = get_resolved_storefront_config(db, vendor) - - 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, - "storefront_locale": storefront_config["locale"], - "storefront_currency": storefront_config["currency"], - } - - # 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, - "storefront_locale": storefront_config["locale"], - "storefront_currency": storefront_config["currency"], - "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( - "storefront/products.html", get_storefront_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"), - db: Session = Depends(get_db), -): - """ - 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( - "storefront/product.html", get_storefront_context(request, db=db, 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"), - db: Session = Depends(get_db), -): - """ - 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( - "storefront/category.html", get_storefront_context(request, db=db, category_slug=category_slug) - ) - - -@router.get("/cart", response_class=HTMLResponse, include_in_schema=False) -async def shop_cart_page(request: Request, db: Session = Depends(get_db)): - """ - 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("storefront/cart.html", get_storefront_context(request, db=db)) - - -@router.get("/checkout", response_class=HTMLResponse, include_in_schema=False) -async def shop_checkout_page(request: Request, db: Session = Depends(get_db)): - """ - 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("storefront/checkout.html", get_storefront_context(request, db=db)) - - -@router.get("/search", response_class=HTMLResponse, include_in_schema=False) -async def shop_search_page(request: Request, db: Session = Depends(get_db)): - """ - 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("storefront/search.html", get_storefront_context(request, db=db)) - - -# ============================================================================ -# CUSTOMER ACCOUNT - PUBLIC ROUTES (No Authentication) -# ============================================================================ - - -@router.get("/account/register", response_class=HTMLResponse, include_in_schema=False) -async def shop_register_page(request: Request, db: Session = Depends(get_db)): - """ - 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( - "storefront/account/register.html", get_storefront_context(request, db=db) - ) - - -@router.get("/account/login", response_class=HTMLResponse, include_in_schema=False) -async def shop_login_page(request: Request, db: Session = Depends(get_db)): - """ - 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( - "storefront/account/login.html", get_storefront_context(request, db=db) - ) - - -@router.get( - "/account/forgot-password", response_class=HTMLResponse, include_in_schema=False -) -async def shop_forgot_password_page(request: Request, db: Session = Depends(get_db)): - """ - 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( - "storefront/account/forgot-password.html", get_storefront_context(request, db=db) - ) - - -@router.get( - "/account/reset-password", response_class=HTMLResponse, include_in_schema=False -) -async def shop_reset_password_page( - request: Request, token: str = None, db: Session = Depends(get_db) -): - """ - Render reset password page. - User lands here after clicking the reset link in their email. - Token is passed as query parameter. - """ - logger.debug( - "[SHOP_HANDLER] shop_reset_password_page REACHED", - extra={ - "path": request.url.path, - "vendor": getattr(request.state, "vendor", "NOT SET"), - "context": getattr(request.state, "context_type", "NOT SET"), - "has_token": bool(token), - }, - ) - - return templates.TemplateResponse( - "storefront/account/reset-password.html", get_storefront_context(request, db=db) - ) - - -# ============================================================================ -# 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( - "storefront/account/dashboard.html", get_storefront_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( - "storefront/account/orders.html", get_storefront_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( - "storefront/account/order-detail.html", - get_storefront_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( - "storefront/account/profile.html", get_storefront_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( - "storefront/account/addresses.html", get_storefront_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( - "storefront/account/wishlist.html", get_storefront_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( - "storefront/account/settings.html", get_storefront_context(request, user=current_customer) - ) - - -@router.get("/account/messages", response_class=HTMLResponse, include_in_schema=False) -async def shop_messages_page( - request: Request, - current_customer: Customer = Depends(get_current_customer_from_cookie_or_header), - db: Session = Depends(get_db), -): - """ - Render customer messages page. - View and reply to conversations with the vendor. - Requires customer authentication. - """ - logger.debug( - "[SHOP_HANDLER] shop_messages_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( - "storefront/account/messages.html", get_storefront_context(request, db=db, user=current_customer) - ) - - -@router.get( - "/account/messages/{conversation_id}", - response_class=HTMLResponse, - include_in_schema=False, -) -async def shop_message_detail_page( - request: Request, - conversation_id: int = Path(..., description="Conversation ID"), - current_customer: Customer = Depends(get_current_customer_from_cookie_or_header), - db: Session = Depends(get_db), -): - """ - Render message conversation detail page. - Shows the full conversation thread. - Requires customer authentication. - """ - logger.debug( - "[SHOP_HANDLER] shop_message_detail_page REACHED", - extra={ - "path": request.url.path, - "conversation_id": conversation_id, - "vendor": getattr(request.state, "vendor", "NOT SET"), - "context": getattr(request.state, "context_type", "NOT SET"), - }, - ) - - return templates.TemplateResponse( - "storefront/account/messages.html", - get_storefront_context( - request, db=db, user=current_customer, conversation_id=conversation_id - ), - ) - - -# ============================================================================ -# 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) - platform = getattr(request.state, "platform", None) - vendor_id = vendor.id if vendor else None - platform_id = platform.id if platform else 1 # Default to OMS - - # Load content page from database (vendor override → vendor default) - page = content_page_service.get_page_for_vendor( - db, platform_id=platform_id, 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( - "storefront/content-page.html", get_storefront_context(request, db=db, 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""" - - - - Debug Context - - - -

Request Context Debug

-
{json.dumps(debug_info, indent=2)}
- -

Status

-

- 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) diff --git a/app/routes/vendor_pages.py b/app/routes/vendor_pages.py deleted file mode 100644 index c8b021ab..00000000 --- a/app/routes/vendor_pages.py +++ /dev/null @@ -1,691 +0,0 @@ -# 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) diff --git a/docs/architecture/request-flow.md b/docs/architecture/request-flow.md index 651b7d3f..b5a86f64 100644 --- a/docs/architecture/request-flow.md +++ b/docs/architecture/request-flow.md @@ -226,9 +226,9 @@ async def get_shop_products(request: Request): **Example Handler**: ```python -from app.routes import shop_pages +# Routes are defined in modules: app/modules//routes/pages/storefront.py -@router.get("/shop/products") +@router.get("/products") async def shop_products_page( request: Request, db: Session = Depends(get_db)