ソースを参照

Merge pull request #28470 from overleaf/ls-script-to-migration-user-id-metadata

Add scripts to migrate userId metadata

GitOrigin-RevId: 2365afd7a6772f147d2a3eb2f3e033cab3a4e7dd
Liangjun Song 10 ヶ月 前
コミット
f582a77de0

+ 248 - 0
services/web/scripts/stripe/add_user_id_to_stripe_customer.mjs

@@ -0,0 +1,248 @@
+#!/usr/bin/env node
+
+/**
+ * This script iterates through all Stripe subscriptions, checks if they have metadata adminUserId,
+ * and sets it to customer metadata "userId" if present.
+ *
+ * Usage:
+ *   node scripts/stripe/add_user_id_to_stripe_customer.mjs --region=us [options]
+ *   node scripts/stripe/add_user_id_to_stripe_customer.mjs --region=uk [options]
+ *
+ * Options:
+ *   --region=us|uk     Required. Stripe region to process (us or uk)
+ *   --commit           Actually perform the updates (default: dry-run mode)
+ *   --verbose          Enable verbose logging
+ *   --limit=N          Limit processing to N subscriptions (for testing)
+ *
+ * Examples:
+ *   # Dry run for US region with verbose output
+ *   node scripts/stripe/add_user_id_to_stripe_customer.mjs --region=us --verbose
+ *
+ *   # Commit changes for UK region
+ *   node scripts/stripe/add_user_id_to_stripe_customer.mjs --region=uk --commit
+ *
+ *   # Test with limited subscriptions
+ *   node scripts/stripe/add_user_id_to_stripe_customer.mjs --region=us --limit=10 --verbose
+ */
+
+import minimist from 'minimist'
+import { z } from '../../app/src/infrastructure/Validation.js'
+import { scriptRunner } from '../lib/ScriptRunner.mjs'
+import {
+  getRegionClient,
+  CUSTOMER_SEGMENT_MAPPING,
+} from '../../modules/subscriptions/app/src/StripeClient.mjs'
+
+const paramsSchema = z.object({
+  region: z.enum(['us', 'uk']),
+  commit: z.boolean().default(false),
+  verbose: z.boolean().default(false),
+  limit: z.number().int().min(1).optional(),
+})
+
+let processedCount = 0
+let updatedCount = 0
+let errorCount = 0
+
+/**
+ * Sleep function to respect Stripe rate limits (100 requests per second)
+ * We'll be conservative and sleep for 50ms between requests to stay well under the limit
+ */
+async function rateLimitSleep() {
+  return new Promise(resolve => setTimeout(resolve, 50))
+}
+
+/**
+ * Process a single subscription and update customer metadata if needed
+ */
+async function processSubscription(
+  subscription,
+  stripeClient,
+  commit,
+  verbose
+) {
+  try {
+    processedCount++
+
+    // Check if subscription has adminUserId metadata
+    const adminUserId = subscription.metadata?.adminUserId
+
+    if (verbose) {
+      console.info(
+        `Processing subscription ${subscription.id} (customer: ${subscription.customer.id}) - adminUserId: ${adminUserId || 'none'}`
+      )
+    }
+
+    if (!adminUserId) {
+      // No adminUserId to migrate
+      return
+    }
+
+    // Get customer details to check current metadata
+    const customer = subscription.customer
+
+    if (customer.metadata?.userId === adminUserId) {
+      if (verbose) {
+        console.info(
+          `Customer ${customer.id} already has userId=${adminUserId}, skipping`
+        )
+      }
+      return
+    }
+
+    if (customer.metadata?.userId && customer.metadata.userId !== adminUserId) {
+      console.warn(
+        `Customer ${customer.id} has existing userId=${customer.metadata.userId} which differs from adminUserId=${adminUserId}, skipping to avoid overwrite`
+      )
+      return
+    }
+
+    if (commit) {
+      // Update customer metadata using the StripeClient method
+      await stripeClient.updateCustomerMetadata(customer.id, {
+        ...customer.metadata,
+        userId: adminUserId,
+        segment: CUSTOMER_SEGMENT_MAPPING.B2C,
+      })
+
+      console.info(
+        `Updated customer ${customer.id} metadata: userId=${adminUserId}`
+      )
+    } else {
+      console.info(
+        `DRY RUN: Would update customer ${customer.id} metadata: userId=${adminUserId}`
+      )
+    }
+
+    updatedCount++
+  } catch (error) {
+    errorCount++
+    console.log(error)
+  }
+
+  // Respect rate limits
+  await rateLimitSleep()
+}
+
+/**
+ * Main script function
+ */
+async function main(trackProgress) {
+  const parseResult = paramsSchema.safeParse(
+    minimist(process.argv.slice(2), {
+      boolean: ['commit', 'verbose'],
+      string: ['region'],
+      number: ['limit'],
+    })
+  )
+
+  if (!parseResult.success) {
+    throw new Error(`Invalid parameters: ${parseResult.error.message}`)
+  }
+
+  const { region, commit, verbose, limit } = parseResult.data
+
+  const mode = commit ? 'COMMIT MODE' : 'DRY RUN MODE'
+  await trackProgress(
+    `Starting script in ${mode} for Stripe ${region.toUpperCase()} region`
+  )
+
+  if (limit) {
+    await trackProgress(`Processing limited to ${limit} subscriptions`)
+  }
+
+  // Get Stripe client for the specified region
+  const stripeClient = getRegionClient(region)
+
+  // Reset counters
+  processedCount = 0
+  updatedCount = 0
+  errorCount = 0
+
+  await trackProgress('Starting to iterate through Stripe subscriptions...')
+
+  const listParams = {
+    limit: 100, // Stripe's maximum limit per request
+    expand: ['data.customer'], // Expand customer data to reduce additional API calls
+  }
+
+  let hasMore = true
+  let startingAfter = null
+  let totalProcessed = 0
+
+  while (hasMore) {
+    const params = { ...listParams }
+    if (startingAfter) {
+      params.starting_after = startingAfter
+    }
+
+    // Get batch of subscriptions
+    const subscriptions = await stripeClient.stripe.subscriptions.list(params)
+
+    await trackProgress(
+      `Retrieved ${subscriptions.data.length} subscriptions (total processed so far: ${totalProcessed})`
+    )
+
+    // Process each subscription in the batch
+    for (const subscription of subscriptions.data) {
+      await processSubscription(subscription, stripeClient, commit, verbose)
+
+      totalProcessed++
+
+      // Check if we've hit the limit
+      if (limit && totalProcessed >= limit) {
+        await trackProgress(`Reached limit of ${limit} subscriptions, stopping`)
+        hasMore = false
+        break
+      }
+
+      // Progress update every 50 subscriptions
+      if (totalProcessed % 50 === 0) {
+        await trackProgress(
+          `Progress: ${totalProcessed} processed, ${updatedCount} customers updated, ${errorCount} errors`
+        )
+      }
+    }
+
+    // Check if there are more subscriptions to process
+    hasMore = hasMore && subscriptions.has_more
+    if (hasMore && subscriptions.data.length > 0) {
+      startingAfter = subscriptions.data[subscriptions.data.length - 1].id
+    }
+
+    // Rate limit between batch requests
+    await rateLimitSleep()
+  }
+
+  // Final summary
+  await trackProgress('FINAL SUMMARY:')
+  await trackProgress(`   Total subscriptions processed: ${processedCount}`)
+  await trackProgress(
+    `   Customers ${commit ? 'updated' : 'would be updated'}: ${updatedCount}`
+  )
+  await trackProgress(`   Errors encountered: ${errorCount}`)
+
+  if (!commit && updatedCount > 0) {
+    await trackProgress('')
+    await trackProgress(
+      'To actually perform the updates, run the script with --commit flag'
+    )
+  }
+
+  if (errorCount > 0) {
+    await trackProgress(
+      'Some errors were encountered. Check the logs above for details.'
+    )
+  }
+
+  await trackProgress(`Script completed successfully in ${mode}`)
+}
+
+// Execute the script using the runner
+try {
+  await scriptRunner(main)
+  process.exit(0)
+} catch (error) {
+  console.error('Script failed:', error.message)
+  process.exit(1)
+}

+ 227 - 0
services/web/scripts/stripe/remove_admin_user_id_from_stripe_subscription.mjs

@@ -0,0 +1,227 @@
+#!/usr/bin/env node
+
+/**
+ * This script iterates through all Stripe subscriptions and removes the adminUserId metadata
+ * from subscription objects that have it.
+ *
+ * Usage:
+ *   node scripts/stripe/remove_admin_user_id_from_stripe_subscription.mjs --region=us [options]
+ *   node scripts/stripe/remove_admin_user_id_from_stripe_subscription.mjs --region=uk [options]
+ *
+ * Options:
+ *   --region=us|uk     Required. Stripe region to process (us or uk)
+ *   --commit           Actually perform the updates (default: dry-run mode)
+ *   --verbose          Enable verbose logging
+ *   --limit=N          Limit processing to N subscriptions (for testing)
+ *
+ * Examples:
+ *   # Dry run for US region with verbose output
+ *   node scripts/stripe/remove_admin_user_id_from_stripe_subscription.mjs --region=us --verbose
+ *
+ *   # Commit changes for UK region
+ *   node scripts/stripe/remove_admin_user_id_from_stripe_subscription.mjs --region=uk --commit
+ *
+ *   # Test with limited subscriptions
+ *   node scripts/stripe/remove_admin_user_id_from_stripe_subscription.mjs --region=us --limit=10 --verbose
+ */
+
+import minimist from 'minimist'
+import { z } from '../../app/src/infrastructure/Validation.js'
+import { scriptRunner } from '../lib/ScriptRunner.mjs'
+import { getRegionClient } from '../../modules/subscriptions/app/src/StripeClient.mjs'
+
+const paramsSchema = z.object({
+  region: z.enum(['us', 'uk']),
+  commit: z.boolean().default(false),
+  verbose: z.boolean().default(false),
+  limit: z.number().int().min(1).optional(),
+})
+
+let processedCount = 0
+let updatedCount = 0
+let errorCount = 0
+
+/**
+ * Sleep function to respect Stripe rate limits (100 requests per second)
+ * We'll be conservative and sleep for 50ms between requests to stay well under the limit
+ */
+async function rateLimitSleep() {
+  return new Promise(resolve => setTimeout(resolve, 50))
+}
+
+/**
+ * Process a single subscription and remove adminUserId metadata if present
+ */
+async function processSubscription(
+  subscription,
+  stripeClient,
+  commit,
+  verbose
+) {
+  try {
+    processedCount++
+
+    // Check if subscription has adminUserId metadata
+    const adminUserId = subscription.metadata?.adminUserId
+
+    if (verbose) {
+      console.info(
+        `Processing subscription ${subscription.id} - adminUserId: ${adminUserId || 'none'}`
+      )
+    }
+
+    if (!adminUserId) {
+      // No adminUserId to remove
+      return
+    }
+
+    if (commit) {
+      // Create a new metadata object that will remove adminUserId
+      const updatedMetadata = { ...subscription.metadata }
+      updatedMetadata.adminUserId = ''
+
+      // Update subscription metadata using Stripe API directly
+      await stripeClient.stripe.subscriptions.update(subscription.id, {
+        metadata: updatedMetadata,
+      })
+
+      console.info(
+        `Removed adminUserId metadata from subscription ${subscription.id}`
+      )
+    } else {
+      console.info(
+        `DRY RUN: Would remove adminUserId metadata from subscription ${subscription.id}`
+      )
+    }
+
+    updatedCount++
+  } catch (error) {
+    errorCount++
+    console.log(error)
+  }
+
+  // Respect rate limits
+  await rateLimitSleep()
+}
+
+/**
+ * Main script function
+ */
+async function main(trackProgress) {
+  const parseResult = paramsSchema.safeParse(
+    minimist(process.argv.slice(2), {
+      boolean: ['commit', 'verbose'],
+      string: ['region'],
+      number: ['limit'],
+    })
+  )
+
+  if (!parseResult.success) {
+    throw new Error(`Invalid parameters: ${parseResult.error.message}`)
+  }
+
+  const { region, commit, verbose, limit } = parseResult.data
+
+  const mode = commit ? 'COMMIT MODE' : 'DRY RUN MODE'
+  await trackProgress(
+    `Starting script in ${mode} for Stripe ${region.toUpperCase()} region`
+  )
+
+  if (limit) {
+    await trackProgress(`Processing limited to ${limit} subscriptions`)
+  }
+
+  // Get Stripe client for the specified region
+  const stripeClient = getRegionClient(region)
+
+  // Reset counters
+  processedCount = 0
+  updatedCount = 0
+  errorCount = 0
+
+  await trackProgress('Starting to iterate through Stripe subscriptions...')
+
+  const listParams = {
+    limit: 100, // Stripe's maximum limit per request
+  }
+
+  let hasMore = true
+  let startingAfter = null
+  let totalProcessed = 0
+
+  while (hasMore) {
+    const params = { ...listParams }
+    if (startingAfter) {
+      params.starting_after = startingAfter
+    }
+
+    // Get batch of subscriptions
+    const subscriptions = await stripeClient.stripe.subscriptions.list(params)
+
+    await trackProgress(
+      `Retrieved ${subscriptions.data.length} subscriptions (total processed so far: ${totalProcessed})`
+    )
+
+    // Process each subscription in the batch
+    for (const subscription of subscriptions.data) {
+      await processSubscription(subscription, stripeClient, commit, verbose)
+
+      totalProcessed++
+
+      // Check if we've hit the limit
+      if (limit && totalProcessed >= limit) {
+        await trackProgress(`Reached limit of ${limit} subscriptions, stopping`)
+        hasMore = false
+        break
+      }
+
+      // Progress update every 50 subscriptions
+      if (totalProcessed % 50 === 0) {
+        await trackProgress(
+          `Progress: ${totalProcessed} processed, ${updatedCount} subscriptions updated, ${errorCount} errors`
+        )
+      }
+    }
+
+    // Check if there are more subscriptions to process
+    hasMore = hasMore && subscriptions.has_more
+    if (hasMore && subscriptions.data.length > 0) {
+      startingAfter = subscriptions.data[subscriptions.data.length - 1].id
+    }
+
+    // Rate limit between batch requests
+    await rateLimitSleep()
+  }
+
+  // Final summary
+  await trackProgress('FINAL SUMMARY:')
+  await trackProgress(`   Total subscriptions processed: ${processedCount}`)
+  await trackProgress(
+    `   Subscriptions ${commit ? 'updated' : 'would be updated'}: ${updatedCount}`
+  )
+  await trackProgress(`   Errors encountered: ${errorCount}`)
+
+  if (!commit && updatedCount > 0) {
+    await trackProgress('')
+    await trackProgress(
+      'To actually perform the updates, run the script with --commit flag'
+    )
+  }
+
+  if (errorCount > 0) {
+    await trackProgress(
+      'Some errors were encountered. Check the logs above for details.'
+    )
+  }
+
+  await trackProgress(`Script completed successfully in ${mode}`)
+}
+
+// Execute the script using the runner
+try {
+  await scriptRunner(main)
+  process.exit(0)
+} catch (error) {
+  console.error('Script failed:', error.message)
+  process.exit(1)
+}