Kaynağa Gözat

[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
Kristina 6 ay önce
ebeveyn
işleme
d6fbed2a74

+ 41 - 6
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,
     }
   }
 }

+ 16 - 14
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

+ 24 - 19
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()
-
-  if (targetBillingDateIsInFuture) {
-    try {
-      await RecurlyWrapper.promises.apiRequest({
-        url: `subscriptions/${recurlySubscriptionId}/postpone`,
-        qs: { bulk: true, next_bill_date: nextBillingDate },
-        method: 'PUT',
-      })
-    } catch (err) {
+  const nineYearsFromNow = new Date()
+  nineYearsFromNow.setFullYear(new Date().getFullYear() + 9)
+
+  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