Add subcription feature 9
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -114,10 +114,12 @@ async def register_bakery(
|
||||
error=str(linking_error))
|
||||
|
||||
elif bakery_data.coupon_code:
|
||||
coupon_validation = payment_service.validate_coupon_code(
|
||||
from app.services.coupon_service import CouponService
|
||||
|
||||
coupon_service = CouponService(db)
|
||||
coupon_validation = await coupon_service.validate_coupon_code(
|
||||
bakery_data.coupon_code,
|
||||
tenant_id,
|
||||
db
|
||||
tenant_id
|
||||
)
|
||||
|
||||
if not coupon_validation["valid"]:
|
||||
@@ -131,10 +133,10 @@ async def register_bakery(
|
||||
detail=coupon_validation["error_message"]
|
||||
)
|
||||
|
||||
success, discount, error = payment_service.redeem_coupon(
|
||||
success, discount, error = await coupon_service.redeem_coupon(
|
||||
bakery_data.coupon_code,
|
||||
tenant_id,
|
||||
db
|
||||
base_trial_days=0
|
||||
)
|
||||
|
||||
if success:
|
||||
@@ -194,13 +196,15 @@ async def register_bakery(
|
||||
|
||||
if coupon_validation and coupon_validation["valid"]:
|
||||
from app.core.config import settings
|
||||
from app.services.coupon_service import CouponService
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
|
||||
async with database_manager.get_session() as session:
|
||||
success, discount, error = payment_service.redeem_coupon(
|
||||
coupon_service = CouponService(session)
|
||||
success, discount, error = await coupon_service.redeem_coupon(
|
||||
bakery_data.coupon_code,
|
||||
result.id,
|
||||
session
|
||||
base_trial_days=0
|
||||
)
|
||||
|
||||
if success:
|
||||
|
||||
Reference in New Issue
Block a user