From 5e61fce3b4699a252fc002ef5b5db404b3c54831 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 11 Jan 2022 15:57:03 +0100 Subject: [PATCH] Enable additional currencies when purchasing (or upgrading to) a group plan (#4884) * Add script to fetch group data pricing from Recurly * Update groups pricing data using script to fetch prices from Recurly * Add additional currencies to saas settings * Refactor group plans upgrade modal to use shared options from settings GitOrigin-RevId: 6d13d5b152d01e0399f9d2b8f6f8bf99784589e8 --- .../Features/Subscription/GroupPlansData.js | 3 + .../Subscription/SubscriptionController.js | 1 + services/web/app/templates/plans/groups.json | 478 ++++++++++++++---- .../web/app/views/subscriptions/dashboard.pug | 1 + .../js/main/subscription-dashboard.js | 51 +- .../recurly/get_recurly_group_prices.js | 45 ++ 6 files changed, 441 insertions(+), 138 deletions(-) create mode 100644 services/web/scripts/recurly/get_recurly_group_prices.js diff --git a/services/web/app/src/Features/Subscription/GroupPlansData.js b/services/web/app/src/Features/Subscription/GroupPlansData.js index a6fd24301e..b43c405c62 100644 --- a/services/web/app/src/Features/Subscription/GroupPlansData.js +++ b/services/web/app/src/Features/Subscription/GroupPlansData.js @@ -8,6 +8,9 @@ const Path = require('path') // It is also used by scripts/recurly/sync_recurly.rb, which will make sure // Recurly has a plan configured for all the groups, and that the prices are // up to date with the data in groups.json. +// Alternatively, scripts/recurly/get_recurly_group_prices.rb can be used to +// fetch pricing data and generate a groups.json using the current Recurly +// prices const data = fs.readFileSync( Path.join(__dirname, '/../../../templates/plans/groups.json') ) diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index a436207fc1..713dac081e 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -160,6 +160,7 @@ async function userSubscriptionPage(req, res) { managedPublishers, v1SubscriptionStatus, currentInstitutionsWithLicence, + groupPlanModalOptions, } res.render('subscriptions/dashboard', data) } diff --git a/services/web/app/templates/plans/groups.json b/services/web/app/templates/plans/groups.json index f7556afd0e..eb7adc1de5 100644 --- a/services/web/app/templates/plans/groups.json +++ b/services/web/app/templates/plans/groups.json @@ -1,104 +1,42 @@ { - "enterprise": { - "collaborator": { - "USD": { - "2": 252, - "3": 376, - "4": 495, - "5": 615, - "10": 1170, - "20": 2160, - "50": 4950 - }, - "EUR": { - "2": 235, - "3": 352, - "4": 468, - "5": 584, - "10": 1090, - "20": 2015, - "50": 4620 - }, - "GBP": { - "2": 198, - "3": 296, - "4": 394, - "5": 492, - "10": 935, - "20": 1730, - "50": 3960 - } - }, - "professional": { - "USD": { - "2": 504, - "3": 752, - "4": 990, - "5": 1230, - "10": 2340, - "20": 4320, - "50": 9900 - }, - "EUR": { - "2": 470, - "3": 704, - "4": 936, - "5": 1168, - "10": 2185, - "20": 4030, - "50": 9240 - }, - "GBP": { - "2": 396, - "3": 592, - "4": 788, - "5": 984, - "10": 1870, - "20": 3455, - "50": 7920 - } - } - }, "educational": { - "collaborator": { - "USD": { - "2": 252, - "3": 376, - "4": 495, - "5": 615, - "10": 695, - "20": 1295, - "50": 2970 - }, - "EUR": { - "2": 235, - "3": 352, - "4": 468, - "5": 584, - "10": 655, - "20": 1210, - "50": 2770 - }, - "GBP": { - "2": 198, - "3": 296, - "4": 394, - "5": 492, - "10": 560, - "20": 1035, - "50": 2375 - } - }, "professional": { - "USD": { + "AUD": { + "2": 604, + "3": 906, + "4": 1208, + "5": 1510, + "10": 1685, + "20": 3110, + "50": 7130 + }, + "CAD": { + "2": 570, + "3": 854, + "4": 1138, + "5": 1420, + "10": 1590, + "20": 2940, + "50": 6730 + }, + "CHF": { "2": 504, - "3": 752, - "4": 990, - "5": 1230, - "10": 1390, + "3": 756, + "4": 1008, + "5": 1260, + "10": 1405, "20": 2590, "50": 5940 }, + "DKK": { + "2": 3024, + "3": 4536, + "4": 6048, + "5": 7560, + "10": 8425, + "20": 15550, + "50": 35640 + }, "EUR": { "2": 470, "3": 704, @@ -116,7 +54,357 @@ "10": 1125, "20": 2075, "50": 4750 + }, + "NOK": { + "2": 3696, + "3": 5544, + "4": 7392, + "5": 9240, + "10": 10295, + "20": 19010, + "50": 43560 + }, + "NZD": { + "2": 604, + "3": 906, + "4": 1208, + "5": 1510, + "10": 1685, + "20": 3110, + "50": 7130 + }, + "SEK": { + "2": 3696, + "3": 5544, + "4": 7392, + "5": 9240, + "10": 10295, + "20": 19010, + "50": 43560 + }, + "SGD": { + "2": 672, + "3": 1008, + "4": 1344, + "5": 1680, + "10": 1870, + "20": 3455, + "50": 7920 + }, + "USD": { + "2": 504, + "3": 752, + "4": 990, + "5": 1230, + "10": 1390, + "20": 2590, + "50": 5940 + } + }, + "collaborator": { + "AUD": { + "2": 302, + "3": 453, + "4": 604, + "5": 755, + "10": 840, + "20": 1555, + "50": 3565 + }, + "CAD": { + "2": 285, + "3": 427, + "4": 569, + "5": 710, + "10": 795, + "20": 1470, + "50": 3365 + }, + "CHF": { + "2": 252, + "3": 378, + "4": 504, + "5": 630, + "10": 700, + "20": 1295, + "50": 2970 + }, + "DKK": { + "2": 1512, + "3": 2268, + "4": 3024, + "5": 3780, + "10": 4210, + "20": 7775, + "50": 17820 + }, + "EUR": { + "2": 235, + "3": 352, + "4": 468, + "5": 584, + "10": 655, + "20": 1210, + "50": 2770 + }, + "GBP": { + "2": 198, + "3": 296, + "4": 394, + "5": 492, + "10": 560, + "20": 1035, + "50": 2375 + }, + "NOK": { + "2": 1848, + "3": 2772, + "4": 3696, + "5": 4620, + "10": 5150, + "20": 9505, + "50": 21780 + }, + "NZD": { + "2": 302, + "3": 453, + "4": 604, + "5": 755, + "10": 840, + "20": 1555, + "50": 3565 + }, + "SEK": { + "2": 1848, + "3": 2772, + "4": 3696, + "5": 4620, + "10": 5150, + "20": 9505, + "50": 21780 + }, + "SGD": { + "2": 336, + "3": 504, + "4": 672, + "5": 840, + "10": 935, + "20": 1730, + "50": 3960 + }, + "USD": { + "2": 252, + "3": 376, + "4": 495, + "5": 615, + "10": 695, + "20": 1295, + "50": 2970 + } + } + }, + "enterprise": { + "professional": { + "AUD": { + "2": 604, + "3": 906, + "4": 1208, + "5": 1510, + "10": 2810, + "20": 5185, + "50": 11880 + }, + "CAD": { + "2": 570, + "3": 854, + "4": 1138, + "5": 1420, + "10": 2650, + "20": 4895, + "50": 11220 + }, + "CHF": { + "2": 504, + "3": 756, + "4": 1008, + "5": 1260, + "10": 2340, + "20": 4320, + "50": 9900 + }, + "DKK": { + "2": 3024, + "3": 4536, + "4": 6048, + "5": 7560, + "10": 14040, + "20": 25920, + "50": 59400 + }, + "EUR": { + "2": 470, + "3": 704, + "4": 936, + "5": 1168, + "10": 2185, + "20": 4030, + "50": 9240 + }, + "GBP": { + "2": 396, + "3": 592, + "4": 788, + "5": 984, + "10": 1870, + "20": 3455, + "50": 7920 + }, + "NOK": { + "2": 3696, + "3": 5544, + "4": 7392, + "5": 9240, + "10": 17160, + "20": 31680, + "50": 72600 + }, + "NZD": { + "2": 604, + "3": 906, + "4": 1208, + "5": 1510, + "10": 2810, + "20": 5185, + "50": 11880 + }, + "SEK": { + "2": 3696, + "3": 5544, + "4": 7392, + "5": 9240, + "10": 17160, + "20": 31680, + "50": 72600 + }, + "SGD": { + "2": 672, + "3": 1008, + "4": 1344, + "5": 1680, + "10": 3120, + "20": 5760, + "50": 13200 + }, + "USD": { + "2": 504, + "3": 752, + "4": 990, + "5": 1230, + "10": 2340, + "20": 4320, + "50": 9900 + } + }, + "collaborator": { + "AUD": { + "2": 302, + "3": 453, + "4": 604, + "5": 755, + "10": 1405, + "20": 2590, + "50": 5940 + }, + "CAD": { + "2": 285, + "3": 427, + "4": 569, + "5": 710, + "10": 1325, + "20": 2450, + "50": 5610 + }, + "CHF": { + "2": 252, + "3": 378, + "4": 504, + "5": 630, + "10": 1170, + "20": 2160, + "50": 4950 + }, + "DKK": { + "2": 1512, + "3": 2268, + "4": 3024, + "5": 3780, + "10": 7020, + "20": 12960, + "50": 29700 + }, + "EUR": { + "2": 235, + "3": 352, + "4": 468, + "5": 584, + "10": 1090, + "20": 2015, + "50": 4620 + }, + "GBP": { + "2": 198, + "3": 296, + "4": 394, + "5": 492, + "10": 935, + "20": 1730, + "50": 3960 + }, + "NOK": { + "2": 1848, + "3": 2772, + "4": 3696, + "5": 4620, + "10": 8580, + "20": 15840, + "50": 36300 + }, + "NZD": { + "2": 302, + "3": 453, + "4": 604, + "5": 755, + "10": 1405, + "20": 2590, + "50": 5940 + }, + "SEK": { + "2": 1848, + "3": 2772, + "4": 3696, + "5": 4620, + "10": 8580, + "20": 15840, + "50": 36300 + }, + "SGD": { + "2": 336, + "3": 504, + "4": 672, + "5": 840, + "10": 1560, + "20": 2880, + "50": 6600 + }, + "USD": { + "2": 252, + "3": 376, + "4": 495, + "5": 615, + "10": 1170, + "20": 2160, + "50": 4950 } } } -} \ No newline at end of file +} diff --git a/services/web/app/views/subscriptions/dashboard.pug b/services/web/app/views/subscriptions/dashboard.pug index 5dd7cbffb4..e7fae5ca60 100644 --- a/services/web/app/views/subscriptions/dashboard.pug +++ b/services/web/app/views/subscriptions/dashboard.pug @@ -13,6 +13,7 @@ block append meta meta(name="ol-subscription" data-type="json" content=personalSubscription) meta(name="ol-recomendedCurrency" content=personalSubscription.recurly.currency) meta(name="ol-groupPlans" data-type="json" content=groupPlans) + meta(name="ol-groupPlanModalOptions" data-type="json" content=groupPlanModalOptions) block content main.content.content-alt#main-content(ng-cloak) diff --git a/services/web/frontend/js/main/subscription-dashboard.js b/services/web/frontend/js/main/subscription-dashboard.js index e80e9e4d4d..16cb578bd4 100644 --- a/services/web/frontend/js/main/subscription-dashboard.js +++ b/services/web/frontend/js/main/subscription-dashboard.js @@ -21,6 +21,8 @@ import App from '../base' import getMeta from '../utils/meta' const SUBSCRIPTION_URL = '/user/subscription/update' +const GROUP_PLAN_MODAL_OPTIONS = getMeta('ol-groupPlanModalOptions') + const ensureRecurlyIsSetup = _.once(() => { if (typeof recurly === 'undefined' || !recurly) { return false @@ -102,7 +104,11 @@ App.controller('ChangePlanToGroupFormController', function ($scope, $modal) { const subscription = getMeta('ol-subscription') const currency = subscription.recurly.currency - if (['USD', 'GBP', 'EUR'].includes(currency)) { + const validCurrencies = GROUP_PLAN_MODAL_OPTIONS.currencies.map( + item => item.code + ) + + if (validCurrencies.includes(currency)) { $scope.isValidCurrencyForUpgrade = true } @@ -123,48 +129,7 @@ App.controller('ChangePlanToGroupFormController', function ($scope, $modal) { App.controller( 'GroupPlansModalUpgradeController', function ($scope, $modal, $location, $http, RecurlyPricing) { - $scope.options = { - plan_codes: [ - { - display: 'Collaborator', - code: 'collaborator', - }, - { - display: 'Professional', - code: 'professional', - }, - ], - currencies: [ - { - display: 'USD ($)', - code: 'USD', - }, - { - display: 'GBP (£)', - code: 'GBP', - }, - { - display: 'EUR (€)', - code: 'EUR', - }, - ], - currencySymbols: { - USD: '$', - EUR: '€', - GBP: '£', - }, - sizes: [2, 3, 4, 5, 10, 20, 50], - usages: [ - { - display: 'Enterprise', - code: 'enterprise', - }, - { - display: 'Educational', - code: 'educational', - }, - ], - } + $scope.options = GROUP_PLAN_MODAL_OPTIONS $scope.prices = getMeta('ol-groupPlans') diff --git a/services/web/scripts/recurly/get_recurly_group_prices.js b/services/web/scripts/recurly/get_recurly_group_prices.js new file mode 100644 index 0000000000..df23c4a885 --- /dev/null +++ b/services/web/scripts/recurly/get_recurly_group_prices.js @@ -0,0 +1,45 @@ +// Get prices from Recurly in GroupPlansData format, ie to update: +// app/templates/plans/groups.json +// +// Usage example: +// node scripts/recurly/get_recurly_group_prices.js + +const recurly = require('recurly') +const Settings = require('@overleaf/settings') + +const recurlySettings = Settings.apis.recurly +const recurlyApiKey = recurlySettings ? recurlySettings.apiKey : undefined + +const client = new recurly.Client(recurlyApiKey) + +async function getRecurlyGroupPrices() { + const prices = {} + const plans = client.listPlans({ params: { limit: 200 } }) + for await (const plan of plans.each()) { + if (plan.code.substr(0, 6) === 'group_') { + const [, type, size, usage] = plan.code.split('_') + plan.currencies.forEach(planPricing => { + const { currency, unitAmount } = planPricing + prices[usage] = prices[usage] || {} + prices[usage][type] = prices[usage][type] || {} + prices[usage][type][currency] = prices[usage][type][currency] || {} + prices[usage][type][currency][size] = unitAmount + }) + } + } + return prices +} + +async function main() { + const prices = await getRecurlyGroupPrices() + console.log(JSON.stringify(prices, undefined, 2)) +} + +main() + .then(() => { + process.exit(0) + }) + .catch(error => { + console.error({ error }) + process.exit(1) + })