refactor(customers): migrate routes to module with auto-discovery

- Move customer route implementations to app/modules/customers/routes/
- Convert legacy app/api/v1/{admin,vendor}/customers.py to re-exports
- Update router registrations to use module routers with access control
- Fix CustomerListResponse pagination (page/per_page/total_pages)
- Update URL routing docs to use storefront consistently
- Fix mkdocs.yml nav references (shop -> storefront)
- Fix broken doc links in logging.md and cdn-fallback-strategy.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 23:24:10 +01:00
parent 7245f79f7b
commit e0b69f5a7d
10 changed files with 533 additions and 444 deletions

View File

@@ -49,7 +49,7 @@ from . import (
code_quality, code_quality,
companies, companies,
# content_pages - moved to app.modules.cms.routes.api.admin # content_pages - moved to app.modules.cms.routes.api.admin
customers, # customers - moved to app.modules.customers.routes.admin
dashboard, dashboard,
email_templates, email_templates,
features, features,
@@ -92,6 +92,9 @@ from app.modules.marketplace.routes.api.admin import admin_letzshop_router as le
# CMS module router # CMS module router
from app.modules.cms.routes.api.admin import router as cms_admin_router from app.modules.cms.routes.api.admin import router as cms_admin_router
# Customers module router
from app.modules.customers.routes.admin import admin_router as customers_admin_router
# Create admin router # Create admin router
router = APIRouter() router = APIRouter()
@@ -149,8 +152,9 @@ router.include_router(users.router, tags=["admin-users"])
# Include admin user management endpoints (super admin only) # Include admin user management endpoints (super admin only)
router.include_router(admin_users.router, tags=["admin-admin-users"]) router.include_router(admin_users.router, tags=["admin-admin-users"])
# Include customer management endpoints # Include customers module router (with module access control)
router.include_router(customers.router, tags=["admin-customers"]) router.include_router(customers_admin_router, tags=["admin-customers"])
# Legacy: router.include_router(customers.router, tags=["admin-customers"])
# ============================================================================ # ============================================================================

View File

@@ -1,112 +1,31 @@
# app/api/v1/admin/customers.py # app/api/v1/admin/customers.py
""" """
Customer management endpoints for admin. LEGACY LOCATION - Re-exports from module for backwards compatibility.
Provides admin-level access to customer data across all vendors. The canonical implementation is now in:
app/modules/customers/routes/admin.py
This file exists to maintain backwards compatibility with code that
imports from the old location. All new code should import directly
from the module:
from app.modules.customers.routes.admin import admin_router
""" """
from fastapi import APIRouter, Depends, Query from app.modules.customers.routes.admin import (
from sqlalchemy.orm import Session admin_router,
router,
from app.api.deps import get_current_admin_api list_customers,
from app.core.database import get_db get_customer_stats,
from app.services.admin_customer_service import admin_customer_service get_customer,
from models.schema.auth import UserContext toggle_customer_status,
from app.modules.customers.schemas import (
CustomerDetailResponse,
CustomerListResponse,
CustomerMessageResponse,
CustomerStatisticsResponse,
) )
router = APIRouter(prefix="/customers") __all__ = [
"admin_router",
"router",
# ============================================================================ "list_customers",
# List Customers "get_customer_stats",
# ============================================================================ "get_customer",
"toggle_customer_status",
]
@router.get("", response_model=CustomerListResponse)
def list_customers(
vendor_id: int | None = Query(None, description="Filter by vendor ID"),
search: str = Query("", description="Search by email, name, or customer number"),
is_active: bool | None = Query(None, description="Filter by active status"),
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
) -> CustomerListResponse:
"""
Get paginated list of customers across all vendors.
Admin can filter by vendor, search, and active status.
"""
customers, total = admin_customer_service.list_customers(
db=db,
vendor_id=vendor_id,
search=search if search else None,
is_active=is_active,
skip=skip,
limit=limit,
)
return CustomerListResponse(
customers=customers,
total=total,
skip=skip,
limit=limit,
)
# ============================================================================
# Customer Statistics
# ============================================================================
@router.get("/stats", response_model=CustomerStatisticsResponse)
def get_customer_stats(
vendor_id: int | None = Query(None, description="Filter by vendor ID"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
) -> CustomerStatisticsResponse:
"""Get customer statistics."""
stats = admin_customer_service.get_customer_stats(db=db, vendor_id=vendor_id)
return CustomerStatisticsResponse(**stats)
# ============================================================================
# Get Single Customer
# ============================================================================
@router.get("/{customer_id}", response_model=CustomerDetailResponse)
def get_customer(
customer_id: int,
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
) -> CustomerDetailResponse:
"""Get customer details by ID."""
customer = admin_customer_service.get_customer(db=db, customer_id=customer_id)
return CustomerDetailResponse(**customer)
# ============================================================================
# Toggle Customer Status
# ============================================================================
@router.patch("/{customer_id}/toggle-status", response_model=CustomerMessageResponse)
def toggle_customer_status(
customer_id: int,
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
) -> CustomerMessageResponse:
"""Toggle customer active status."""
result = admin_customer_service.toggle_customer_status(
db=db,
customer_id=customer_id,
admin_email=current_admin.email,
)
db.commit()
return CustomerMessageResponse(message=result["message"])

View File

@@ -35,7 +35,7 @@ from . import (
auth, auth,
billing, billing,
# content_pages - moved to app.modules.cms.routes.api.vendor # content_pages - moved to app.modules.cms.routes.api.vendor
customers, # customers - moved to app.modules.customers.routes.vendor
dashboard, dashboard,
email_settings, email_settings,
email_templates, email_templates,
@@ -71,6 +71,9 @@ from app.modules.marketplace.routes.api.vendor import vendor_letzshop_router as
# CMS module router # CMS module router
from app.modules.cms.routes.api.vendor import router as cms_vendor_router from app.modules.cms.routes.api.vendor import router as cms_vendor_router
# Customers module router
from app.modules.customers.routes.vendor import vendor_router as customers_vendor_router
# Create vendor router # Create vendor router
router = APIRouter() router = APIRouter()
@@ -104,7 +107,11 @@ router.include_router(orders_exceptions_router, tags=["vendor-order-exceptions"]
# Legacy: router.include_router(order_item_exceptions.router, tags=["vendor-order-exceptions"]) # Legacy: router.include_router(order_item_exceptions.router, tags=["vendor-order-exceptions"])
router.include_router(invoices.router, tags=["vendor-invoices"]) router.include_router(invoices.router, tags=["vendor-invoices"])
router.include_router(customers.router, tags=["vendor-customers"])
# Include customers module router (with module access control)
router.include_router(customers_vendor_router, tags=["vendor-customers"])
# Legacy: router.include_router(customers.router, tags=["vendor-customers"])
router.include_router(team.router, tags=["vendor-team"]) router.include_router(team.router, tags=["vendor-team"])
# Include inventory module router (with module access control) # Include inventory module router (with module access control)

View File

@@ -1,223 +1,35 @@
# app/api/v1/vendor/customers.py # app/api/v1/vendor/customers.py
""" """
Vendor customer management endpoints. LEGACY LOCATION - Re-exports from module for backwards compatibility.
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern). The canonical implementation is now in:
The get_current_vendor_api dependency guarantees token_vendor_id is present. app/modules/customers/routes/vendor.py
This file exists to maintain backwards compatibility with code that
imports from the old location. All new code should import directly
from the module:
from app.modules.customers.routes.vendor import vendor_router
""" """
import logging from app.modules.customers.routes.vendor import (
vendor_router,
from fastapi import APIRouter, Depends, Query router,
from sqlalchemy.orm import Session get_vendor_customers,
get_customer_details,
from app.api.deps import get_current_vendor_api get_customer_orders,
from app.core.database import get_db update_customer,
from app.services.customer_service import customer_service toggle_customer_status,
from models.schema.auth import UserContext get_customer_statistics,
from app.modules.customers.schemas import (
CustomerDetailResponse,
CustomerMessageResponse,
CustomerOrdersResponse,
CustomerResponse,
CustomerStatisticsResponse,
CustomerUpdate,
VendorCustomerListResponse,
) )
router = APIRouter(prefix="/customers") __all__ = [
logger = logging.getLogger(__name__) "vendor_router",
"router",
"get_vendor_customers",
@router.get("", response_model=VendorCustomerListResponse) "get_customer_details",
def get_vendor_customers( "get_customer_orders",
skip: int = Query(0, ge=0), "update_customer",
limit: int = Query(100, ge=1, le=1000), "toggle_customer_status",
search: str | None = Query(None), "get_customer_statistics",
is_active: bool | None = Query(None), ]
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get all customers for this vendor.
- Query customers filtered by vendor_id
- Support search by name/email
- Support filtering by active status
- Return paginated results
"""
customers, total = customer_service.get_vendor_customers(
db=db,
vendor_id=current_user.token_vendor_id,
skip=skip,
limit=limit,
search=search,
is_active=is_active,
)
return VendorCustomerListResponse(
customers=[CustomerResponse.model_validate(c) for c in customers],
total=total,
skip=skip,
limit=limit,
)
@router.get("/{customer_id}", response_model=CustomerDetailResponse)
def get_customer_details(
customer_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get detailed customer information.
- Get customer by ID
- Verify customer belongs to vendor
- Include order statistics
"""
# Service will raise CustomerNotFoundException if not found
customer = customer_service.get_customer(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
)
# Get statistics
stats = customer_service.get_customer_statistics(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
)
return CustomerDetailResponse(
id=customer.id,
email=customer.email,
first_name=customer.first_name,
last_name=customer.last_name,
phone=customer.phone,
customer_number=customer.customer_number,
is_active=customer.is_active,
marketing_consent=customer.marketing_consent,
total_orders=stats["total_orders"],
total_spent=stats["total_spent"],
average_order_value=stats["average_order_value"],
last_order_date=stats["last_order_date"],
created_at=customer.created_at,
)
@router.get("/{customer_id}/orders", response_model=CustomerOrdersResponse)
def get_customer_orders(
customer_id: int,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get order history for a specific customer.
- Get all orders for customer
- Filter by vendor_id
- Return order details
"""
# Service will raise CustomerNotFoundException if not found
orders, total = customer_service.get_customer_orders(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
skip=skip,
limit=limit,
)
return CustomerOrdersResponse(
orders=[
{
"id": o.id,
"order_number": o.order_number,
"status": o.status,
"total": o.total_cents / 100 if o.total_cents else 0,
"created_at": o.created_at,
}
for o in orders
],
total=total,
skip=skip,
limit=limit,
)
@router.put("/{customer_id}", response_model=CustomerMessageResponse)
def update_customer(
customer_id: int,
customer_data: CustomerUpdate,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Update customer information.
- Update customer details
- Verify customer belongs to vendor
"""
# Service will raise CustomerNotFoundException if not found
customer_service.update_customer(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
customer_data=customer_data,
)
db.commit()
return CustomerMessageResponse(message="Customer updated successfully")
@router.put("/{customer_id}/status", response_model=CustomerMessageResponse)
def toggle_customer_status(
customer_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Activate/deactivate customer account.
- Toggle customer is_active status
- Verify customer belongs to vendor
"""
# Service will raise CustomerNotFoundException if not found
customer = customer_service.toggle_customer_status(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
)
db.commit()
status = "activated" if customer.is_active else "deactivated"
return CustomerMessageResponse(message=f"Customer {status} successfully")
@router.get("/{customer_id}/stats", response_model=CustomerStatisticsResponse)
def get_customer_statistics(
customer_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get customer statistics and metrics.
- Total orders
- Total spent
- Average order value
- Last order date
"""
# Service will raise CustomerNotFoundException if not found
stats = customer_service.get_customer_statistics(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
)
return CustomerStatisticsResponse(**stats)

View File

@@ -1,18 +1,23 @@
# app/modules/customers/routes/admin.py # app/modules/customers/routes/admin.py
""" """
Customers module admin routes. Customer management endpoints for admin.
This module wraps the existing admin customers routes and adds Provides admin-level access to customer data across all vendors.
module-based access control. Routes are re-exported from the
original location with the module access dependency.
""" """
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.api.deps import require_module_access from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
# Import original router (direct import to avoid circular dependency) from app.modules.customers.services import admin_customer_service
from app.api.v1.admin.customers import router as original_router from models.schema.auth import UserContext
from app.modules.customers.schemas import (
CustomerDetailResponse,
CustomerListResponse,
CustomerMessageResponse,
CustomerStatisticsResponse,
)
# Create module-aware router # Create module-aware router
admin_router = APIRouter( admin_router = APIRouter(
@@ -20,6 +25,102 @@ admin_router = APIRouter(
dependencies=[Depends(require_module_access("customers"))], dependencies=[Depends(require_module_access("customers"))],
) )
# Re-export all routes from the original module with module access control
for route in original_router.routes: # ============================================================================
admin_router.routes.append(route) # List Customers
# ============================================================================
@admin_router.get("", response_model=CustomerListResponse)
def list_customers(
vendor_id: int | None = Query(None, description="Filter by vendor ID"),
search: str = Query("", description="Search by email, name, or customer number"),
is_active: bool | None = Query(None, description="Filter by active status"),
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
) -> CustomerListResponse:
"""
Get paginated list of customers across all vendors.
Admin can filter by vendor, search, and active status.
"""
customers, total = admin_customer_service.list_customers(
db=db,
vendor_id=vendor_id,
search=search if search else None,
is_active=is_active,
skip=skip,
limit=limit,
)
# Calculate pagination values
page = (skip // limit) + 1 if limit > 0 else 1
per_page = limit
total_pages = (total + limit - 1) // limit if limit > 0 else 1
return CustomerListResponse(
customers=customers,
total=total,
page=page,
per_page=per_page,
total_pages=total_pages,
)
# ============================================================================
# Customer Statistics
# ============================================================================
@admin_router.get("/stats", response_model=CustomerStatisticsResponse)
def get_customer_stats(
vendor_id: int | None = Query(None, description="Filter by vendor ID"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
) -> CustomerStatisticsResponse:
"""Get customer statistics."""
stats = admin_customer_service.get_customer_stats(db=db, vendor_id=vendor_id)
return CustomerStatisticsResponse(**stats)
# ============================================================================
# Get Single Customer
# ============================================================================
@admin_router.get("/{customer_id}", response_model=CustomerDetailResponse)
def get_customer(
customer_id: int,
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
) -> CustomerDetailResponse:
"""Get customer details by ID."""
customer = admin_customer_service.get_customer(db=db, customer_id=customer_id)
return CustomerDetailResponse(**customer)
# ============================================================================
# Toggle Customer Status
# ============================================================================
@admin_router.patch("/{customer_id}/toggle-status", response_model=CustomerMessageResponse)
def toggle_customer_status(
customer_id: int,
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
) -> CustomerMessageResponse:
"""Toggle customer active status."""
result = admin_customer_service.toggle_customer_status(
db=db,
customer_id=customer_id,
admin_email=current_admin.email,
)
db.commit()
return CustomerMessageResponse(message=result["message"])
# Legacy alias for backwards compatibility
router = admin_router

View File

@@ -1,25 +1,231 @@
# app/modules/customers/routes/vendor.py # app/modules/customers/routes/vendor.py
""" """
Customers module vendor routes. Vendor customer management endpoints.
This module wraps the existing vendor customers routes and adds Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
module-based access control. Routes are re-exported from the The get_current_vendor_api dependency guarantees token_vendor_id is present.
original location with the module access dependency.
""" """
from fastapi import APIRouter, Depends import logging
from app.api.deps import require_module_access from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
# Import original router (direct import to avoid circular dependency) from app.api.deps import get_current_vendor_api, require_module_access
from app.api.v1.vendor.customers import router as original_router from app.core.database import get_db
from app.modules.customers.services import customer_service
from models.schema.auth import UserContext
from app.modules.customers.schemas import (
CustomerDetailResponse,
CustomerMessageResponse,
CustomerOrdersResponse,
CustomerResponse,
CustomerStatisticsResponse,
CustomerUpdate,
VendorCustomerListResponse,
)
# Create module-aware router # Create module-aware router
vendor_router = APIRouter( vendor_router = APIRouter(
prefix="/customers", prefix="/customers",
dependencies=[Depends(require_module_access("customers"))], dependencies=[Depends(require_module_access("customers"))],
) )
logger = logging.getLogger(__name__)
# Re-export all routes from the original module with module access control
for route in original_router.routes: @vendor_router.get("", response_model=VendorCustomerListResponse)
vendor_router.routes.append(route) def get_vendor_customers(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
search: str | None = Query(None),
is_active: bool | None = Query(None),
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get all customers for this vendor.
- Query customers filtered by vendor_id
- Support search by name/email
- Support filtering by active status
- Return paginated results
"""
customers, total = customer_service.get_vendor_customers(
db=db,
vendor_id=current_user.token_vendor_id,
skip=skip,
limit=limit,
search=search,
is_active=is_active,
)
return VendorCustomerListResponse(
customers=[CustomerResponse.model_validate(c) for c in customers],
total=total,
skip=skip,
limit=limit,
)
@vendor_router.get("/{customer_id}", response_model=CustomerDetailResponse)
def get_customer_details(
customer_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get detailed customer information.
- Get customer by ID
- Verify customer belongs to vendor
- Include order statistics
"""
# Service will raise CustomerNotFoundException if not found
customer = customer_service.get_customer(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
)
# Get statistics
stats = customer_service.get_customer_statistics(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
)
return CustomerDetailResponse(
id=customer.id,
email=customer.email,
first_name=customer.first_name,
last_name=customer.last_name,
phone=customer.phone,
customer_number=customer.customer_number,
is_active=customer.is_active,
marketing_consent=customer.marketing_consent,
total_orders=stats["total_orders"],
total_spent=stats["total_spent"],
average_order_value=stats["average_order_value"],
last_order_date=stats["last_order_date"],
created_at=customer.created_at,
)
@vendor_router.get("/{customer_id}/orders", response_model=CustomerOrdersResponse)
def get_customer_orders(
customer_id: int,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get order history for a specific customer.
- Get all orders for customer
- Filter by vendor_id
- Return order details
"""
# Service will raise CustomerNotFoundException if not found
orders, total = customer_service.get_customer_orders(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
skip=skip,
limit=limit,
)
return CustomerOrdersResponse(
orders=[
{
"id": o.id,
"order_number": o.order_number,
"status": o.status,
"total": o.total_cents / 100 if o.total_cents else 0,
"created_at": o.created_at,
}
for o in orders
],
total=total,
skip=skip,
limit=limit,
)
@vendor_router.put("/{customer_id}", response_model=CustomerMessageResponse)
def update_customer(
customer_id: int,
customer_data: CustomerUpdate,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Update customer information.
- Update customer details
- Verify customer belongs to vendor
"""
# Service will raise CustomerNotFoundException if not found
customer_service.update_customer(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
customer_data=customer_data,
)
db.commit()
return CustomerMessageResponse(message="Customer updated successfully")
@vendor_router.put("/{customer_id}/status", response_model=CustomerMessageResponse)
def toggle_customer_status(
customer_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Activate/deactivate customer account.
- Toggle customer is_active status
- Verify customer belongs to vendor
"""
# Service will raise CustomerNotFoundException if not found
customer = customer_service.toggle_customer_status(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
)
db.commit()
status = "activated" if customer.is_active else "deactivated"
return CustomerMessageResponse(message=f"Customer {status} successfully")
@vendor_router.get("/{customer_id}/stats", response_model=CustomerStatisticsResponse)
def get_customer_statistics(
customer_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get customer statistics and metrics.
- Total orders
- Total spent
- Average order value
- Last order date
"""
# Service will raise CustomerNotFoundException if not found
stats = customer_service.get_customer_statistics(
db=db,
vendor_id=current_user.token_vendor_id,
customer_id=customer_id,
)
return CustomerStatisticsResponse(**stats)
# Legacy alias for backwards compatibility
router = vendor_router

View File

@@ -2,37 +2,37 @@
## Quick Answer ## Quick Answer
**How do customers access a vendor's shop in Wizamart?** **How do customers access a vendor's storefront in Wizamart?**
There are three ways depending on the deployment mode: There are three ways depending on the deployment mode:
**⚠️ Important:** This guide describes **customer-facing shop routes**. For vendor dashboard/management routes, see [Vendor Frontend Architecture](../../frontend/vendor/architecture.md). The shop uses `/vendors/{code}/shop/*` (plural) in path-based mode, while the vendor dashboard uses `/vendor/{code}/*` (singular). **⚠️ Important:** This guide describes **customer-facing storefront routes**. For vendor dashboard/management routes, see [Vendor Frontend Architecture](../../frontend/vendor/architecture.md). The storefront uses `/vendors/{code}/storefront/*` (plural) in path-based mode, while the vendor dashboard uses `/vendor/{code}/*` (singular).
### 1. **SUBDOMAIN MODE** (Production - Recommended) ### 1. **SUBDOMAIN MODE** (Production - Recommended)
``` ```
https://VENDOR_SUBDOMAIN.platform.com/shop/products https://VENDOR_SUBDOMAIN.platform.com/storefront/products
Example: Example:
https://acme.wizamart.com/shop/products https://acme.wizamart.com/storefront/products
https://techpro.wizamart.com/shop/categories/electronics https://techpro.wizamart.com/storefront/categories/electronics
``` ```
### 2. **CUSTOM DOMAIN MODE** (Production - Premium) ### 2. **CUSTOM DOMAIN MODE** (Production - Premium)
``` ```
https://VENDOR_CUSTOM_DOMAIN/shop/products https://VENDOR_CUSTOM_DOMAIN/storefront/products
Example: Example:
https://store.acmecorp.com/shop/products https://store.acmecorp.com/storefront/products
https://shop.techpro.io/shop/cart https://shop.techpro.io/storefront/cart
``` ```
### 3. **PATH-BASED MODE** (Development Only) ### 3. **PATH-BASED MODE** (Development Only)
``` ```
http://localhost:PORT/vendors/VENDOR_CODE/shop/products http://localhost:PORT/platforms/PLATFORM_CODE/vendors/VENDOR_CODE/storefront/products
Example: Example:
http://localhost:8000/vendors/acme/shop/products http://localhost:8000/platforms/oms/vendors/acme/storefront/products
http://localhost:8000/vendors/techpro/shop/checkout http://localhost:8000/platforms/loyalty/vendors/techpro/storefront/checkout
``` ```
--- ---
@@ -51,7 +51,9 @@ Wizamart supports multiple platforms (OMS, Loyalty, Site Builder), each with its
| `/about` | Main marketing site about page | | `/about` | Main marketing site about page |
| `/platforms/oms/` | OMS platform homepage | | `/platforms/oms/` | OMS platform homepage |
| `/platforms/oms/pricing` | OMS platform pricing page | | `/platforms/oms/pricing` | OMS platform pricing page |
| `/platforms/oms/vendors/{code}/` | Vendor storefront on OMS | | `/platforms/oms/vendors/{code}/storefront/` | Vendor storefront on OMS |
| `/platforms/oms/admin/` | Admin panel for OMS platform |
| `/platforms/oms/vendor/{code}/` | Vendor dashboard on OMS |
| `/platforms/loyalty/` | Loyalty platform homepage | | `/platforms/loyalty/` | Loyalty platform homepage |
| `/platforms/loyalty/features` | Loyalty platform features page | | `/platforms/loyalty/features` | Loyalty platform features page |
@@ -63,9 +65,45 @@ Wizamart supports multiple platforms (OMS, Loyalty, Site Builder), each with its
| `wizamart.lu/about` | Main marketing site about page | | `wizamart.lu/about` | Main marketing site about page |
| `oms.lu/` | OMS platform homepage | | `oms.lu/` | OMS platform homepage |
| `oms.lu/pricing` | OMS platform pricing page | | `oms.lu/pricing` | OMS platform pricing page |
| `oms.lu/vendors/{code}/` | Vendor storefront on OMS | | `oms.lu/admin/` | Admin panel for OMS platform |
| `oms.lu/vendor/{code}/` | Vendor dashboard on OMS |
| `https://mybakery.lu/storefront/` | Vendor storefront (vendor's custom domain) |
| `loyalty.lu/` | Loyalty platform homepage | | `loyalty.lu/` | Loyalty platform homepage |
**Note:** In production, vendors configure their own custom domains for storefronts. The platform domain (e.g., `oms.lu`) is used for admin and vendor dashboards, while storefronts use vendor-owned domains.
### Quick Reference by Platform
#### For "oms" Platform
```
Dev:
Platform: http://localhost:8000/platforms/oms/
Admin: http://localhost:8000/platforms/oms/admin/
Vendor: http://localhost:8000/platforms/oms/vendor/{vendor_code}/
Storefront: http://localhost:8000/platforms/oms/vendors/{vendor_code}/storefront/
Prod:
Platform: https://oms.lu/
Admin: https://oms.lu/admin/
Vendor: https://oms.lu/vendor/{vendor_code}/
Storefront: https://mybakery.lu/storefront/ (vendor's custom domain)
```
#### For "loyalty" Platform
```
Dev:
Platform: http://localhost:8000/platforms/loyalty/
Admin: http://localhost:8000/platforms/loyalty/admin/
Vendor: http://localhost:8000/platforms/loyalty/vendor/{vendor_code}/
Storefront: http://localhost:8000/platforms/loyalty/vendors/{vendor_code}/storefront/
Prod:
Platform: https://loyalty.lu/
Admin: https://loyalty.lu/admin/
Vendor: https://loyalty.lu/vendor/{vendor_code}/
Storefront: https://myrewards.lu/storefront/ (vendor's custom domain)
```
### Platform Routing Logic ### Platform Routing Logic
``` ```
@@ -100,10 +138,10 @@ Request arrives
| Platform | Code | Dev URL | Prod Domain | | Platform | Code | Dev URL | Prod Domain |
|----------|------|---------|-------------| |----------|------|---------|-------------|
| Main Marketing | `main` | `localhost:9999/` | `wizamart.lu` | | Main Marketing | `main` | `localhost:8000/` | `wizamart.lu` |
| OMS | `oms` | `localhost:9999/platforms/oms/` | `oms.lu` | | OMS | `oms` | `localhost:8000/platforms/oms/` | `oms.lu` |
| Loyalty | `loyalty` | `localhost:9999/platforms/loyalty/` | `loyalty.lu` | | Loyalty | `loyalty` | `localhost:8000/platforms/loyalty/` | `loyalty.lu` |
| Site Builder | `site-builder` | `localhost:9999/platforms/site-builder/` | `sitebuilder.lu` | | Site Builder | `site-builder` | `localhost:8000/platforms/site-builder/` | `sitebuilder.lu` |
**See:** [Multi-Platform CMS Architecture](../multi-platform-cms.md) for content management details. **See:** [Multi-Platform CMS Architecture](../multi-platform-cms.md) for content management details.
@@ -113,23 +151,23 @@ Request arrives
### 1. SUBDOMAIN MODE (Production - Recommended) ### 1. SUBDOMAIN MODE (Production - Recommended)
**URL Pattern:** `https://VENDOR_SUBDOMAIN.platform.com/shop/...` **URL Pattern:** `https://VENDOR_SUBDOMAIN.platform.com/storefront/...`
**Example:** **Example:**
- Vendor subdomain: `acme` - Vendor subdomain: `acme`
- Platform domain: `wizamart.com` - Platform domain: `wizamart.com`
- Customer Shop URL: `https://acme.wizamart.com/shop/products` - Customer Storefront URL: `https://acme.wizamart.com/storefront/products`
- Product Detail: `https://acme.wizamart.com/shop/products/123` - Product Detail: `https://acme.wizamart.com/storefront/products/123`
**How It Works:** **How It Works:**
1. Customer visits `https://acme.wizamart.com/shop/products` 1. Customer visits `https://acme.wizamart.com/storefront/products`
2. `vendor_context_middleware` detects subdomain `"acme"` 2. `vendor_context_middleware` detects subdomain `"acme"`
3. Queries: `SELECT * FROM vendors WHERE subdomain = 'acme'` 3. Queries: `SELECT * FROM vendors WHERE subdomain = 'acme'`
4. Finds Vendor with ID=1 (ACME Store) 4. Finds Vendor with ID=1 (ACME Store)
5. Sets `request.state.vendor = Vendor(ACME Store)` 5. Sets `request.state.vendor = Vendor(ACME Store)`
6. `context_middleware` detects it's a SHOP request 6. `context_middleware` detects it's a STOREFRONT request
7. `theme_context_middleware` loads ACME's theme 7. `theme_context_middleware` loads ACME's theme
8. Routes to `shop_pages.py``shop_products_page()` 8. Routes to `storefront_pages.py``storefront_products_page()`
9. Renders template with ACME's colors, logo, and products 9. Renders template with ACME's colors, logo, and products
**Advantages:** **Advantages:**
@@ -141,12 +179,12 @@ Request arrives
### 2. CUSTOM DOMAIN MODE (Production - Premium) ### 2. CUSTOM DOMAIN MODE (Production - Premium)
**URL Pattern:** `https://CUSTOM_DOMAIN/shop/...` **URL Pattern:** `https://CUSTOM_DOMAIN/storefront/...`
**Example:** **Example:**
- Vendor name: "ACME Store" - Vendor name: "ACME Store"
- Custom domain: `store.acme-corp.com` - Custom domain: `store.acme-corp.com`
- Customer Shop URL: `https://store.acme-corp.com/shop/products` - Customer Storefront URL: `https://store.acme-corp.com/storefront/products`
**Database Setup:** **Database Setup:**
```sql ```sql
@@ -160,7 +198,7 @@ id | vendor_id | domain | is_active | is_verified
``` ```
**How It Works:** **How It Works:**
1. Customer visits `https://store.acme-corp.com/shop/products` 1. Customer visits `https://store.acme-corp.com/storefront/products`
2. `vendor_context_middleware` detects custom domain (not *.wizamart.com, not localhost) 2. `vendor_context_middleware` detects custom domain (not *.wizamart.com, not localhost)
3. Normalizes domain to `"store.acme-corp.com"` 3. Normalizes domain to `"store.acme-corp.com"`
4. Queries: `SELECT * FROM vendor_domains WHERE domain = 'store.acme-corp.com'` 4. Queries: `SELECT * FROM vendor_domains WHERE domain = 'store.acme-corp.com'`
@@ -181,24 +219,25 @@ id | vendor_id | domain | is_active | is_verified
### 3. PATH-BASED MODE (Development Only) ### 3. PATH-BASED MODE (Development Only)
**URL Pattern:** `http://localhost:PORT/vendors/VENDOR_CODE/shop/...` **URL Pattern:** `http://localhost:PORT/platforms/PLATFORM_CODE/vendors/VENDOR_CODE/storefront/...`
**Example:** **Example:**
- Development: `http://localhost:8000/vendors/acme/shop/products` - Development: `http://localhost:8000/platforms/oms/vendors/acme/storefront/products`
- With port: `http://localhost:8000/vendors/acme/shop/products/123` - With port: `http://localhost:8000/platforms/loyalty/vendors/acme/storefront/products/123`
**How It Works:** **How It Works:**
1. Developer visits `http://localhost:8000/vendors/acme/shop/products` 1. Developer visits `http://localhost:8000/platforms/oms/vendors/acme/storefront/products`
2. `vendor_context_middleware` detects path-based routing pattern `/vendors/acme/...` 2. Platform middleware detects `/platforms/oms/` prefix, sets platform context
3. Extracts vendor code `"acme"` from the path 3. `vendor_context_middleware` detects path-based routing pattern `/vendors/acme/...`
4. Looks up Vendor: `SELECT * FROM vendors WHERE subdomain = 'acme'` 4. Extracts vendor code `"acme"` from the path
5. Sets `request.state.vendor = Vendor(acme)` 5. Looks up Vendor: `SELECT * FROM vendors WHERE subdomain = 'acme'`
6. Routes to shop pages 6. Sets `request.state.vendor = Vendor(acme)`
7. Routes to storefront pages
**Advantages:** **Advantages:**
- Perfect for local development - Perfect for local development
- No need to configure DNS/domains - No need to configure DNS/domains
- Test multiple vendors easily without domain setup - Test multiple vendors and platforms easily without domain setup
**Limitations:** **Limitations:**
- Only for development (not production-ready) - Only for development (not production-ready)
@@ -210,34 +249,34 @@ id | vendor_id | domain | is_active | is_verified
### Subdomain/Custom Domain (PRODUCTION) ### Subdomain/Custom Domain (PRODUCTION)
``` ```
https://acme.wizamart.com/shop/ → Homepage https://acme.wizamart.com/storefront/ → Homepage
https://acme.wizamart.com/shop/products → Product Catalog https://acme.wizamart.com/storefront/products → Product Catalog
https://acme.wizamart.com/shop/products/123 → Product Detail https://acme.wizamart.com/storefront/products/123 → Product Detail
https://acme.wizamart.com/shop/categories/electronics → Category Page https://acme.wizamart.com/storefront/categories/electronics → Category Page
https://acme.wizamart.com/shop/cart → Shopping Cart https://acme.wizamart.com/storefront/cart → Shopping Cart
https://acme.wizamart.com/shop/checkout → Checkout https://acme.wizamart.com/storefront/checkout → Checkout
https://acme.wizamart.com/shop/search?q=laptop → Search Results https://acme.wizamart.com/storefront/search?q=laptop → Search Results
https://acme.wizamart.com/shop/account/login → Customer Login https://acme.wizamart.com/storefront/account/login → Customer Login
https://acme.wizamart.com/shop/account/dashboard → Account Dashboard (Auth Required) https://acme.wizamart.com/storefront/account/dashboard → Account Dashboard (Auth Required)
https://acme.wizamart.com/shop/account/orders → Order History (Auth Required) https://acme.wizamart.com/storefront/account/orders → Order History (Auth Required)
https://acme.wizamart.com/shop/account/profile → Profile (Auth Required) https://acme.wizamart.com/storefront/account/profile → Profile (Auth Required)
``` ```
### Path-Based (DEVELOPMENT) ### Path-Based (DEVELOPMENT)
``` ```
http://localhost:8000/vendors/acme/shop/ → Homepage http://localhost:8000/platforms/oms/vendors/acme/storefront/ → Homepage
http://localhost:8000/vendors/acme/shop/products → Products http://localhost:8000/platforms/oms/vendors/acme/storefront/products → Products
http://localhost:8000/vendors/acme/shop/products/123 → Product Detail http://localhost:8000/platforms/oms/vendors/acme/storefront/products/123 → Product Detail
http://localhost:8000/vendors/acme/shop/cart → Cart http://localhost:8000/platforms/oms/vendors/acme/storefront/cart → Cart
http://localhost:8000/vendors/acme/shop/checkout → Checkout http://localhost:8000/platforms/oms/vendors/acme/storefront/checkout → Checkout
http://localhost:8000/vendors/acme/shop/account/login → Login http://localhost:8000/platforms/oms/vendors/acme/storefront/account/login → Login
``` ```
### API Endpoints (Same for All Modes) ### API Endpoints (Same for All Modes)
``` ```
GET /api/v1/public/vendors/1/products → Get vendor products GET /api/v1/storefront/vendors/1/products → Get vendor products
GET /api/v1/public/vendors/1/products/123 → Get product details GET /api/v1/storefront/vendors/1/products/123 → Get product details
POST /api/v1/public/vendors/1/products/{id}/reviews → Add product review POST /api/v1/storefront/vendors/1/products/{id}/reviews → Add product review
``` ```
--- ---
@@ -266,19 +305,19 @@ POST /api/v1/public/vendors/1/products/{id}/reviews → Add product review
### Example: No Cross-Vendor Leakage ### Example: No Cross-Vendor Leakage
```python ```python
# Customer on acme.wizamart.com tries to access TechPro's products # Customer on acme.wizamart.com tries to access TechPro's products
# They make API call to /api/v1/public/vendors/2/products # They make API call to /api/v1/storefront/vendors/2/products
# Backend checks: # Backend checks:
vendor = get_vendor_from_request(request) # Returns Vendor(id=1, name="ACME") vendor = get_vendor_from_request(request) # Returns Vendor(id=1, name="ACME")
if vendor.id != requested_vendor_id: # if 1 != 2 if vendor.id != requested_vendor_id: # if 1 != 2
raise UnauthorizedShopAccessException() raise UnauthorizedStorefrontAccessException()
``` ```
--- ---
## Request Lifecycle: Complete Flow ## Request Lifecycle: Complete Flow
### Scenario: Customer visits `https://acme.wizamart.com/shop/products` ### Scenario: Customer visits `https://acme.wizamart.com/storefront/products`
``` ```
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
@@ -286,7 +325,7 @@ if vendor.id != requested_vendor_id: # if 1 != 2
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
method: GET method: GET
host: acme.wizamart.com host: acme.wizamart.com
path: /shop/products path: /storefront/products
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
│ 2. MIDDLEWARE CHAIN │ │ 2. MIDDLEWARE CHAIN │
@@ -299,9 +338,9 @@ if vendor.id != requested_vendor_id: # if 1 != 2
└─ Sets: request.state.vendor = Vendor(ACME Store) └─ Sets: request.state.vendor = Vendor(ACME Store)
B) context_middleware B) context_middleware
├─ Checks path: "/shop/products" ├─ Checks path: "/storefront/products"
├─ Has request.state.vendor? YES ├─ Has request.state.vendor? YES
└─ Sets: request.state.context_type = RequestContext.SHOP └─ Sets: request.state.context_type = RequestContext.STOREFRONT
C) theme_context_middleware C) theme_context_middleware
├─ Queries: SELECT * FROM vendor_themes WHERE vendor_id = 1 ├─ Queries: SELECT * FROM vendor_themes WHERE vendor_id = 1
@@ -310,17 +349,17 @@ if vendor.id != requested_vendor_id: # if 1 != 2
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
│ 3. ROUTE MATCHING │ │ 3. ROUTE MATCHING │
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
Path: /shop/products Path: /storefront/products
Matches: @router.get("/shop/products") Matches: @router.get("/storefront/products")
Handler: shop_products_page(request) Handler: storefront_products_page(request)
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
│ 4. HANDLER EXECUTES │ │ 4. HANDLER EXECUTES │
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
@router.get("/shop/products", response_class=HTMLResponse) @router.get("/storefront/products", response_class=HTMLResponse)
async def shop_products_page(request: Request): async def storefront_products_page(request: Request):
return templates.TemplateResponse( return templates.TemplateResponse(
"shop/products.html", "storefront/products.html",
{"request": request} {"request": request}
) )
@@ -336,7 +375,7 @@ if vendor.id != requested_vendor_id: # if 1 != 2
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
│ 6. JAVASCRIPT LOADS PRODUCTS (Client-Side) │ │ 6. JAVASCRIPT LOADS PRODUCTS (Client-Side) │
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
fetch(`/api/v1/public/vendors/1/products`) fetch(`/api/v1/storefront/vendors/1/products`)
.then(data => renderProducts(data.products, {theme})) .then(data => renderProducts(data.products, {theme}))
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
@@ -349,7 +388,7 @@ if vendor.id != requested_vendor_id: # if 1 != 2
## Theme Integration ## Theme Integration
Each vendor's shop is fully branded with their custom theme: Each vendor's storefront is fully branded with their custom theme:
```python ```python
# Theme loaded for https://acme.wizamart.com # Theme loaded for https://acme.wizamart.com
@@ -421,25 +460,26 @@ In Jinja2 template:
**Current Solution: Double Router Mounting** **Current Solution: Double Router Mounting**
The application handles path-based routing by registering shop routes **twice** with different prefixes: The application handles path-based routing by registering storefront routes **twice** with different prefixes:
```python ```python
# In main.py # In main.py
app.include_router(shop_pages.router, prefix="/shop") app.include_router(storefront_pages.router, prefix="/storefront")
app.include_router(shop_pages.router, prefix="/vendors/{vendor_code}/shop") app.include_router(storefront_pages.router, prefix="/vendors/{vendor_code}/storefront")
``` ```
**How This Works:** **How This Works:**
1. **For Subdomain/Custom Domain Mode:** 1. **For Subdomain/Custom Domain Mode:**
- URL: `https://acme.wizamart.com/shop/products` - URL: `https://acme.wizamart.com/storefront/products`
- Matches: First router with `/shop` prefix - Matches: First router with `/storefront` prefix
- Route: `@router.get("/products")` → Full path: `/shop/products` - Route: `@router.get("/products")` → Full path: `/storefront/products`
2. **For Path-Based Development Mode:** 2. **For Path-Based Development Mode:**
- URL: `http://localhost:8000/vendors/acme/shop/products` - URL: `http://localhost:8000/platforms/oms/vendors/acme/storefront/products`
- Matches: Second router with `/vendors/{vendor_code}/shop` prefix - Platform middleware strips `/platforms/oms/` prefix, sets platform context
- Route: `@router.get("/products")` → Full path: `/vendors/{vendor_code}/shop/products` - Matches: Second router with `/vendors/{vendor_code}/storefront` prefix
- Route: `@router.get("/products")` → Full path: `/vendors/{vendor_code}/storefront/products`
- Bonus: `vendor_code` available as path parameter! - Bonus: `vendor_code` available as path parameter!
**Benefits:** **Benefits:**
@@ -451,13 +491,13 @@ app.include_router(shop_pages.router, prefix="/vendors/{vendor_code}/shop")
--- ---
## Authentication in Multi-Tenant Shop ## Authentication in Multi-Tenant Storefront
Customer authentication uses vendor-scoped cookies: Customer authentication uses vendor-scoped cookies:
```python ```python
# Login sets cookie scoped to vendor's shop # Login sets cookie scoped to vendor's storefront
Set-Cookie: customer_token=eyJ...; Path=/shop; HttpOnly; SameSite=Lax Set-Cookie: customer_token=eyJ...; Path=/storefront; HttpOnly; SameSite=Lax
# This prevents: # This prevents:
# - Tokens leaking across vendors # - Tokens leaking across vendors
@@ -471,9 +511,9 @@ Set-Cookie: customer_token=eyJ...; Path=/shop; HttpOnly; SameSite=Lax
| Mode | URL | Use Case | SSL | DNS | | Mode | URL | Use Case | SSL | DNS |
|------|-----|----------|-----|-----| |------|-----|----------|-----|-----|
| Subdomain | `vendor.platform.com/shop` | Production (standard) | *.platform.com | Add subdomains | | Subdomain | `vendor.platform.com/storefront` | Production (standard) | *.platform.com | Add subdomains |
| Custom Domain | `vendor-domain.com/shop` | Production (premium) | Per vendor | Vendor configures | | Custom Domain | `vendor-domain.com/storefront` | Production (premium) | Per vendor | Vendor configures |
| Path-Based | `localhost:8000/vendors/v/shop` | Development only | None | None | | Path-Based | `localhost:8000/platforms/{p}/vendors/{v}/storefront` | Development only | None | None |
--- ---
@@ -487,5 +527,5 @@ Set-Cookie: customer_token=eyJ...; Path=/shop; HttpOnly; SameSite=Lax
--- ---
Generated: November 7, 2025 Generated: January 30, 2026
Wizamart Version: Current Development Wizamart Version: Current Development

View File

@@ -493,7 +493,7 @@ Content-Security-Policy:
## Related Documentation ## Related Documentation
- [Shop Frontend Architecture](shop/architecture.md) - [Storefront Architecture](storefront/architecture.md)
- [Vendor Frontend Architecture](vendor/architecture.md) - [Vendor Frontend Architecture](vendor/architecture.md)
- [Admin Frontend Architecture](admin/architecture.md) - [Admin Frontend Architecture](admin/architecture.md)
- [Production Deployment](../deployment/production.md) - [Production Deployment](../deployment/production.md)

View File

@@ -395,7 +395,7 @@ window.LogConfig = {
- [Admin Page Templates](../admin/page-templates.md) - [Admin Page Templates](../admin/page-templates.md)
- [Vendor Page Templates](../vendor/page-templates.md) - [Vendor Page Templates](../vendor/page-templates.md)
- [Shop Page Templates](../shop/page-templates.md) - [Storefront Page Templates](../storefront/page-templates.md)
--- ---

View File

@@ -64,7 +64,7 @@ nav:
# --- API Reference --- # --- API Reference ---
- API Reference: - API Reference:
- Overview: api/index.md - Overview: api/index.md
- Shop API Reference: api/shop-api-reference.md - Storefront API Reference: api/storefront-api-reference.md
- Authentication: - Authentication:
- Guide: api/authentication.md - Guide: api/authentication.md
- Quick Reference: api/authentication-quick-reference.md - Quick Reference: api/authentication-quick-reference.md
@@ -106,12 +106,12 @@ nav:
- Vendor Frontend: - Vendor Frontend:
- Architecture: frontend/vendor/architecture.md - Architecture: frontend/vendor/architecture.md
- Page Templates: frontend/vendor/page-templates.md - Page Templates: frontend/vendor/page-templates.md
- Shop Frontend: - Storefront:
- Architecture: frontend/shop/architecture.md - Architecture: frontend/storefront/architecture.md
- Page Templates: frontend/shop/page-templates.md - Page Templates: frontend/storefront/page-templates.md
- E-commerce Components Proposal: frontend/shop/ecommerce-components-proposal.md - E-commerce Components Proposal: frontend/storefront/ecommerce-components-proposal.md
- Authentication Pages: frontend/shop/authentication-pages.md - Authentication Pages: frontend/storefront/authentication-pages.md
- Navigation Flow: frontend/shop/navigation-flow.md - Navigation Flow: frontend/storefront/navigation-flow.md
# --- Development --- # --- Development ---
- Development: - Development: