setup_assistant_addon.mjs 5.3 KB

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