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

@@ -107,18 +107,31 @@ class AdminService:
per_page: int = 10,
search: str | None = None,
role: str | None = None,
scope: str | None = None,
is_active: bool | None = None,
) -> tuple[list[User], int, int]:
"""
Get paginated list of users with filtering.
Args:
scope: Optional scope filter. 'merchant' returns users who are
merchant owners or store team members.
Returns:
Tuple of (users, total_count, total_pages)
"""
import math
from app.modules.tenancy.models import Merchant, StoreUser
query = db.query(User)
# Apply scope filter
if scope == "merchant":
owner_ids = db.query(Merchant.owner_user_id).distinct()
team_ids = db.query(StoreUser.user_id).distinct()
query = query.filter(User.id.in_(owner_ids.union(team_ids)))
# Apply filters
if search:
search_term = f"%{search.lower()}%"

View File

@@ -12,6 +12,7 @@ Provides metrics for:
import logging
from typing import TYPE_CHECKING
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.modules.contracts.metrics import (
@@ -123,7 +124,7 @@ class TenancyMetricsProvider:
- Total users
- Active users
"""
from app.modules.tenancy.models import AdminPlatform, User, Store, StorePlatform
from app.modules.tenancy.models import AdminPlatform, Merchant, StoreUser, User, Store, StorePlatform
try:
# Store metrics - using StorePlatform junction table
@@ -216,6 +217,40 @@ class TenancyMetricsProvider:
inactive_users = total_users - active_users
# Merchant user metrics
# Owners: distinct users who own a merchant
merchant_owners = (
db.query(func.count(func.distinct(Merchant.owner_user_id))).scalar() or 0
)
# Team members: distinct StoreUser users who are NOT merchant owners
# Uses user_type="member" AND excludes owner user IDs to avoid overlap
team_members = (
db.query(func.count(func.distinct(StoreUser.user_id)))
.filter(
StoreUser.user_type == "member",
~StoreUser.user_id.in_(db.query(Merchant.owner_user_id)),
)
.scalar() or 0
)
# Total: union of both sets (deduplicated)
owner_ids = db.query(Merchant.owner_user_id).distinct()
team_ids = db.query(StoreUser.user_id).distinct()
merchant_users_total = (
db.query(func.count(func.distinct(User.id)))
.filter(User.id.in_(owner_ids.union(team_ids)))
.scalar() or 0
)
merchant_users_active = (
db.query(func.count(func.distinct(User.id)))
.filter(
User.id.in_(owner_ids.union(team_ids)),
User.is_active == True,
)
.scalar() or 0
)
# Calculate rates
verification_rate = (
(verified_stores / total_stores * 100) if total_stores > 0 else 0
@@ -317,6 +352,39 @@ class TenancyMetricsProvider:
unit="%",
description="Percentage of users that are active",
),
# Merchant user metrics
MetricValue(
key="tenancy.merchant_users_total",
value=merchant_users_total,
label="Total Merchant Users",
category="tenancy",
icon="users",
description="Total merchant-related users (owners and team members)",
),
MetricValue(
key="tenancy.merchant_users_active",
value=merchant_users_active,
label="Active Merchant Users",
category="tenancy",
icon="user-check",
description="Active merchant users",
),
MetricValue(
key="tenancy.merchant_owners",
value=merchant_owners,
label="Merchant Owners",
category="tenancy",
icon="briefcase",
description="Distinct merchant owners",
),
MetricValue(
key="tenancy.merchant_team_members",
value=team_members,
label="Team Members",
category="tenancy",
icon="user-group",
description="Distinct store team members",
),
]
except Exception as e:
logger.warning(f"Failed to get tenancy platform metrics: {e}")