register-analytics-mapping.mjs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #!/usr/bin/env node
  2. /**
  3. * This script registers analytics account mapping for subscriptions migrated to Stripe.
  4. *
  5. * // TODO: delete this when the migration is complete
  6. *
  7. * Usage:
  8. * node scripts/stripe/register-analytics-mapping.mjs [OPTS] [INPUT-FILE]
  9. *
  10. * Options:
  11. * --output PATH Output file path (default: /tmp/register_output_<timestamp>.csv)
  12. * --commit Apply changes (without this, runs in dry-run mode)
  13. * --help Show help message
  14. *
  15. * CSV Input Format:
  16. * recurly_account_code,target_stripe_account,stripe_customer_id
  17. * 507f1f77bcf86cd799439011,stripe-uk,cus_1234567890abcdef
  18. *
  19. * CSV Output Format:
  20. * recurly_account_code,target_stripe_account,stripe_customer_id,status,note
  21. */
  22. import fs from 'node:fs'
  23. import path from 'node:path'
  24. import * as csv from 'csv'
  25. import minimist from 'minimist'
  26. import { z } from '../../app/src/infrastructure/Validation.mjs'
  27. import { scriptRunner } from '../lib/ScriptRunner.mjs'
  28. import { Subscription } from '../../app/src/models/Subscription.mjs'
  29. import AnalyticsManager from '../../app/src/Features/Analytics/AnalyticsManager.mjs'
  30. import AccountMappingHelper from '../../app/src/Features/Analytics/AccountMappingHelper.mjs'
  31. import { ReportError } from './helpers.mjs'
  32. function usage() {
  33. console.error(`Usage: node scripts/stripe/register-analytics-mapping.mjs [OPTS] [INPUT-FILE]
  34. Options:
  35. --output PATH Output file path (default: /tmp/register_output_<timestamp>.csv)
  36. --commit Apply changes (without this, runs in dry-run mode)
  37. --help Show this help message
  38. `)
  39. }
  40. async function main(trackProgress) {
  41. const opts = parseArgs()
  42. const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
  43. const outputFile = opts.output ?? `/tmp/register_output_${timestamp}.csv`
  44. await trackProgress('Starting analytics mapping registration')
  45. await trackProgress(`Run mode: ${opts.commit ? 'COMMIT' : 'DRY RUN'}`)
  46. const inputStream = opts.inputFile
  47. ? fs.createReadStream(opts.inputFile)
  48. : process.stdin
  49. const csvReader = getCsvReader(inputStream)
  50. const csvWriter = getCsvWriter(outputFile)
  51. await trackProgress(`Output: ${outputFile}`)
  52. let processedCount = 0
  53. let successCount = 0
  54. let errorCount = 0
  55. for await (const input of csvReader) {
  56. processedCount++
  57. try {
  58. const result = await processRow(input, opts.commit)
  59. csvWriter.write({
  60. recurly_account_code: input.recurly_account_code,
  61. target_stripe_account: input.target_stripe_account,
  62. stripe_customer_id: input.stripe_customer_id,
  63. status: result.status,
  64. note: result.note,
  65. })
  66. if (result.status === 'registered' || result.status === 'dry-run') {
  67. successCount++
  68. } else {
  69. errorCount++
  70. }
  71. if (processedCount % 25 === 0) {
  72. await trackProgress(
  73. `Progress: ${processedCount} processed, ${successCount} successful, ${errorCount} errors`
  74. )
  75. }
  76. } catch (err) {
  77. errorCount++
  78. if (err instanceof ReportError) {
  79. csvWriter.write({
  80. recurly_account_code: input.recurly_account_code,
  81. target_stripe_account: input.target_stripe_account,
  82. stripe_customer_id: input.stripe_customer_id,
  83. status: err.status,
  84. note: err.message,
  85. })
  86. } else {
  87. csvWriter.write({
  88. recurly_account_code: input.recurly_account_code,
  89. target_stripe_account: input.target_stripe_account,
  90. stripe_customer_id: input.stripe_customer_id,
  91. status: 'error',
  92. note: err.message,
  93. })
  94. }
  95. }
  96. }
  97. await trackProgress(`✅ Total processed: ${processedCount}`)
  98. if (opts.commit) {
  99. await trackProgress(`✅ Successfully registered: ${successCount}`)
  100. } else {
  101. await trackProgress(`✅ Successfully validated: ${successCount}`)
  102. await trackProgress('ℹ️ DRY RUN: No changes were applied')
  103. }
  104. await trackProgress(`❌ Errors: ${errorCount}`)
  105. await trackProgress('🎉 Script completed!')
  106. csvWriter.end()
  107. }
  108. function getCsvReader(inputStream) {
  109. const parser = csv.parse({ columns: true })
  110. inputStream.pipe(parser)
  111. return parser
  112. }
  113. function getCsvWriter(outputFile) {
  114. fs.mkdirSync(path.dirname(outputFile), { recursive: true })
  115. const outputStream = fs.createWriteStream(outputFile)
  116. const writer = csv.stringify({
  117. columns: [
  118. 'recurly_account_code',
  119. 'target_stripe_account',
  120. 'stripe_customer_id',
  121. 'status',
  122. 'note',
  123. ],
  124. header: true,
  125. })
  126. writer.on('error', err => {
  127. console.error(err)
  128. process.exit(1)
  129. })
  130. writer.pipe(outputStream)
  131. return writer
  132. }
  133. async function processRow(input, commit) {
  134. const {
  135. recurly_account_code: accountCode,
  136. target_stripe_account: targetStripeAccount,
  137. } = input
  138. // 1. Fetch Mongo subscription
  139. const mongoSubscription = await Subscription.findOne({
  140. admin_id: accountCode,
  141. }).exec()
  142. if (!mongoSubscription) {
  143. throw new ReportError(
  144. 'no-mongo-subscription',
  145. 'No subscription found in Mongo'
  146. )
  147. }
  148. // 2. Check if migrated to Stripe
  149. if (!mongoSubscription.paymentProvider?.service?.includes('stripe')) {
  150. throw new ReportError('not-stripe', 'Subscription not using Stripe')
  151. }
  152. const subscriptionId = mongoSubscription.paymentProvider.subscriptionId
  153. if (!subscriptionId) {
  154. throw new ReportError(
  155. 'no-subscription-id',
  156. 'No Stripe subscription ID in Mongo'
  157. )
  158. }
  159. // 3. Register analytics mapping
  160. if (commit) {
  161. AnalyticsManager.registerAccountMapping(
  162. AccountMappingHelper.generateSubscriptionToStripeMapping(
  163. mongoSubscription._id,
  164. subscriptionId,
  165. targetStripeAccount
  166. )
  167. )
  168. return {
  169. status: 'registered',
  170. note: 'Analytics mapping registered',
  171. }
  172. } else {
  173. return {
  174. status: 'dry-run',
  175. note: 'DRY RUN: Would register analytics mapping',
  176. }
  177. }
  178. }
  179. function parseArgs() {
  180. const args = minimist(process.argv.slice(2), {
  181. string: ['output'],
  182. boolean: ['commit', 'help'],
  183. default: { commit: false },
  184. })
  185. if (args.help) {
  186. usage()
  187. process.exit(0)
  188. }
  189. const inputFile = args._[0]
  190. const paramsSchema = z.object({
  191. output: z.string().optional(),
  192. commit: z.boolean(),
  193. inputFile: z.string().optional(),
  194. })
  195. try {
  196. return paramsSchema.parse({
  197. output: args.output,
  198. commit: args.commit,
  199. inputFile,
  200. })
  201. } catch (err) {
  202. console.error('Invalid arguments:', err.message)
  203. usage()
  204. process.exit(1)
  205. }
  206. }
  207. try {
  208. await scriptRunner(main)
  209. process.exit(0)
  210. } catch (error) {
  211. console.error(error)
  212. process.exit(1)
  213. }