feat: multi-module improvements across merchant, store, i18n, and customer systems
All checks were successful
CI / ruff (push) Successful in 12s
CI / pytest (push) Successful in 50m57s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Successful in 40s
CI / deploy (push) Successful in 51s

- Fix platform-grouped merchant sidebar menu with core items at root level
- Add merchant store management (detail page, create store, team page)
- Fix store settings 500 error by removing dead stripe/API tab
- Move onboarding translations to module-owned locale files
- Fix onboarding banner i18n with server-side rendering + context inheritance
- Refactor login language selectors to use languageSelector() function (LANG-002)
- Move HTTPException handling to global exception handler in merchant routes (API-003)
- Add language selector to all login pages and portal headers
- Fix customer module: drop order stats from customer model, add to orders module
- Fix admin menu config visibility for super admin platform context
- Fix storefront auth and layout issues
- Add missing i18n translations for onboarding steps (en/fr/de/lb)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 23:48:25 +01:00
parent f141cc4e6a
commit a77a8a3a98
113 changed files with 3741 additions and 2923 deletions

View File

@@ -164,6 +164,7 @@ def get_accessible_platforms(
],
"is_super_admin": current_user.is_super_admin,
"requires_platform_selection": not current_user.is_super_admin and len(platforms) > 0,
"current_platform_id": current_user.token_platform_id,
}
@@ -175,10 +176,10 @@ def select_platform(
current_user: UserContext = Depends(get_current_admin_from_cookie_or_header),
):
"""
Select platform context for platform admin.
Select platform context for an admin.
Issues a new JWT token with platform context.
Super admins skip this step (they have global access).
Available to both platform admins and super admins.
Args:
platform_id: Platform ID to select
@@ -186,13 +187,9 @@ def select_platform(
Returns:
PlatformSelectResponse with new token and platform info
"""
if current_user.is_super_admin:
raise InvalidCredentialsException(
"Super admins don't need platform selection - they have global access"
)
# Verify admin has access to this platform (raises exception if not)
admin_platform_service.validate_admin_platform_access(current_user, platform_id)
# Platform admins must have access; super admins can access any platform
if not current_user.is_super_admin:
admin_platform_service.validate_admin_platform_access(current_user, platform_id)
# Load platform
platform = admin_platform_service.get_platform_by_id(db, platform_id)
@@ -227,3 +224,45 @@ def select_platform(
platform_id=platform.id,
platform_code=platform.code,
)
@admin_auth_router.post("/deselect-platform")
def deselect_platform(
response: Response,
current_user: UserContext = Depends(get_current_admin_from_cookie_or_header),
):
"""
Deselect platform context (return to global mode).
Only available to super admins. Issues a new JWT without platform context.
Returns:
New token without platform context
"""
if not current_user.is_super_admin:
raise InvalidCredentialsException(
"Only super admins can deselect platform (platform admins must always have a platform)"
)
# Issue new token without platform context
auth_manager = AuthManager()
token_data = auth_manager.create_access_token(user=current_user)
# Set cookie with new token
response.set_cookie(
key="admin_token",
value=token_data["access_token"],
httponly=True,
secure=should_use_secure_cookies(),
samesite="lax",
max_age=token_data["expires_in"],
path="/admin",
)
logger.info(f"Super admin {current_user.username} deselected platform (global mode)")
return {
"access_token": token_data["access_token"],
"token_type": token_data["token_type"],
"expires_in": token_data["expires_in"],
}

View File

@@ -20,9 +20,13 @@ from app.modules.tenancy.schemas import (
MerchantPortalProfileResponse,
MerchantPortalProfileUpdate,
MerchantPortalStoreListResponse,
MerchantStoreCreate,
MerchantStoreDetailResponse,
MerchantStoreUpdate,
)
from app.modules.tenancy.schemas.auth import UserContext
from app.modules.tenancy.services.merchant_service import merchant_service
from app.modules.tenancy.services.merchant_store_service import merchant_store_service
from .email_verification import email_verification_api_router
from .merchant_auth import merchant_auth_router
@@ -63,14 +67,113 @@ async def merchant_stores(
db, merchant.id, skip=skip, limit=limit
)
can_create, _ = merchant_store_service.can_create_store(db, merchant.id)
return MerchantPortalStoreListResponse(
stores=stores,
total=total,
skip=skip,
limit=limit,
can_create_store=can_create,
)
@_account_router.post("/stores", response_model=MerchantStoreDetailResponse)
async def create_merchant_store(
store_data: MerchantStoreCreate,
current_user: UserContext = Depends(get_current_merchant_api),
merchant=Depends(get_merchant_for_current_user),
db: Session = Depends(get_db),
):
"""
Create a new store under the merchant.
Checks subscription tier store limits before creation.
New stores are created with is_active=True, is_verified=False.
"""
# Service raises MaxStoresReachedException, StoreAlreadyExistsException,
# or StoreValidationException — all handled by global exception handler.
result = merchant_store_service.create_store(
db,
merchant.id,
store_data.model_dump(),
)
db.commit()
logger.info(
f"Merchant {merchant.id} ({current_user.username}) created store "
f"'{store_data.store_code}'"
)
return result
@_account_router.get("/stores/{store_id}", response_model=MerchantStoreDetailResponse)
async def get_merchant_store_detail(
store_id: int,
merchant=Depends(get_merchant_for_current_user),
db: Session = Depends(get_db),
):
"""
Get detailed store information with ownership validation.
Returns store details including platform assignments.
"""
# StoreNotFoundException handled by global exception handler
return merchant_store_service.get_store_detail(db, merchant.id, store_id)
@_account_router.put("/stores/{store_id}", response_model=MerchantStoreDetailResponse)
async def update_merchant_store(
store_id: int,
update_data: MerchantStoreUpdate,
current_user: UserContext = Depends(get_current_merchant_api),
merchant=Depends(get_merchant_for_current_user),
db: Session = Depends(get_db),
):
"""
Update store details (merchant-allowed fields only).
Only name, description, and contact info fields can be updated.
"""
# StoreNotFoundException handled by global exception handler
result = merchant_store_service.update_store(
db,
merchant.id,
store_id,
update_data.model_dump(exclude_unset=True),
)
db.commit()
logger.info(
f"Merchant {merchant.id} ({current_user.username}) updated store {store_id}"
)
return result
@_account_router.get("/platforms")
async def get_merchant_platforms(
merchant=Depends(get_merchant_for_current_user),
db: Session = Depends(get_db),
):
"""
Get platforms available for the merchant (from active subscriptions).
Used by the store creation/edit UI to show platform selection options.
"""
return merchant_store_service.get_subscribed_platform_ids(db, merchant.id)
@_account_router.get("/team")
async def merchant_team_overview(
merchant=Depends(get_merchant_for_current_user),
db: Session = Depends(get_db),
):
"""
Get team members across all stores owned by the merchant.
Returns a list of stores with their team members grouped by store.
"""
return merchant_store_service.get_merchant_team_overview(db, merchant.id)
@_account_router.get("/profile", response_model=MerchantPortalProfileResponse)
async def merchant_profile(
request: Request,