[web] rm unnecessary scripts (#31631)
GitOrigin-RevId: c6388685bd9cd43d8d22e38f25db4ee579def808
This commit is contained in:
@@ -1,255 +0,0 @@
|
||||
#!/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.mjs'
|
||||
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.deleted) {
|
||||
if (verbose) {
|
||||
console.info(`Customer ${customer.id} is deleted, skipping`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
status: 'all', // Include subscriptions in all statuses (active, past_due, unpaid, canceled, etc.)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* This script validates subscriptions that are canceled or expired and have migration metadata set to "in_progress",
|
||||
* then updates the metadata to "cancelled" if validation passes.
|
||||
*
|
||||
* TODO: This script can be deleted after being run in production.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/stripe/bulk-update-migration-status.mjs [OPTS] [INPUT-FILE]
|
||||
*
|
||||
* Options:
|
||||
* --output PATH Output file path (default: /tmp/bulk_update_output_<timestamp>.csv)
|
||||
* Use '-' to write to stdout
|
||||
* --commit Apply changes (without this flag, runs in dry-run mode)
|
||||
* --concurrency N Number of subscriptions to process concurrently (default: 10)
|
||||
* --stripe-rate-limit N Requests per second for Stripe (default: 50)
|
||||
* --stripe-api-retries N Number of retries on Stripe 429s (default: 5)
|
||||
* --stripe-retry-delay-ms N Delay between Stripe retries in ms (default: 1000)
|
||||
* --help Show a help message
|
||||
*
|
||||
* CSV Input Format:
|
||||
* The CSV must have the following columns:
|
||||
* - subscription_id: Stripe subscription id
|
||||
* - target_stripe_account: Either 'stripe-uk' or 'stripe-us'
|
||||
*
|
||||
* Output:
|
||||
* Writes a CSV with columns:
|
||||
* - subscription_id: The subscription id processed
|
||||
* - target_stripe_account: The Stripe account
|
||||
* - status: Result status (validated, updated, invalid-status, invalid-metadata, or error)
|
||||
* - note: Additional information about the status
|
||||
*/
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import * as csv from 'csv'
|
||||
import minimist from 'minimist'
|
||||
import PQueue from 'p-queue'
|
||||
import { z } from '../../app/src/infrastructure/Validation.mjs'
|
||||
import { scriptRunner } from '../lib/ScriptRunner.mjs'
|
||||
import { getRegionClient } from '../../modules/subscriptions/app/src/StripeClient.mjs'
|
||||
import { ReportError } from './helpers.mjs'
|
||||
import {
|
||||
createRateLimitedApiWrappers,
|
||||
DEFAULT_STRIPE_RATE_LIMIT,
|
||||
DEFAULT_STRIPE_API_RETRIES,
|
||||
DEFAULT_STRIPE_RETRY_DELAY_MS,
|
||||
} from './RateLimiter.mjs'
|
||||
|
||||
const DEFAULT_CONCURRENCY = 10
|
||||
|
||||
// rate limiters - initialized in main()
|
||||
let rateLimiters
|
||||
|
||||
function usage() {
|
||||
console.error(`Usage: node scripts/stripe/bulk-update-migration-status.mjs [OPTS] [INPUT-FILE]
|
||||
|
||||
Options:
|
||||
--output PATH Output file path (default: /tmp/bulk_update_output_<timestamp>.csv)
|
||||
Use '-' to write to stdout
|
||||
--commit Apply changes (without this, runs in dry-run mode)
|
||||
--concurrency N Number of subscriptions to process concurrently (default: ${DEFAULT_CONCURRENCY})
|
||||
--stripe-rate-limit N Requests per second for Stripe (default: ${DEFAULT_STRIPE_RATE_LIMIT})
|
||||
--stripe-api-retries N Number of retries on Stripe 429s (default: ${DEFAULT_STRIPE_API_RETRIES})
|
||||
--stripe-retry-delay-ms N Delay between Stripe retries in ms (default: ${DEFAULT_STRIPE_RETRY_DELAY_MS})
|
||||
--help Show this help message
|
||||
`)
|
||||
}
|
||||
|
||||
async function main(trackProgress) {
|
||||
const opts = parseArgs()
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||
const outputFile = opts.output ?? `/tmp/bulk_update_output_${timestamp}.csv`
|
||||
|
||||
// initialize rate limiters
|
||||
rateLimiters = createRateLimitedApiWrappers({
|
||||
stripeRateLimit: opts.stripeRateLimit,
|
||||
stripeApiRetries: opts.stripeApiRetries,
|
||||
stripeRetryDelayMs: opts.stripeRetryDelayMs,
|
||||
})
|
||||
|
||||
await trackProgress(
|
||||
'Starting bulk validation and update of subscription metadata'
|
||||
)
|
||||
await trackProgress(`Run mode: ${opts.commit ? 'COMMIT' : 'DRY RUN'}`)
|
||||
await trackProgress(`Rate limit: Stripe ${opts.stripeRateLimit}/s`)
|
||||
await trackProgress(`Concurrency: ${opts.concurrency}`)
|
||||
|
||||
const inputStream = opts.inputFile
|
||||
? fs.createReadStream(opts.inputFile)
|
||||
: process.stdin
|
||||
const csvReader = getCsvReader(inputStream)
|
||||
const csvWriter = getCsvWriter(outputFile)
|
||||
|
||||
await trackProgress(`Output: ${outputFile === '-' ? 'stdout' : outputFile}`)
|
||||
|
||||
let processedCount = 0
|
||||
let successCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
const queue = new PQueue({ concurrency: opts.concurrency })
|
||||
const maxQueueSize = opts.concurrency
|
||||
|
||||
try {
|
||||
for await (const input of csvReader) {
|
||||
if (queue.size >= maxQueueSize) {
|
||||
await queue.onSizeLessThan(maxQueueSize)
|
||||
}
|
||||
|
||||
queue.add(async () => {
|
||||
try {
|
||||
const result = await processValidation(input, opts.commit)
|
||||
|
||||
csvWriter.write({
|
||||
subscription_id: input.subscription_id,
|
||||
target_stripe_account: input.target_stripe_account,
|
||||
status: result.status,
|
||||
note:
|
||||
result.note ||
|
||||
(opts.commit ? '' : 'dry run - no changes applied'),
|
||||
})
|
||||
|
||||
if (result.status === 'updated' || result.status === 'validated') {
|
||||
successCount++
|
||||
} else {
|
||||
errorCount++
|
||||
}
|
||||
} catch (err) {
|
||||
errorCount++
|
||||
if (err instanceof ReportError) {
|
||||
csvWriter.write({
|
||||
subscription_id: input.subscription_id,
|
||||
target_stripe_account: input.target_stripe_account,
|
||||
status: err.status,
|
||||
note: err.message,
|
||||
})
|
||||
} else {
|
||||
csvWriter.write({
|
||||
subscription_id: input.subscription_id,
|
||||
target_stripe_account: input.target_stripe_account,
|
||||
status: 'error',
|
||||
note: err.message,
|
||||
})
|
||||
await trackProgress(
|
||||
`Error processing ${input.subscription_id}: ${err.message}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
processedCount++
|
||||
if (processedCount % 10 === 0) {
|
||||
await trackProgress(
|
||||
`Processed ${processedCount} subscriptions (${successCount} ${opts.commit ? 'updated' : 'validated'}, ${errorCount} errors)`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
await queue.onIdle()
|
||||
}
|
||||
|
||||
await trackProgress(`✅ Total processed: ${processedCount}`)
|
||||
if (opts.commit) {
|
||||
await trackProgress(`✅ Successfully updated: ${successCount}`)
|
||||
} else {
|
||||
await trackProgress(`✅ Successfully validated: ${successCount}`)
|
||||
await trackProgress('ℹ️ DRY RUN: No changes were applied')
|
||||
}
|
||||
await trackProgress(`❌ Errors: ${errorCount}`)
|
||||
await trackProgress('🎉 Script completed!')
|
||||
|
||||
csvWriter.end()
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const args = minimist(process.argv.slice(2), {
|
||||
string: [
|
||||
'output',
|
||||
'concurrency',
|
||||
'stripe-rate-limit',
|
||||
'stripe-api-retries',
|
||||
'stripe-retry-delay-ms',
|
||||
],
|
||||
boolean: ['commit', 'help'],
|
||||
default: {
|
||||
commit: false,
|
||||
concurrency: DEFAULT_CONCURRENCY,
|
||||
'stripe-rate-limit': DEFAULT_STRIPE_RATE_LIMIT,
|
||||
'stripe-api-retries': DEFAULT_STRIPE_API_RETRIES,
|
||||
'stripe-retry-delay-ms': DEFAULT_STRIPE_RETRY_DELAY_MS,
|
||||
},
|
||||
unknown: arg => {
|
||||
if (arg.startsWith('-')) {
|
||||
console.error(`Unknown option: ${arg}`)
|
||||
usage()
|
||||
process.exit(1)
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
if (args.help) {
|
||||
usage()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const inputFile = args._[0]
|
||||
const paramsSchema = z.object({
|
||||
output: z.string().optional(),
|
||||
commit: z.boolean(),
|
||||
concurrency: z.number().int().positive(),
|
||||
stripeRateLimit: z.number().positive(),
|
||||
stripeApiRetries: z.number().int().nonnegative(),
|
||||
stripeRetryDelayMs: z.number().int().nonnegative(),
|
||||
inputFile: z.string().optional(),
|
||||
})
|
||||
|
||||
try {
|
||||
return paramsSchema.parse({
|
||||
output: args.output,
|
||||
commit: args.commit,
|
||||
concurrency: Number(args.concurrency),
|
||||
stripeRateLimit: Number(args['stripe-rate-limit']),
|
||||
stripeApiRetries: Number(args['stripe-api-retries']),
|
||||
stripeRetryDelayMs: Number(args['stripe-retry-delay-ms']),
|
||||
inputFile,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Invalid arguments:', err.message)
|
||||
usage()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function getCsvReader(inputStream) {
|
||||
const parser = csv.parse({ columns: true })
|
||||
inputStream.pipe(parser)
|
||||
return parser
|
||||
}
|
||||
|
||||
function getCsvWriter(outputFile) {
|
||||
if (outputFile === '-') {
|
||||
const writer = csv.stringify({
|
||||
columns: ['subscription_id', 'target_stripe_account', 'status', 'note'],
|
||||
header: true,
|
||||
})
|
||||
writer.on('error', err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
writer.pipe(process.stdout)
|
||||
return writer
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(outputFile), { recursive: true })
|
||||
const outputStream = fs.createWriteStream(outputFile)
|
||||
|
||||
const writer = csv.stringify({
|
||||
columns: ['subscription_id', 'target_stripe_account', 'status', 'note'],
|
||||
header: true,
|
||||
})
|
||||
|
||||
writer.on('error', err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
writer.pipe(outputStream)
|
||||
return writer
|
||||
}
|
||||
|
||||
async function processValidation(input, commit) {
|
||||
const {
|
||||
subscription_id: subscriptionId,
|
||||
target_stripe_account: targetStripeAccount,
|
||||
} = input
|
||||
|
||||
// get Stripe client for the target account (strip 'stripe-' prefix if present)
|
||||
const region = targetStripeAccount.replace(/^stripe-/, '')
|
||||
const stripeClient = getRegionClient(region)
|
||||
|
||||
// fetch subscription
|
||||
let subscription
|
||||
try {
|
||||
subscription = await rateLimiters.requestWithRetries(
|
||||
stripeClient.serviceName,
|
||||
() => stripeClient.stripe.subscriptions.retrieve(subscriptionId),
|
||||
{
|
||||
operation: 'subscriptions.retrieve',
|
||||
subscriptionId,
|
||||
region: stripeClient.serviceName,
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
throw new ReportError(
|
||||
'subscription-not-found',
|
||||
`Subscription not found: ${err.message}`
|
||||
)
|
||||
}
|
||||
|
||||
const validStatuses = ['canceled']
|
||||
if (!validStatuses.includes(subscription.status)) {
|
||||
throw new ReportError(
|
||||
'invalid-status',
|
||||
`Subscription status is ${subscription.status}, expected canceled`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
subscription.metadata?.recurly_to_stripe_migration_status !== 'in_progress'
|
||||
) {
|
||||
throw new ReportError(
|
||||
'invalid-metadata',
|
||||
`Migration status is ${subscription.metadata?.recurly_to_stripe_migration_status}, expected in_progress`
|
||||
)
|
||||
}
|
||||
|
||||
if (!commit) {
|
||||
return {
|
||||
status: 'validated',
|
||||
note: 'Subscription is valid for update',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await rateLimiters.requestWithRetries(
|
||||
stripeClient.serviceName,
|
||||
() =>
|
||||
stripeClient.updateSubscriptionMetadata(subscriptionId, {
|
||||
recurly_to_stripe_migration_status: 'cancelled',
|
||||
}),
|
||||
{
|
||||
operation: 'updateSubscriptionMetadata',
|
||||
subscriptionId,
|
||||
region: stripeClient.serviceName,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
status: 'updated',
|
||||
note: `Updated metadata for subscription ${subscriptionId}`,
|
||||
}
|
||||
} catch (err) {
|
||||
throw new ReportError(
|
||||
'update-failed',
|
||||
`Failed to update metadata: ${err.message}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await scriptRunner(main)
|
||||
process.exit(0)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* This script registers analytics account mapping for subscriptions migrated to Stripe.
|
||||
*
|
||||
* // TODO: delete this when the migration is complete
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/stripe/register-analytics-mapping.mjs [OPTS] [INPUT-FILE]
|
||||
*
|
||||
* Options:
|
||||
* --output PATH Output file path (default: /tmp/register_output_<timestamp>.csv)
|
||||
* --commit Apply changes (without this, runs in dry-run mode)
|
||||
* --help Show help message
|
||||
*
|
||||
* CSV Input Format:
|
||||
* recurly_account_code,target_stripe_account,stripe_customer_id
|
||||
* 507f1f77bcf86cd799439011,stripe-uk,cus_1234567890abcdef
|
||||
*
|
||||
* CSV Output Format:
|
||||
* recurly_account_code,target_stripe_account,stripe_customer_id,status,note
|
||||
*/
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import * as csv from 'csv'
|
||||
import minimist from 'minimist'
|
||||
import { z } from '../../app/src/infrastructure/Validation.mjs'
|
||||
import { scriptRunner } from '../lib/ScriptRunner.mjs'
|
||||
import { Subscription } from '../../app/src/models/Subscription.mjs'
|
||||
import AnalyticsManager from '../../app/src/Features/Analytics/AnalyticsManager.mjs'
|
||||
import AccountMappingHelper from '../../app/src/Features/Analytics/AccountMappingHelper.mjs'
|
||||
import { ReportError } from './helpers.mjs'
|
||||
|
||||
function usage() {
|
||||
console.error(`Usage: node scripts/stripe/register-analytics-mapping.mjs [OPTS] [INPUT-FILE]
|
||||
|
||||
Options:
|
||||
--output PATH Output file path (default: /tmp/register_output_<timestamp>.csv)
|
||||
--commit Apply changes (without this, runs in dry-run mode)
|
||||
--help Show this help message
|
||||
`)
|
||||
}
|
||||
|
||||
async function main(trackProgress) {
|
||||
const opts = parseArgs()
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||
const outputFile = opts.output ?? `/tmp/register_output_${timestamp}.csv`
|
||||
|
||||
await trackProgress('Starting analytics mapping registration')
|
||||
await trackProgress(`Run mode: ${opts.commit ? 'COMMIT' : 'DRY RUN'}`)
|
||||
|
||||
const inputStream = opts.inputFile
|
||||
? fs.createReadStream(opts.inputFile)
|
||||
: process.stdin
|
||||
const csvReader = getCsvReader(inputStream)
|
||||
const csvWriter = getCsvWriter(outputFile)
|
||||
|
||||
await trackProgress(`Output: ${outputFile}`)
|
||||
|
||||
let processedCount = 0
|
||||
let successCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
for await (const input of csvReader) {
|
||||
processedCount++
|
||||
|
||||
try {
|
||||
const result = await processRow(input, opts.commit)
|
||||
|
||||
csvWriter.write({
|
||||
recurly_account_code: input.recurly_account_code,
|
||||
target_stripe_account: input.target_stripe_account,
|
||||
stripe_customer_id: input.stripe_customer_id,
|
||||
status: result.status,
|
||||
note: result.note,
|
||||
})
|
||||
|
||||
if (result.status === 'registered' || result.status === 'dry-run') {
|
||||
successCount++
|
||||
} else {
|
||||
errorCount++
|
||||
}
|
||||
|
||||
if (processedCount % 25 === 0) {
|
||||
await trackProgress(
|
||||
`Progress: ${processedCount} processed, ${successCount} successful, ${errorCount} errors`
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
errorCount++
|
||||
if (err instanceof ReportError) {
|
||||
csvWriter.write({
|
||||
recurly_account_code: input.recurly_account_code,
|
||||
target_stripe_account: input.target_stripe_account,
|
||||
stripe_customer_id: input.stripe_customer_id,
|
||||
status: err.status,
|
||||
note: err.message,
|
||||
})
|
||||
} else {
|
||||
csvWriter.write({
|
||||
recurly_account_code: input.recurly_account_code,
|
||||
target_stripe_account: input.target_stripe_account,
|
||||
stripe_customer_id: input.stripe_customer_id,
|
||||
status: 'error',
|
||||
note: err.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await trackProgress(`✅ Total processed: ${processedCount}`)
|
||||
if (opts.commit) {
|
||||
await trackProgress(`✅ Successfully registered: ${successCount}`)
|
||||
} else {
|
||||
await trackProgress(`✅ Successfully validated: ${successCount}`)
|
||||
await trackProgress('ℹ️ DRY RUN: No changes were applied')
|
||||
}
|
||||
await trackProgress(`❌ Errors: ${errorCount}`)
|
||||
await trackProgress('🎉 Script completed!')
|
||||
|
||||
csvWriter.end()
|
||||
}
|
||||
|
||||
function getCsvReader(inputStream) {
|
||||
const parser = csv.parse({ columns: true })
|
||||
inputStream.pipe(parser)
|
||||
return parser
|
||||
}
|
||||
|
||||
function getCsvWriter(outputFile) {
|
||||
fs.mkdirSync(path.dirname(outputFile), { recursive: true })
|
||||
const outputStream = fs.createWriteStream(outputFile)
|
||||
|
||||
const writer = csv.stringify({
|
||||
columns: [
|
||||
'recurly_account_code',
|
||||
'target_stripe_account',
|
||||
'stripe_customer_id',
|
||||
'status',
|
||||
'note',
|
||||
],
|
||||
header: true,
|
||||
})
|
||||
|
||||
writer.on('error', err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
writer.pipe(outputStream)
|
||||
return writer
|
||||
}
|
||||
|
||||
async function processRow(input, commit) {
|
||||
const {
|
||||
recurly_account_code: accountCode,
|
||||
target_stripe_account: targetStripeAccount,
|
||||
} = input
|
||||
|
||||
// 1. Fetch Mongo subscription
|
||||
const mongoSubscription = await Subscription.findOne({
|
||||
admin_id: accountCode,
|
||||
}).exec()
|
||||
if (!mongoSubscription) {
|
||||
throw new ReportError(
|
||||
'no-mongo-subscription',
|
||||
'No subscription found in Mongo'
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Check if migrated to Stripe
|
||||
if (!mongoSubscription.paymentProvider?.service?.includes('stripe')) {
|
||||
throw new ReportError('not-stripe', 'Subscription not using Stripe')
|
||||
}
|
||||
|
||||
const subscriptionId = mongoSubscription.paymentProvider.subscriptionId
|
||||
if (!subscriptionId) {
|
||||
throw new ReportError(
|
||||
'no-subscription-id',
|
||||
'No Stripe subscription ID in Mongo'
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Register analytics mapping
|
||||
if (commit) {
|
||||
AnalyticsManager.registerAccountMapping(
|
||||
AccountMappingHelper.generateSubscriptionToStripeMapping(
|
||||
mongoSubscription._id,
|
||||
subscriptionId,
|
||||
targetStripeAccount
|
||||
)
|
||||
)
|
||||
return {
|
||||
status: 'registered',
|
||||
note: 'Analytics mapping registered',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: 'dry-run',
|
||||
note: 'DRY RUN: Would register analytics mapping',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const args = minimist(process.argv.slice(2), {
|
||||
string: ['output'],
|
||||
boolean: ['commit', 'help'],
|
||||
default: { commit: false },
|
||||
})
|
||||
|
||||
if (args.help) {
|
||||
usage()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const inputFile = args._[0]
|
||||
const paramsSchema = z.object({
|
||||
output: z.string().optional(),
|
||||
commit: z.boolean(),
|
||||
inputFile: z.string().optional(),
|
||||
})
|
||||
|
||||
try {
|
||||
return paramsSchema.parse({
|
||||
output: args.output,
|
||||
commit: args.commit,
|
||||
inputFile,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Invalid arguments:', err.message)
|
||||
usage()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await scriptRunner(main)
|
||||
process.exit(0)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
#!/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.mjs'
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user