feat: platform-aware storefront routing and billing improvements

Overhaul storefront URL routing to be platform-aware:
- Dev: /platforms/{code}/storefront/{store_code}/
- Prod: subdomain.platform.lu/ (internally rewritten to /storefront/)
- Add subdomain detection in PlatformContextMiddleware
- Add /storefront/ path rewrite for prod mode (subdomain/custom domain)
- Remove all silent platform fallbacks (platform_id=1)
- Add require_platform dependency for clean endpoint validation
- Update route registration, templates, module definitions, base_url calc
- Update StoreContextMiddleware for /storefront/ path detection
- Remove /stores/ from FrontendDetector STOREFRONT_PATH_PREFIXES

Billing service improvements:
- Add store_platform_sync_service to keep store_platforms in sync
- Make tier lookups platform-aware across billing services
- Add tiers for all platforms in seed data
- Add demo subscriptions to seed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 23:42:41 +01:00
parent d36783a7f1
commit 32acc76b49
56 changed files with 951 additions and 306 deletions

View File

@@ -6,7 +6,7 @@ Detects store from host/domain/path and injects into request.state.
Handles three routing modes:
1. Custom domains (customdomain1.com → Store 1)
2. Subdomains (store1.platform.com → Store 1)
3. Path-based (/store/store1/ or /stores/store1/ → Store 1)
3. Path-based (/store/store1/, /stores/store1/, or /storefront/store1/ → Store 1)
Also extracts clean_path for nested routing patterns.
@@ -89,11 +89,12 @@ class StoreContextManager:
"host": host,
}
# Method 3: Path-based detection (/store/storename/ or /stores/storename/)
# Support BOTH patterns for flexibility
if path.startswith(("/store/", "/stores/")):
# Method 3: Path-based detection (/store/storename/, /stores/storename/, /storefront/storename/)
if path.startswith(("/store/", "/stores/", "/storefront/")):
# Determine which pattern
if path.startswith("/stores/"):
if path.startswith("/storefront/"):
prefix_len = len("/storefront/")
elif path.startswith("/stores/"):
prefix_len = len("/stores/")
else:
prefix_len = len("/store/")
@@ -105,7 +106,7 @@ class StoreContextManager:
"subdomain": store_code,
"detection_method": "path",
"path_prefix": path[: prefix_len + len(store_code)],
"full_prefix": path[:prefix_len], # /store/ or /stores/
"full_prefix": path[:prefix_len], # /store/, /stores/, or /storefront/
"host": host,
}
@@ -269,13 +270,27 @@ class StoreContextManager:
},
)
# Method 1: Path-based detection from referer path
# Method 1: Path-based detection from referer path (local hosts only)
# /platforms/oms/storefront/WIZATECH/products → WIZATECH
# /stores/orion/storefront/products → orion
if referer_path.startswith(("/stores/", "/store/")):
prefix = (
"/stores/" if referer_path.startswith("/stores/") else "/store/"
)
path_parts = referer_path[len(prefix) :].split("/")
# /storefront/WIZATECH/products → WIZATECH
# Note: For subdomain/custom domain hosts, the store code is NOT in the path
# (e.g., orion.platform.com/storefront/products — "products" is a page, not a store)
is_local_referer = referer_host in ("localhost", "127.0.0.1", "testserver")
if is_local_referer and referer_path.startswith("/platforms/"):
# Strip /platforms/{code}/ to get clean path
after_platforms = referer_path[11:] # Remove "/platforms/"
parts = after_platforms.split("/", 1)
referer_path = "/" + parts[1] if len(parts) > 1 and parts[1] else "/"
if is_local_referer and referer_path.startswith(("/stores/", "/store/", "/storefront/")):
if referer_path.startswith("/storefront/"):
prefix = "/storefront/"
elif referer_path.startswith("/stores/"):
prefix = "/stores/"
else:
prefix = "/store/"
path_parts = referer_path[len(prefix):].split("/")
if len(path_parts) >= 1 and path_parts[0]:
store_code = path_parts[0]
prefix_len = len(prefix)
@@ -283,15 +298,13 @@ class StoreContextManager:
f"[STORE] Extracted store from Referer path: {store_code}",
extra={"store_code": store_code, "method": "referer_path"},
)
# Use "path" as detection_method to be consistent with direct path detection
# This allows cookie path logic to work the same way
return {
"subdomain": store_code,
"detection_method": "path", # Consistent with direct path detection
"detection_method": "path",
"path_prefix": referer_path[
: prefix_len + len(store_code)
], # /store/store1
"full_prefix": prefix, # /store/ or /stores/
],
"full_prefix": prefix,
"host": referer_host,
"referer": referer,
}