mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #3681 from overleaf/em-group-plans
Configure group plans for additional licenses GitOrigin-RevId: 57822de9f490505c4b083afa80220e4d5b4c7d23
This commit is contained in:
parent
2138bd2a80
commit
e5c49ea19a
5 changed files with 72 additions and 120 deletions
|
@ -1,18 +1,6 @@
|
||||||
/* eslint-disable
|
|
||||||
camelcase,
|
|
||||||
max-len,
|
|
||||||
no-path-concat,
|
|
||||||
no-unused-vars,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
const Settings = require('settings-sharelatex')
|
const Settings = require('settings-sharelatex')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
const Path = require('path')
|
||||||
|
|
||||||
// The groups.json file encodes the various group plan options we provide, and
|
// The groups.json file encodes the various group plan options we provide, and
|
||||||
// is used in the app the render the appropriate dialog in the plans page, and
|
// is used in the app the render the appropriate dialog in the plans page, and
|
||||||
|
@ -21,7 +9,7 @@ const fs = require('fs')
|
||||||
// Recurly has a plan configured for all the groups, and that the prices are
|
// Recurly has a plan configured for all the groups, and that the prices are
|
||||||
// up to date with the data in groups.json.
|
// up to date with the data in groups.json.
|
||||||
const data = fs.readFileSync(
|
const data = fs.readFileSync(
|
||||||
__dirname + '/../../../templates/plans/groups.json'
|
Path.join(__dirname, '/../../../templates/plans/groups.json')
|
||||||
)
|
)
|
||||||
const groups = JSON.parse(data.toString())
|
const groups = JSON.parse(data.toString())
|
||||||
|
|
||||||
|
@ -34,28 +22,32 @@ const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1)
|
||||||
// a particularly clean pattern, since it's a little surprising that settings
|
// a particularly clean pattern, since it's a little surprising that settings
|
||||||
// are modified at boot-time, but I think it's a better option than trying to
|
// are modified at boot-time, but I think it's a better option than trying to
|
||||||
// keep two sources of data in sync.
|
// keep two sources of data in sync.
|
||||||
for (let usage in groups) {
|
for (const [usage, planData] of Object.entries(groups)) {
|
||||||
const plan_data = groups[usage]
|
for (const [planCode, currencyData] of Object.entries(planData)) {
|
||||||
for (let plan_code in plan_data) {
|
// Gather all possible sizes that are set up in at least one currency
|
||||||
const currency_data = plan_data[plan_code]
|
const sizes = new Set()
|
||||||
for (let currency in currency_data) {
|
for (const priceData of Object.values(currencyData)) {
|
||||||
const price_data = currency_data[currency]
|
for (const size in priceData) {
|
||||||
for (let size in price_data) {
|
sizes.add(size)
|
||||||
const price = price_data[size]
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate plans in settings
|
||||||
|
for (const size of sizes) {
|
||||||
Settings.plans.push({
|
Settings.plans.push({
|
||||||
planCode: `group_${plan_code}_${size}_${usage}`,
|
planCode: `group_${planCode}_${size}_${usage}`,
|
||||||
name: `${Settings.appName} ${capitalize(
|
name: `${Settings.appName} ${capitalize(
|
||||||
plan_code
|
planCode
|
||||||
)} - Group Account (${size} licenses) - ${capitalize(usage)}`,
|
)} - Group Account (${size} licenses) - ${capitalize(usage)}`,
|
||||||
hideFromUsers: true,
|
hideFromUsers: true,
|
||||||
annual: true,
|
annual: true,
|
||||||
features: Settings.features[plan_code],
|
features: Settings.features[planCode],
|
||||||
groupPlan: true,
|
groupPlan: true,
|
||||||
membersLimit: parseInt(size)
|
membersLimit: parseInt(size),
|
||||||
|
membersLimitAddOn: 'additional-license'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = groups
|
module.exports = groups
|
||||||
|
|
|
@ -1,34 +1,16 @@
|
||||||
/* eslint-disable
|
|
||||||
node/handle-callback-err,
|
|
||||||
max-len,
|
|
||||||
no-return-assign,
|
|
||||||
no-unused-vars,
|
|
||||||
*/
|
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
|
||||||
// Fix any style issues and re-enable lint.
|
|
||||||
/*
|
|
||||||
* decaffeinate suggestions:
|
|
||||||
* DS101: Remove unnecessary use of Array.from
|
|
||||||
* DS102: Remove unnecessary code created because of implicit returns
|
|
||||||
* DS103: Rewrite code to no longer use __guard__
|
|
||||||
* DS207: Consider shorter variations of null checks
|
|
||||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
||||||
*/
|
|
||||||
const Settings = require('settings-sharelatex')
|
const Settings = require('settings-sharelatex')
|
||||||
const RecurlyWrapper = require('./RecurlyWrapper')
|
const RecurlyWrapper = require('./RecurlyWrapper')
|
||||||
const PlansLocator = require('./PlansLocator')
|
const PlansLocator = require('./PlansLocator')
|
||||||
const SubscriptionFormatters = require('./SubscriptionFormatters')
|
const SubscriptionFormatters = require('./SubscriptionFormatters')
|
||||||
const LimitationsManager = require('./LimitationsManager')
|
|
||||||
const SubscriptionLocator = require('./SubscriptionLocator')
|
const SubscriptionLocator = require('./SubscriptionLocator')
|
||||||
const V1SubscriptionManager = require('./V1SubscriptionManager')
|
const V1SubscriptionManager = require('./V1SubscriptionManager')
|
||||||
const InstitutionsGetter = require('../Institutions/InstitutionsGetter')
|
const InstitutionsGetter = require('../Institutions/InstitutionsGetter')
|
||||||
const PublishersGetter = require('../Publishers/PublishersGetter')
|
const PublishersGetter = require('../Publishers/PublishersGetter')
|
||||||
const sanitizeHtml = require('sanitize-html')
|
const sanitizeHtml = require('sanitize-html')
|
||||||
const logger = require('logger-sharelatex')
|
|
||||||
const _ = require('underscore')
|
const _ = require('underscore')
|
||||||
const async = require('async')
|
const async = require('async')
|
||||||
|
|
||||||
const buildHostedLink = function(recurlySubscription, type) {
|
function buildHostedLink(recurlySubscription, type) {
|
||||||
const recurlySubdomain = Settings.apis.recurly.subdomain
|
const recurlySubdomain = Settings.apis.recurly.subdomain
|
||||||
const hostedLoginToken = recurlySubscription.account.hosted_login_token
|
const hostedLoginToken = recurlySubscription.account.hosted_login_token
|
||||||
let path = ''
|
let path = ''
|
||||||
|
@ -48,28 +30,22 @@ const buildHostedLink = function(recurlySubscription, type) {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
buildUsersSubscriptionViewModel(user, callback) {
|
buildUsersSubscriptionViewModel(user, callback) {
|
||||||
if (callback == null) {
|
async.auto(
|
||||||
callback = function(error, data) {}
|
|
||||||
}
|
|
||||||
return async.auto(
|
|
||||||
{
|
{
|
||||||
personalSubscription(cb) {
|
personalSubscription(cb) {
|
||||||
return SubscriptionLocator.getUsersSubscription(user, cb)
|
SubscriptionLocator.getUsersSubscription(user, cb)
|
||||||
},
|
},
|
||||||
recurlySubscription: [
|
recurlySubscription: [
|
||||||
'personalSubscription',
|
'personalSubscription',
|
||||||
function(cb, { personalSubscription }) {
|
(cb, { personalSubscription }) => {
|
||||||
if (
|
if (
|
||||||
(personalSubscription != null
|
personalSubscription == null ||
|
||||||
? personalSubscription.recurlySubscription_id
|
personalSubscription.recurlySubscription_id == null ||
|
||||||
: undefined) == null ||
|
personalSubscription.recurlySubscription_id === ''
|
||||||
(personalSubscription != null
|
|
||||||
? personalSubscription.recurlySubscription_id
|
|
||||||
: undefined) === ''
|
|
||||||
) {
|
) {
|
||||||
return cb(null, null)
|
return cb(null, null)
|
||||||
}
|
}
|
||||||
return RecurlyWrapper.getSubscription(
|
RecurlyWrapper.getSubscription(
|
||||||
personalSubscription.recurlySubscription_id,
|
personalSubscription.recurlySubscription_id,
|
||||||
{ includeAccount: true },
|
{ includeAccount: true },
|
||||||
cb
|
cb
|
||||||
|
@ -78,17 +54,17 @@ module.exports = {
|
||||||
],
|
],
|
||||||
recurlyCoupons: [
|
recurlyCoupons: [
|
||||||
'recurlySubscription',
|
'recurlySubscription',
|
||||||
function(cb, { recurlySubscription }) {
|
(cb, { recurlySubscription }) => {
|
||||||
if (!recurlySubscription) {
|
if (!recurlySubscription) {
|
||||||
return cb(null, null)
|
return cb(null, null)
|
||||||
}
|
}
|
||||||
const accountId = recurlySubscription.account.account_code
|
const accountId = recurlySubscription.account.account_code
|
||||||
return RecurlyWrapper.getAccountActiveCoupons(accountId, cb)
|
RecurlyWrapper.getAccountActiveCoupons(accountId, cb)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
plan: [
|
plan: [
|
||||||
'personalSubscription',
|
'personalSubscription',
|
||||||
function(cb, { personalSubscription }) {
|
(cb, { personalSubscription }) => {
|
||||||
if (personalSubscription == null) {
|
if (personalSubscription == null) {
|
||||||
return cb()
|
return cb()
|
||||||
}
|
}
|
||||||
|
@ -102,38 +78,38 @@ module.exports = {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return cb(null, plan)
|
cb(null, plan)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
memberGroupSubscriptions(cb) {
|
memberGroupSubscriptions(cb) {
|
||||||
return SubscriptionLocator.getMemberSubscriptions(user, cb)
|
SubscriptionLocator.getMemberSubscriptions(user, cb)
|
||||||
},
|
},
|
||||||
managedGroupSubscriptions(cb) {
|
managedGroupSubscriptions(cb) {
|
||||||
return SubscriptionLocator.getManagedGroupSubscriptions(user, cb)
|
SubscriptionLocator.getManagedGroupSubscriptions(user, cb)
|
||||||
},
|
},
|
||||||
confirmedMemberAffiliations(cb) {
|
confirmedMemberAffiliations(cb) {
|
||||||
return InstitutionsGetter.getConfirmedAffiliations(user._id, cb)
|
InstitutionsGetter.getConfirmedAffiliations(user._id, cb)
|
||||||
},
|
},
|
||||||
managedInstitutions(cb) {
|
managedInstitutions(cb) {
|
||||||
return InstitutionsGetter.getManagedInstitutions(user._id, cb)
|
InstitutionsGetter.getManagedInstitutions(user._id, cb)
|
||||||
},
|
},
|
||||||
managedPublishers(cb) {
|
managedPublishers(cb) {
|
||||||
return PublishersGetter.getManagedPublishers(user._id, cb)
|
PublishersGetter.getManagedPublishers(user._id, cb)
|
||||||
},
|
},
|
||||||
v1SubscriptionStatus(cb) {
|
v1SubscriptionStatus(cb) {
|
||||||
return V1SubscriptionManager.getSubscriptionStatusFromV1(
|
V1SubscriptionManager.getSubscriptionStatusFromV1(
|
||||||
user._id,
|
user._id,
|
||||||
function(error, status, v1Id) {
|
(error, status, v1Id) => {
|
||||||
if (error != null) {
|
if (error) {
|
||||||
return cb(error)
|
return cb(error)
|
||||||
}
|
}
|
||||||
return cb(null, status)
|
cb(null, status)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function(err, results) {
|
(err, results) => {
|
||||||
if (err != null) {
|
if (err) {
|
||||||
return callback(err)
|
return callback(err)
|
||||||
}
|
}
|
||||||
let {
|
let {
|
||||||
|
@ -168,9 +144,8 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(personalSubscription != null
|
personalSubscription &&
|
||||||
? personalSubscription.toObject
|
typeof personalSubscription.toObject === 'function'
|
||||||
: undefined) != null
|
|
||||||
) {
|
) {
|
||||||
// Downgrade from Mongoose object, so we can add a recurly and plan attribute
|
// Downgrade from Mongoose object, so we can add a recurly and plan attribute
|
||||||
personalSubscription = personalSubscription.toObject()
|
personalSubscription = personalSubscription.toObject()
|
||||||
|
@ -180,15 +155,13 @@ module.exports = {
|
||||||
personalSubscription.plan = plan
|
personalSubscription.plan = plan
|
||||||
}
|
}
|
||||||
|
|
||||||
if (personalSubscription != null && recurlySubscription != null) {
|
if (personalSubscription && recurlySubscription) {
|
||||||
const tax =
|
const tax = recurlySubscription.tax_in_cents || 0
|
||||||
(recurlySubscription != null
|
|
||||||
? recurlySubscription.tax_in_cents
|
|
||||||
: undefined) || 0
|
|
||||||
// Some plans allow adding more seats than the base plan provides.
|
// Some plans allow adding more seats than the base plan provides.
|
||||||
// This is recorded as a subscription add on.
|
// This is recorded as a subscription add on.
|
||||||
// Note: tax_in_cents already includes the tax for any addon.
|
// Note: tax_in_cents already includes the tax for any addon.
|
||||||
let addOnPrice = 0
|
let addOnPrice = 0
|
||||||
|
let additionalLicenses = 0
|
||||||
if (
|
if (
|
||||||
plan.membersLimitAddOn &&
|
plan.membersLimitAddOn &&
|
||||||
Array.isArray(recurlySubscription.subscription_add_ons)
|
Array.isArray(recurlySubscription.subscription_add_ons)
|
||||||
|
@ -196,18 +169,15 @@ module.exports = {
|
||||||
recurlySubscription.subscription_add_ons.forEach(addOn => {
|
recurlySubscription.subscription_add_ons.forEach(addOn => {
|
||||||
if (addOn.add_on_code === plan.membersLimitAddOn) {
|
if (addOn.add_on_code === plan.membersLimitAddOn) {
|
||||||
addOnPrice += addOn.quantity * addOn.unit_amount_in_cents
|
addOnPrice += addOn.quantity * addOn.unit_amount_in_cents
|
||||||
|
additionalLicenses += addOn.quantity
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const totalLicenses = (plan.membersLimit || 0) + additionalLicenses
|
||||||
personalSubscription.recurly = {
|
personalSubscription.recurly = {
|
||||||
tax,
|
tax,
|
||||||
taxRate: parseFloat(
|
taxRate: parseFloat(
|
||||||
__guard__(
|
recurlySubscription.tax_rate && recurlySubscription.tax_rate._
|
||||||
recurlySubscription != null
|
|
||||||
? recurlySubscription.tax_rate
|
|
||||||
: undefined,
|
|
||||||
x => x._
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
billingDetailsLink: buildHostedLink(
|
billingDetailsLink: buildHostedLink(
|
||||||
recurlySubscription,
|
recurlySubscription,
|
||||||
|
@ -215,26 +185,18 @@ module.exports = {
|
||||||
),
|
),
|
||||||
accountManagementLink: buildHostedLink(recurlySubscription),
|
accountManagementLink: buildHostedLink(recurlySubscription),
|
||||||
price: SubscriptionFormatters.formatPrice(
|
price: SubscriptionFormatters.formatPrice(
|
||||||
(recurlySubscription != null
|
recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
|
||||||
? recurlySubscription.unit_amount_in_cents
|
recurlySubscription.currency
|
||||||
: undefined) +
|
|
||||||
addOnPrice +
|
|
||||||
tax,
|
|
||||||
recurlySubscription != null
|
|
||||||
? recurlySubscription.currency
|
|
||||||
: undefined
|
|
||||||
),
|
),
|
||||||
|
additionalLicenses,
|
||||||
|
totalLicenses,
|
||||||
nextPaymentDueAt: SubscriptionFormatters.formatDate(
|
nextPaymentDueAt: SubscriptionFormatters.formatDate(
|
||||||
recurlySubscription != null
|
recurlySubscription.current_period_ends_at
|
||||||
? recurlySubscription.current_period_ends_at
|
|
||||||
: undefined
|
|
||||||
),
|
),
|
||||||
currency: recurlySubscription.currency,
|
currency: recurlySubscription.currency,
|
||||||
state: recurlySubscription.state,
|
state: recurlySubscription.state,
|
||||||
trialEndsAtFormatted: SubscriptionFormatters.formatDate(
|
trialEndsAtFormatted: SubscriptionFormatters.formatDate(
|
||||||
recurlySubscription != null
|
recurlySubscription.trial_ends_at
|
||||||
? recurlySubscription.trial_ends_at
|
|
||||||
: undefined
|
|
||||||
),
|
),
|
||||||
trial_ends_at: recurlySubscription.trial_ends_at,
|
trial_ends_at: recurlySubscription.trial_ends_at,
|
||||||
activeCoupons: recurlyCoupons,
|
activeCoupons: recurlyCoupons,
|
||||||
|
@ -242,9 +204,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let memberGroupSubscription of Array.from(
|
for (const memberGroupSubscription of memberGroupSubscriptions) {
|
||||||
memberGroupSubscriptions
|
|
||||||
)) {
|
|
||||||
if (memberGroupSubscription.teamNotice) {
|
if (memberGroupSubscription.teamNotice) {
|
||||||
memberGroupSubscription.teamNotice = sanitizeHtml(
|
memberGroupSubscription.teamNotice = sanitizeHtml(
|
||||||
memberGroupSubscription.teamNotice
|
memberGroupSubscription.teamNotice
|
||||||
|
@ -252,7 +212,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(null, {
|
callback(null, {
|
||||||
personalSubscription,
|
personalSubscription,
|
||||||
managedGroupSubscriptions,
|
managedGroupSubscriptions,
|
||||||
memberGroupSubscriptions,
|
memberGroupSubscriptions,
|
||||||
|
@ -308,9 +268,3 @@ module.exports = {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function __guard__(value, transform) {
|
|
||||||
return typeof value !== 'undefined' && value !== null
|
|
||||||
? transform(value)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,6 +8,9 @@ div(ng-controller="RecurlySubscriptionController")
|
||||||
case personalSubscription.recurly.state
|
case personalSubscription.recurly.state
|
||||||
when "active"
|
when "active"
|
||||||
p !{translate("currently_subscribed_to_plan", {planName: personalSubscription.plan.name}, ['strong'])}
|
p !{translate("currently_subscribed_to_plan", {planName: personalSubscription.plan.name}, ['strong'])}
|
||||||
|
-if (personalSubscription.recurly.additionalLicenses > 0)
|
||||||
|
|
|
||||||
|
| !{translate("additional_licenses", {additionalLicenses: personalSubscription.recurly.additionalLicenses, totalLicenses: personalSubscription.recurly.totalLicenses}, ['strong', 'strong'])}
|
||||||
|
|
|
|
||||||
a(href, ng-click="switchToChangePlanView()", ng-if="showChangePlanButton") !{translate("change_plan")}.
|
a(href, ng-click="switchToChangePlanView()", ng-if="showChangePlanButton") !{translate("change_plan")}.
|
||||||
-if (personalSubscription.recurly.trialEndsAtFormatted && personalSubscription.recurly.trial_ends_at > Date.now())
|
-if (personalSubscription.recurly.trialEndsAtFormatted && personalSubscription.recurly.trial_ends_at > Date.now())
|
||||||
|
|
|
@ -1005,6 +1005,7 @@
|
||||||
"currently_subscribed_to_plan": "You are currently subscribed to the <0>__planName__</0> plan.",
|
"currently_subscribed_to_plan": "You are currently subscribed to the <0>__planName__</0> plan.",
|
||||||
"change_plan": "Change plan",
|
"change_plan": "Change plan",
|
||||||
"next_payment_of_x_collectected_on_y": "The next payment of <0>__paymentAmmount__</0> will be collected on <1>__collectionDate__</1>.",
|
"next_payment_of_x_collectected_on_y": "The next payment of <0>__paymentAmmount__</0> will be collected on <1>__collectionDate__</1>.",
|
||||||
|
"additional_licenses": "Your subscription includes <0>__additionalLicenses__</0> additional license(s) for a total of <1>__totalLicenses__</1> licenses.",
|
||||||
"update_your_billing_details": "Update Your Billing Details",
|
"update_your_billing_details": "Update Your Billing Details",
|
||||||
"subscription_canceled_and_terminate_on_x": " Your subscription has been canceled and will terminate on <0>__terminateDate__</0>. No further payments will be taken.",
|
"subscription_canceled_and_terminate_on_x": " Your subscription has been canceled and will terminate on <0>__terminateDate__</0>. No further payments will be taken.",
|
||||||
"your_subscription_has_expired": "Your subscription has expired.",
|
"your_subscription_has_expired": "Your subscription has expired.",
|
||||||
|
|
|
@ -134,7 +134,9 @@ describe('Subscriptions', function() {
|
||||||
account_code: this.user._id,
|
account_code: this.user._id,
|
||||||
email: 'mock@email.com',
|
email: 'mock@email.com',
|
||||||
hosted_login_token: 'mock-login-token'
|
hosted_login_token: 'mock-login-token'
|
||||||
}
|
},
|
||||||
|
additionalLicenses: 0,
|
||||||
|
totalLicenses: 0
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue