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

Add scripts to migrate userId metadata

GitOrigin-RevId: 2365afd7a6772f147d2a3eb2f3e033cab3a4e7dd
This commit is contained in:
Liangjun Song
2025-09-26 17:44:35 +08:00
committed by Copybot
parent 3b853e9434
commit f582a77de0
2 changed files with 475 additions and 0 deletions

View File

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

View File

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