The merchant dashboard was showing subscription count as "Total Stores". Add get_merchant_metrics() to MetricsProviderProtocol and implement it in tenancy, billing, and customer providers. Dashboard now fetches real stats from a new /merchants/core/dashboard/stats endpoint and displays 4 cards: active subscriptions, total stores, customers, team members. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
480 lines
16 KiB
Python
480 lines
16 KiB
Python
# app/modules/tenancy/services/tenancy_metrics.py
|
|
"""
|
|
Metrics provider for the tenancy module.
|
|
|
|
Provides metrics for:
|
|
- Store counts and status
|
|
- User counts and activation
|
|
- Team members (store users)
|
|
- Custom domains
|
|
"""
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.modules.contracts.metrics import (
|
|
MetricsContext,
|
|
MetricValue,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
pass
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TenancyMetricsProvider:
|
|
"""
|
|
Metrics provider for tenancy module.
|
|
|
|
Provides store, user, and organizational metrics.
|
|
"""
|
|
|
|
@property
|
|
def metrics_category(self) -> str:
|
|
return "tenancy"
|
|
|
|
def get_store_metrics(
|
|
self,
|
|
db: Session,
|
|
store_id: int,
|
|
context: MetricsContext | None = None,
|
|
) -> list[MetricValue]:
|
|
"""
|
|
Get metrics for a specific store.
|
|
|
|
For stores, this provides:
|
|
- Team member count
|
|
- Custom domains count
|
|
"""
|
|
from app.modules.tenancy.models import StoreDomain, StoreUser
|
|
|
|
try:
|
|
# Team members count
|
|
team_count = (
|
|
db.query(StoreUser)
|
|
.filter(StoreUser.store_id == store_id, StoreUser.is_active == True)
|
|
.count()
|
|
)
|
|
|
|
# Custom domains count
|
|
domains_count = (
|
|
db.query(StoreDomain)
|
|
.filter(StoreDomain.store_id == store_id)
|
|
.count()
|
|
)
|
|
|
|
# Verified domains count
|
|
verified_domains_count = (
|
|
db.query(StoreDomain)
|
|
.filter(
|
|
StoreDomain.store_id == store_id,
|
|
StoreDomain.is_verified == True,
|
|
)
|
|
.count()
|
|
)
|
|
|
|
return [
|
|
MetricValue(
|
|
key="tenancy.team_members",
|
|
value=team_count,
|
|
label="Team Members",
|
|
category="tenancy",
|
|
icon="users",
|
|
description="Active team members with access to this store",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.domains",
|
|
value=domains_count,
|
|
label="Custom Domains",
|
|
category="tenancy",
|
|
icon="globe",
|
|
description="Custom domains configured for this store",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.verified_domains",
|
|
value=verified_domains_count,
|
|
label="Verified Domains",
|
|
category="tenancy",
|
|
icon="check-circle",
|
|
description="Custom domains that have been verified",
|
|
),
|
|
]
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get tenancy store metrics: {e}")
|
|
return []
|
|
|
|
def get_platform_metrics(
|
|
self,
|
|
db: Session,
|
|
platform_id: int,
|
|
context: MetricsContext | None = None,
|
|
) -> list[MetricValue]:
|
|
"""
|
|
Get metrics aggregated for a platform.
|
|
|
|
For platforms, this provides:
|
|
- Total stores
|
|
- Active stores
|
|
- Verified stores
|
|
- Total users
|
|
- Active users
|
|
"""
|
|
from app.modules.tenancy.models import (
|
|
AdminPlatform,
|
|
Merchant,
|
|
Store,
|
|
StorePlatform,
|
|
StoreUser,
|
|
User,
|
|
)
|
|
|
|
try:
|
|
# Store metrics - using StorePlatform junction table
|
|
# Get store IDs that are on this platform
|
|
platform_store_ids = (
|
|
db.query(StorePlatform.store_id)
|
|
.filter(StorePlatform.platform_id == platform_id)
|
|
.subquery()
|
|
)
|
|
|
|
total_stores = (
|
|
db.query(Store)
|
|
.filter(Store.id.in_(platform_store_ids))
|
|
.count()
|
|
)
|
|
|
|
# Active stores on this platform (store active AND membership active)
|
|
active_store_ids = (
|
|
db.query(StorePlatform.store_id)
|
|
.filter(
|
|
StorePlatform.platform_id == platform_id,
|
|
StorePlatform.is_active == True,
|
|
)
|
|
.subquery()
|
|
)
|
|
active_stores = (
|
|
db.query(Store)
|
|
.filter(
|
|
Store.id.in_(active_store_ids),
|
|
Store.is_active == True,
|
|
)
|
|
.count()
|
|
)
|
|
|
|
verified_stores = (
|
|
db.query(Store)
|
|
.filter(
|
|
Store.id.in_(platform_store_ids),
|
|
Store.is_verified == True,
|
|
)
|
|
.count()
|
|
)
|
|
|
|
pending_stores = (
|
|
db.query(Store)
|
|
.filter(
|
|
Store.id.in_(active_store_ids),
|
|
Store.is_active == True,
|
|
Store.is_verified == False,
|
|
)
|
|
.count()
|
|
)
|
|
|
|
inactive_stores = total_stores - active_stores
|
|
|
|
# User metrics - using AdminPlatform junction table
|
|
# Get user IDs that have access to this platform
|
|
platform_user_ids = (
|
|
db.query(AdminPlatform.user_id)
|
|
.filter(
|
|
AdminPlatform.platform_id == platform_id,
|
|
AdminPlatform.is_active == True,
|
|
)
|
|
.subquery()
|
|
)
|
|
|
|
total_users = (
|
|
db.query(User)
|
|
.filter(User.id.in_(platform_user_ids))
|
|
.count()
|
|
)
|
|
|
|
active_users = (
|
|
db.query(User)
|
|
.filter(
|
|
User.id.in_(platform_user_ids),
|
|
User.is_active == True,
|
|
)
|
|
.count()
|
|
)
|
|
|
|
admin_users = (
|
|
db.query(User)
|
|
.filter(
|
|
User.id.in_(platform_user_ids),
|
|
User.role.in_(["super_admin", "platform_admin"]),
|
|
)
|
|
.count()
|
|
)
|
|
|
|
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
|
|
team_members = (
|
|
db.query(func.count(func.distinct(StoreUser.user_id)))
|
|
.filter(
|
|
~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
|
|
)
|
|
user_activation_rate = (
|
|
(active_users / total_users * 100) if total_users > 0 else 0
|
|
)
|
|
|
|
return [
|
|
# Store metrics
|
|
MetricValue(
|
|
key="tenancy.total_stores",
|
|
value=total_stores,
|
|
label="Total Stores",
|
|
category="tenancy",
|
|
icon="store",
|
|
description="Total number of stores on this platform",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.active_stores",
|
|
value=active_stores,
|
|
label="Active Stores",
|
|
category="tenancy",
|
|
icon="check-circle",
|
|
description="Stores that are currently active",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.verified_stores",
|
|
value=verified_stores,
|
|
label="Verified Stores",
|
|
category="tenancy",
|
|
icon="badge-check",
|
|
description="Stores that have been verified",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.pending_stores",
|
|
value=pending_stores,
|
|
label="Pending Stores",
|
|
category="tenancy",
|
|
icon="clock",
|
|
description="Active stores pending verification",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.inactive_stores",
|
|
value=inactive_stores,
|
|
label="Inactive Stores",
|
|
category="tenancy",
|
|
icon="pause-circle",
|
|
description="Stores that are not currently active",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.store_verification_rate",
|
|
value=round(verification_rate, 1),
|
|
label="Verification Rate",
|
|
category="tenancy",
|
|
icon="percent",
|
|
unit="%",
|
|
description="Percentage of stores that are verified",
|
|
),
|
|
# User metrics
|
|
MetricValue(
|
|
key="tenancy.total_users",
|
|
value=total_users,
|
|
label="Total Users",
|
|
category="tenancy",
|
|
icon="users",
|
|
description="Total number of users on this platform",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.active_users",
|
|
value=active_users,
|
|
label="Active Users",
|
|
category="tenancy",
|
|
icon="user-check",
|
|
description="Users that are currently active",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.admin_users",
|
|
value=admin_users,
|
|
label="Admin Users",
|
|
category="tenancy",
|
|
icon="shield",
|
|
description="Users with admin role",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.inactive_users",
|
|
value=inactive_users,
|
|
label="Inactive Users",
|
|
category="tenancy",
|
|
icon="user-x",
|
|
description="Users that are not currently active",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.user_activation_rate",
|
|
value=round(user_activation_rate, 1),
|
|
label="User Activation Rate",
|
|
category="tenancy",
|
|
icon="percent",
|
|
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="office-building",
|
|
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}")
|
|
return []
|
|
|
|
def get_merchant_metrics(
|
|
self,
|
|
db: Session,
|
|
merchant_id: int,
|
|
context: MetricsContext | None = None,
|
|
) -> list[MetricValue]:
|
|
"""
|
|
Get metrics scoped to a specific merchant.
|
|
|
|
Provides:
|
|
- Total stores owned by this merchant
|
|
- Active stores
|
|
- Distinct active team members across all merchant stores
|
|
"""
|
|
from app.modules.tenancy.models import Store, StoreUser
|
|
|
|
try:
|
|
total_stores = (
|
|
db.query(Store)
|
|
.filter(Store.merchant_id == merchant_id)
|
|
.count()
|
|
)
|
|
|
|
active_stores = (
|
|
db.query(Store)
|
|
.filter(
|
|
Store.merchant_id == merchant_id,
|
|
Store.is_active == True,
|
|
)
|
|
.count()
|
|
)
|
|
|
|
# Distinct active team members across all merchant stores
|
|
merchant_store_ids = (
|
|
db.query(Store.id)
|
|
.filter(Store.merchant_id == merchant_id)
|
|
.subquery()
|
|
)
|
|
team_members = (
|
|
db.query(func.count(func.distinct(StoreUser.user_id)))
|
|
.filter(
|
|
StoreUser.store_id.in_(merchant_store_ids),
|
|
StoreUser.is_active == True,
|
|
)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
return [
|
|
MetricValue(
|
|
key="tenancy.total_stores",
|
|
value=total_stores,
|
|
label="Total Stores",
|
|
category="tenancy",
|
|
icon="shopping-bag",
|
|
description="Total stores owned by this merchant",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.active_stores",
|
|
value=active_stores,
|
|
label="Active Stores",
|
|
category="tenancy",
|
|
icon="check-circle",
|
|
description="Active stores owned by this merchant",
|
|
),
|
|
MetricValue(
|
|
key="tenancy.team_members",
|
|
value=team_members,
|
|
label="Team Members",
|
|
category="tenancy",
|
|
icon="users",
|
|
description="Distinct active team members across all stores",
|
|
),
|
|
]
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get tenancy merchant metrics: {e}")
|
|
return []
|
|
|
|
|
|
# Singleton instance
|
|
tenancy_metrics_provider = TenancyMetricsProvider()
|
|
|
|
__all__ = ["TenancyMetricsProvider", "tenancy_metrics_provider"]
|