feat: consolidate media service, add merchant users page, fix metrics overlap

- Merge ImageService into MediaService with WebP variant generation,
  DB-backed storage stats, and module-driven media usage discovery
  via new MediaUsageProviderProtocol
- Add merchant users admin page with scoped user listing, stats
  endpoint, template, JS, and i18n strings (de/en/fr/lb)
- Fix merchant user metrics so Owners and Team Members are mutually
  exclusive (filter team_members on user_type="member" and exclude
  owner IDs) ensuring stat cards add up correctly
- Update billing and monitoring services to use media_service
- Update subscription-billing and feature-gating docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 21:17:11 +01:00
parent 4cb2bda575
commit 2250054ba2
30 changed files with 1220 additions and 805 deletions

View File

@@ -38,6 +38,7 @@ def get_all_users(
per_page: int = Query(10, ge=1, le=100),
search: str = Query("", description="Search by username or email"),
role: str = Query("", description="Filter by role"),
scope: str = Query("", description="Filter scope: 'merchant' for merchant owners and team members"),
is_active: str = Query("", description="Filter by active status"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
@@ -54,11 +55,35 @@ def get_all_users(
per_page=per_page,
search=search if search else None,
role=role if role else None,
scope=scope if scope else None,
is_active=is_active_bool,
)
if scope == "merchant":
items = [
UserDetailResponse(
id=user.id,
email=user.email,
username=user.username,
role=user.role,
is_active=user.is_active,
last_login=user.last_login,
created_at=user.created_at,
updated_at=user.updated_at,
first_name=user.first_name,
last_name=user.last_name,
full_name=user.full_name,
is_email_verified=user.is_email_verified,
owned_merchants_count=len(user.owned_merchants) if user.owned_merchants else 0,
store_memberships_count=len(user.store_memberships) if user.store_memberships else 0,
)
for user in users
]
else:
items = [UserResponse.model_validate(user) for user in users]
return UserListResponse(
items=[UserResponse.model_validate(user) for user in users],
items=items,
total=total,
page=page,
per_page=per_page,
@@ -161,6 +186,42 @@ def get_user_statistics(
return stats
@admin_platform_users_router.get("/merchant-stats")
def get_merchant_user_statistics(
request: Request,
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""Get merchant user statistics for admin dashboard.
Uses the stats_aggregator to get merchant user metrics from the tenancy
module's MetricsProvider.
"""
platform_id = _get_platform_id(request, current_admin)
metrics = stats_aggregator.get_admin_dashboard_stats(db=db, platform_id=platform_id)
tenancy_metrics = metrics.get("tenancy", [])
stats = {
"merchant_users_total": 0,
"merchant_users_active": 0,
"merchant_owners": 0,
"merchant_team_members": 0,
}
for metric in tenancy_metrics:
if metric.key == "tenancy.merchant_users_total":
stats["merchant_users_total"] = int(metric.value)
elif metric.key == "tenancy.merchant_users_active":
stats["merchant_users_active"] = int(metric.value)
elif metric.key == "tenancy.merchant_owners":
stats["merchant_owners"] = int(metric.value)
elif metric.key == "tenancy.merchant_team_members":
stats["merchant_team_members"] = int(metric.value)
return stats
@admin_platform_users_router.get("/search", response_model=UserSearchResponse)
def search_users(
q: str = Query(..., min_length=2, description="Search query (username or email)"),