add_user_id_to_stripe_customer.mjs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. #!/usr/bin/env node
  2. /**
  3. * This script iterates through all Stripe subscriptions, checks if they have metadata adminUserId,
  4. * and sets it to customer metadata "userId" if present.
  5. *
  6. * Usage:
  7. * node scripts/stripe/add_user_id_to_stripe_customer.mjs --region=us [options]
  8. * node scripts/stripe/add_user_id_to_stripe_customer.mjs --region=uk [options]
  9. *
  10. * Options:
  11. * --region=us|uk Required. Stripe region to process (us or uk)
  12. * --commit Actually perform the updates (default: dry-run mode)
  13. * --verbose Enable verbose logging
  14. * --limit=N Limit processing to N subscriptions (for testing)
  15. *
  16. * Examples:
  17. * # Dry run for US region with verbose output
  18. * node scripts/stripe/add_user_id_to_stripe_customer.mjs --region=us --verbose
  19. *
  20. * # Commit changes for UK region
  21. * node scripts/stripe/add_user_id_to_stripe_customer.mjs --region=uk --commit
  22. *
  23. * # Test with limited subscriptions
  24. * node scripts/stripe/add_user_id_to_stripe_customer.mjs --region=us --limit=10 --verbose
  25. */
  26. import minimist from 'minimist'
  27. import { z } from '../../app/src/infrastructure/Validation.mjs'
  28. import { scriptRunner } from '../lib/ScriptRunner.mjs'
  29. import {
  30. getRegionClient,
  31. CUSTOMER_SEGMENT_MAPPING,
  32. } from '../../modules/subscriptions/app/src/StripeClient.mjs'
  33. const paramsSchema = z.object({
  34. region: z.enum(['us', 'uk']),
  35. commit: z.boolean().default(false),
  36. verbose: z.boolean().default(false),
  37. limit: z.number().int().min(1).optional(),
  38. })
  39. let processedCount = 0
  40. let updatedCount = 0
  41. let errorCount = 0
  42. /**
  43. * Sleep function to respect Stripe rate limits (100 requests per second)
  44. * We'll be conservative and sleep for 50ms between requests to stay well under the limit
  45. */
  46. async function rateLimitSleep() {
  47. return new Promise(resolve => setTimeout(resolve, 50))
  48. }
  49. /**
  50. * Process a single subscription and update customer metadata if needed
  51. */
  52. async function processSubscription(
  53. subscription,
  54. stripeClient,
  55. commit,
  56. verbose
  57. ) {
  58. try {
  59. processedCount++
  60. // Check if subscription has adminUserId metadata
  61. const adminUserId = subscription.metadata?.adminUserId
  62. if (verbose) {
  63. console.info(
  64. `Processing subscription ${subscription.id} (customer: ${subscription.customer.id}) - adminUserId: ${adminUserId || 'none'}`
  65. )
  66. }
  67. if (!adminUserId) {
  68. // No adminUserId to migrate
  69. return
  70. }
  71. // Get customer details to check current metadata
  72. const customer = subscription.customer
  73. if (customer.deleted) {
  74. if (verbose) {
  75. console.info(`Customer ${customer.id} is deleted, skipping`)
  76. }
  77. return
  78. }
  79. if (customer.metadata?.userId === adminUserId) {
  80. if (verbose) {
  81. console.info(
  82. `Customer ${customer.id} already has userId=${adminUserId}, skipping`
  83. )
  84. }
  85. return
  86. }
  87. if (customer.metadata?.userId && customer.metadata.userId !== adminUserId) {
  88. console.warn(
  89. `Customer ${customer.id} has existing userId=${customer.metadata.userId} which differs from adminUserId=${adminUserId}, skipping to avoid overwrite`
  90. )
  91. return
  92. }
  93. if (commit) {
  94. // Update customer metadata using the StripeClient method
  95. await stripeClient.updateCustomerMetadata(customer.id, {
  96. ...customer.metadata,
  97. userId: adminUserId,
  98. segment: CUSTOMER_SEGMENT_MAPPING.B2C,
  99. })
  100. console.info(
  101. `Updated customer ${customer.id} metadata: userId=${adminUserId}`
  102. )
  103. } else {
  104. console.info(
  105. `DRY RUN: Would update customer ${customer.id} metadata: userId=${adminUserId}`
  106. )
  107. }
  108. updatedCount++
  109. } catch (error) {
  110. errorCount++
  111. console.log(error)
  112. }
  113. // Respect rate limits
  114. await rateLimitSleep()
  115. }
  116. /**
  117. * Main script function
  118. */
  119. async function main(trackProgress) {
  120. const parseResult = paramsSchema.safeParse(
  121. minimist(process.argv.slice(2), {
  122. boolean: ['commit', 'verbose'],
  123. string: ['region'],
  124. number: ['limit'],
  125. })
  126. )
  127. if (!parseResult.success) {
  128. throw new Error(`Invalid parameters: ${parseResult.error.message}`)
  129. }
  130. const { region, commit, verbose, limit } = parseResult.data
  131. const mode = commit ? 'COMMIT MODE' : 'DRY RUN MODE'
  132. await trackProgress(
  133. `Starting script in ${mode} for Stripe ${region.toUpperCase()} region`
  134. )
  135. if (limit) {
  136. await trackProgress(`Processing limited to ${limit} subscriptions`)
  137. }
  138. // Get Stripe client for the specified region
  139. const stripeClient = getRegionClient(region)
  140. // Reset counters
  141. processedCount = 0
  142. updatedCount = 0
  143. errorCount = 0
  144. await trackProgress('Starting to iterate through Stripe subscriptions...')
  145. const listParams = {
  146. limit: 100, // Stripe's maximum limit per request
  147. expand: ['data.customer'], // Expand customer data to reduce additional API calls
  148. status: 'all', // Include subscriptions in all statuses (active, past_due, unpaid, canceled, etc.)
  149. }
  150. let hasMore = true
  151. let startingAfter = null
  152. let totalProcessed = 0
  153. while (hasMore) {
  154. const params = { ...listParams }
  155. if (startingAfter) {
  156. params.starting_after = startingAfter
  157. }
  158. // Get batch of subscriptions
  159. const subscriptions = await stripeClient.stripe.subscriptions.list(params)
  160. await trackProgress(
  161. `Retrieved ${subscriptions.data.length} subscriptions (total processed so far: ${totalProcessed})`
  162. )
  163. // Process each subscription in the batch
  164. for (const subscription of subscriptions.data) {
  165. await processSubscription(subscription, stripeClient, commit, verbose)
  166. totalProcessed++
  167. // Check if we've hit the limit
  168. if (limit && totalProcessed >= limit) {
  169. await trackProgress(`Reached limit of ${limit} subscriptions, stopping`)
  170. hasMore = false
  171. break
  172. }
  173. // Progress update every 50 subscriptions
  174. if (totalProcessed % 50 === 0) {
  175. await trackProgress(
  176. `Progress: ${totalProcessed} processed, ${updatedCount} customers updated, ${errorCount} errors`
  177. )
  178. }
  179. }
  180. // Check if there are more subscriptions to process
  181. hasMore = hasMore && subscriptions.has_more
  182. if (hasMore && subscriptions.data.length > 0) {
  183. startingAfter = subscriptions.data[subscriptions.data.length - 1].id
  184. }
  185. // Rate limit between batch requests
  186. await rateLimitSleep()
  187. }
  188. // Final summary
  189. await trackProgress('FINAL SUMMARY:')
  190. await trackProgress(` Total subscriptions processed: ${processedCount}`)
  191. await trackProgress(
  192. ` Customers ${commit ? 'updated' : 'would be updated'}: ${updatedCount}`
  193. )
  194. await trackProgress(` Errors encountered: ${errorCount}`)
  195. if (!commit && updatedCount > 0) {
  196. await trackProgress('')
  197. await trackProgress(
  198. 'To actually perform the updates, run the script with --commit flag'
  199. )
  200. }
  201. if (errorCount > 0) {
  202. await trackProgress(
  203. 'Some errors were encountered. Check the logs above for details.'
  204. )
  205. }
  206. await trackProgress(`Script completed successfully in ${mode}`)
  207. }
  208. // Execute the script using the runner
  209. try {
  210. await scriptRunner(main)
  211. process.exit(0)
  212. } catch (error) {
  213. console.error('Script failed:', error.message)
  214. process.exit(1)
  215. }