fix(subscription): Cancel subscription in Stripe before local downgrade
Some checks are pending
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions
CI / Backend Lint (push) Waiting to run
CI / Backend Tests (push) Blocked by required conditions
CI / Docker Build (push) Blocked by required conditions
CI / Security Scan (push) Waiting to run
Deploy / Build & Push Images (push) Waiting to run
Deploy / Deploy to Server (push) Blocked by required conditions
Deploy / Notify (push) Blocked by required conditions

- 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:
2025-12-19 07:39:24 +01:00
parent 5d81f8d71e
commit 108b0ae775
2 changed files with 52 additions and 3 deletions

View File

@ -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),
}

View File

@ -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,