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:
@@ -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()}%"
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user