collect_paypal_past_due_invoice.mjs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import RecurlyWrapper from '../../app/src/Features/Subscription/RecurlyWrapper.js'
  2. import minimist from 'minimist'
  3. import logger from '@overleaf/logger'
  4. import { fileURLToPath } from 'node:url'
  5. import { scriptRunner } from '../lib/ScriptRunner.mjs'
  6. const waitMs =
  7. fileURLToPath(import.meta.url) === process.argv[1]
  8. ? timeout => new Promise(resolve => setTimeout(() => resolve(), timeout))
  9. : () => Promise.resolve()
  10. // NOTE: Errors are not propagated to the caller
  11. const handleAPIError = async (source, id, error) => {
  12. logger.warn(`Errors in ${source} with id=${id}`, error)
  13. if (typeof error === 'string' && error.match(/429$/)) {
  14. return waitMs(1000 * 60 * 5)
  15. }
  16. await waitMs(80)
  17. }
  18. /**
  19. * @returns {Promise<{
  20. * INVOICES_COLLECTED: string[],
  21. * INVOICES_COLLECTED_SUCCESS: string[],
  22. * USERS_COLLECTED: string[],
  23. * }>}
  24. */
  25. const main = async () => {
  26. const attemptInvoiceCollection = async invoice => {
  27. const isPaypal = await isAccountUsingPaypal(invoice)
  28. if (!isPaypal) {
  29. return
  30. }
  31. const accountId = invoice.account.url.match(/accounts\/(.*)/)[1]
  32. if (USERS_COLLECTED.indexOf(accountId) > -1) {
  33. logger.warn(`Skipping duplicate user ${accountId}`)
  34. return
  35. }
  36. INVOICES_COLLECTED.push(invoice.invoice_number)
  37. USERS_COLLECTED.push(accountId)
  38. if (DRY_RUN) {
  39. return
  40. }
  41. try {
  42. await RecurlyWrapper.promises.attemptInvoiceCollection(
  43. invoice.invoice_number
  44. )
  45. INVOICES_COLLECTED_SUCCESS.push(invoice.invoice_number)
  46. await waitMs(80)
  47. } catch (error) {
  48. return handleAPIError(
  49. 'attemptInvoiceCollection',
  50. invoice.invoice_number,
  51. error
  52. )
  53. }
  54. }
  55. const isAccountUsingPaypal = async invoice => {
  56. const accountId = invoice.account.url.match(/accounts\/(.*)/)[1]
  57. try {
  58. const response = await RecurlyWrapper.promises.getBillingInfo(accountId)
  59. await waitMs(80)
  60. return !!response.billing_info.paypal_billing_agreement_id
  61. } catch (error) {
  62. return handleAPIError('billing info', accountId, error)
  63. }
  64. }
  65. const attemptInvoicesCollection = async () => {
  66. let getPage = await RecurlyWrapper.promises.getPaginatedEndpointIterator(
  67. 'invoices',
  68. { state: 'past_due' }
  69. )
  70. while (getPage) {
  71. const { items, getNextPage } = await getPage()
  72. logger.info('invoices', items?.length)
  73. for (const invoice of items) {
  74. await attemptInvoiceCollection(invoice)
  75. }
  76. getPage = getNextPage
  77. }
  78. }
  79. const argv = minimist(process.argv.slice(2))
  80. const DRY_RUN = argv.n !== undefined
  81. const INVOICES_COLLECTED = []
  82. const INVOICES_COLLECTED_SUCCESS = []
  83. const USERS_COLLECTED = []
  84. try {
  85. await attemptInvoicesCollection()
  86. const diff = INVOICES_COLLECTED.length - INVOICES_COLLECTED_SUCCESS.length
  87. if (diff !== 0) {
  88. logger.warn(`Invoices collection failed for ${diff} invoices`)
  89. }
  90. return {
  91. INVOICES_COLLECTED,
  92. INVOICES_COLLECTED_SUCCESS,
  93. USERS_COLLECTED,
  94. }
  95. } finally {
  96. logger.info(
  97. `DONE (DRY_RUN=${DRY_RUN}). ${INVOICES_COLLECTED.length} invoices collection attempts for ${USERS_COLLECTED.length} users. ${INVOICES_COLLECTED_SUCCESS.length} successful collections`
  98. )
  99. console.dir(
  100. {
  101. INVOICES_COLLECTED,
  102. INVOICES_COLLECTED_SUCCESS,
  103. USERS_COLLECTED,
  104. },
  105. { maxArrayLength: null }
  106. )
  107. }
  108. }
  109. if (fileURLToPath(import.meta.url) === process.argv[1]) {
  110. try {
  111. await scriptRunner(main)
  112. logger.info('Done.')
  113. process.exit(0)
  114. } catch (error) {
  115. logger.error({ error }, 'Error')
  116. process.exit(1)
  117. }
  118. }
  119. export default { main }