backfill_stripe_to_subscription_mapping.mjs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. /**
  2. * This script backfills the account mapping for subscriptions that are active and backed by Stripe.
  3. *
  4. * The mapping joins a Stripe subscription ID to a subscription _id in BigQuery.
  5. *
  6. * This script has an assumption that it is being run in a clean slate condition, it will create some
  7. * duplicate mappings if run multiple times. The Analytics team will have the expectation
  8. * that this table may need to be deduplicated as it is an event sourcing record.
  9. *
  10. * Call it with `--commit` to actually register the mappings.
  11. * Call it with `--verbose` to see debug logs.
  12. * Call it with `--endDate=<EndDate>` to stop processing at a certain date, for example `--endDate=2024-01-01`
  13. */
  14. import logger from '@overleaf/logger'
  15. import minimist from 'minimist'
  16. import { batchedUpdate } from '@overleaf/mongo-utils/batchedUpdate.js'
  17. import { db } from '../../app/src/infrastructure/mongodb.mjs'
  18. import AccountMappingHelper from '../../app/src/Features/Analytics/AccountMappingHelper.mjs'
  19. import AnalyticsManager from '../../app/src/Features/Analytics/AnalyticsManager.mjs'
  20. import { gracefulShutdown } from '../../app/src/infrastructure/GracefulShutdown.mjs'
  21. import Validation from '../../app/src/infrastructure/Validation.mjs'
  22. import { scriptRunner } from '../lib/ScriptRunner.mjs'
  23. const { registerAccountMapping } = AnalyticsManager
  24. const paramsSchema = Validation.z.object({
  25. endDate: Validation.z.iso
  26. .date()
  27. .transform(v => new Date(v).toISOString())
  28. .optional(),
  29. commit: Validation.z.boolean().default(false).optional(),
  30. verbose: Validation.z.boolean().default(false).optional(),
  31. })
  32. let mapped = 0
  33. let subscriptionCount = 0
  34. const now = new Date().toISOString() // use the same timestamp for all mappings
  35. const seenSubscriptions = new Set()
  36. function registerMapping(subscription) {
  37. if (seenSubscriptions.has(subscription._id)) {
  38. logger.warn({ subscription }, 'duplicate subscription found, skipping')
  39. return
  40. }
  41. seenSubscriptions.add(subscription._id)
  42. subscriptionCount++
  43. const mapping = AccountMappingHelper.generateSubscriptionToStripeMapping(
  44. subscription._id,
  45. subscription.paymentProvider.subscriptionId,
  46. subscription.paymentProvider.service,
  47. now
  48. )
  49. logger.debug(
  50. {
  51. stripe: subscription.paymentProvider.subscriptionId,
  52. stripeService: subscription.paymentProvider.service,
  53. mapping,
  54. },
  55. `processing subscription ${subscription._id}`
  56. )
  57. if (commit) {
  58. registerAccountMapping(mapping)
  59. mapped++
  60. }
  61. }
  62. async function main(trackProgress) {
  63. const additionalBatchedUpdateOptions = {}
  64. if (endDate) {
  65. additionalBatchedUpdateOptions.BATCH_RANGE_END = endDate
  66. }
  67. await batchedUpdate(
  68. db.subscriptions,
  69. {
  70. 'paymentProvider.service': { $in: ['stripe-us', 'stripe-uk'] },
  71. },
  72. subscriptions => subscriptions.forEach(registerMapping),
  73. {
  74. _id: 1,
  75. 'paymentProvider.subscriptionId': 1,
  76. 'paymentProvider.service': 1,
  77. },
  78. {
  79. readPreference: 'secondaryPreferred',
  80. },
  81. {
  82. verboseLogging: verbose,
  83. ...additionalBatchedUpdateOptions,
  84. trackProgress,
  85. }
  86. )
  87. logger.debug({}, `${subscriptionCount} subscriptions processed`)
  88. if (commit) {
  89. logger.debug({}, `${mapped} mappings registered`)
  90. }
  91. }
  92. const { error, data } = paramsSchema.safeParse(
  93. minimist(process.argv.slice(2), {
  94. boolean: ['commit', 'verbose'],
  95. string: ['endDate'],
  96. })
  97. )
  98. if (error) {
  99. logger.error({ error }, 'error with parameters')
  100. await gracefulShutdown({
  101. close(done) {
  102. logger.info({}, 'shutting down')
  103. done()
  104. },
  105. })
  106. process.exit(1)
  107. }
  108. const { commit, endDate, verbose } = data
  109. logger.logger.level(verbose ? 'debug' : 'info')
  110. logger.info({ verbose, commit, endDate }, commit ? 'COMMITTING' : 'DRY RUN')
  111. await scriptRunner(main)
  112. await gracefulShutdown({
  113. close(done) {
  114. logger.info({}, 'shutting down')
  115. done()
  116. },
  117. })
  118. process.exit()