plans.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. // Creates data for localizedPlanPricing object in settings.overrides.saas.js
  2. // and plans object in main/plans.js
  3. const csv = require('csv/sync')
  4. const fs = require('fs')
  5. const path = require('path')
  6. const minimist = require('minimist')
  7. function readCSVFile(fileName) {
  8. // Pick the csv file
  9. const filePath = path.resolve(__dirname, fileName)
  10. const input = fs.readFileSync(filePath, 'utf8')
  11. const rawRecords = csv.parse(input, { columns: true })
  12. return rawRecords
  13. }
  14. function readJSONFile(fileName) {
  15. const filePath = path.resolve(__dirname, fileName)
  16. const file = fs.readFileSync(filePath)
  17. const plans = JSON.parse(file)
  18. // convert the plans JSON from recurly to an array of
  19. // objects matching the spreadsheet format
  20. const result = []
  21. for (const plan of plans) {
  22. const newRow = { plan_code: plan.code }
  23. for (const price of plan.currencies) {
  24. newRow[price.currency] = price.unitAmount
  25. }
  26. result.push(newRow)
  27. }
  28. return result
  29. }
  30. // Mapping of [output_keys]:[actual_keys]
  31. const plansMap = {
  32. student: 'student',
  33. personal: 'paid-personal',
  34. collaborator: 'collaborator',
  35. professional: 'professional',
  36. }
  37. const currencies = {
  38. USD: {
  39. symbol: '$',
  40. placement: 'before',
  41. },
  42. EUR: {
  43. symbol: '€',
  44. placement: 'before',
  45. },
  46. GBP: {
  47. symbol: '£',
  48. placement: 'before',
  49. },
  50. SEK: {
  51. symbol: ' kr',
  52. placement: 'after',
  53. },
  54. CAD: {
  55. symbol: '$',
  56. placement: 'before',
  57. },
  58. NOK: {
  59. symbol: ' kr',
  60. placement: 'after',
  61. },
  62. DKK: {
  63. symbol: ' kr',
  64. placement: 'after',
  65. },
  66. AUD: {
  67. symbol: '$',
  68. placement: 'before',
  69. },
  70. NZD: {
  71. symbol: '$',
  72. placement: 'before',
  73. },
  74. CHF: {
  75. symbol: 'Fr ',
  76. placement: 'before',
  77. },
  78. SGD: {
  79. symbol: '$',
  80. placement: 'before',
  81. },
  82. INR: {
  83. symbol: '₹',
  84. placement: 'before',
  85. },
  86. BRL: {
  87. code: 'BRL',
  88. locale: 'pt-BR',
  89. symbol: 'R$ ',
  90. placement: 'before',
  91. },
  92. MXN: {
  93. code: 'MXN',
  94. locale: 'es-MX',
  95. symbol: '$ ',
  96. placement: 'before',
  97. },
  98. COP: {
  99. code: 'COP',
  100. locale: 'es-CO',
  101. symbol: '$ ',
  102. placement: 'before',
  103. },
  104. CLP: {
  105. code: 'CLP',
  106. locale: 'es-CL',
  107. symbol: '$ ',
  108. placement: 'before',
  109. },
  110. PEN: {
  111. code: 'PEN',
  112. locale: 'es-PE',
  113. symbol: 'S/ ',
  114. placement: 'before',
  115. },
  116. }
  117. const buildCurrencyValue = (amount, currency) => {
  118. // Test using toLocaleString to format currencies for new LATAM regions
  119. if (currency.locale && currency.code) {
  120. return amount.toLocaleString(currency.locale, {
  121. style: 'currency',
  122. currency: currency.code,
  123. minimumFractionDigits: 0,
  124. })
  125. }
  126. return currency.placement === 'before'
  127. ? `${currency.symbol}${amount}`
  128. : `${amount}${currency.symbol}`
  129. }
  130. function generatePlans(workSheetJSON) {
  131. // localizedPlanPricing object for settings.overrides.saas.js
  132. const localizedPlanPricing = {}
  133. // plans object for main/plans.js
  134. const plans = {}
  135. for (const [currency, currencyDetails] of Object.entries(currencies)) {
  136. localizedPlanPricing[currency] = {
  137. symbol: currencyDetails.symbol.trim(),
  138. free: {
  139. monthly: buildCurrencyValue(0, currencyDetails),
  140. annual: buildCurrencyValue(0, currencyDetails),
  141. },
  142. }
  143. plans[currency] = {
  144. symbol: currencyDetails.symbol.trim(),
  145. }
  146. for (const [outputKey, actualKey] of Object.entries(plansMap)) {
  147. const monthlyPlan = workSheetJSON.find(
  148. data => data.plan_code === actualKey
  149. )
  150. if (!monthlyPlan) throw new Error(`Missing plan: ${actualKey}`)
  151. if (!(currency in monthlyPlan))
  152. throw new Error(
  153. `Missing currency "${currency}" for plan "${actualKey}"`
  154. )
  155. const actualKeyAnnual = `${actualKey}-annual`
  156. const annualPlan = workSheetJSON.find(
  157. data => data.plan_code === actualKeyAnnual
  158. )
  159. if (!annualPlan) throw new Error(`Missing plan: ${actualKeyAnnual}`)
  160. if (!(currency in annualPlan))
  161. throw new Error(
  162. `Missing currency "${currency}" for plan "${actualKeyAnnual}"`
  163. )
  164. const monthly = buildCurrencyValue(monthlyPlan[currency], currencyDetails)
  165. const monthlyTimesTwelve = buildCurrencyValue(
  166. monthlyPlan[currency] * 12,
  167. currencyDetails
  168. )
  169. const annual = buildCurrencyValue(annualPlan[currency], currencyDetails)
  170. localizedPlanPricing[currency] = {
  171. ...localizedPlanPricing[currency],
  172. [outputKey]: { monthly, monthlyTimesTwelve, annual },
  173. }
  174. plans[currency] = {
  175. ...plans[currency],
  176. [outputKey]: { monthly, annual },
  177. }
  178. }
  179. }
  180. return { localizedPlanPricing, plans }
  181. }
  182. function generateGroupPlans(workSheetJSON) {
  183. const groupPlans = workSheetJSON.filter(data =>
  184. data.plan_code.startsWith('group')
  185. )
  186. const currencies = [
  187. 'AUD',
  188. 'BRL',
  189. 'CAD',
  190. 'CHF',
  191. 'CLP',
  192. 'COP',
  193. 'DKK',
  194. 'EUR',
  195. 'GBP',
  196. 'INR',
  197. 'MXN',
  198. 'NOK',
  199. 'NZD',
  200. 'SEK',
  201. 'SGD',
  202. 'USD',
  203. 'PEN',
  204. ]
  205. const sizes = ['2', '3', '4', '5', '10', '20', '50']
  206. const result = {}
  207. for (const type1 of ['educational', 'enterprise']) {
  208. result[type1] = {}
  209. for (const type2 of ['professional', 'collaborator']) {
  210. result[type1][type2] = {}
  211. for (const currency of currencies) {
  212. result[type1][type2][currency] = {}
  213. for (const size of sizes) {
  214. const planCode = `group_${type2}_${size}_${type1}`
  215. const plan = groupPlans.find(data => data.plan_code === planCode)
  216. if (!plan) throw new Error(`Missing plan: ${planCode}`)
  217. result[type1][type2][currency][size] = {
  218. price_in_cents: plan[currency] * 100,
  219. }
  220. }
  221. }
  222. }
  223. }
  224. return result
  225. }
  226. const argv = minimist(process.argv.slice(2), {
  227. string: ['output', 'file'],
  228. alias: { o: 'output', f: 'file' },
  229. })
  230. let input
  231. if (argv.file) {
  232. const ext = path.extname(argv.file)
  233. switch (ext) {
  234. case '.csv':
  235. input = readCSVFile(argv.file)
  236. break
  237. case '.json':
  238. input = readJSONFile(argv.file)
  239. break
  240. default:
  241. console.log('Invalid file type: must be csv or json')
  242. }
  243. } else {
  244. console.log('usage: node plans.js -f <file.csv|file.json> -o <dir>')
  245. process.exit(1)
  246. }
  247. // removes quotes from object keys
  248. const formatJS = obj =>
  249. JSON.stringify(obj, null, 2).replace(/"([^"]+)":/g, '$1:')
  250. const formatJSON = obj => JSON.stringify(obj, null, 2)
  251. function writeFile(outputFile, data) {
  252. console.log(`Writing ${outputFile}`)
  253. fs.writeFileSync(outputFile, data)
  254. }
  255. const { localizedPlanPricing, plans } = generatePlans(input)
  256. const groupPlans = generateGroupPlans(input)
  257. if (argv.output) {
  258. const dir = argv.output
  259. // check if output directory exists
  260. if (!fs.existsSync(dir)) {
  261. console.log(`Creating output directory ${dir}`)
  262. fs.mkdirSync(dir)
  263. }
  264. // check if output directory is a directory and report error if not
  265. if (!fs.lstatSync(dir).isDirectory()) {
  266. console.error(`Error: output dir ${dir} is not a directory`)
  267. process.exit(1)
  268. }
  269. writeFile(`${dir}/localizedPlanPricing.json`, formatJS(localizedPlanPricing))
  270. writeFile(`${dir}/plans.json`, formatJS(plans))
  271. writeFile(`${dir}/groups.json`, formatJSON(groupPlans))
  272. } else {
  273. console.log('PLANS', plans)
  274. console.log('LOCALIZED', localizedPlanPricing)
  275. console.log('GROUP PLANS', JSON.stringify(groupPlans, null, 2))
  276. }
  277. console.log('Completed!')