feat: complete dynamic menu system across all frontends
All checks were successful
CI / ruff (push) Successful in 11s
CI / pytest (push) Successful in 44m40s
CI / validate (push) Successful in 22s
CI / dependency-scanning (push) Successful in 28s
CI / docs (push) Successful in 39s
CI / deploy (push) Successful in 49s

- Add "Merchant Frontend" tab to admin menu-config page
- Merchant render endpoint now respects AdminMenuConfig visibility
  via get_merchant_primary_platform_id() platform resolution
- New store menu render endpoint (GET /store/core/menu/render/store)
  with platform-scoped visibility and store_code interpolation
- Store sidebar migrated from hardcoded Jinja2 macros to dynamic
  Alpine.js x-for rendering with loading skeleton and fallback
- Store init-alpine.js: add loadMenuConfig(), expandSectionForCurrentPage()
- Include store page route fixes, login template updates, and tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 02:14:42 +01:00
parent be248222bc
commit 506171503d
14 changed files with 1364 additions and 158 deletions

View File

@@ -325,6 +325,106 @@ class MenuService:
return all_enabled
def get_merchant_primary_platform_id(
self,
db: Session,
merchant_id: int,
) -> int | None:
"""
Get the primary platform ID for a merchant's visibility config.
Resolution order:
1. Platform from the store marked is_primary in StorePlatform
2. First active subscription's platform (fallback)
Args:
db: Database session
merchant_id: Merchant ID
Returns:
Platform ID or None if no active subscriptions
"""
from app.modules.billing.models.merchant_subscription import (
MerchantSubscription,
)
from app.modules.billing.models.subscription import SubscriptionStatus
from app.modules.tenancy.models import Store
from app.modules.tenancy.models.store_platform import StorePlatform
active_statuses = [
SubscriptionStatus.TRIAL.value,
SubscriptionStatus.ACTIVE.value,
SubscriptionStatus.PAST_DUE.value,
SubscriptionStatus.CANCELLED.value,
]
# Try to find the primary store's platform
primary_platform_id = (
db.query(StorePlatform.platform_id)
.join(Store, Store.id == StorePlatform.store_id)
.join(
MerchantSubscription,
(MerchantSubscription.platform_id == StorePlatform.platform_id)
& (MerchantSubscription.merchant_id == merchant_id),
)
.filter(
Store.merchant_id == merchant_id,
Store.is_active == True, # noqa: E712
StorePlatform.is_primary == True, # noqa: E712
StorePlatform.is_active == True, # noqa: E712
MerchantSubscription.status.in_(active_statuses),
)
.first()
)
if primary_platform_id:
return primary_platform_id[0]
# Fallback: first active subscription's platform
first_sub = (
db.query(MerchantSubscription.platform_id)
.filter(
MerchantSubscription.merchant_id == merchant_id,
MerchantSubscription.status.in_(active_statuses),
)
.order_by(MerchantSubscription.id)
.first()
)
return first_sub[0] if first_sub else None
def get_store_primary_platform_id(
self,
db: Session,
store_id: int,
) -> int | None:
"""
Get the primary platform ID for a store's menu visibility config.
Prefers the active StorePlatform marked is_primary, falls back to
the first active StorePlatform by ID.
Args:
db: Database session
store_id: Store ID
Returns:
Platform ID or None if no active store-platform link
"""
from app.modules.tenancy.models.store_platform import StorePlatform
sp = (
db.query(StorePlatform.platform_id)
.filter(
StorePlatform.store_id == store_id,
StorePlatform.is_active == True, # noqa: E712
)
.order_by(StorePlatform.is_primary.desc(), StorePlatform.id)
.first()
)
return sp[0] if sp else None
def get_merchant_for_menu(
self,
db: Session,