# app/api/v1/vendor/usage.py """ Vendor usage and limits API endpoints. Provides endpoints for: - Current usage vs limits - Upgrade recommendations - Approaching limit warnings """ import logging from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy import func from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_api from app.core.database import get_db from app.services.subscription_service import subscription_service from models.database.product import Product from models.database.subscription import SubscriptionTier from models.database.user import User from models.database.vendor import VendorUser router = APIRouter(prefix="/usage") logger = logging.getLogger(__name__) # ============================================================================ # Response Schemas # ============================================================================ class UsageMetric(BaseModel): """Single usage metric.""" name: str current: int limit: int | None # None = unlimited percentage: float # 0-100, or 0 if unlimited is_unlimited: bool is_at_limit: bool is_approaching_limit: bool # >= 80% class TierInfo(BaseModel): """Current tier information.""" code: str name: str price_monthly_cents: int is_highest_tier: bool class UpgradeTierInfo(BaseModel): """Next tier upgrade information.""" code: str name: str price_monthly_cents: int price_increase_cents: int benefits: list[str] class UsageResponse(BaseModel): """Full usage response with limits and upgrade info.""" tier: TierInfo usage: list[UsageMetric] has_limits_approaching: bool has_limits_reached: bool upgrade_available: bool upgrade_tier: UpgradeTierInfo | None = None upgrade_reasons: list[str] class LimitCheckResponse(BaseModel): """Response for checking a specific limit.""" limit_type: str can_proceed: bool current: int limit: int | None percentage: float message: str | None = None upgrade_tier_code: str | None = None upgrade_tier_name: str | None = None # ============================================================================ # Endpoints # ============================================================================ @router.get("", response_model=UsageResponse) def get_usage( current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get current usage, limits, and upgrade recommendations. Returns comprehensive usage info for displaying in dashboard and determining when to show upgrade prompts. """ vendor_id = current_user.token_vendor_id # Get subscription subscription = subscription_service.get_or_create_subscription(db, vendor_id) # Get current tier tier = subscription.tier_obj if not tier: tier = ( db.query(SubscriptionTier) .filter(SubscriptionTier.code == subscription.tier) .first() ) # Calculate usage metrics usage_metrics = [] # Orders this period orders_current = subscription.orders_this_period or 0 orders_limit = subscription.orders_limit orders_unlimited = orders_limit is None or orders_limit < 0 orders_percentage = 0 if orders_unlimited else (orders_current / orders_limit * 100 if orders_limit > 0 else 100) usage_metrics.append( UsageMetric( name="orders", current=orders_current, limit=None if orders_unlimited else orders_limit, percentage=orders_percentage, is_unlimited=orders_unlimited, is_at_limit=not orders_unlimited and orders_current >= orders_limit, is_approaching_limit=not orders_unlimited and orders_percentage >= 80, ) ) # Products products_count = ( db.query(func.count(Product.id)) .filter(Product.vendor_id == vendor_id) .scalar() or 0 ) products_limit = subscription.products_limit products_unlimited = products_limit is None or products_limit < 0 products_percentage = 0 if products_unlimited else (products_count / products_limit * 100 if products_limit > 0 else 100) usage_metrics.append( UsageMetric( name="products", current=products_count, limit=None if products_unlimited else products_limit, percentage=products_percentage, is_unlimited=products_unlimited, is_at_limit=not products_unlimited and products_count >= products_limit, is_approaching_limit=not products_unlimited and products_percentage >= 80, ) ) # Team members team_count = ( db.query(func.count(VendorUser.id)) .filter(VendorUser.vendor_id == vendor_id, VendorUser.is_active == True) # noqa: E712 .scalar() or 0 ) team_limit = subscription.team_members_limit team_unlimited = team_limit is None or team_limit < 0 team_percentage = 0 if team_unlimited else (team_count / team_limit * 100 if team_limit > 0 else 100) usage_metrics.append( UsageMetric( name="team_members", current=team_count, limit=None if team_unlimited else team_limit, percentage=team_percentage, is_unlimited=team_unlimited, is_at_limit=not team_unlimited and team_count >= team_limit, is_approaching_limit=not team_unlimited and team_percentage >= 80, ) ) # Check for approaching/reached limits has_limits_approaching = any(m.is_approaching_limit for m in usage_metrics) has_limits_reached = any(m.is_at_limit for m in usage_metrics) # Get next tier for upgrade all_tiers = ( db.query(SubscriptionTier) .filter(SubscriptionTier.is_active == True) # noqa: E712 .order_by(SubscriptionTier.display_order) .all() ) current_tier_order = tier.display_order if tier else 0 next_tier = None for t in all_tiers: if t.display_order > current_tier_order: next_tier = t break is_highest_tier = next_tier is None # Build upgrade info upgrade_tier_info = None upgrade_reasons = [] if next_tier: # Calculate benefits benefits = [] if next_tier.orders_per_month and (not tier or (tier.orders_per_month and next_tier.orders_per_month > tier.orders_per_month)): if next_tier.orders_per_month < 0: benefits.append("Unlimited orders per month") else: benefits.append(f"{next_tier.orders_per_month:,} orders/month") if next_tier.products_limit and (not tier or (tier.products_limit and next_tier.products_limit > tier.products_limit)): if next_tier.products_limit < 0: benefits.append("Unlimited products") else: benefits.append(f"{next_tier.products_limit:,} products") if next_tier.team_members and (not tier or (tier.team_members and next_tier.team_members > tier.team_members)): if next_tier.team_members < 0: benefits.append("Unlimited team members") else: benefits.append(f"{next_tier.team_members} team members") # Add feature benefits current_features = set(tier.features) if tier and tier.features else set() next_features = set(next_tier.features) if next_tier.features else set() new_features = next_features - current_features feature_names = { "analytics_dashboard": "Advanced Analytics", "api_access": "API Access", "automation_rules": "Automation Rules", "team_roles": "Team Roles & Permissions", "custom_domain": "Custom Domain", "webhooks": "Webhooks", "accounting_export": "Accounting Export", } for feature in list(new_features)[:3]: # Show top 3 if feature in feature_names: benefits.append(feature_names[feature]) current_price = tier.price_monthly_cents if tier else 0 upgrade_tier_info = UpgradeTierInfo( code=next_tier.code, name=next_tier.name, price_monthly_cents=next_tier.price_monthly_cents, price_increase_cents=next_tier.price_monthly_cents - current_price, benefits=benefits, ) # Build upgrade reasons if has_limits_reached: for m in usage_metrics: if m.is_at_limit: upgrade_reasons.append(f"You've reached your {m.name.replace('_', ' ')} limit") elif has_limits_approaching: for m in usage_metrics: if m.is_approaching_limit: upgrade_reasons.append(f"You're approaching your {m.name.replace('_', ' ')} limit ({int(m.percentage)}%)") return UsageResponse( tier=TierInfo( code=tier.code if tier else subscription.tier, name=tier.name if tier else subscription.tier.title(), price_monthly_cents=tier.price_monthly_cents if tier else 0, is_highest_tier=is_highest_tier, ), usage=usage_metrics, has_limits_approaching=has_limits_approaching, has_limits_reached=has_limits_reached, upgrade_available=not is_highest_tier, upgrade_tier=upgrade_tier_info, upgrade_reasons=upgrade_reasons, ) @router.get("/check/{limit_type}", response_model=LimitCheckResponse) def check_limit( limit_type: str, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Check a specific limit before performing an action. Use this before creating orders, products, or inviting team members. Args: limit_type: One of "orders", "products", "team_members" Returns: Whether the action can proceed and upgrade info if not """ vendor_id = current_user.token_vendor_id if limit_type == "orders": can_proceed, message = subscription_service.can_create_order(db, vendor_id) subscription = subscription_service.get_subscription(db, vendor_id) current = subscription.orders_this_period if subscription else 0 limit = subscription.orders_limit if subscription else 0 elif limit_type == "products": can_proceed, message = subscription_service.can_add_product(db, vendor_id) subscription = subscription_service.get_subscription(db, vendor_id) current = ( db.query(func.count(Product.id)) .filter(Product.vendor_id == vendor_id) .scalar() or 0 ) limit = subscription.products_limit if subscription else 0 elif limit_type == "team_members": can_proceed, message = subscription_service.can_add_team_member(db, vendor_id) subscription = subscription_service.get_subscription(db, vendor_id) current = ( db.query(func.count(VendorUser.id)) .filter(VendorUser.vendor_id == vendor_id, VendorUser.is_active == True) # noqa: E712 .scalar() or 0 ) limit = subscription.team_members_limit if subscription else 0 else: return LimitCheckResponse( limit_type=limit_type, can_proceed=True, current=0, limit=None, percentage=0, message=f"Unknown limit type: {limit_type}", ) # Calculate percentage is_unlimited = limit is None or limit < 0 percentage = 0 if is_unlimited else (current / limit * 100 if limit > 0 else 100) # Get upgrade info if at limit upgrade_tier_code = None upgrade_tier_name = None if not can_proceed: subscription = subscription_service.get_subscription(db, vendor_id) current_tier = subscription.tier_obj if subscription else None if current_tier: next_tier = ( db.query(SubscriptionTier) .filter( SubscriptionTier.is_active == True, # noqa: E712 SubscriptionTier.display_order > current_tier.display_order, ) .order_by(SubscriptionTier.display_order) .first() ) if next_tier: upgrade_tier_code = next_tier.code upgrade_tier_name = next_tier.name return LimitCheckResponse( limit_type=limit_type, can_proceed=can_proceed, current=current, limit=None if is_unlimited else limit, percentage=percentage, message=message, upgrade_tier_code=upgrade_tier_code, upgrade_tier_name=upgrade_tier_name, )