plans.js 7.7 KB

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