backfill_stripe_to_subscription_mapping.mjs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  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 { registerAccountMapping } 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 paramsSchema = Validation.z.object({
  24. endDate: Validation.z.iso
  25. .date()
  26. .transform(v => new Date(v).toISOString())
  27. .optional(),
  28. commit: Validation.z.boolean().default(false).optional(),
  29. verbose: Validation.z.boolean().default(false).optional(),
  30. })
  31. let mapped = 0
  32. let subscriptionCount = 0
  33. const now = new Date().toISOString() // use the same timestamp for all mappings
  34. const seenSubscriptions = new Set()
  35. function registerMapping(subscription) {
  36. if (seenSubscriptions.has(subscription._id)) {
  37. logger.warn({ subscription }, 'duplicate subscription found, skipping')
  38. return
  39. }
  40. seenSubscriptions.add(subscription._id)
  41. subscriptionCount++
  42. const mapping = AccountMappingHelper.generateSubscriptionToStripeMapping(
  43. subscription._id,
  44. subscription.paymentProvider.subscriptionId,
  45. subscription.paymentProvider.service,
  46. now
  47. )
  48. logger.debug(
  49. {
  50. stripe: subscription.paymentProvider.subscriptionId,
  51. stripeService: subscription.paymentProvider.service,
  52. mapping,
  53. },
  54. `processing subscription ${subscription._id}`
  55. )
  56. if (commit) {
  57. registerAccountMapping(mapping)
  58. mapped++
  59. }
  60. }
  61. async function main(trackProgress) {
  62. const additionalBatchedUpdateOptions = {}
  63. if (endDate) {
  64. additionalBatchedUpdateOptions.BATCH_RANGE_END = endDate
  65. }
  66. await batchedUpdate(
  67. db.subscriptions,
  68. {
  69. 'paymentProvider.service': { $in: ['stripe-us', 'stripe-uk'] },
  70. },
  71. subscriptions => subscriptions.forEach(registerMapping),
  72. {
  73. _id: 1,
  74. 'paymentProvider.subscriptionId': 1,
  75. 'paymentProvider.service': 1,
  76. },
  77. {
  78. readPreference: 'secondaryPreferred',
  79. },
  80. {
  81. verboseLogging: verbose,
  82. ...additionalBatchedUpdateOptions,
  83. trackProgress,
  84. }
  85. )
  86. logger.debug({}, `${subscriptionCount} subscriptions processed`)
  87. if (commit) {
  88. logger.debug({}, `${mapped} mappings registered`)
  89. }
  90. }
  91. const { error, data } = paramsSchema.safeParse(
  92. minimist(process.argv.slice(2), {
  93. boolean: ['commit', 'verbose'],
  94. string: ['endDate'],
  95. })
  96. )
  97. if (error) {
  98. logger.error({ error }, 'error with parameters')
  99. await gracefulShutdown({
  100. close(done) {
  101. logger.info({}, 'shutting down')
  102. done()
  103. },
  104. })
  105. process.exit(1)
  106. }
  107. const { commit, endDate, verbose } = data
  108. logger.logger.level(verbose ? 'debug' : 'info')
  109. logger.info({ verbose, commit, endDate }, commit ? 'COMMITTING' : 'DRY RUN')
  110. await scriptRunner(main)
  111. await gracefulShutdown({
  112. close(done) {
  113. logger.info({}, 'shutting down')
  114. done()
  115. },
  116. })
  117. process.exit()