plans.mjs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. // Creates data for localizedPlanPricing object in settings.overrides.saas.js
  2. // and group plans object in app/templates/plans/groups.json
  3. // https://github.com/import-js/eslint-plugin-import/issues/1810
  4. // eslint-disable-next-line import/no-unresolved
  5. import * as csv from 'csv/sync'
  6. import fs from 'node:fs'
  7. import path from 'node:path'
  8. import minimist from 'minimist'
  9. import { fileURLToPath } from 'node:url'
  10. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  11. function readCSVFile(fileName) {
  12. // Pick the csv file
  13. const filePath = path.resolve(__dirname, fileName)
  14. const input = fs.readFileSync(filePath, 'utf8')
  15. const rawRecords = csv.parse(input, { columns: true })
  16. return rawRecords
  17. }
  18. function readJSONFile(fileName) {
  19. const filePath = path.resolve(__dirname, fileName)
  20. const file = fs.readFileSync(filePath)
  21. const plans = JSON.parse(file)
  22. // convert the plans JSON from recurly to an array of
  23. // objects matching the spreadsheet format
  24. const result = []
  25. for (const plan of plans) {
  26. const newRow = { plan_code: plan.code }
  27. for (const price of plan.currencies) {
  28. newRow[price.currency] = price.unitAmount
  29. }
  30. result.push(newRow)
  31. }
  32. return result
  33. }
  34. // Mapping of [output_keys]:[actual_keys]
  35. const plansMap = {
  36. student: 'student',
  37. personal: 'paid-personal',
  38. collaborator: 'collaborator',
  39. professional: 'professional',
  40. }
  41. const currencies = [
  42. 'AUD',
  43. 'BRL',
  44. 'CAD',
  45. 'CHF',
  46. 'CLP',
  47. 'COP',
  48. 'DKK',
  49. 'EUR',
  50. 'GBP',
  51. 'INR',
  52. 'MXN',
  53. 'NOK',
  54. 'NZD',
  55. 'PEN',
  56. 'SEK',
  57. 'SGD',
  58. 'USD',
  59. ]
  60. /**
  61. * This is duplicated in:
  62. * - services/web/app/src/Features/Subscription/SubscriptionHelper.js
  63. * - services/web/modules/subscriptions/frontend/js/pages/plans-new-design/group-member-picker/group-plan-pricing.js
  64. */
  65. function roundUpToNearest5Cents(number) {
  66. return Math.ceil(number * 20) / 20
  67. }
  68. function generatePlans(workSheetJSON) {
  69. // localizedPlanPricing object for settings.overrides.saas.js
  70. const localizedPlanPricing = {}
  71. for (const currency of currencies) {
  72. localizedPlanPricing[currency] = {
  73. free: {
  74. monthly: 0,
  75. annual: 0,
  76. },
  77. }
  78. for (const [outputKey, actualKey] of Object.entries(plansMap)) {
  79. const monthlyPlan = workSheetJSON.find(
  80. data => data.plan_code === actualKey
  81. )
  82. if (!monthlyPlan) throw new Error(`Missing plan: ${actualKey}`)
  83. if (!(currency in monthlyPlan))
  84. throw new Error(
  85. `Missing currency "${currency}" for plan "${actualKey}"`
  86. )
  87. const actualKeyAnnual = `${actualKey}-annual`
  88. const annualPlan = workSheetJSON.find(
  89. data => data.plan_code === actualKeyAnnual
  90. )
  91. if (!annualPlan) throw new Error(`Missing plan: ${actualKeyAnnual}`)
  92. if (!(currency in annualPlan))
  93. throw new Error(
  94. `Missing currency "${currency}" for plan "${actualKeyAnnual}"`
  95. )
  96. const monthly = Number(monthlyPlan[currency])
  97. const monthlyTimesTwelve = Number(monthlyPlan[currency] * 12)
  98. const annual = Number(annualPlan[currency])
  99. const annualDividedByTwelve = Number(
  100. roundUpToNearest5Cents(annualPlan[currency] / 12)
  101. )
  102. localizedPlanPricing[currency] = {
  103. ...localizedPlanPricing[currency],
  104. [outputKey]: {
  105. monthly,
  106. monthlyTimesTwelve,
  107. annual,
  108. annualDividedByTwelve,
  109. },
  110. }
  111. }
  112. }
  113. return localizedPlanPricing
  114. }
  115. function generateGroupPlans(workSheetJSON) {
  116. // group plans object for app/templates/plans/groups.json
  117. const groupPlans = workSheetJSON.filter(data =>
  118. data.plan_code.startsWith('group')
  119. )
  120. const sizes = ['2', '3', '4', '5', '10', '20', '50']
  121. const additionalLicenseAddOnLegacyPricesFilePath = path.resolve(
  122. __dirname,
  123. 'additional-license-add-on-legacy-prices.json'
  124. )
  125. const additionalLicenseAddOnLegacyPricesFile = fs.readFileSync(
  126. additionalLicenseAddOnLegacyPricesFilePath
  127. )
  128. const additionalLicenseAddOnLegacyPrices = JSON.parse(
  129. additionalLicenseAddOnLegacyPricesFile
  130. )
  131. const result = {}
  132. for (const type1 of ['educational', 'enterprise']) {
  133. result[type1] = {}
  134. for (const type2 of ['professional', 'collaborator']) {
  135. result[type1][type2] = {}
  136. for (const currency of currencies) {
  137. result[type1][type2][currency] = {}
  138. for (const size of sizes) {
  139. const planCode = `group_${type2}_${size}_${type1}`
  140. const plan = groupPlans.find(data => data.plan_code === planCode)
  141. if (!plan) throw new Error(`Missing plan: ${planCode}`)
  142. result[type1][type2][currency][size] = {
  143. price_in_cents: plan[currency] * 100,
  144. }
  145. const additionalLicenseAddOnLegacyPrice =
  146. additionalLicenseAddOnLegacyPrices[type1][type2][size]?.[currency]
  147. if (additionalLicenseAddOnLegacyPrice) {
  148. Object.assign(result[type1][type2][currency][size], {
  149. additional_license_legacy_price_in_cents:
  150. additionalLicenseAddOnLegacyPrice * 100,
  151. })
  152. }
  153. }
  154. }
  155. }
  156. }
  157. return result
  158. }
  159. const argv = minimist(process.argv.slice(2), {
  160. string: ['output', 'file'],
  161. alias: { o: 'output', f: 'file' },
  162. })
  163. let input
  164. if (argv.file) {
  165. const ext = path.extname(argv.file)
  166. switch (ext) {
  167. case '.csv':
  168. input = readCSVFile(argv.file)
  169. break
  170. case '.json':
  171. input = readJSONFile(argv.file)
  172. break
  173. default:
  174. console.log('Invalid file type: must be csv or json')
  175. }
  176. } else {
  177. console.log('usage: node plans.mjs -f <file.csv|file.json> -o <dir>')
  178. process.exit(1)
  179. }
  180. // removes quotes from object keys
  181. const formatJS = obj =>
  182. JSON.stringify(obj, null, 2).replace(/"([^"]+)":/g, '$1:')
  183. const formatJSON = obj => JSON.stringify(obj, null, 2)
  184. function writeFile(outputFile, data) {
  185. console.log(`Writing ${outputFile}`)
  186. fs.writeFileSync(outputFile, data)
  187. }
  188. const localizedPlanPricing = generatePlans(input)
  189. const groupPlans = generateGroupPlans(input)
  190. if (argv.output) {
  191. const dir = argv.output
  192. // check if output directory exists
  193. if (!fs.existsSync(dir)) {
  194. console.log(`Creating output directory ${dir}`)
  195. fs.mkdirSync(dir)
  196. }
  197. // check if output directory is a directory and report error if not
  198. if (!fs.lstatSync(dir).isDirectory()) {
  199. console.error(`Error: output dir ${dir} is not a directory`)
  200. process.exit(1)
  201. }
  202. writeFile(`${dir}/localizedPlanPricing.json`, formatJS(localizedPlanPricing))
  203. writeFile(`${dir}/groups.json`, formatJSON(groupPlans))
  204. } else {
  205. console.log('LOCALIZED', localizedPlanPricing)
  206. console.log('GROUP PLANS', JSON.stringify(groupPlans, null, 2))
  207. }
  208. console.log('Completed!')