From d6fbed2a74cc679c3e7f22cd7ff25cd43eb9e939 Mon Sep 17 00:00:00 2001 From: Kristina <7614497+khjrtbrg@users.noreply.github.com> Date: Fri, 30 Jan 2026 09:42:07 +0100 Subject: [PATCH] [web] check end state before terminating (#31136) * check and log unexpected end states before terminating Recurly subscriptions * update finalize and rollback scripts to only postpone active subscriptions GitOrigin-RevId: 7fe6ffa56cb8ddf19133eb0cb59e39fd783430b7 --- ...p-recurly-subscriptions-post-migration.mjs | 47 ++++++++++++++++--- ...finalize-stripe-subscription-migration.mjs | 30 ++++++------ .../rollback-finalized-stripe-migration.mjs | 41 +++++++++------- 3 files changed, 80 insertions(+), 38 deletions(-) diff --git a/services/web/scripts/recurly/cleanup-recurly-subscriptions-post-migration.mjs b/services/web/scripts/recurly/cleanup-recurly-subscriptions-post-migration.mjs index 2dd6b4217d..64280d4bc2 100755 --- a/services/web/scripts/recurly/cleanup-recurly-subscriptions-post-migration.mjs +++ b/services/web/scripts/recurly/cleanup-recurly-subscriptions-post-migration.mjs @@ -195,25 +195,60 @@ async function processTermination(input, commit) { ) } - // 3. If commit mode, terminate the subscription + // 3. Fetch Recurly subscription and verify it is in our expected state + let recurlySubscription + let isInExpectedEndState = true + try { + recurlySubscription = + await RecurlyClient.promises.getSubscription(subscriptionUuid) + } catch (err) { + isInExpectedEndState = false + } + + if (recurlySubscription) { + const nineYearsFromNow = new Date() + nineYearsFromNow.setFullYear(new Date().getFullYear() + 9) + + if ( + recurlySubscription.periodEnd > nineYearsFromNow && + recurlySubscription.state === 'canceled' + ) { + isInExpectedEndState = false + } + } else { + throw new ReportError( + 'missing-subscription', + 'Recurly subscription not found' + ) + } + const warning = isInExpectedEndState + ? '' + : `(subscription was NOT in expected state: periodEnd=${recurlySubscription?.periodEnd?.toISOString()}, state=${recurlySubscription?.state})` + + // 4. If commit mode, terminate the subscription if (commit) { try { await RecurlyClient.promises.terminateSubscriptionByUuid(subscriptionUuid) - return { - status: 'terminated', - note: 'Successfully terminated Recurly subscription', + status: isInExpectedEndState + ? 'terminated' + : 'terminated-with-warnings', + note: `Successfully terminated Recurly subscription ${warning}`, } } catch (err) { throw new ReportError( 'terminate-failed', - `Failed to terminate: ${err.message}` + `Failed to terminate: ${err.message} ${warning}` ) } } else { + const note = isInExpectedEndState + ? 'DRY RUN: Ready to terminate' + : `DRY RUN: Ready to terminate ${warning}` + return { status: 'validated', - note: 'DRY RUN: Ready to terminate', + note, } } } diff --git a/services/web/scripts/stripe/finalize-stripe-subscription-migration.mjs b/services/web/scripts/stripe/finalize-stripe-subscription-migration.mjs index 797cedd40d..e2a5b1527a 100755 --- a/services/web/scripts/stripe/finalize-stripe-subscription-migration.mjs +++ b/services/web/scripts/stripe/finalize-stripe-subscription-migration.mjs @@ -431,21 +431,23 @@ async function performCutover( } ) - // Step 3: Postpone Recurly billing by +10 years - const currentBillingDate = new Date( - recurlySubscription.current_period_ends_at - ) - const postponedDate = new Date(currentBillingDate) - postponedDate.setFullYear(currentBillingDate.getFullYear() + 10) + // Step 3: Postpone Recurly billing by +10 years if Recurly subscription is active + if (recurlySubscription.state !== 'canceled') { + const currentBillingDate = new Date( + recurlySubscription.current_period_ends_at + ) + const postponedDate = new Date(currentBillingDate) + postponedDate.setFullYear(currentBillingDate.getFullYear() + 10) - try { - await RecurlyWrapper.promises.apiRequest({ - url: `subscriptions/${recurlySubscription.uuid}/postpone`, - qs: { bulk: true, next_bill_date: postponedDate }, - method: 'PUT', - }) - } catch (err) { - throw new Error(`Failed to postpone Recurly billing: ${err.message}`) + try { + await RecurlyWrapper.promises.apiRequest({ + url: `subscriptions/${recurlySubscription.uuid}/postpone`, + qs: { bulk: true, next_bill_date: postponedDate }, + method: 'PUT', + }) + } catch (err) { + throw new Error(`Failed to postpone Recurly billing: ${err.message}`) + } } // Step 4: Remove migration metadata from Stripe diff --git a/services/web/scripts/stripe/rollback-finalized-stripe-migration.mjs b/services/web/scripts/stripe/rollback-finalized-stripe-migration.mjs index db46f188af..182b16e938 100755 --- a/services/web/scripts/stripe/rollback-finalized-stripe-migration.mjs +++ b/services/web/scripts/stripe/rollback-finalized-stripe-migration.mjs @@ -301,30 +301,35 @@ async function performRollback( } ) - // Step 3: Un-postpone Recurly billing by 10 years + // Step 3: Un-postpone Recurly billing by 10 years if next billing period was postponed const currentPeriodEnd = new Date(recurlySubscription.current_period_ends_at) - const nextBillingDate = new Date(currentPeriodEnd) - nextBillingDate.setFullYear(currentPeriodEnd.getFullYear() - 10) - const targetBillingDateIsInFuture = nextBillingDate.getTime() > Date.now() + const nineYearsFromNow = new Date() + nineYearsFromNow.setFullYear(new Date().getFullYear() + 9) - if (targetBillingDateIsInFuture) { - try { - await RecurlyWrapper.promises.apiRequest({ - url: `subscriptions/${recurlySubscriptionId}/postpone`, - qs: { bulk: true, next_bill_date: nextBillingDate }, - method: 'PUT', - }) - } catch (err) { + if (currentPeriodEnd > nineYearsFromNow) { + const nextBillingDate = new Date(currentPeriodEnd) + nextBillingDate.setFullYear(currentPeriodEnd.getFullYear() - 10) + const targetBillingDateIsInFuture = nextBillingDate.getTime() > Date.now() + + if (targetBillingDateIsInFuture) { + try { + await RecurlyWrapper.promises.apiRequest({ + url: `subscriptions/${recurlySubscriptionId}/postpone`, + qs: { bulk: true, next_bill_date: nextBillingDate }, + method: 'PUT', + }) + } catch (err) { + throw new ReportError( + 'rolled-back-recurly-restore-failed', + `Restored Mongo but failed to restore Recurly billing: ${err.message}` + ) + } + } else { throw new ReportError( 'rolled-back-recurly-restore-failed', - `Restored Mongo but failed to restore Recurly billing: ${err.message}` + `Restored Mongo and Recurly but failed to restore Recurly billing: target next billing date is in the past (${nextBillingDate.toISOString()})` ) } - } else { - throw new ReportError( - 'rolled-back-recurly-restore-failed', - `Restored Mongo and Recurly but failed to restore Recurly billing: target next billing date is in the past (${nextBillingDate.toISOString()})` - ) } // Step 4: Restore migration metadata to Stripe