setup_assistant_addon.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. // @ts-check
  2. const _ = require('lodash')
  3. const recurly = require('recurly')
  4. const minimist = require('minimist')
  5. const Settings = require('@overleaf/settings')
  6. const ADD_ON_CODE = 'assistant'
  7. const ADD_ON_NAME = 'AI Assist'
  8. const INDIVIDUAL_PLANS = [
  9. 'student',
  10. 'collaborator',
  11. 'professional',
  12. 'paid-personal',
  13. ]
  14. const INDIVIDUAL_VARIANTS = ['', '_free_trial_7_days']
  15. const GROUP_PLANS = ['collaborator', 'professional']
  16. const GROUP_SIZES = [2, 3, 4, 5, 10, 20, 50]
  17. const GROUP_SEGMENTS = ['educational', 'enterprise']
  18. const ARGS = parseArgs()
  19. const recurlyClient = new recurly.Client(Settings.apis.recurly.apiKey)
  20. function usage() {
  21. console.log(`Usage: setup_assistant_addon.js [--commit]
  22. This script will copy prices from the ${ADD_ON_CODE} and ${ADD_ON_CODE}-annual
  23. plans into the ${ADD_ON_CODE} add-on for every other plan
  24. Options:
  25. --commit Make actual changes to Recurly
  26. `)
  27. }
  28. function parseArgs() {
  29. const args = minimist(process.argv.slice(2), {
  30. boolean: ['commit', 'help'],
  31. })
  32. if (args.help) {
  33. usage()
  34. process.exit(0)
  35. }
  36. return { commit: args.commit }
  37. }
  38. async function main() {
  39. const monthlyPlan = await getPlan(ADD_ON_CODE)
  40. if (monthlyPlan == null) {
  41. console.error(`Monthly plan missing in Recurly: ${ADD_ON_CODE}`)
  42. process.exit(1)
  43. }
  44. console.log('\nMonthly prices:')
  45. for (const { currency, unitAmount } of monthlyPlan.currencies ?? []) {
  46. console.log(`- ${unitAmount} ${currency}`)
  47. }
  48. const annualPlan = await getPlan(`${ADD_ON_CODE}-annual`)
  49. if (annualPlan == null) {
  50. console.error(`Annual plan missing in Recurly: ${ADD_ON_CODE}-annual`)
  51. process.exit(1)
  52. }
  53. console.log('\nAnnual prices:')
  54. for (const { currency, unitAmount } of annualPlan.currencies ?? []) {
  55. console.log(`- ${unitAmount} ${currency}`)
  56. }
  57. console.log()
  58. for (const { code, annual } of getPlanSpecs()) {
  59. const prices = annual ? annualPlan.currencies : monthlyPlan.currencies
  60. await setupAddOn(code, prices ?? [])
  61. }
  62. if (ARGS.commit) {
  63. console.log('Done')
  64. } else {
  65. console.log('This was a dry run. Re-run with --commit to apply changes.')
  66. }
  67. }
  68. function* getPlanSpecs() {
  69. for (const plan of INDIVIDUAL_PLANS) {
  70. for (const variant of INDIVIDUAL_VARIANTS) {
  71. yield { code: `${plan}${variant}`, annual: false }
  72. yield { code: `${plan}-annual${variant}`, annual: true }
  73. }
  74. }
  75. for (const plan of GROUP_PLANS) {
  76. for (const size of GROUP_SIZES) {
  77. for (const segment of GROUP_SEGMENTS) {
  78. yield { code: `group_${plan}_${size}_${segment}`, annual: true }
  79. }
  80. }
  81. }
  82. }
  83. /**
  84. * Create or update the assistant add-on for a plan
  85. *
  86. * @param {string} planCode
  87. * @param {recurly.AddOnPricing[]} prices
  88. */
  89. async function setupAddOn(planCode, prices) {
  90. const currentAddOn = await getAddOn(planCode, ADD_ON_CODE)
  91. const newAddOnConfig = getAddOnConfig(prices)
  92. if (currentAddOn == null || currentAddOn.deletedAt != null) {
  93. await createAddOn(planCode, newAddOnConfig)
  94. } else if (_.isMatch(currentAddOn, newAddOnConfig)) {
  95. console.log(`No changes for plan ${planCode}`)
  96. } else {
  97. await updateAddOn(planCode, newAddOnConfig)
  98. }
  99. }
  100. /**
  101. * Get a plan configuration from Recurly
  102. *
  103. * @param {string} planCode
  104. */
  105. async function getPlan(planCode) {
  106. try {
  107. return await recurlyClient.getPlan(`code-${planCode}`)
  108. } catch (err) {
  109. if (err instanceof recurly.errors.NotFoundError) {
  110. return null
  111. } else {
  112. throw err
  113. }
  114. }
  115. }
  116. /**
  117. * Get an add-on configuration from Recurly
  118. *
  119. * @param {string} planCode
  120. * @param {string} addOnCode
  121. */
  122. async function getAddOn(planCode, addOnCode) {
  123. try {
  124. return await recurlyClient.getPlanAddOn(
  125. `code-${planCode}`,
  126. `code-${addOnCode}`
  127. )
  128. } catch (err) {
  129. if (err instanceof recurly.errors.NotFoundError) {
  130. return null
  131. } else {
  132. throw err
  133. }
  134. }
  135. }
  136. /**
  137. * Create the add-on described by the given config on the given plan
  138. *
  139. * @param {string} planCode
  140. * @param {recurly.AddOnCreate} config
  141. */
  142. async function createAddOn(planCode, config) {
  143. if (ARGS.commit) {
  144. console.log(`Creating ${ADD_ON_CODE} add-on for plan ${planCode}...`)
  145. await recurlyClient.createPlanAddOn(`code-${planCode}`, config)
  146. } else {
  147. console.log(`Would create ${ADD_ON_CODE} add-on for plan ${planCode}`)
  148. }
  149. }
  150. /**
  151. * Update the add-on described by the given config on the given plan
  152. *
  153. * @param {string} planCode
  154. * @param {recurly.AddOnUpdate} config
  155. */
  156. async function updateAddOn(planCode, config) {
  157. if (ARGS.commit) {
  158. console.log(`Updating ${ADD_ON_CODE} add-on for plan ${planCode}...`)
  159. await recurlyClient.updatePlanAddOn(
  160. `code-${planCode}`,
  161. `code-${ADD_ON_CODE}`,
  162. config
  163. )
  164. } else {
  165. console.log(`Would update ${ADD_ON_CODE} add-on for plan ${planCode}`)
  166. }
  167. }
  168. /**
  169. * Get an assistant add-on config
  170. *
  171. * @param {recurly.AddOnPricing[]} prices
  172. */
  173. function getAddOnConfig(prices) {
  174. return {
  175. code: ADD_ON_CODE,
  176. name: ADD_ON_NAME,
  177. optional: true,
  178. currencies: prices.map(price =>
  179. _.pick(
  180. price,
  181. 'currency',
  182. 'unitAmount',
  183. 'unitAmountDecimal',
  184. 'taxInclusive'
  185. )
  186. ),
  187. }
  188. }
  189. main()
  190. .then(() => {
  191. process.exit(0)
  192. })
  193. .catch(err => {
  194. console.error(err)
  195. process.exit(1)
  196. })