test: add service tests and fix architecture violations
- Add comprehensive unit tests for FeatureService (24 tests) - Add comprehensive unit tests for UsageService (11 tests) - Fix API-002/API-003 architecture violations in feature/usage APIs - Move database queries from API layer to service layer - Create UsageService for usage and limits management - Create custom exceptions (FeatureNotFoundError, TierNotFoundError) - Fix ValidationException usage in content_pages.py - Refactor vendor features API to use proper response models - All 35 new tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api, get_db
|
||||
from app.exceptions import ValidationException
|
||||
from app.services.content_page_service import content_page_service
|
||||
from models.database.user import User
|
||||
|
||||
@@ -178,11 +179,9 @@ def create_vendor_page(
|
||||
Vendor pages override platform defaults for a specific vendor.
|
||||
"""
|
||||
if not page_data.vendor_id:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="vendor_id is required for vendor pages. Use /platform for platform defaults.",
|
||||
raise ValidationException(
|
||||
message="vendor_id is required for vendor pages. Use /platform for platform defaults.",
|
||||
field="vendor_id",
|
||||
)
|
||||
|
||||
page = content_page_service.create_page(
|
||||
|
||||
@@ -11,15 +11,13 @@ Provides endpoints for:
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.services.feature_service import feature_service
|
||||
from models.database.feature import Feature
|
||||
from models.database.subscription import SubscriptionTier
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter(prefix="/features")
|
||||
@@ -103,6 +101,41 @@ class CategoryListResponse(BaseModel):
|
||||
categories: list[str]
|
||||
|
||||
|
||||
class TierFeatureDetailResponse(BaseModel):
|
||||
"""Tier features with full details."""
|
||||
|
||||
tier_code: str
|
||||
tier_name: str
|
||||
features: list[dict]
|
||||
feature_count: int
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _feature_to_response(feature) -> FeatureResponse:
|
||||
"""Convert Feature model to response."""
|
||||
return FeatureResponse(
|
||||
id=feature.id,
|
||||
code=feature.code,
|
||||
name=feature.name,
|
||||
description=feature.description,
|
||||
category=feature.category,
|
||||
ui_location=feature.ui_location,
|
||||
ui_icon=feature.ui_icon,
|
||||
ui_route=feature.ui_route,
|
||||
ui_badge_text=feature.ui_badge_text,
|
||||
minimum_tier_id=feature.minimum_tier_id,
|
||||
minimum_tier_code=feature.minimum_tier.code if feature.minimum_tier else None,
|
||||
minimum_tier_name=feature.minimum_tier.name if feature.minimum_tier else None,
|
||||
is_active=feature.is_active,
|
||||
is_visible=feature.is_visible,
|
||||
display_order=feature.display_order,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Endpoints
|
||||
# ============================================================================
|
||||
@@ -121,26 +154,7 @@ def list_features(
|
||||
)
|
||||
|
||||
return FeatureListResponse(
|
||||
features=[
|
||||
FeatureResponse(
|
||||
id=f.id,
|
||||
code=f.code,
|
||||
name=f.name,
|
||||
description=f.description,
|
||||
category=f.category,
|
||||
ui_location=f.ui_location,
|
||||
ui_icon=f.ui_icon,
|
||||
ui_route=f.ui_route,
|
||||
ui_badge_text=f.ui_badge_text,
|
||||
minimum_tier_id=f.minimum_tier_id,
|
||||
minimum_tier_code=f.minimum_tier.code if f.minimum_tier else None,
|
||||
minimum_tier_name=f.minimum_tier.name if f.minimum_tier else None,
|
||||
is_active=f.is_active,
|
||||
is_visible=f.is_visible,
|
||||
display_order=f.display_order,
|
||||
)
|
||||
for f in features
|
||||
],
|
||||
features=[_feature_to_response(f) for f in features],
|
||||
total=len(features),
|
||||
)
|
||||
|
||||
@@ -161,12 +175,7 @@ def list_tiers_with_features(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List all tiers with their feature assignments."""
|
||||
tiers = (
|
||||
db.query(SubscriptionTier)
|
||||
.filter(SubscriptionTier.is_active == True) # noqa: E712
|
||||
.order_by(SubscriptionTier.display_order)
|
||||
.all()
|
||||
)
|
||||
tiers = feature_service.get_all_tiers_with_features(db)
|
||||
|
||||
return TierListWithFeaturesResponse(
|
||||
tiers=[
|
||||
@@ -189,29 +198,19 @@ def get_feature(
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get a single feature by code."""
|
||||
"""
|
||||
Get a single feature by code.
|
||||
|
||||
Raises 404 if feature not found.
|
||||
"""
|
||||
feature = feature_service.get_feature_by_code(db, feature_code)
|
||||
|
||||
if not feature:
|
||||
raise HTTPException(status_code=404, detail=f"Feature '{feature_code}' not found")
|
||||
from app.exceptions import FeatureNotFoundError
|
||||
|
||||
return FeatureResponse(
|
||||
id=feature.id,
|
||||
code=feature.code,
|
||||
name=feature.name,
|
||||
description=feature.description,
|
||||
category=feature.category,
|
||||
ui_location=feature.ui_location,
|
||||
ui_icon=feature.ui_icon,
|
||||
ui_route=feature.ui_route,
|
||||
ui_badge_text=feature.ui_badge_text,
|
||||
minimum_tier_id=feature.minimum_tier_id,
|
||||
minimum_tier_code=feature.minimum_tier.code if feature.minimum_tier else None,
|
||||
minimum_tier_name=feature.minimum_tier.name if feature.minimum_tier else None,
|
||||
is_active=feature.is_active,
|
||||
is_visible=feature.is_visible,
|
||||
display_order=feature.display_order,
|
||||
)
|
||||
raise FeatureNotFoundError(feature_code)
|
||||
|
||||
return _feature_to_response(feature)
|
||||
|
||||
|
||||
@router.put("/{feature_code}", response_model=FeatureResponse)
|
||||
@@ -221,73 +220,33 @@ def update_feature(
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update feature metadata."""
|
||||
feature = db.query(Feature).filter(Feature.code == feature_code).first()
|
||||
"""
|
||||
Update feature metadata.
|
||||
|
||||
if not feature:
|
||||
raise HTTPException(status_code=404, detail=f"Feature '{feature_code}' not found")
|
||||
|
||||
# Update fields if provided
|
||||
if request.name is not None:
|
||||
feature.name = request.name
|
||||
if request.description is not None:
|
||||
feature.description = request.description
|
||||
if request.category is not None:
|
||||
feature.category = request.category
|
||||
if request.ui_location is not None:
|
||||
feature.ui_location = request.ui_location
|
||||
if request.ui_icon is not None:
|
||||
feature.ui_icon = request.ui_icon
|
||||
if request.ui_route is not None:
|
||||
feature.ui_route = request.ui_route
|
||||
if request.ui_badge_text is not None:
|
||||
feature.ui_badge_text = request.ui_badge_text
|
||||
if request.is_active is not None:
|
||||
feature.is_active = request.is_active
|
||||
if request.is_visible is not None:
|
||||
feature.is_visible = request.is_visible
|
||||
if request.display_order is not None:
|
||||
feature.display_order = request.display_order
|
||||
|
||||
# Update minimum tier if provided
|
||||
if request.minimum_tier_code is not None:
|
||||
if request.minimum_tier_code == "":
|
||||
feature.minimum_tier_id = None
|
||||
else:
|
||||
tier = (
|
||||
db.query(SubscriptionTier)
|
||||
.filter(SubscriptionTier.code == request.minimum_tier_code)
|
||||
.first()
|
||||
)
|
||||
if not tier:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Tier '{request.minimum_tier_code}' not found",
|
||||
)
|
||||
feature.minimum_tier_id = tier.id
|
||||
Raises 404 if feature not found, 400 if tier code is invalid.
|
||||
"""
|
||||
feature = feature_service.update_feature(
|
||||
db,
|
||||
feature_code,
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
category=request.category,
|
||||
ui_location=request.ui_location,
|
||||
ui_icon=request.ui_icon,
|
||||
ui_route=request.ui_route,
|
||||
ui_badge_text=request.ui_badge_text,
|
||||
minimum_tier_code=request.minimum_tier_code,
|
||||
is_active=request.is_active,
|
||||
is_visible=request.is_visible,
|
||||
display_order=request.display_order,
|
||||
)
|
||||
|
||||
db.commit()
|
||||
db.refresh(feature)
|
||||
|
||||
logger.info(f"Updated feature {feature_code} by admin {current_user.id}")
|
||||
|
||||
return FeatureResponse(
|
||||
id=feature.id,
|
||||
code=feature.code,
|
||||
name=feature.name,
|
||||
description=feature.description,
|
||||
category=feature.category,
|
||||
ui_location=feature.ui_location,
|
||||
ui_icon=feature.ui_icon,
|
||||
ui_route=feature.ui_route,
|
||||
ui_badge_text=feature.ui_badge_text,
|
||||
minimum_tier_id=feature.minimum_tier_id,
|
||||
minimum_tier_code=feature.minimum_tier.code if feature.minimum_tier else None,
|
||||
minimum_tier_name=feature.minimum_tier.name if feature.minimum_tier else None,
|
||||
is_active=feature.is_active,
|
||||
is_visible=feature.is_visible,
|
||||
display_order=feature.display_order,
|
||||
)
|
||||
return _feature_to_response(feature)
|
||||
|
||||
|
||||
@router.put("/tiers/{tier_code}/features", response_model=TierFeaturesResponse)
|
||||
@@ -297,54 +256,46 @@ def update_tier_features(
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update features assigned to a tier."""
|
||||
try:
|
||||
tier = feature_service.update_tier_features(db, tier_code, request.feature_codes)
|
||||
db.commit()
|
||||
"""
|
||||
Update features assigned to a tier.
|
||||
|
||||
logger.info(
|
||||
f"Updated tier {tier_code} features to {len(request.feature_codes)} features "
|
||||
f"by admin {current_user.id}"
|
||||
)
|
||||
Raises 404 if tier not found, 422 if any feature codes are invalid.
|
||||
"""
|
||||
tier = feature_service.update_tier_features(db, tier_code, request.feature_codes)
|
||||
db.commit()
|
||||
|
||||
return TierFeaturesResponse(
|
||||
id=tier.id,
|
||||
code=tier.code,
|
||||
name=tier.name,
|
||||
description=tier.description,
|
||||
features=tier.features or [],
|
||||
feature_count=len(tier.features or []),
|
||||
)
|
||||
logger.info(
|
||||
f"Updated tier {tier_code} features to {len(request.feature_codes)} features "
|
||||
f"by admin {current_user.id}"
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return TierFeaturesResponse(
|
||||
id=tier.id,
|
||||
code=tier.code,
|
||||
name=tier.name,
|
||||
description=tier.description,
|
||||
features=tier.features or [],
|
||||
feature_count=len(tier.features or []),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tiers/{tier_code}/features")
|
||||
@router.get("/tiers/{tier_code}/features", response_model=TierFeatureDetailResponse)
|
||||
def get_tier_features(
|
||||
tier_code: str,
|
||||
current_user: User = Depends(get_current_admin_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get features assigned to a specific tier."""
|
||||
tier = db.query(SubscriptionTier).filter(SubscriptionTier.code == tier_code).first()
|
||||
"""
|
||||
Get features assigned to a specific tier with full details.
|
||||
|
||||
if not tier:
|
||||
raise HTTPException(status_code=404, detail=f"Tier '{tier_code}' not found")
|
||||
Raises 404 if tier not found.
|
||||
"""
|
||||
tier, features = feature_service.get_tier_features_with_details(db, tier_code)
|
||||
|
||||
# Get full feature details for the tier's features
|
||||
feature_codes = tier.features or []
|
||||
features = (
|
||||
db.query(Feature)
|
||||
.filter(Feature.code.in_(feature_codes))
|
||||
.order_by(Feature.category, Feature.display_order)
|
||||
.all()
|
||||
)
|
||||
|
||||
return {
|
||||
"tier_code": tier.code,
|
||||
"tier_name": tier.name,
|
||||
"features": [
|
||||
return TierFeatureDetailResponse(
|
||||
tier_code=tier.code,
|
||||
tier_name=tier.name,
|
||||
features=[
|
||||
{
|
||||
"code": f.code,
|
||||
"name": f.name,
|
||||
@@ -353,5 +304,5 @@ def get_tier_features(
|
||||
}
|
||||
for f in features
|
||||
],
|
||||
"feature_count": len(features),
|
||||
}
|
||||
feature_count=len(features),
|
||||
)
|
||||
|
||||
18
app/api/v1/vendor/features.py
vendored
18
app/api/v1/vendor/features.py
vendored
@@ -16,12 +16,13 @@ Endpoints:
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import FeatureNotFoundError
|
||||
from app.services.feature_service import feature_service
|
||||
from models.database.user import User
|
||||
|
||||
@@ -99,6 +100,13 @@ class FeatureGroupedResponse(BaseModel):
|
||||
total_count: int
|
||||
|
||||
|
||||
class FeatureCheckResponse(BaseModel):
|
||||
"""Quick feature availability check response."""
|
||||
|
||||
has_feature: bool
|
||||
feature_code: str
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Endpoints
|
||||
# ============================================================================
|
||||
@@ -285,7 +293,7 @@ def get_feature_detail(
|
||||
# Get feature
|
||||
feature = feature_service.get_feature_by_code(db, feature_code)
|
||||
if not feature:
|
||||
raise HTTPException(status_code=404, detail=f"Feature '{feature_code}' not found")
|
||||
raise FeatureNotFoundError(feature_code)
|
||||
|
||||
# Check availability
|
||||
is_available = feature_service.has_feature(db, vendor_id, feature_code)
|
||||
@@ -317,7 +325,7 @@ def get_feature_detail(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/check/{feature_code}")
|
||||
@router.get("/check/{feature_code}", response_model=FeatureCheckResponse)
|
||||
def check_feature(
|
||||
feature_code: str,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
@@ -332,9 +340,9 @@ def check_feature(
|
||||
feature_code: The feature code
|
||||
|
||||
Returns:
|
||||
{"has_feature": true/false}
|
||||
has_feature and feature_code
|
||||
"""
|
||||
vendor_id = current_user.token_vendor_id
|
||||
has_feature = feature_service.has_feature(db, vendor_id, feature_code)
|
||||
|
||||
return {"has_feature": has_feature, "feature_code": feature_code}
|
||||
return FeatureCheckResponse(has_feature=has_feature, feature_code=feature_code)
|
||||
|
||||
293
app/api/v1/vendor/usage.py
vendored
293
app/api/v1/vendor/usage.py
vendored
@@ -12,16 +12,12 @@ 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 app.services.usage_service import usage_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor import VendorUser
|
||||
|
||||
router = APIRouter(prefix="/usage")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -106,181 +102,44 @@ def get_usage(
|
||||
"""
|
||||
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)}%)")
|
||||
# Get usage data from service
|
||||
usage_data = usage_service.get_vendor_usage(db, vendor_id)
|
||||
|
||||
# Convert to response
|
||||
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,
|
||||
code=usage_data.tier.code,
|
||||
name=usage_data.tier.name,
|
||||
price_monthly_cents=usage_data.tier.price_monthly_cents,
|
||||
is_highest_tier=usage_data.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,
|
||||
usage=[
|
||||
UsageMetric(
|
||||
name=m.name,
|
||||
current=m.current,
|
||||
limit=m.limit,
|
||||
percentage=m.percentage,
|
||||
is_unlimited=m.is_unlimited,
|
||||
is_at_limit=m.is_at_limit,
|
||||
is_approaching_limit=m.is_approaching_limit,
|
||||
)
|
||||
for m in usage_data.usage
|
||||
],
|
||||
has_limits_approaching=usage_data.has_limits_approaching,
|
||||
has_limits_reached=usage_data.has_limits_reached,
|
||||
upgrade_available=usage_data.upgrade_available,
|
||||
upgrade_tier=(
|
||||
UpgradeTierInfo(
|
||||
code=usage_data.upgrade_tier.code,
|
||||
name=usage_data.upgrade_tier.name,
|
||||
price_monthly_cents=usage_data.upgrade_tier.price_monthly_cents,
|
||||
price_increase_cents=usage_data.upgrade_tier.price_increase_cents,
|
||||
benefits=usage_data.upgrade_tier.benefits,
|
||||
)
|
||||
if usage_data.upgrade_tier
|
||||
else None
|
||||
),
|
||||
upgrade_reasons=usage_data.upgrade_reasons,
|
||||
)
|
||||
|
||||
|
||||
@@ -303,78 +162,16 @@ def check_limit(
|
||||
"""
|
||||
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
|
||||
# Check limit using service
|
||||
check_data = usage_service.check_limit(db, vendor_id, limit_type)
|
||||
|
||||
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,
|
||||
limit_type=check_data.limit_type,
|
||||
can_proceed=check_data.can_proceed,
|
||||
current=check_data.current,
|
||||
limit=check_data.limit,
|
||||
percentage=check_data.percentage,
|
||||
message=check_data.message,
|
||||
upgrade_tier_code=check_data.upgrade_tier_code,
|
||||
upgrade_tier_name=check_data.upgrade_tier_name,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user