diff --git a/services/web/scripts/plan-prices/.gitignore b/services/web/scripts/plan-prices/.gitignore new file mode 100644 index 0000000000..05c5e72581 --- /dev/null +++ b/services/web/scripts/plan-prices/.gitignore @@ -0,0 +1,3 @@ +node_modules +output +*.xlsx diff --git a/services/web/scripts/plan-prices/README.md b/services/web/scripts/plan-prices/README.md new file mode 100644 index 0000000000..d4f888eb67 --- /dev/null +++ b/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` diff --git a/services/web/scripts/plan-prices/groups.js b/services/web/scripts/plan-prices/groups.js new file mode 100644 index 0000000000..b1ecdcd4ad --- /dev/null +++ b/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!') diff --git a/services/web/scripts/plan-prices/package-lock.json b/services/web/scripts/plan-prices/package-lock.json new file mode 100644 index 0000000000..1d85132940 --- /dev/null +++ b/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" + } + } + } +} diff --git a/services/web/scripts/plan-prices/package.json b/services/web/scripts/plan-prices/package.json new file mode 100644 index 0000000000..0556e8bd86 --- /dev/null +++ b/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" + } +} diff --git a/services/web/scripts/plan-prices/plans.js b/services/web/scripts/plan-prices/plans.js new file mode 100644 index 0000000000..c7e7f7513a --- /dev/null +++ b/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!')