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.
|
Cancel subscription and downgrade to Scout.
|
||||||
|
|
||||||
Note: For Stripe-managed subscriptions, use the Customer Portal instead.
|
This will:
|
||||||
This endpoint is for manual cancellation.
|
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(
|
result = await db.execute(
|
||||||
select(Subscription).where(Subscription.user_id == current_user.id)
|
select(Subscription).where(Subscription.user_id == current_user.id)
|
||||||
)
|
)
|
||||||
@ -330,12 +334,24 @@ async def cancel_subscription(
|
|||||||
detail="Already on free plan",
|
detail="Already on free plan",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Downgrade to Scout
|
|
||||||
old_tier = subscription.tier.value
|
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.tier = SubscriptionTier.SCOUT
|
||||||
subscription.max_domains = TIER_CONFIG[SubscriptionTier.SCOUT]["domain_limit"]
|
subscription.max_domains = TIER_CONFIG[SubscriptionTier.SCOUT]["domain_limit"]
|
||||||
subscription.check_frequency = TIER_CONFIG[SubscriptionTier.SCOUT]["check_frequency"]
|
subscription.check_frequency = TIER_CONFIG[SubscriptionTier.SCOUT]["check_frequency"]
|
||||||
subscription.stripe_subscription_id = None
|
subscription.stripe_subscription_id = None
|
||||||
|
subscription.cancelled_at = datetime.utcnow()
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@ -343,4 +359,5 @@ async def cancel_subscription(
|
|||||||
"status": "cancelled",
|
"status": "cancelled",
|
||||||
"message": f"Subscription cancelled. Downgraded from {old_tier} to Scout.",
|
"message": f"Subscription cancelled. Downgraded from {old_tier} to Scout.",
|
||||||
"new_tier": "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}")
|
logger.error(f"Stripe error creating portal session: {e}")
|
||||||
raise
|
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
|
@staticmethod
|
||||||
async def handle_webhook(
|
async def handle_webhook(
|
||||||
payload: bytes,
|
payload: bytes,
|
||||||
|
|||||||
Reference in New Issue
Block a user