[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
This commit is contained in:
Kristina
2026-01-30 09:42:07 +01:00
committed by Copybot
parent a370a72a5a
commit d6fbed2a74
3 changed files with 80 additions and 38 deletions

View File

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

View File

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

View File

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