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 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 21:23:26 +01:00
parent 6d7accfa25
commit 843703258f
6 changed files with 2 additions and 3578 deletions

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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,
)

View File

@@ -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"""
<!DOCTYPE html>
<html>
<head>
<title>Debug Context</title>
<style>
body {{ font-family: monospace; margin: 20px; }}
pre {{ background: #f0f0f0; padding: 20px; border-radius: 5px; }}
.good {{ color: green; }}
.bad {{ color: red; }}
</style>
</head>
<body>
<h1>Request Context Debug</h1>
<pre>{json.dumps(debug_info, indent=2)}</pre>
<h2>Status</h2>
<p class="{"good" if vendor else "bad"}">
Vendor: {"✓ Found" if vendor else "✗ Not Found"}
</p>
<p class="{"good" if theme else "bad"}">
Theme: {"✓ Found" if theme else "✗ Not Found"}
</p>
<p class="{"good" if str(getattr(request.state, "context_type", "NOT SET")) == "shop" else "bad"}">
Context Type: {str(getattr(request.state, "context_type", "NOT SET"))}
</p>
</body>
</html>
"""
return HTMLResponse(content=html_content)

View File

@@ -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)

View File

@@ -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/<module>/routes/pages/storefront.py
@router.get("/shop/products")
@router.get("/products")
async def shop_products_page(
request: Request,
db: Session = Depends(get_db)