From 929c8a316cba2955c40557728ea877d69a6721e4 Mon Sep 17 00:00:00 2001 From: majdyz Date: Mon, 13 Apr 2026 04:59:32 +0000 Subject: [PATCH] fix(platform): move stale-sub cleanup after idempotency check in sync_subscription_from_stripe _cleanup_stale_subscriptions was called before the idempotency guard (current_tier == tier -> return), so webhook replays for an already- applied event would fire another cleanup round and could inadvertently cancel a new subscription the user signed up for between the original event and its replay. Move the cleanup call to after the idempotency check so it only runs when we are actually going to apply a tier change. Add status in ("active", "trialing") and new_sub_id guard to ensure cleanup is only triggered for paid-sub activation events, not cancellations. --- .../backend/backend/data/credit.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/autogpt_platform/backend/backend/data/credit.py b/autogpt_platform/backend/backend/data/credit.py index 59d6779431..e18d3e7b23 100644 --- a/autogpt_platform/backend/backend/data/credit.py +++ b/autogpt_platform/backend/backend/data/credit.py @@ -1474,13 +1474,6 @@ async def sync_subscription_from_stripe(stripe_subscription: dict) -> None: customer_id, ) return - # When a new subscription becomes active (e.g. paid-to-paid tier upgrade - # via a fresh Checkout Session), cancel any OTHER active subscriptions - # for the same customer so the user isn't billed twice. We do this in - # the webhook rather than the API handler so that abandoning the - # checkout doesn't leave the user without a subscription. - if new_sub_id: - await _cleanup_stale_subscriptions(customer_id, new_sub_id) else: # A subscription was cancelled or ended. DO NOT unconditionally downgrade # to FREE — Stripe does not guarantee webhook delivery order, so a @@ -1542,6 +1535,17 @@ async def sync_subscription_from_stripe(stripe_subscription: dict) -> None: # when the tier is already correct to avoid redundant writes on replay. if current_tier == tier: return + # When a new subscription becomes active (e.g. paid-to-paid tier upgrade + # via a fresh Checkout Session), cancel any OTHER active subscriptions for + # the same customer so the user isn't billed twice. We do this in the + # webhook rather than the API handler so that abandoning the checkout + # doesn't leave the user without a subscription. + # IMPORTANT: this runs AFTER the idempotency check above so that webhook + # replays for an already-applied event do NOT trigger another cleanup round + # (which could otherwise cancel a legitimately new subscription the user + # signed up for between the original event and its replay). + if status in ("active", "trialing") and new_sub_id: + await _cleanup_stale_subscriptions(customer_id, new_sub_id) await set_subscription_tier(user.id, tier)