fix(subscription): Cancel subscription in Stripe before local downgrade
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
- Add StripeService.cancel_subscription() method - Update /cancel endpoint to call Stripe API - Set cancelled_at timestamp - Clean up PostgreSQL reference in server .env
This commit is contained in:
@ -310,9 +310,13 @@ async def cancel_subscription(
|
||||
"""
|
||||
Cancel subscription and downgrade to Scout.
|
||||
|
||||
Note: For Stripe-managed subscriptions, use the Customer Portal instead.
|
||||
This endpoint is for manual cancellation.
|
||||
This will:
|
||||
1. Cancel the subscription in Stripe (if exists)
|
||||
2. Downgrade the user to Scout tier locally
|
||||
"""
|
||||
from app.services.stripe_service import StripeService
|
||||
from datetime import datetime
|
||||
|
||||
result = await db.execute(
|
||||
select(Subscription).where(Subscription.user_id == current_user.id)
|
||||
)
|
||||
@ -330,12 +334,24 @@ async def cancel_subscription(
|
||||
detail="Already on free plan",
|
||||
)
|
||||
|
||||
# Downgrade to Scout
|
||||
old_tier = subscription.tier.value
|
||||
stripe_sub_id = subscription.stripe_subscription_id
|
||||
|
||||
# Cancel in Stripe first (if we have a Stripe subscription)
|
||||
if stripe_sub_id:
|
||||
cancelled = await StripeService.cancel_subscription(stripe_sub_id)
|
||||
if not cancelled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to cancel subscription in Stripe. Please try again or contact support.",
|
||||
)
|
||||
|
||||
# Downgrade to Scout locally
|
||||
subscription.tier = SubscriptionTier.SCOUT
|
||||
subscription.max_domains = TIER_CONFIG[SubscriptionTier.SCOUT]["domain_limit"]
|
||||
subscription.check_frequency = TIER_CONFIG[SubscriptionTier.SCOUT]["check_frequency"]
|
||||
subscription.stripe_subscription_id = None
|
||||
subscription.cancelled_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
|
||||
@ -343,4 +359,5 @@ async def cancel_subscription(
|
||||
"status": "cancelled",
|
||||
"message": f"Subscription cancelled. Downgraded from {old_tier} to Scout.",
|
||||
"new_tier": "scout",
|
||||
"stripe_cancelled": bool(stripe_sub_id),
|
||||
}
|
||||
|
||||
@ -206,6 +206,38 @@ class StripeService:
|
||||
logger.error(f"Stripe error creating portal session: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
async def cancel_subscription(stripe_subscription_id: str) -> bool:
|
||||
"""
|
||||
Cancel a subscription in Stripe.
|
||||
|
||||
Args:
|
||||
stripe_subscription_id: The Stripe subscription ID to cancel
|
||||
|
||||
Returns:
|
||||
True if cancelled successfully, False otherwise
|
||||
"""
|
||||
if not StripeService.is_configured():
|
||||
logger.warning("Stripe not configured, skipping cancel")
|
||||
return False
|
||||
|
||||
if not stripe_subscription_id:
|
||||
logger.warning("No Stripe subscription ID provided")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Cancel the subscription immediately
|
||||
stripe.Subscription.cancel(stripe_subscription_id)
|
||||
logger.info(f"Cancelled Stripe subscription: {stripe_subscription_id}")
|
||||
return True
|
||||
except stripe.error.InvalidRequestError as e:
|
||||
# Subscription might already be cancelled
|
||||
logger.warning(f"Stripe subscription cancel failed (may already be cancelled): {e}")
|
||||
return True # Consider it success if already cancelled
|
||||
except stripe.error.StripeError as e:
|
||||
logger.error(f"Stripe error cancelling subscription: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def handle_webhook(
|
||||
payload: bytes,
|
||||
|
||||
Reference in New Issue
Block a user