diff --git a/app/modules/billing/routes/api/admin.py b/app/modules/billing/routes/api/admin.py index bb9591ad..f34c09e6 100644 --- a/app/modules/billing/routes/api/admin.py +++ b/app/modules/billing/routes/api/admin.py @@ -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} diff --git a/app/modules/billing/services/admin_subscription_service.py b/app/modules/billing/services/admin_subscription_service.py index 8e63c006..ed204e28 100644 --- a/app/modules/billing/services/admin_subscription_service.py +++ b/app/modules/billing/services/admin_subscription_service.py @@ -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 # ========================================================================= diff --git a/app/modules/billing/services/subscription_service.py b/app/modules/billing/services/subscription_service.py index b348b407..45e27fdf 100644 --- a/app/modules/billing/services/subscription_service.py +++ b/app/modules/billing/services/subscription_service.py @@ -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) diff --git a/app/modules/tenancy/static/admin/js/merchant-detail.js b/app/modules/tenancy/static/admin/js/merchant-detail.js index 166c6b3b..84f69d83 100644 --- a/app/modules/tenancy/static/admin/js/merchant-detail.js +++ b/app/modules/tenancy/static/admin/js/merchant-detail.js @@ -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:', {