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:
@@ -68,12 +68,14 @@ from app.modules.orders.models import Order, OrderItem
|
||||
# cross-module string relationships (e.g. Store→StoreEmailTemplate,
|
||||
# Platform→SubscriptionTier, Product→Inventory).
|
||||
# Core modules
|
||||
from app.modules.billing.models.merchant_subscription import MerchantSubscription
|
||||
from app.modules.tenancy.models import (
|
||||
Merchant,
|
||||
PlatformAlert,
|
||||
Role,
|
||||
Store,
|
||||
StoreDomain,
|
||||
StorePlatform,
|
||||
StoreUser,
|
||||
User,
|
||||
)
|
||||
@@ -207,6 +209,17 @@ DEMO_STORES = [
|
||||
},
|
||||
]
|
||||
|
||||
# Demo subscriptions (linked to merchants by index)
|
||||
DEMO_SUBSCRIPTIONS = [
|
||||
# WizaCorp: OMS (professional, active) + Loyalty (essential, trial)
|
||||
{"merchant_index": 0, "platform_code": "oms", "tier_code": "professional", "trial_days": 0},
|
||||
{"merchant_index": 0, "platform_code": "loyalty", "tier_code": "essential", "trial_days": 14},
|
||||
# Fashion Group: Loyalty only (essential, trial)
|
||||
{"merchant_index": 1, "platform_code": "loyalty", "tier_code": "essential", "trial_days": 14},
|
||||
# BookWorld: OMS (business, active)
|
||||
{"merchant_index": 2, "platform_code": "oms", "tier_code": "business", "trial_days": 0},
|
||||
]
|
||||
|
||||
# Demo team members (linked to merchants by index, assigned to stores by store_code)
|
||||
DEMO_TEAM_MEMBERS = [
|
||||
# WizaCorp team
|
||||
@@ -656,6 +669,76 @@ def create_demo_merchants(db: Session, auth_manager: AuthManager) -> list[Mercha
|
||||
return merchants
|
||||
|
||||
|
||||
def create_demo_subscriptions(db: Session, merchants: list[Merchant]) -> None:
|
||||
"""Create demo merchant subscriptions."""
|
||||
from app.modules.billing.models.subscription import SubscriptionTier
|
||||
from app.modules.tenancy.models import Platform
|
||||
|
||||
sub_count = 1 if SEED_MODE == "minimal" else len(DEMO_SUBSCRIPTIONS)
|
||||
subs_to_create = DEMO_SUBSCRIPTIONS[:sub_count]
|
||||
|
||||
for sub_data in subs_to_create:
|
||||
merchant_index = sub_data["merchant_index"]
|
||||
if merchant_index >= len(merchants):
|
||||
print_error(f"Invalid merchant_index {merchant_index} for subscription")
|
||||
continue
|
||||
|
||||
merchant = merchants[merchant_index]
|
||||
|
||||
# Look up platform by code
|
||||
platform = db.execute(
|
||||
select(Platform).where(Platform.code == sub_data["platform_code"])
|
||||
).scalar_one_or_none()
|
||||
|
||||
if not platform:
|
||||
print_warning(f"Platform '{sub_data['platform_code']}' not found, skipping")
|
||||
continue
|
||||
|
||||
# Check if subscription already exists
|
||||
existing = db.execute(
|
||||
select(MerchantSubscription).where(
|
||||
MerchantSubscription.merchant_id == merchant.id,
|
||||
MerchantSubscription.platform_id == platform.id,
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
print_warning(
|
||||
f"Subscription already exists: {merchant.name} on {platform.name}"
|
||||
)
|
||||
continue
|
||||
|
||||
# Look up tier by code + platform
|
||||
tier = db.execute(
|
||||
select(SubscriptionTier).where(
|
||||
SubscriptionTier.code == sub_data["tier_code"],
|
||||
SubscriptionTier.platform_id == platform.id,
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if not tier:
|
||||
print_warning(
|
||||
f"Tier '{sub_data['tier_code']}' not found for {platform.name}, skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
from app.modules.billing.services.subscription_service import subscription_service
|
||||
|
||||
subscription = subscription_service.create_merchant_subscription(
|
||||
db,
|
||||
merchant_id=merchant.id,
|
||||
platform_id=platform.id,
|
||||
tier_code=sub_data["tier_code"],
|
||||
trial_days=sub_data.get("trial_days", 14),
|
||||
)
|
||||
print_success(
|
||||
f"Created subscription: {merchant.name} → {platform.name} "
|
||||
f"({tier.name}, {subscription.status})"
|
||||
)
|
||||
|
||||
db.flush()
|
||||
|
||||
|
||||
def create_demo_stores(
|
||||
db: Session, merchants: list[Merchant], auth_manager: AuthManager
|
||||
) -> list[Store]:
|
||||
@@ -703,6 +786,29 @@ def create_demo_stores(
|
||||
db.add(store) # noqa: PERF006
|
||||
db.flush()
|
||||
|
||||
# Link store to merchant's subscribed platforms
|
||||
merchant_subs = db.execute(
|
||||
select(MerchantSubscription.platform_id).where(
|
||||
MerchantSubscription.merchant_id == merchant.id
|
||||
)
|
||||
).all()
|
||||
|
||||
for i, (platform_id,) in enumerate(merchant_subs):
|
||||
sp = StorePlatform(
|
||||
store_id=store.id,
|
||||
platform_id=platform_id,
|
||||
is_active=True,
|
||||
is_primary=(i == 0),
|
||||
)
|
||||
db.add(sp)
|
||||
|
||||
if merchant_subs:
|
||||
db.flush()
|
||||
print_success(
|
||||
f" Linked to {len(merchant_subs)} platform(s): "
|
||||
f"{[pid for (pid,) in merchant_subs]}"
|
||||
)
|
||||
|
||||
# Owner relationship is via Merchant.owner_user_id — no StoreUser needed
|
||||
|
||||
# Create store theme
|
||||
@@ -1060,28 +1166,32 @@ def seed_demo_data(db: Session, auth_manager: AuthManager):
|
||||
print_step(4, "Creating demo merchants...")
|
||||
merchants = create_demo_merchants(db, auth_manager)
|
||||
|
||||
# Step 5: Create stores
|
||||
print_step(5, "Creating demo stores...")
|
||||
# Step 5: Create merchant subscriptions (before stores, so StorePlatform linking works)
|
||||
print_step(5, "Creating demo subscriptions...")
|
||||
create_demo_subscriptions(db, merchants)
|
||||
|
||||
# Step 6: Create stores
|
||||
print_step(6, "Creating demo stores...")
|
||||
stores = create_demo_stores(db, merchants, auth_manager)
|
||||
|
||||
# Step 6: Create team members
|
||||
print_step(6, "Creating demo team members...")
|
||||
# Step 7: Create team members
|
||||
print_step(7, "Creating demo team members...")
|
||||
create_demo_team_members(db, stores, auth_manager)
|
||||
|
||||
# Step 7: Create customers
|
||||
print_step(7, "Creating demo customers...")
|
||||
# Step 8: Create customers
|
||||
print_step(8, "Creating demo customers...")
|
||||
for store in stores:
|
||||
create_demo_customers(
|
||||
db, store, auth_manager, count=settings.seed_customers_per_store
|
||||
)
|
||||
|
||||
# Step 8: Create products
|
||||
print_step(8, "Creating demo products...")
|
||||
# Step 9: Create products
|
||||
print_step(9, "Creating demo products...")
|
||||
for store in stores:
|
||||
create_demo_products(db, store, count=settings.seed_products_per_store)
|
||||
|
||||
# Step 9: Create store content pages
|
||||
print_step(9, "Creating store content page overrides...")
|
||||
# Step 10: Create store content pages
|
||||
print_step(10, "Creating store content page overrides...")
|
||||
create_demo_store_content_pages(db, stores)
|
||||
|
||||
# Commit all changes
|
||||
@@ -1199,26 +1309,46 @@ def print_summary(db: Session):
|
||||
print(" (Replace {subdomain} with store subdomain, e.g., wizatech)")
|
||||
print()
|
||||
|
||||
print("\n🏪 Shop Access (Development):")
|
||||
# Build store → platform code mapping from store_platforms
|
||||
from app.modules.tenancy.models import Platform
|
||||
|
||||
store_platform_rows = db.execute(
|
||||
select(StorePlatform.store_id, Platform.code).join(
|
||||
Platform, Platform.id == StorePlatform.platform_id
|
||||
).where(StorePlatform.is_active == True).order_by( # noqa: E712
|
||||
StorePlatform.store_id, StorePlatform.is_primary.desc()
|
||||
)
|
||||
).all()
|
||||
store_platform_map: dict[int, list[str]] = {}
|
||||
for store_id, platform_code in store_platform_rows:
|
||||
store_platform_map.setdefault(store_id, []).append(platform_code)
|
||||
|
||||
port = settings.api_port
|
||||
base = f"http://localhost:{port}"
|
||||
|
||||
print("\n🏪 Store Access (Development):")
|
||||
print("─" * 70)
|
||||
for store in stores:
|
||||
print(f" {store.name}:")
|
||||
print(
|
||||
f" Path-based: http://localhost:8000/stores/{store.store_code}/shop/"
|
||||
)
|
||||
print(f" Subdomain: http://{store.subdomain}.localhost:8000/") # noqa: SEC034
|
||||
platform_codes = store_platform_map.get(store.id, [])
|
||||
print(f" {store.name} ({store.store_code}):")
|
||||
if platform_codes:
|
||||
for pc in platform_codes:
|
||||
print(f" [{pc}] Storefront: {base}/platforms/{pc}/storefront/{store.store_code}/")
|
||||
print(f" [{pc}] Dashboard: {base}/platforms/{pc}/store/{store.store_code}/")
|
||||
print(f" [{pc}] Login: {base}/platforms/{pc}/store/{store.store_code}/login")
|
||||
else:
|
||||
print(" (!) No platform assigned")
|
||||
print()
|
||||
|
||||
print("⚠️ ALL DEMO CREDENTIALS ARE INSECURE - For development only!")
|
||||
|
||||
port = settings.api_port
|
||||
print("\n🚀 NEXT STEPS:")
|
||||
print(" 1. Start development: make dev")
|
||||
print(f" 2. Admin panel: http://localhost:{port}/admin/login")
|
||||
print(f" 3. Merchant panel: http://localhost:{port}/merchants/login")
|
||||
print(f" 4. Store panel: http://localhost:{port}/store/WIZATECH/login")
|
||||
print(f" 5. Storefront: http://localhost:{port}/stores/WIZATECH/shop/")
|
||||
print(f" 6. Customer login: http://localhost:{port}/stores/WIZATECH/shop/account")
|
||||
print(f" 2. Admin panel: {base}/admin/login")
|
||||
print(f" 3. Merchant panel: {base}/merchants/login")
|
||||
print(f" 4. Store panel: {base}/platforms/oms/store/WIZATECH/login")
|
||||
print(f" 5. Storefront: {base}/platforms/oms/storefront/WIZATECH/")
|
||||
print(f" 6. Customer login: {base}/platforms/oms/storefront/WIZATECH/account")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user