feat: complete subscription billing system phases 6-10
Phase 6 - Database-driven tiers: - Update subscription_service to query database first with legacy fallback - Add get_tier_info() db parameter and _get_tier_from_legacy() method Phase 7 - Platform health integration: - Add get_subscription_capacity() for theoretical vs actual capacity - Include subscription capacity in full health report Phase 8 - Background subscription tasks: - Add reset_period_counters() for billing period resets - Add check_trial_expirations() for trial management - Add sync_stripe_status() for Stripe synchronization - Add cleanup_stale_subscriptions() for maintenance - Add capture_capacity_snapshot() for daily metrics Phase 10 - Capacity planning & forecasting: - Add CapacitySnapshot model for historical tracking - Create capacity_forecast_service with growth trends - Add /subscription-capacity, /trends, /recommendations endpoints - Add /snapshot endpoint for manual captures Also includes billing API enhancements from phase 4: - Add upcoming-invoice, change-tier, addon purchase/cancel endpoints - Add UsageSummary schema for billing page - Enhance billing.js with addon management functions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -166,6 +166,101 @@ class PlatformHealthService:
|
||||
"active_vendors": active_vendors,
|
||||
}
|
||||
|
||||
def get_subscription_capacity(self, db: Session) -> dict:
|
||||
"""
|
||||
Calculate theoretical capacity based on all vendor subscriptions.
|
||||
|
||||
Returns aggregated limits and current usage for capacity planning.
|
||||
"""
|
||||
from models.database.subscription import VendorSubscription
|
||||
from models.database.vendor import VendorUser
|
||||
|
||||
# Get all active subscriptions with their limits
|
||||
subscriptions = (
|
||||
db.query(VendorSubscription)
|
||||
.filter(VendorSubscription.status.in_(["active", "trial"]))
|
||||
.all()
|
||||
)
|
||||
|
||||
# Aggregate theoretical limits
|
||||
total_products_limit = 0
|
||||
total_orders_limit = 0
|
||||
total_team_limit = 0
|
||||
unlimited_products = 0
|
||||
unlimited_orders = 0
|
||||
unlimited_team = 0
|
||||
|
||||
tier_distribution = {}
|
||||
|
||||
for sub in subscriptions:
|
||||
# Track tier distribution
|
||||
tier = sub.tier or "unknown"
|
||||
tier_distribution[tier] = tier_distribution.get(tier, 0) + 1
|
||||
|
||||
# Aggregate limits
|
||||
if sub.products_limit is None:
|
||||
unlimited_products += 1
|
||||
else:
|
||||
total_products_limit += sub.products_limit
|
||||
|
||||
if sub.orders_limit is None:
|
||||
unlimited_orders += 1
|
||||
else:
|
||||
total_orders_limit += sub.orders_limit
|
||||
|
||||
if sub.team_members_limit is None:
|
||||
unlimited_team += 1
|
||||
else:
|
||||
total_team_limit += sub.team_members_limit
|
||||
|
||||
# Get actual usage
|
||||
actual_products = db.query(func.count(Product.id)).scalar() or 0
|
||||
actual_team = (
|
||||
db.query(func.count(VendorUser.id))
|
||||
.filter(VendorUser.is_active == True) # noqa: E712
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Orders this period (aggregate across all subscriptions)
|
||||
total_orders_used = sum(s.orders_this_period for s in subscriptions)
|
||||
|
||||
def calc_utilization(actual: int, limit: int, unlimited: int) -> dict:
|
||||
if unlimited > 0:
|
||||
# Some subscriptions have unlimited - can't calculate true %
|
||||
return {
|
||||
"actual": actual,
|
||||
"theoretical_limit": limit,
|
||||
"unlimited_count": unlimited,
|
||||
"utilization_percent": None,
|
||||
"has_unlimited": True,
|
||||
}
|
||||
elif limit > 0:
|
||||
return {
|
||||
"actual": actual,
|
||||
"theoretical_limit": limit,
|
||||
"unlimited_count": 0,
|
||||
"utilization_percent": round((actual / limit) * 100, 1),
|
||||
"headroom": limit - actual,
|
||||
"has_unlimited": False,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"actual": actual,
|
||||
"theoretical_limit": 0,
|
||||
"unlimited_count": 0,
|
||||
"utilization_percent": 0,
|
||||
"has_unlimited": False,
|
||||
}
|
||||
|
||||
return {
|
||||
"total_subscriptions": len(subscriptions),
|
||||
"tier_distribution": tier_distribution,
|
||||
"products": calc_utilization(actual_products, total_products_limit, unlimited_products),
|
||||
"orders_monthly": calc_utilization(total_orders_used, total_orders_limit, unlimited_orders),
|
||||
"team_members": calc_utilization(actual_team, total_team_limit, unlimited_team),
|
||||
}
|
||||
|
||||
def get_full_health_report(self, db: Session) -> dict:
|
||||
"""Get comprehensive platform health report."""
|
||||
# System metrics
|
||||
@@ -177,6 +272,9 @@ class PlatformHealthService:
|
||||
# Image storage metrics
|
||||
image_storage = self.get_image_storage_metrics()
|
||||
|
||||
# Subscription capacity
|
||||
subscription_capacity = self.get_subscription_capacity(db)
|
||||
|
||||
# Calculate thresholds
|
||||
thresholds = self._calculate_thresholds(system, database, image_storage)
|
||||
|
||||
@@ -197,6 +295,7 @@ class PlatformHealthService:
|
||||
"system": system,
|
||||
"database": database,
|
||||
"image_storage": image_storage,
|
||||
"subscription_capacity": subscription_capacity,
|
||||
"thresholds": thresholds,
|
||||
"recommendations": recommendations,
|
||||
"infrastructure_tier": tier,
|
||||
|
||||
Reference in New Issue
Block a user