diff --git a/services/web/scripts/plan-prices/README.md b/services/web/scripts/plan-prices/README.md index d4f888eb67..2c60f0957a 100644 --- a/services/web/scripts/plan-prices/README.md +++ b/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. -### 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` - `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 diff --git a/services/web/scripts/plan-prices/groups.js b/services/web/scripts/plan-prices/groups.js deleted file mode 100644 index b1ecdcd4ad..0000000000 --- a/services/web/scripts/plan-prices/groups.js +++ /dev/null @@ -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!') diff --git a/services/web/scripts/plan-prices/package-lock.json b/services/web/scripts/plan-prices/package-lock.json index 1d85132940..154aa323a6 100644 --- a/services/web/scripts/plan-prices/package-lock.json +++ b/services/web/scripts/plan-prices/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { + "csv": "^6.2.10", + "minimist": "^1.2.8", "xlsx": "^0.18.5" } }, @@ -55,6 +57,39 @@ "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", @@ -64,6 +99,15 @@ "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": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", @@ -76,6 +120,12 @@ "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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", @@ -145,12 +195,48 @@ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", "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": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", @@ -160,6 +246,12 @@ "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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", diff --git a/services/web/scripts/plan-prices/package.json b/services/web/scripts/plan-prices/package.json index 0556e8bd86..b97b2e93d2 100644 --- a/services/web/scripts/plan-prices/package.json +++ b/services/web/scripts/plan-prices/package.json @@ -9,6 +9,8 @@ "author": "", "license": "ISC", "devDependencies": { + "csv": "^6.2.10", + "minimist": "^1.2.8", "xlsx": "^0.18.5" } } diff --git a/services/web/scripts/plan-prices/plans.js b/services/web/scripts/plan-prices/plans.js index c7e7f7513a..17d9218403 100644 --- a/services/web/scripts/plan-prices/plans.js +++ b/services/web/scripts/plan-prices/plans.js @@ -2,21 +2,56 @@ // and plans object in main/plans.js const xlsx = require('xlsx') +const csv = require('csv/sync') const fs = require('fs') 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] const plansMap = { @@ -71,6 +106,10 @@ const currencies = { symbol: '$', placement: 'before', }, + INR: { + symbol: '₹', + placement: 'before', + }, } const buildCurrencyValue = (amount, currency) => { @@ -79,64 +118,160 @@ const buildCurrencyValue = (amount, currency) => { : `${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) +function generatePlans(workSheetJSON) { + // localizedPlanPricing object for settings.overrides.saas.js + const localizedPlanPricing = {} + // plans object for main/plans.js + const plans = {} + for (const [currency, currencyDetails] of Object.entries(currencies)) { localizedPlanPricing[currency] = { - ...localizedPlanPricing[currency], - [outputKey]: { monthly, monthlyTimesTwelve, annual }, + symbol: currencyDetails.symbol.trim(), + free: { + monthly: buildCurrencyValue(0, currencyDetails), + annual: buildCurrencyValue(0, currencyDetails), + }, } plans[currency] = { - ...plans[currency], - [outputKey]: { monthly, annual }, + 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 }, + } } } + 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 +} + +const argv = minimist(process.argv.slice(2), { + string: ['output', 'file', 'sheet'], + alias: { o: 'output', f: 'file', s: 'sheet' }, +}) + +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 [-s ] -o ' + ) + process.exit(1) +} // 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) +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 (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!')