Add subcription feature 9

This commit is contained in:
Urtzi Alfaro
2026-01-16 20:25:45 +01:00
parent fa7b62bd6c
commit 3a7d57ef90
19 changed files with 1833 additions and 985 deletions

View File

@@ -737,15 +737,27 @@ async def validate_plan_upgrade(
async def upgrade_subscription_plan(
tenant_id: str = Path(..., description="Tenant ID"),
new_plan: str = Query(..., description="New plan name"),
billing_cycle: str = Query("monthly", description="Billing cycle (monthly/yearly)"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service)
limit_service: SubscriptionLimitService = Depends(get_subscription_limit_service),
orchestration_service: SubscriptionOrchestrationService = Depends(get_subscription_orchestration_service)
) -> Dict[str, Any]:
"""
Upgrade subscription plan for a tenant
Includes validation, cache invalidation, and token refresh.
This endpoint handles:
- Plan upgrade validation
- Stripe subscription update (preserves trial status if in trial)
- Local database update
- Cache invalidation
- Token refresh for immediate UI update
Trial handling:
- If user is in trial, they remain in trial after upgrade
- The upgraded tier price will be charged when trial ends
"""
try:
# Step 1: Validate upgrade eligibility
validation = await limit_service.validate_plan_upgrade(tenant_id, new_plan)
if not validation.get("can_upgrade", False):
raise HTTPException(
@@ -768,22 +780,77 @@ async def upgrade_subscription_plan(
detail="No active subscription found for this tenant"
)
old_plan = active_subscription.plan
is_trialing = active_subscription.status == 'trialing'
trial_ends_at = active_subscription.trial_ends_at
logger.info("Starting subscription upgrade",
extra={
"tenant_id": tenant_id,
"subscription_id": str(active_subscription.id),
"stripe_subscription_id": active_subscription.subscription_id,
"old_plan": old_plan,
"new_plan": new_plan,
"is_trialing": is_trialing,
"trial_ends_at": str(trial_ends_at) if trial_ends_at else None,
"user_id": current_user["user_id"]
})
# Step 2: Update Stripe subscription if Stripe subscription ID exists
stripe_updated = False
if active_subscription.subscription_id:
try:
# Use orchestration service to handle Stripe update with trial preservation
upgrade_result = await orchestration_service.orchestrate_plan_upgrade(
tenant_id=tenant_id,
new_plan=new_plan,
proration_behavior="none" if is_trialing else "create_prorations",
immediate_change=not is_trialing, # Don't change billing anchor if trialing
billing_cycle=billing_cycle
)
stripe_updated = True
logger.info("Stripe subscription updated successfully",
extra={
"tenant_id": tenant_id,
"stripe_subscription_id": active_subscription.subscription_id,
"upgrade_result": upgrade_result
})
except Exception as stripe_error:
logger.error("Failed to update Stripe subscription, falling back to local update only",
extra={"tenant_id": tenant_id, "error": str(stripe_error)})
# Continue with local update even if Stripe fails
# This ensures the user gets access to features immediately
# Step 3: Update local database
updated_subscription = await subscription_repo.update_subscription_plan(
str(active_subscription.id),
new_plan
)
# Preserve trial status if was trialing
if is_trialing and trial_ends_at:
# Ensure trial_ends_at is preserved after plan update
await subscription_repo.update_subscription_status(
str(active_subscription.id),
'trialing',
{'trial_ends_at': trial_ends_at}
)
await session.commit()
logger.info("Subscription plan upgraded successfully",
logger.info("Subscription plan upgraded successfully in database",
extra={
"tenant_id": tenant_id,
"subscription_id": str(active_subscription.id),
"old_plan": active_subscription.plan,
"old_plan": old_plan,
"new_plan": new_plan,
"stripe_updated": stripe_updated,
"preserved_trial": is_trialing,
"user_id": current_user["user_id"]
})
# Step 4: Invalidate subscription cache
redis_client = None
try:
from app.services.subscription_cache import get_subscription_cache_service
@@ -797,14 +864,17 @@ async def upgrade_subscription_plan(
logger.error("Failed to invalidate subscription cache after upgrade",
extra={"tenant_id": tenant_id, "error": str(cache_error)})
# Step 5: Invalidate tokens for immediate UI refresh
try:
await _invalidate_tenant_tokens(tenant_id, redis_client)
logger.info("Invalidated all tokens for tenant after subscription upgrade",
extra={"tenant_id": tenant_id})
if redis_client:
await _invalidate_tenant_tokens(tenant_id, redis_client)
logger.info("Invalidated all tokens for tenant after subscription upgrade",
extra={"tenant_id": tenant_id})
except Exception as token_error:
logger.error("Failed to invalidate tenant tokens after upgrade",
extra={"tenant_id": tenant_id, "error": str(token_error)})
# Step 6: Publish subscription change event for other services
try:
from shared.messaging import UnifiedEventPublisher
event_publisher = UnifiedEventPublisher()
@@ -813,9 +883,12 @@ async def upgrade_subscription_plan(
tenant_id=tenant_id,
data={
"tenant_id": tenant_id,
"old_tier": active_subscription.plan,
"old_tier": old_plan,
"new_tier": new_plan,
"action": "upgrade"
"action": "upgrade",
"is_trialing": is_trialing,
"trial_ends_at": trial_ends_at.isoformat() if trial_ends_at else None,
"stripe_updated": stripe_updated
}
)
logger.info("Published subscription change event",
@@ -826,10 +899,13 @@ async def upgrade_subscription_plan(
return {
"success": True,
"message": f"Plan successfully upgraded to {new_plan}",
"old_plan": active_subscription.plan,
"message": f"Plan successfully upgraded to {new_plan}" + (" (trial preserved)" if is_trialing else ""),
"old_plan": old_plan,
"new_plan": new_plan,
"new_monthly_price": updated_subscription.monthly_price,
"is_trialing": is_trialing,
"trial_ends_at": trial_ends_at.isoformat() if trial_ends_at else None,
"stripe_updated": stripe_updated,
"validation": validation,
"requires_token_refresh": True
}