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

Merge pull request #10372 from overleaf/ii-adjust-plans-prices

Adjust plans prices and automate the process of generating the prices lists

GitOrigin-RevId: 06be1f9a26837ed9bb7eca598cd88b6288244338
Eric Mc Sween 3 лет назад
Родитель
Сommit
c9f8080769

+ 3 - 0
services/web/scripts/plan-prices/.gitignore

@@ -0,0 +1,3 @@
+node_modules
+output
+*.xlsx

+ 15 - 0
services/web/scripts/plan-prices/README.md

@@ -0,0 +1,15 @@
+A nodejs tool for reading plans prices from an Excel file and creating JSON objects.
+
+Run `npm install` in order to install the `xlsx` dependency.
+
+The scripts will put the output results into the `output` folder.
+
+### Create group plans
+
+_Command_ `node groups.js fileName sheetName` - generates group plans prices. To be used for `/services/web/app/templates/plans/groups.json`
+
+### Create localized plan pricing
+
+_Command_ `node plans.js fileName sheetName` - generates two json files:
+- `localizedPlanPricing.json` for `/services/web/config/settings.overrides.saas.js`
+- `plans.json` for `/services/web/frontend/js/main/plans.js`

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

@@ -0,0 +1,68 @@
+// 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!')

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

@@ -0,0 +1,191 @@
+{
+  "name": "prices",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "prices",
+      "version": "1.0.0",
+      "license": "ISC",
+      "devDependencies": {
+        "xlsx": "^0.18.5"
+      }
+    },
+    "node_modules/adler-32": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
+      "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/cfb": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
+      "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+      "dev": true,
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "crc-32": "~1.2.0"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/crc-32": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+      "dev": true,
+      "bin": {
+        "crc32": "bin/crc32.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/ssf": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+      "dev": true,
+      "dependencies": {
+        "frac": "~1.1.2"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/wmf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/word": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/xlsx": {
+      "version": "0.18.5",
+      "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
+      "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+      "dev": true,
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "cfb": "~1.2.1",
+        "codepage": "~1.15.0",
+        "crc-32": "~1.2.1",
+        "ssf": "~0.11.2",
+        "wmf": "~1.0.1",
+        "word": "~0.3.0"
+      },
+      "bin": {
+        "xlsx": "bin/xlsx.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    }
+  },
+  "dependencies": {
+    "adler-32": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
+      "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+      "dev": true
+    },
+    "cfb": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
+      "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+      "dev": true,
+      "requires": {
+        "adler-32": "~1.3.0",
+        "crc-32": "~1.2.0"
+      }
+    },
+    "codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+      "dev": true
+    },
+    "crc-32": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+      "dev": true
+    },
+    "frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+      "dev": true
+    },
+    "ssf": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+      "dev": true,
+      "requires": {
+        "frac": "~1.1.2"
+      }
+    },
+    "wmf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+      "dev": true
+    },
+    "word": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+      "dev": true
+    },
+    "xlsx": {
+      "version": "0.18.5",
+      "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
+      "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+      "dev": true,
+      "requires": {
+        "adler-32": "~1.3.0",
+        "cfb": "~1.2.1",
+        "codepage": "~1.15.0",
+        "crc-32": "~1.2.1",
+        "ssf": "~0.11.2",
+        "wmf": "~1.0.1",
+        "word": "~0.3.0"
+      }
+    }
+  }
+}

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

@@ -0,0 +1,14 @@
+{
+  "name": "prices",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "devDependencies": {
+    "xlsx": "^0.18.5"
+  }
+}

+ 142 - 0
services/web/scripts/plan-prices/plans.js

@@ -0,0 +1,142 @@
+// Creates data for localizedPlanPricing object in settings.overrides.saas.js
+// and plans object in main/plans.js
+
+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)
+
+// Mapping of [output_keys]:[actual_keys]
+const plansMap = {
+  student: 'student',
+  personal: 'paid-personal',
+  collaborator: 'collaborator',
+  professional: 'professional',
+}
+
+const currencies = {
+  USD: {
+    symbol: '$',
+    placement: 'before',
+  },
+  EUR: {
+    symbol: '€',
+    placement: 'before',
+  },
+  GBP: {
+    symbol: '£',
+    placement: 'before',
+  },
+  SEK: {
+    symbol: ' kr',
+    placement: 'after',
+  },
+  CAD: {
+    symbol: '$',
+    placement: 'before',
+  },
+  NOK: {
+    symbol: ' kr',
+    placement: 'after',
+  },
+  DKK: {
+    symbol: ' kr',
+    placement: 'after',
+  },
+  AUD: {
+    symbol: '$',
+    placement: 'before',
+  },
+  NZD: {
+    symbol: '$',
+    placement: 'before',
+  },
+  CHF: {
+    symbol: 'Fr ',
+    placement: 'before',
+  },
+  SGD: {
+    symbol: '$',
+    placement: 'before',
+  },
+}
+
+const buildCurrencyValue = (amount, currency) => {
+  return currency.placement === 'before'
+    ? `${currency.symbol}${amount}`
+    : `${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(),
+  }
+
+  for (const [outputKey, actualKey] of Object.entries(plansMap)) {
+    const monthlyPlan = workSheetJSON.find(data => data.plan_code === actualKey)
+
+    if (!monthlyPlan) throw new Error(`Missing plan: ${actualKey}`)
+
+    const actualKeyAnnual = `${actualKey}-annual`
+    const annualPlan = workSheetJSON.find(
+      data => data.plan_code === actualKeyAnnual
+    )
+
+    if (!annualPlan) throw new Error(`Missing plan: ${actualKeyAnnual}`)
+
+    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 },
+    }
+  }
+}
+
+// removes quotes from object keys
+const format = obj => JSON.stringify(obj, null, 2).replace(/"([^"]+)":/g, '$1:')
+const dir = './output'
+
+localizedPlanPricing = format(localizedPlanPricing)
+plans = format(plans)
+
+if (!fs.existsSync(dir)) {
+  fs.mkdirSync(dir)
+}
+fs.writeFileSync(`${dir}/localizedPlanPricing.json`, localizedPlanPricing)
+fs.writeFileSync(`${dir}/plans.json`, plans)
+
+console.log('Completed!')