Просмотр исходного кода

Merge pull request #12817 from overleaf/bg-plan-scripts

refactor plan-prices scripts and add new currency (INR)

GitOrigin-RevId: 12b515143e0426f19b2d224569bf9c5490b7d72a
Brian Gough 3 лет назад
Родитель
Сommit
80fdef796c

+ 9 - 5
services/web/scripts/plan-prices/README.md

@@ -4,12 +4,16 @@ Run `npm install` in order to install the `xlsx` dependency.
 
 
 The scripts will put the output results into the `output` folder.
 The scripts will put the output results into the `output` folder.
 
 
-### Create group plans
+### Create localized and group plan pricing
 
 
-_Command_ `node groups.js fileName sheetName` - generates group plans prices. To be used for `/services/web/app/templates/plans/groups.json`
+_Command_ `node plans.js -f fileName -s sheetName -o outputdir` - generates three json files:
 
 
-### Create localized plan pricing
-
-_Command_ `node plans.js fileName sheetName` - generates two json files:
 - `localizedPlanPricing.json` for `/services/web/config/settings.overrides.saas.js`
 - `localizedPlanPricing.json` for `/services/web/config/settings.overrides.saas.js`
 - `plans.json` for `/services/web/frontend/js/main/plans.js`
 - `plans.json` for `/services/web/frontend/js/main/plans.js`
+- `groups.json` for `/services/web/app/templates/plans/groups.json`
+
+The input file can be in `.xls`, `.csv` or `.json` format
+
+- `.xlsx` excel spreadsheet, requires the `-s sheetName` option
+- `.csv` csv format, same data as for excel spreadsheet
+- `.json` json format from the `recurly_prices.js --download` script output

+ 0 - 68
services/web/scripts/plan-prices/groups.js

@@ -1,68 +0,0 @@
-// Creates data for groups.json
-
-const xlsx = require('xlsx')
-const fs = require('fs')
-const path = require('path')
-const [fileName, sheetName] = process.argv.slice(2)
-
-// Pick the xlsx file
-const filePath = path.resolve(__dirname, fileName)
-const file = xlsx.readFile(filePath)
-
-if (!file.SheetNames.includes(sheetName)) {
-  throw new Error('Sheet not found!')
-}
-
-const workSheet = Object.values(file.Sheets)[file.SheetNames.indexOf(sheetName)]
-// Convert to JSON
-const workSheetJSON = xlsx.utils.sheet_to_json(workSheet)
-
-const groupPlans = workSheetJSON.filter(data =>
-  data.plan_code.startsWith('group')
-)
-
-const currencies = [
-  'AUD',
-  'CAD',
-  'CHF',
-  'DKK',
-  'EUR',
-  'GBP',
-  'NOK',
-  'NZD',
-  'SEK',
-  'SGD',
-  'USD',
-]
-const sizes = ['2', '3', '4', '5', '10', '20', '50']
-
-const result = {}
-for (const type1 of ['educational', 'enterprise']) {
-  result[type1] = {}
-  for (const type2 of ['professional', 'collaborator']) {
-    result[type1][type2] = {}
-    for (const currency of currencies) {
-      result[type1][type2][currency] = {}
-      for (const size of sizes) {
-        const planCode = `group_${type2}_${size}_${type1}`
-        const plan = groupPlans.find(data => data.plan_code === planCode)
-
-        if (!plan) throw new Error(`Missing plan: ${planCode}`)
-
-        result[type1][type2][currency][size] = {
-          price_in_cents: plan[currency] * 100,
-        }
-      }
-    }
-  }
-}
-
-const output = JSON.stringify(result, null, 2)
-const dir = './output'
-
-if (!fs.existsSync(dir)) {
-  fs.mkdirSync(dir)
-}
-fs.writeFileSync(`${dir}/groups.json`, output)
-
-console.log('Completed!')

+ 92 - 0
services/web/scripts/plan-prices/package-lock.json

@@ -9,6 +9,8 @@
       "version": "1.0.0",
       "version": "1.0.0",
       "license": "ISC",
       "license": "ISC",
       "devDependencies": {
       "devDependencies": {
+        "csv": "^6.2.10",
+        "minimist": "^1.2.8",
         "xlsx": "^0.18.5"
         "xlsx": "^0.18.5"
       }
       }
     },
     },
@@ -55,6 +57,39 @@
         "node": ">=0.8"
         "node": ">=0.8"
       }
       }
     },
     },
+    "node_modules/csv": {
+      "version": "6.2.10",
+      "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.10.tgz",
+      "integrity": "sha512-aO1dkeMlzWHvtKOdiTeqt7G4SwF/JtJ2fYNOMtlrGiKERD+ASq+QZelGqpFCzHGvZSIhzDtwqRVEgPMkme2BQg==",
+      "dev": true,
+      "dependencies": {
+        "csv-generate": "^4.2.4",
+        "csv-parse": "^5.3.8",
+        "csv-stringify": "^6.3.2",
+        "stream-transform": "^3.2.4"
+      },
+      "engines": {
+        "node": ">= 0.1.90"
+      }
+    },
+    "node_modules/csv-generate": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.4.tgz",
+      "integrity": "sha512-PvEwuRksnW30I1DlZnVuCVMOiff7ZoUXOCMQJ1c0DPKXQkIC87hWvqJ4ztO70ceQMQER1hp/Lajo8KIy7at1PA==",
+      "dev": true
+    },
+    "node_modules/csv-parse": {
+      "version": "5.3.8",
+      "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.8.tgz",
+      "integrity": "sha512-ird8lzMv9I64oqIVIHdaTbT7Yr55n2C/Nv6m1LxO7nddLEeI67468VQ9Ik+r6lwYbK9kTE1oSqAVcVKc/Uqx6g==",
+      "dev": true
+    },
+    "node_modules/csv-stringify": {
+      "version": "6.3.2",
+      "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.3.2.tgz",
+      "integrity": "sha512-dD9gfbxNKa5v90NHiE2Qd6F9I52GtJjGTfowwzqiNDZD/+NPW3h19d2Nvv311a8QUW11rYRobco27nvVAnCrLw==",
+      "dev": true
+    },
     "node_modules/frac": {
     "node_modules/frac": {
       "version": "1.1.2",
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
       "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
@@ -64,6 +99,15 @@
         "node": ">=0.8"
         "node": ">=0.8"
       }
       }
     },
     },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/ssf": {
     "node_modules/ssf": {
       "version": "0.11.2",
       "version": "0.11.2",
       "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
       "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
@@ -76,6 +120,12 @@
         "node": ">=0.8"
         "node": ">=0.8"
       }
       }
     },
     },
+    "node_modules/stream-transform": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.4.tgz",
+      "integrity": "sha512-YoZm/eoh6f/MH7uHkq+NK3fx3JkyXbck7FcTpJavwEUg0aMINqMPkDj5uNW0CoRy7c/2NSJm0HvoyFv6dVauPA==",
+      "dev": true
+    },
     "node_modules/wmf": {
     "node_modules/wmf": {
       "version": "1.0.2",
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
       "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
@@ -145,12 +195,48 @@
       "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
       "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
       "dev": true
       "dev": true
     },
     },
+    "csv": {
+      "version": "6.2.10",
+      "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.10.tgz",
+      "integrity": "sha512-aO1dkeMlzWHvtKOdiTeqt7G4SwF/JtJ2fYNOMtlrGiKERD+ASq+QZelGqpFCzHGvZSIhzDtwqRVEgPMkme2BQg==",
+      "dev": true,
+      "requires": {
+        "csv-generate": "^4.2.4",
+        "csv-parse": "^5.3.8",
+        "csv-stringify": "^6.3.2",
+        "stream-transform": "^3.2.4"
+      }
+    },
+    "csv-generate": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.4.tgz",
+      "integrity": "sha512-PvEwuRksnW30I1DlZnVuCVMOiff7ZoUXOCMQJ1c0DPKXQkIC87hWvqJ4ztO70ceQMQER1hp/Lajo8KIy7at1PA==",
+      "dev": true
+    },
+    "csv-parse": {
+      "version": "5.3.8",
+      "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.8.tgz",
+      "integrity": "sha512-ird8lzMv9I64oqIVIHdaTbT7Yr55n2C/Nv6m1LxO7nddLEeI67468VQ9Ik+r6lwYbK9kTE1oSqAVcVKc/Uqx6g==",
+      "dev": true
+    },
+    "csv-stringify": {
+      "version": "6.3.2",
+      "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.3.2.tgz",
+      "integrity": "sha512-dD9gfbxNKa5v90NHiE2Qd6F9I52GtJjGTfowwzqiNDZD/+NPW3h19d2Nvv311a8QUW11rYRobco27nvVAnCrLw==",
+      "dev": true
+    },
     "frac": {
     "frac": {
       "version": "1.1.2",
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
       "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
       "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
       "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
       "dev": true
       "dev": true
     },
     },
+    "minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true
+    },
     "ssf": {
     "ssf": {
       "version": "0.11.2",
       "version": "0.11.2",
       "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
       "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
@@ -160,6 +246,12 @@
         "frac": "~1.1.2"
         "frac": "~1.1.2"
       }
       }
     },
     },
+    "stream-transform": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.4.tgz",
+      "integrity": "sha512-YoZm/eoh6f/MH7uHkq+NK3fx3JkyXbck7FcTpJavwEUg0aMINqMPkDj5uNW0CoRy7c/2NSJm0HvoyFv6dVauPA==",
+      "dev": true
+    },
     "wmf": {
     "wmf": {
       "version": "1.0.2",
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
       "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",

+ 2 - 0
services/web/scripts/plan-prices/package.json

@@ -9,6 +9,8 @@
   "author": "",
   "author": "",
   "license": "ISC",
   "license": "ISC",
   "devDependencies": {
   "devDependencies": {
+    "csv": "^6.2.10",
+    "minimist": "^1.2.8",
     "xlsx": "^0.18.5"
     "xlsx": "^0.18.5"
   }
   }
 }
 }

+ 189 - 54
services/web/scripts/plan-prices/plans.js

@@ -2,21 +2,56 @@
 // and plans object in main/plans.js
 // and plans object in main/plans.js
 
 
 const xlsx = require('xlsx')
 const xlsx = require('xlsx')
+const csv = require('csv/sync')
 const fs = require('fs')
 const fs = require('fs')
 const path = require('path')
 const path = require('path')
-const [fileName, sheetName] = process.argv.slice(2)
+const minimist = require('minimist')
 
 
-// Pick the xlsx file
-const filePath = path.resolve(__dirname, fileName)
-const file = xlsx.readFile(filePath)
+function readXLSXFile(fileName, sheetName) {
+  // Pick the xlsx file
+  const filePath = path.resolve(__dirname, fileName)
+  const file = xlsx.readFile(filePath)
 
 
-if (!file.SheetNames.includes(sheetName)) {
-  throw new Error('Sheet not found!')
+  if (!file.SheetNames.includes(sheetName)) {
+    console.error(
+      `Error: sheet '${sheetName}' not found.\n` +
+        `Valid sheet names are: ${file.SheetNames.join(',')}`
+    )
+    process.exit(1)
+  }
+
+  const workSheet = Object.values(file.Sheets)[
+    file.SheetNames.indexOf(sheetName)
+  ]
+  // Convert to JSON
+  const workSheetJSON = xlsx.utils.sheet_to_json(workSheet)
+  return workSheetJSON
 }
 }
 
 
-const workSheet = Object.values(file.Sheets)[file.SheetNames.indexOf(sheetName)]
-// Convert to JSON
-const workSheetJSON = xlsx.utils.sheet_to_json(workSheet)
+function readCSVFile(fileName) {
+  // Pick the csv file
+  const filePath = path.resolve(__dirname, fileName)
+  const input = fs.readFileSync(filePath, 'utf8')
+  const rawRecords = csv.parse(input, { columns: true })
+  return rawRecords
+}
+
+function readJSONFile(fileName) {
+  const filePath = path.resolve(__dirname, fileName)
+  const file = fs.readFileSync(filePath)
+  const plans = JSON.parse(file)
+  // convert the plans JSON from recurly to an array of
+  // objects matching the spreadsheet format
+  const result = []
+  for (const plan of plans) {
+    const newRow = { plan_code: plan.code }
+    for (const price of plan.currencies) {
+      newRow[price.currency] = price.unitAmount
+    }
+    result.push(newRow)
+  }
+  return result
+}
 
 
 // Mapping of [output_keys]:[actual_keys]
 // Mapping of [output_keys]:[actual_keys]
 const plansMap = {
 const plansMap = {
@@ -71,6 +106,10 @@ const currencies = {
     symbol: '$',
     symbol: '$',
     placement: 'before',
     placement: 'before',
   },
   },
+  INR: {
+    symbol: '₹',
+    placement: 'before',
+  },
 }
 }
 
 
 const buildCurrencyValue = (amount, currency) => {
 const buildCurrencyValue = (amount, currency) => {
@@ -79,64 +118,160 @@ const buildCurrencyValue = (amount, currency) => {
     : `${amount}${currency.symbol}`
     : `${amount}${currency.symbol}`
 }
 }
 
 
-// localizedPlanPricing object for settings.overrides.saas.js
-let localizedPlanPricing = {}
-// plans object for main/plans.js
-let plans = {}
-
-for (const [currency, currencyDetails] of Object.entries(currencies)) {
-  localizedPlanPricing[currency] = {
-    symbol: currencyDetails.symbol.trim(),
-    free: {
-      monthly: buildCurrencyValue(0, currencyDetails),
-      annual: buildCurrencyValue(0, currencyDetails),
-    },
-  }
-  plans[currency] = {
-    symbol: currencyDetails.symbol.trim(),
-  }
+function generatePlans(workSheetJSON) {
+  // localizedPlanPricing object for settings.overrides.saas.js
+  const localizedPlanPricing = {}
+  // plans object for main/plans.js
+  const plans = {}
 
 
-  for (const [outputKey, actualKey] of Object.entries(plansMap)) {
-    const monthlyPlan = workSheetJSON.find(data => data.plan_code === actualKey)
+  for (const [currency, currencyDetails] of Object.entries(currencies)) {
+    localizedPlanPricing[currency] = {
+      symbol: currencyDetails.symbol.trim(),
+      free: {
+        monthly: buildCurrencyValue(0, currencyDetails),
+        annual: buildCurrencyValue(0, currencyDetails),
+      },
+    }
+    plans[currency] = {
+      symbol: currencyDetails.symbol.trim(),
+    }
 
 
-    if (!monthlyPlan) throw new Error(`Missing plan: ${actualKey}`)
+    for (const [outputKey, actualKey] of Object.entries(plansMap)) {
+      const monthlyPlan = workSheetJSON.find(
+        data => data.plan_code === actualKey
+      )
 
 
-    const actualKeyAnnual = `${actualKey}-annual`
-    const annualPlan = workSheetJSON.find(
-      data => data.plan_code === actualKeyAnnual
-    )
+      if (!monthlyPlan) throw new Error(`Missing plan: ${actualKey}`)
 
 
-    if (!annualPlan) throw new Error(`Missing plan: ${actualKeyAnnual}`)
+      const actualKeyAnnual = `${actualKey}-annual`
+      const annualPlan = workSheetJSON.find(
+        data => data.plan_code === actualKeyAnnual
+      )
 
 
-    const monthly = buildCurrencyValue(monthlyPlan[currency], currencyDetails)
-    const monthlyTimesTwelve = buildCurrencyValue(
-      monthlyPlan[currency] * 12,
-      currencyDetails
-    )
-    const annual = buildCurrencyValue(annualPlan[currency], currencyDetails)
+      if (!annualPlan) throw new Error(`Missing plan: ${actualKeyAnnual}`)
 
 
-    localizedPlanPricing[currency] = {
-      ...localizedPlanPricing[currency],
-      [outputKey]: { monthly, monthlyTimesTwelve, annual },
+      const monthly = buildCurrencyValue(monthlyPlan[currency], currencyDetails)
+      const monthlyTimesTwelve = buildCurrencyValue(
+        monthlyPlan[currency] * 12,
+        currencyDetails
+      )
+      const annual = buildCurrencyValue(annualPlan[currency], currencyDetails)
+
+      localizedPlanPricing[currency] = {
+        ...localizedPlanPricing[currency],
+        [outputKey]: { monthly, monthlyTimesTwelve, annual },
+      }
+      plans[currency] = {
+        ...plans[currency],
+        [outputKey]: { monthly, annual },
+      }
     }
     }
-    plans[currency] = {
-      ...plans[currency],
-      [outputKey]: { monthly, annual },
+  }
+  return { localizedPlanPricing, plans }
+}
+
+function generateGroupPlans(workSheetJSON) {
+  const groupPlans = workSheetJSON.filter(data =>
+    data.plan_code.startsWith('group')
+  )
+
+  const currencies = [
+    'AUD',
+    'CAD',
+    'CHF',
+    'DKK',
+    'EUR',
+    'GBP',
+    'INR',
+    'NOK',
+    'NZD',
+    'SEK',
+    'SGD',
+    'USD',
+  ]
+  const sizes = ['2', '3', '4', '5', '10', '20', '50']
+
+  const result = {}
+  for (const type1 of ['educational', 'enterprise']) {
+    result[type1] = {}
+    for (const type2 of ['professional', 'collaborator']) {
+      result[type1][type2] = {}
+      for (const currency of currencies) {
+        result[type1][type2][currency] = {}
+        for (const size of sizes) {
+          const planCode = `group_${type2}_${size}_${type1}`
+          const plan = groupPlans.find(data => data.plan_code === planCode)
+
+          if (!plan) throw new Error(`Missing plan: ${planCode}`)
+
+          result[type1][type2][currency][size] = {
+            price_in_cents: plan[currency] * 100,
+          }
+        }
+      }
     }
     }
   }
   }
+  return result
 }
 }
 
 
-// removes quotes from object keys
-const format = obj => JSON.stringify(obj, null, 2).replace(/"([^"]+)":/g, '$1:')
-const dir = './output'
+const argv = minimist(process.argv.slice(2), {
+  string: ['output', 'file', 'sheet'],
+  alias: { o: 'output', f: 'file', s: 'sheet' },
+})
 
 
-localizedPlanPricing = format(localizedPlanPricing)
-plans = format(plans)
+let input
+if (argv.file) {
+  const ext = path.extname(argv.file)
+  switch (ext) {
+    case '.csv':
+      input = readCSVFile(argv.file)
+      break
+    case '.xls':
+    case '.xlsx':
+      input = readXLSXFile(argv.file, argv.sheet)
+      break
+    case '.json':
+      input = readJSONFile(argv.file)
+      break
+    default:
+      console.log('Invalid file type: must be csv, xls, xlsx, or json')
+  }
+} else {
+  console.log(
+    'usage: node plans.js -f <file.xls|file.csv|file.json> [-s <sheet>] -o <dir>'
+  )
+  process.exit(1)
+}
+// removes quotes from object keys
+const formatJS = obj =>
+  JSON.stringify(obj, null, 2).replace(/"([^"]+)":/g, '$1:')
+const formatJSON = obj => JSON.stringify(obj, null, 2)
+function writeFile(outputFile, data) {
+  console.log(`Writing ${outputFile}`)
+  fs.writeFileSync(outputFile, data)
+}
+const { localizedPlanPricing, plans } = generatePlans(input)
+const groupPlans = generateGroupPlans(input)
 
 
-if (!fs.existsSync(dir)) {
-  fs.mkdirSync(dir)
+if (argv.output) {
+  const dir = argv.output
+  // check if output directory exists
+  if (!fs.existsSync(dir)) {
+    console.log(`Creating output directory ${dir}`)
+    fs.mkdirSync(dir)
+  }
+  // check if output directory is a directory and report error if not
+  if (!fs.lstatSync(dir).isDirectory()) {
+    console.error(`Error: output dir ${dir} is not a directory`)
+    process.exit(1)
+  }
+  writeFile(`${dir}/localizedPlanPricing.json`, formatJS(localizedPlanPricing))
+  writeFile(`${dir}/plans.json`, formatJS(plans))
+  writeFile(`${dir}/groups.json`, formatJSON(groupPlans))
+} else {
+  console.log('PLANS', plans)
+  console.log('LOCALIZED', localizedPlanPricing)
+  console.log('GROUP PLANS', JSON.stringify(groupPlans, null, 2))
 }
 }
-fs.writeFileSync(`${dir}/localizedPlanPricing.json`, localizedPlanPricing)
-fs.writeFileSync(`${dir}/plans.json`, plans)
 
 
 console.log('Completed!')
 console.log('Completed!')