feat: add single endpoint for merchant subscriptions with usage data
Replace N+1 per-platform API calls on merchant detail page with a single
GET /admin/subscriptions/merchants/{id} endpoint. Extract shared
subscription+usage aggregation logic into a reusable service method and
refactor the store endpoint to use it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -176,6 +176,19 @@ def list_merchant_subscriptions(
|
||||
)
|
||||
|
||||
|
||||
@admin_router.get("/merchants/{merchant_id}")
|
||||
def get_merchant_subscriptions(
|
||||
merchant_id: int = Path(..., description="Merchant ID"),
|
||||
current_user: UserContext = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get all subscriptions for a merchant with tier info and feature usage."""
|
||||
results = admin_subscription_service.get_merchant_subscriptions_with_usage(
|
||||
db, merchant_id
|
||||
)
|
||||
return {"subscriptions": results}
|
||||
|
||||
|
||||
@admin_router.post(
|
||||
"/merchants/{merchant_id}/platforms/{platform_id}",
|
||||
response_model=MerchantSubscriptionAdminResponse,
|
||||
@@ -273,53 +286,14 @@ def get_subscription_for_store(
|
||||
"""
|
||||
from app.modules.billing.services.feature_service import feature_service
|
||||
|
||||
# Resolve store to merchant + all platform IDs
|
||||
# Resolve store to merchant
|
||||
merchant_id, platform_ids = feature_service._get_merchant_and_platforms_for_store(db, store_id)
|
||||
if merchant_id is None or not platform_ids:
|
||||
raise ResourceNotFoundException("Store", str(store_id))
|
||||
|
||||
platforms_map = admin_subscription_service.get_platform_names_map(db)
|
||||
|
||||
results = []
|
||||
for pid in platform_ids:
|
||||
try:
|
||||
sub, merchant = admin_subscription_service.get_subscription(db, merchant_id, pid)
|
||||
except ResourceNotFoundException:
|
||||
continue
|
||||
|
||||
# Get feature summary
|
||||
features_summary = feature_service.get_merchant_features_summary(db, merchant_id, pid)
|
||||
|
||||
# Build tier info
|
||||
tier_info = None
|
||||
if sub.tier:
|
||||
tier_info = {
|
||||
"code": sub.tier.code,
|
||||
"name": sub.tier.name,
|
||||
"feature_codes": [fl.feature_code for fl in (sub.tier.feature_limits or [])],
|
||||
}
|
||||
|
||||
# Build usage metrics (quantitative features only)
|
||||
usage_metrics = []
|
||||
for fs in features_summary:
|
||||
if fs.feature_type == "quantitative" and fs.enabled:
|
||||
usage_metrics.append({
|
||||
"name": fs.name_key.replace("_", " ").title(),
|
||||
"current": fs.current or 0,
|
||||
"limit": fs.limit,
|
||||
"percentage": fs.percent_used or 0,
|
||||
"is_unlimited": fs.limit is None,
|
||||
"is_at_limit": fs.remaining == 0 if fs.remaining is not None else False,
|
||||
"is_approaching_limit": (fs.percent_used or 0) >= 80,
|
||||
})
|
||||
|
||||
results.append({
|
||||
"subscription": MerchantSubscriptionAdminResponse.model_validate(sub).model_dump(),
|
||||
"tier": tier_info,
|
||||
"features": usage_metrics,
|
||||
"platform_name": platforms_map.get(pid, ""),
|
||||
})
|
||||
|
||||
results = admin_subscription_service.get_merchant_subscriptions_with_usage(
|
||||
db, merchant_id
|
||||
)
|
||||
return {"subscriptions": results}
|
||||
|
||||
|
||||
|
||||
@@ -286,6 +286,70 @@ class AdminSubscriptionService:
|
||||
p = db.query(Platform).filter(Platform.id == platform_id).first()
|
||||
return p.name if p else None
|
||||
|
||||
# =========================================================================
|
||||
# Merchant Subscriptions with Usage
|
||||
# =========================================================================
|
||||
|
||||
def get_merchant_subscriptions_with_usage(
|
||||
self, db: Session, merchant_id: int
|
||||
) -> list[dict]:
|
||||
"""Get all subscriptions for a merchant with tier info and feature usage.
|
||||
|
||||
Returns a list of dicts, each containing:
|
||||
- subscription: serialized MerchantSubscription
|
||||
- tier: tier info dict (code, name, feature_codes)
|
||||
- features: list of quantitative usage metrics
|
||||
- platform_id: int
|
||||
- platform_name: str
|
||||
"""
|
||||
from app.modules.billing.schemas import MerchantSubscriptionAdminResponse
|
||||
from app.modules.billing.services.feature_service import feature_service
|
||||
from app.modules.billing.services.subscription_service import (
|
||||
subscription_service,
|
||||
)
|
||||
|
||||
subs = subscription_service.get_merchant_subscriptions(db, merchant_id)
|
||||
platforms_map = self.get_platform_names_map(db)
|
||||
|
||||
results = []
|
||||
for sub in subs:
|
||||
features_summary = feature_service.get_merchant_features_summary(
|
||||
db, merchant_id, sub.platform_id
|
||||
)
|
||||
|
||||
tier_info = None
|
||||
if sub.tier:
|
||||
tier_info = {
|
||||
"code": sub.tier.code,
|
||||
"name": sub.tier.name,
|
||||
"feature_codes": [
|
||||
fl.feature_code for fl in (sub.tier.feature_limits or [])
|
||||
],
|
||||
}
|
||||
|
||||
usage_metrics = []
|
||||
for fs in features_summary:
|
||||
if fs.feature_type == "quantitative" and fs.enabled:
|
||||
usage_metrics.append({
|
||||
"name": fs.name_key.replace("_", " ").title(),
|
||||
"current": fs.current or 0,
|
||||
"limit": fs.limit,
|
||||
"percentage": fs.percent_used or 0,
|
||||
"is_unlimited": fs.limit is None,
|
||||
"is_at_limit": fs.remaining == 0 if fs.remaining is not None else False,
|
||||
"is_approaching_limit": (fs.percent_used or 0) >= 80,
|
||||
})
|
||||
|
||||
results.append({
|
||||
"subscription": MerchantSubscriptionAdminResponse.model_validate(sub).model_dump(),
|
||||
"tier": tier_info,
|
||||
"features": usage_metrics,
|
||||
"platform_id": sub.platform_id,
|
||||
"platform_name": platforms_map.get(sub.platform_id, ""),
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
# =========================================================================
|
||||
# Statistics
|
||||
# =========================================================================
|
||||
|
||||
@@ -146,7 +146,8 @@ class SubscriptionService:
|
||||
return (
|
||||
db.query(MerchantSubscription)
|
||||
.options(
|
||||
joinedload(MerchantSubscription.tier),
|
||||
joinedload(MerchantSubscription.tier)
|
||||
.joinedload(SubscriptionTier.feature_limits),
|
||||
joinedload(MerchantSubscription.platform),
|
||||
)
|
||||
.filter(MerchantSubscription.merchant_id == merchant_id)
|
||||
|
||||
@@ -51,8 +51,7 @@ function adminMerchantDetail() {
|
||||
this.merchantId = match[1];
|
||||
merchantDetailLog.info('Viewing merchant:', this.merchantId);
|
||||
await this.loadMerchant();
|
||||
await this.loadPlatforms();
|
||||
await this.loadSubscriptions();
|
||||
await Promise.all([this.loadPlatforms(), this.loadSubscriptions()]);
|
||||
} else {
|
||||
merchantDetailLog.error('No merchant ID in URL');
|
||||
this.error = 'Invalid merchant URL';
|
||||
@@ -110,31 +109,19 @@ function adminMerchantDetail() {
|
||||
}
|
||||
},
|
||||
|
||||
// Load subscriptions for all platforms
|
||||
// Load all subscriptions for this merchant in a single call
|
||||
async loadSubscriptions() {
|
||||
if (!this.merchantId || this.platforms.length === 0) return;
|
||||
if (!this.merchantId) return;
|
||||
|
||||
merchantDetailLog.info('Loading subscriptions for merchant:', this.merchantId);
|
||||
this.subscriptions = [];
|
||||
|
||||
for (const platform of this.platforms) {
|
||||
try {
|
||||
const url = `/admin/subscriptions/merchants/${this.merchantId}/platforms/${platform.id}`;
|
||||
const response = await apiClient.get(url);
|
||||
const sub = response.subscription || response;
|
||||
|
||||
this.subscriptions.push({
|
||||
subscription: sub,
|
||||
tier: response.tier || null,
|
||||
features: response.features || [],
|
||||
platform_id: platform.id,
|
||||
platform_name: platform.name,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
merchantDetailLog.warn(`Failed to load subscription for platform ${platform.name}:`, error.message);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const url = `/admin/subscriptions/merchants/${this.merchantId}`;
|
||||
const response = await apiClient.get(url);
|
||||
this.subscriptions = response.subscriptions || [];
|
||||
} catch (error) {
|
||||
merchantDetailLog.warn('Failed to load subscriptions:', error.message);
|
||||
}
|
||||
|
||||
merchantDetailLog.info('Subscriptions loaded:', {
|
||||
|
||||
Reference in New Issue
Block a user