Add subcription feature 9
This commit is contained in:
@@ -852,7 +852,8 @@ class PaymentService:
|
||||
proration_behavior: str = "create_prorations",
|
||||
billing_cycle_anchor: Optional[str] = None,
|
||||
payment_behavior: str = "error_if_incomplete",
|
||||
immediate_change: bool = True
|
||||
immediate_change: bool = True,
|
||||
preserve_trial: bool = False
|
||||
) -> Any:
|
||||
"""
|
||||
Update subscription price (plan upgrade/downgrade)
|
||||
@@ -860,24 +861,33 @@ class PaymentService:
|
||||
Args:
|
||||
subscription_id: Stripe subscription ID
|
||||
new_price_id: New Stripe price ID
|
||||
proration_behavior: How to handle proration
|
||||
proration_behavior: How to handle proration ('create_prorations', 'none', 'always_invoice')
|
||||
billing_cycle_anchor: Billing cycle anchor ("now" or "unchanged")
|
||||
payment_behavior: Payment behavior on update
|
||||
immediate_change: Whether to apply changes immediately
|
||||
preserve_trial: If True, preserves the trial period after upgrade
|
||||
|
||||
Returns:
|
||||
Updated subscription object with .id, .status, etc. attributes
|
||||
"""
|
||||
try:
|
||||
result = await retry_with_backoff(
|
||||
lambda: self.stripe_client.update_subscription(subscription_id, new_price_id),
|
||||
lambda: self.stripe_client.update_subscription(
|
||||
subscription_id,
|
||||
new_price_id,
|
||||
proration_behavior=proration_behavior,
|
||||
preserve_trial=preserve_trial
|
||||
),
|
||||
max_retries=3,
|
||||
exceptions=(SubscriptionCreationFailed,)
|
||||
)
|
||||
|
||||
logger.info("Subscription updated successfully",
|
||||
subscription_id=subscription_id,
|
||||
new_price_id=new_price_id)
|
||||
new_price_id=new_price_id,
|
||||
proration_behavior=proration_behavior,
|
||||
preserve_trial=preserve_trial,
|
||||
is_trialing=result.get('is_trialing', False))
|
||||
|
||||
# Create wrapper object for compatibility with callers expecting .id, .status etc.
|
||||
class SubscriptionWrapper:
|
||||
@@ -887,6 +897,8 @@ class PaymentService:
|
||||
self.current_period_start = data.get('current_period_start')
|
||||
self.current_period_end = data.get('current_period_end')
|
||||
self.customer = data.get('customer_id')
|
||||
self.trial_end = data.get('trial_end')
|
||||
self.is_trialing = data.get('is_trialing', False)
|
||||
|
||||
return SubscriptionWrapper(result)
|
||||
|
||||
|
||||
@@ -638,17 +638,22 @@ class SubscriptionOrchestrationService:
|
||||
billing_cycle: str = "monthly"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Orchestrate plan upgrade workflow with proration
|
||||
Orchestrate plan upgrade workflow with proration and trial preservation
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant ID
|
||||
new_plan: New plan name
|
||||
proration_behavior: Proration behavior
|
||||
proration_behavior: Proration behavior ('create_prorations', 'none', 'always_invoice')
|
||||
immediate_change: Whether to apply changes immediately
|
||||
billing_cycle: Billing cycle for new plan
|
||||
|
||||
Returns:
|
||||
Dictionary with upgrade results
|
||||
|
||||
Trial Handling:
|
||||
- If subscription is in trial, the trial period is preserved
|
||||
- No proration charges are created during trial
|
||||
- After trial ends, the user is charged at the new tier price
|
||||
"""
|
||||
try:
|
||||
logger.info("Starting plan upgrade orchestration",
|
||||
@@ -665,33 +670,64 @@ class SubscriptionOrchestrationService:
|
||||
if not subscription.subscription_id:
|
||||
raise ValidationError(f"Tenant {tenant_id} does not have a Stripe subscription ID")
|
||||
|
||||
# Step 1.5: Check if subscription is in trial
|
||||
is_trialing = subscription.status == 'trialing'
|
||||
trial_ends_at = subscription.trial_ends_at
|
||||
|
||||
logger.info("Subscription trial status",
|
||||
tenant_id=tenant_id,
|
||||
is_trialing=is_trialing,
|
||||
trial_ends_at=str(trial_ends_at) if trial_ends_at else None,
|
||||
current_plan=subscription.plan)
|
||||
|
||||
# For trial subscriptions:
|
||||
# - No proration charges (proration_behavior='none')
|
||||
# - Preserve trial period
|
||||
# - User gets new tier features immediately
|
||||
# - User is charged new tier price when trial ends
|
||||
if is_trialing:
|
||||
proration_behavior = "none"
|
||||
logger.info("Trial subscription detected, disabling proration",
|
||||
tenant_id=tenant_id)
|
||||
|
||||
# Step 2: Get Stripe price ID for new plan
|
||||
stripe_price_id = self.payment_service._get_stripe_price_id(new_plan, billing_cycle)
|
||||
|
||||
# Step 3: Calculate proration preview
|
||||
proration_details = await self.payment_service.calculate_payment_proration(
|
||||
subscription.subscription_id,
|
||||
stripe_price_id,
|
||||
proration_behavior
|
||||
)
|
||||
# Step 3: Calculate proration preview (only if not trialing)
|
||||
proration_details = {}
|
||||
if not is_trialing:
|
||||
proration_details = await self.payment_service.calculate_payment_proration(
|
||||
subscription.subscription_id,
|
||||
stripe_price_id,
|
||||
proration_behavior
|
||||
)
|
||||
logger.info("Proration calculated for plan upgrade",
|
||||
tenant_id=tenant_id,
|
||||
proration_amount=proration_details.get("net_amount", 0))
|
||||
else:
|
||||
proration_details = {
|
||||
"subscription_id": subscription.subscription_id,
|
||||
"new_price_id": stripe_price_id,
|
||||
"proration_behavior": proration_behavior,
|
||||
"net_amount": 0,
|
||||
"trial_preserved": True
|
||||
}
|
||||
|
||||
logger.info("Proration calculated for plan upgrade",
|
||||
tenant_id=tenant_id,
|
||||
proration_amount=proration_details.get("net_amount", 0))
|
||||
|
||||
# Step 4: Update in payment provider
|
||||
# Step 4: Update in payment provider with trial preservation
|
||||
updated_stripe_subscription = await self.payment_service.update_payment_subscription(
|
||||
subscription.subscription_id,
|
||||
stripe_price_id,
|
||||
proration_behavior=proration_behavior,
|
||||
billing_cycle_anchor="now" if immediate_change else "unchanged",
|
||||
billing_cycle_anchor="now" if immediate_change and not is_trialing else "unchanged",
|
||||
payment_behavior="error_if_incomplete",
|
||||
immediate_change=immediate_change
|
||||
immediate_change=immediate_change,
|
||||
preserve_trial=is_trialing # Preserve trial if currently trialing
|
||||
)
|
||||
|
||||
logger.info("Plan updated in payment provider",
|
||||
subscription_id=updated_stripe_subscription.id,
|
||||
new_status=updated_stripe_subscription.status)
|
||||
new_status=updated_stripe_subscription.status,
|
||||
is_trialing=getattr(updated_stripe_subscription, 'is_trialing', False))
|
||||
|
||||
# Step 5: Update local subscription record
|
||||
update_result = await self.subscription_service.update_subscription_plan_record(
|
||||
@@ -722,8 +758,12 @@ class SubscriptionOrchestrationService:
|
||||
logger.info("Tenant plan information updated",
|
||||
tenant_id=tenant_id)
|
||||
|
||||
# Add immediate_change to result
|
||||
# Add upgrade metadata to result
|
||||
update_result["immediate_change"] = immediate_change
|
||||
update_result["is_trialing"] = is_trialing
|
||||
update_result["trial_preserved"] = is_trialing
|
||||
if trial_ends_at:
|
||||
update_result["trial_ends_at"] = trial_ends_at.isoformat()
|
||||
|
||||
return update_result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user