[web] Display the current plan in the project list dashboard (#8293)

* Display the current plan in the project list dashboard

* Add unit tests for SubscriptionViewModelBuilder#getBestSubscription

* Handle free trial for group subscriptions

* Reuse the info-badge icon for the plan labels

* Do not display subscription status when projects are selected

* Custom tooltip for group subscriptions with team name

GitOrigin-RevId: 40982f70cf9fb7c92058e417b73c84af1648c33e
This commit is contained in:
Alexandre Bourdin 2022-06-16 16:29:49 +02:00 committed by Copybot
parent c30ec5fa7c
commit 57114c4503
10 changed files with 665 additions and 57 deletions

View file

@ -41,6 +41,7 @@ const SpellingHandler = require('../Spelling/SpellingHandler')
const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandler')
const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures')
const SubscriptionViewModelBuilder = require('../Subscription/SubscriptionViewModelBuilder')
const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
if (!affiliation.institution) return false
@ -440,7 +441,18 @@ const ProjectController = {
)
})
},
usersBestSubscription(cb) {
SubscriptionViewModelBuilder.getBestSubscription(
{ _id: userId },
(err, subscription) => {
if (err) {
// do not fail loading the project list when fetching the best subscription fails
return cb(null, { type: 'error' })
}
cb(null, subscription)
}
)
},
primaryEmailCheckActive(cb) {
SplitTestHandler.getAssignment(
req,
@ -595,11 +607,6 @@ const ProjectController = {
)
}
// Persistent upgrade prompts
const showToolbarUpgradePrompt =
!results.hasSubscription &&
!userEmails.some(e => e.emailHasInstitutionLicence)
ProjectController._injectProjectUsers(projects, (error, projects) => {
if (error != null) {
return next(error)
@ -622,7 +629,7 @@ const ProjectController = {
isOverleaf: !!Settings.overleaf,
metadata: { viewport: false },
showThinFooter: true, // don't show the fat footer on the projects dashboard, as there's a fixed space available
showToolbarUpgradePrompt,
usersBestSubscription: results.usersBestSubscription,
}
const paidUser =

View file

@ -249,6 +249,11 @@ async function updateSubscriptionFromRecurly(
subscription.recurlySubscription_id = recurlySubscription.uuid
subscription.planCode = updatedPlanCode
subscription.recurly = {
state: recurlySubscription.state,
trialStartedAt: recurlySubscription.trial_started_at,
trialEndsAt: recurlySubscription.trial_ends_at,
}
if (plan.groupPlan) {
if (!subscription.groupPlan) {

View file

@ -3,6 +3,7 @@ const RecurlyWrapper = require('./RecurlyWrapper')
const PlansLocator = require('./PlansLocator')
const SubscriptionFormatters = require('./SubscriptionFormatters')
const SubscriptionLocator = require('./SubscriptionLocator')
const SubscriptionUpdater = require('./SubscriptionUpdater')
const V1SubscriptionManager = require('./V1SubscriptionManager')
const InstitutionsGetter = require('../Institutions/InstitutionsGetter')
const PublishersGetter = require('../Publishers/PublishersGetter')
@ -10,12 +11,13 @@ const sanitizeHtml = require('sanitize-html')
const _ = require('underscore')
const async = require('async')
const SubscriptionHelper = require('./SubscriptionHelper')
const { promisify } = require('../../util/promises')
const { callbackify, promisify } = require('../../util/promises')
const {
InvalidError,
NotFoundError,
V1ConnectionError,
} = require('../Errors/Errors')
const FeaturesHelper = require('./FeaturesHelper')
function buildHostedLink(type) {
return `/user/subscription/recurly/${type}`
@ -314,6 +316,78 @@ function buildUsersSubscriptionViewModel(user, callback) {
)
}
async function getBestSubscription(user) {
let [
individualSubscription,
memberGroupSubscriptions,
currentInstitutionsWithLicence,
] = await Promise.all([
SubscriptionLocator.promises.getUsersSubscription(user),
SubscriptionLocator.promises.getMemberSubscriptions(user),
InstitutionsGetter.promises.getCurrentInstitutionsWithLicence(user._id),
])
if (individualSubscription && !individualSubscription.recurly?.state) {
const recurlySubscription = await RecurlyWrapper.promises.getSubscription(
individualSubscription.recurlySubscription_id,
{ includeAccount: true }
)
await SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
recurlySubscription,
individualSubscription
)
individualSubscription =
await SubscriptionLocator.promises.getUsersSubscription(user)
}
let bestSubscription = {
type: 'free',
}
if (currentInstitutionsWithLicence?.length) {
for (const institutionMembership of currentInstitutionsWithLicence) {
const plan = PlansLocator.findLocalPlanInSettings(
Settings.institutionPlanCode
)
if (_isPlanEqualOrBetter(plan, bestSubscription.plan)) {
bestSubscription = {
type: 'commons',
subscription: institutionMembership,
plan,
}
}
}
}
if (memberGroupSubscriptions?.length) {
for (const groupSubscription of memberGroupSubscriptions) {
const plan = PlansLocator.findLocalPlanInSettings(
groupSubscription.planCode
)
if (_isPlanEqualOrBetter(plan, bestSubscription.plan)) {
const remainingTrialDays = _getRemainingTrialDays(groupSubscription)
bestSubscription = {
type: 'group',
subscription: groupSubscription,
plan,
remainingTrialDays,
}
}
}
}
if (individualSubscription && !individualSubscription.groupPlan) {
const plan = PlansLocator.findLocalPlanInSettings(
individualSubscription.planCode
)
if (_isPlanEqualOrBetter(plan, bestSubscription.plan)) {
const remainingTrialDays = _getRemainingTrialDays(individualSubscription)
bestSubscription = {
type: 'individual',
subscription: individualSubscription,
plan,
remainingTrialDays,
}
}
}
return bestSubscription
}
function buildPlansList(currentPlan) {
const { plans } = Settings
@ -368,11 +442,30 @@ function buildPlansList(currentPlan) {
return result
}
function _isPlanEqualOrBetter(planA, planB) {
return FeaturesHelper.isFeatureSetBetter(
planA?.features || {},
planB?.features || {}
)
}
function _getRemainingTrialDays(subscription) {
const now = new Date()
const trialEndDate = subscription.recurly?.trialEndsAt
return trialEndDate && trialEndDate > now
? Math.ceil(
(trialEndDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
)
: -1
}
module.exports = {
buildUsersSubscriptionViewModel,
buildPlansList,
getBestSubscription: callbackify(getBestSubscription),
promises: {
buildUsersSubscriptionViewModel: promisify(buildUsersSubscriptionViewModel),
getRedirectToHostedPage,
getBestSubscription,
},
}

View file

@ -38,6 +38,17 @@ const SubscriptionSchema = new Schema({
},
},
},
recurly: {
state: {
type: String,
},
trialStartedAt: {
type: Date,
},
trialEndsAt: {
type: Date,
},
},
})
// Subscriptions have no v1 data to fetch

View file

@ -0,0 +1,91 @@
mixin current_plan()
if (usersBestSubscription)
.text-right.pull-right.current-plan
case usersBestSubscription.type
when 'free'
+free_plan()
when 'individual'
if (usersBestSubscription.remainingTrialDays >= 0)
+individual_plan_trial(usersBestSubscription.subscription, usersBestSubscription.plan, usersBestSubscription.remainingTrialDays)
else
+individual_plan_active(usersBestSubscription.subscription, usersBestSubscription.plan)
when 'group'
if (usersBestSubscription.remainingTrialDays >= 0)
+group_plan_trial(usersBestSubscription.subscription, usersBestSubscription.plan, usersBestSubscription.remainingTrialDays)
else
+group_plan_active(usersBestSubscription.subscription, usersBestSubscription.plan)
when 'commons'
+commons_plan(usersBestSubscription.subscription, usersBestSubscription.plan)
mixin individual_plan_trial(subscription, plan, remainingTrialDays)
a.current-plan-label(
tooltip=translate('plan_tooltip', { plan: plan.name }),
tooltip-placement="bottom"
href="/learn/how-to/Overleaf_premium_features"
)
if (remainingTrialDays === 1)
| !{translate('trial_last_day')}
span.info-badge
else
| !{translate('trial_remaining_days', { days: remainingTrialDays })}
span.info-badge
mixin individual_plan_active(subscription, plan)
a.current-plan-label(
tooltip=translate('plan_tooltip', {plan: plan.name}),
tooltip-placement="bottom"
href="/learn/how-to/Overleaf_premium_features"
)
| !{translate('premium_plan_label')}
span.info-badge
mixin group_plan_trial(subscription, plan, remainingTrialDays)
a.current-plan-label(
tooltip=translate(subscription.teamName != null ? 'group_plan_with_name_tooltip' : 'group_plan_tooltip', { plan: plan.name, groupName: subscription.teamName }),
tooltip-placement="bottom"
href="/learn/how-to/Overleaf_premium_features"
)
if (remainingTrialDays === 1)
| !{translate('trial_last_day')}
span.info-badge
else
| !{translate('trial_remaining_days', { days: remainingTrialDays })}
span.info-badge
mixin group_plan_active(subscription, plan)
a.current-plan-label(
tooltip=translate(subscription.teamName != null ? 'group_plan_with_name_tooltip' : 'group_plan_tooltip', { plan: plan.name, groupName: subscription.teamName }),
tooltip-placement="bottom"
href="/learn/how-to/Overleaf_premium_features"
)
| !{translate('premium_plan_label')}
span.info-badge
mixin commons_plan(subscription, plan)
a.current-plan-label(
tooltip=translate('commons_plan_tooltip', { plan: plan.name, institution: subscription.name }),
tooltip-placement="bottom"
href="/learn/how-to/Overleaf_premium_features"
)
| !{translate('premium_plan_label')}
span.info-badge
mixin free_plan()
a.current-plan-label(
tooltip=translate('free_plan_tooltip'),
tooltip-placement="bottom"
href="/learn/how-to/Overleaf_premium_features"
)
| !{translate('free_plan_label')}
span.info-badge
|
a.btn.btn-primary(
href="/user/subscription/plans"
event-tracking="upgrade-button-click"
event-tracking-mb="true"
event-tracking-ga="subscription-funnel"
event-tracking-action="dashboard-top"
event-tracking-label="upgrade"
event-tracking-trigger="click"
event-segmentation='{"source": "dashboard-top"}'
) Upgrade

View file

@ -1,3 +1,5 @@
include ./_current_plan_mixins
.row
.col-xs-12(ng-cloak)
form.project-search.form-horizontal(role="form")
@ -23,21 +25,9 @@
ng-show="searchText.value.length > 0"
) #{translate('clear_search')}
.project-tools(ng-cloak)
if (showToolbarUpgradePrompt)
.project-list-upgrade-prompt(ng-cloak ng-hide="selectedProjects.length > 0")
span You're on the free plan  
a.btn.btn-primary(
href="/user/subscription/plans"
event-tracking="upgrade-button-click"
event-tracking-mb="true"
event-tracking-ga="subscription-funnel"
event-tracking-action="dashboard-top"
event-tracking-label="upgrade"
event-tracking-trigger="click"
event-segmentation='{"source": "dashboard-top"}'
) Upgrade
.project-list-upgrade-prompt(ng-cloak ng-hide="selectedProjects.length > 0")
+current_plan()
.btn-toolbar
.btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-default(

View file

@ -81,6 +81,7 @@
.project-tools {
display: inline;
float: right;
line-height: @line-height-base;
}
.tags-dropdown-menu {
@ -563,3 +564,12 @@ ul.project-list {
margin-left: -100px;
}
}
.current-plan {
vertical-align: middle;
line-height: @line-height-base;
a.current-plan-label {
text-decoration: none;
color: @text-color;
}
}

View file

@ -1765,5 +1765,14 @@
"try_to_compile_despite_errors": "Try to compile despite errors",
"stop_on_first_error_enabled_title": "No PDF: Stop on first error enabled",
"stop_on_first_error_enabled_description": "<0>“Stop on first error” is enabled.</0> Disabling it may allow the compiler to produce a PDF (but your project will still have errors).",
"disable_stop_on_first_error": "Disable “Stop on first error”"
"disable_stop_on_first_error": "Disable “Stop on first error”",
"free_plan_label": "Youre on the <b>free plan</b>",
"free_plan_tooltip": "Click to find out how you could benefit from Overleaf premium features!",
"premium_plan_label": "Youre using <b>Overleaf Premium</b>",
"plan_tooltip": "Youre on the __plan__ plan. Click to find out how you could benefit from Overleaf premium features!",
"group_plan_tooltip": "You are on the __plan__ plan as a member of a group subscription. Click to find out how you could benefit from Overleaf premium features!",
"group_plan_with_name_tooltip": "You are on the __plan__ plan as a member of a group subscription, __groupName__. Click to find out how you could benefit from Overleaf premium features!",
"commons_plan_tooltip": "Youre on the __plan__ plan because of your affiliation with __institution__. Click to find out how you could benefit from Overleaf premium features!",
"trial_last_day": "This is the last day of your <b>Overleaf Premium</b> trial",
"trial_remaining_days": "__days__ more days on your <b>Overleaf Premium</b> trial"
}

View file

@ -137,6 +137,9 @@ describe('ProjectController', function () {
this.InstitutionsFeatures = {
hasLicence: sinon.stub().callsArgWith(1, null, false),
}
this.SubscriptionViewModelBuilder = {
getBestSubscription: sinon.stub().yields(null, { type: 'free' }),
}
this.ProjectController = SandboxedModule.require(MODULE_PATH, {
requires: {
@ -173,6 +176,8 @@ describe('ProjectController', function () {
'../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
'../../models/Project': {},
'../Analytics/AnalyticsManager': { recordEventForUser: () => {} },
'../Subscription/SubscriptionViewModelBuilder':
this.SubscriptionViewModelBuilder,
'../../infrastructure/Modules': {
hooks: { fire: sinon.stub().yields(null, []) },
},
@ -515,6 +520,14 @@ describe('ProjectController', function () {
this.ProjectController.projectListPage(this.req, this.res)
})
it("should send the user's best subscription", function (done) {
this.res.render = (pageName, opts) => {
expect(opts.usersBestSubscription).to.deep.include({ type: 'free' })
done()
}
this.ProjectController.projectListPage(this.req, this.res)
})
describe('front widget', function (done) {
beforeEach(function () {
this.settings.overleaf = {
@ -555,40 +568,6 @@ describe('ProjectController', function () {
})
})
describe('persistent upgrade prompt', function () {
it('should show for a user without a subscription or only non-paid affiliations', function (done) {
this.res.render = (pageName, opts) => {
expect(opts.showToolbarUpgradePrompt).to.equal(true)
done()
}
this.ProjectController.projectListPage(this.req, this.res)
})
it('should not show for a user with a subscription', function (done) {
this.LimitationsManager.hasPaidSubscription = sinon
.stub()
.callsArgWith(1, null, true)
this.res.render = (pageName, opts) => {
expect(opts.showToolbarUpgradePrompt).to.equal(false)
done()
}
this.ProjectController.projectListPage(this.req, this.res)
})
it('should not show for a user with an affiliated paid university', function (done) {
const emailWithProAffiliation = {
email: 'pro@example.com',
emailHasInstitutionLicence: true,
}
this.UserGetter.getUserFullEmails = sinon
.stub()
.yields(null, [emailWithProAffiliation])
this.res.render = (pageName, opts) => {
expect(opts.showToolbarUpgradePrompt).to.equal(false)
done()
}
this.ProjectController.projectListPage(this.req, this.res)
})
})
describe('With Institution SSO feature', function () {
beforeEach(function (done) {
this.institutionEmail = 'test@overleaf.com'

View file

@ -0,0 +1,413 @@
const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { assert } = require('chai')
const modulePath =
'../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder'
describe('SubscriptionViewModelBuilder', function () {
beforeEach(function () {
this.user = { _id: '5208dd34438842e2db333333' }
this.recurlySubscription_id = '123abc456def'
this.planCode = 'collaborator_monthly'
this.planFeatures = {
compileGroup: 'priority',
collaborators: -1,
compileTimeout: 240,
}
this.plan = {
planCode: this.planCode,
features: this.planFeatures,
}
this.individualSubscription = {
planCode: this.planCode,
plan: this.plan,
recurlySubscription_id: this.recurlySubscription_id,
recurly: {
state: 'active',
},
}
this.groupPlanCode = 'group_collaborator_monthly'
this.groupPlanFeatures = {
compileGroup: 'priority',
collaborators: 10,
compileTimeout: 240,
}
this.groupPlan = {
planCode: this.groupPlanCode,
features: this.groupPlanFeatures,
}
this.groupSubscription = {
planCode: this.groupPlanCode,
plan: this.plan,
recurly: {
state: 'active',
},
}
this.commonsPlanCode = 'commons_license'
this.commonsPlanFeatures = {
compileGroup: 'priority',
collaborators: '-1',
compileTimeout: 240,
}
this.commonsPlan = {
planCode: this.commonsPlanCode,
features: this.commonsPlanFeatures,
}
this.commonsSubscription = {
planCode: this.commonsPlanCode,
plan: this.commonsPlan,
name: 'Digital Science',
}
this.Settings = {
institutionPlanCode: this.commonsPlanCode,
}
this.SubscriptionLocator = {
promises: {
getUsersSubscription: sinon.stub().resolves(),
getMemberSubscriptions: sinon.stub().resolves(),
},
findLocalPlanInSettings: sinon.stub(),
}
this.InstitutionsGetter = {
promises: {
getCurrentInstitutionsWithLicence: sinon.stub().resolves(),
},
}
this.RecurlyWrapper = {
promises: {
getSubscription: sinon.stub().resolves(),
},
}
this.SubscriptionUpdater = {
promises: {
updateSubscriptionFromRecurly: sinon.stub().resolves(),
},
}
this.PlansLocator = {
findLocalPlanInSettings: sinon.stub(),
}
this.SubscriptionViewModelBuilder = SandboxedModule.require(modulePath, {
requires: {
'@overleaf/settings': this.Settings,
'./SubscriptionLocator': this.SubscriptionLocator,
'../Institutions/InstitutionsGetter': this.InstitutionsGetter,
'./RecurlyWrapper': this.RecurlyWrapper,
'./SubscriptionUpdater': this.SubscriptionUpdater,
'./PlansLocator': this.PlansLocator,
'./V1SubscriptionManager': {},
'./SubscriptionFormatters': {},
'../Publishers/PublishersGetter': {},
'./SubscriptionHelper': {},
},
})
this.PlansLocator.findLocalPlanInSettings
.withArgs(this.planCode)
.returns(this.plan)
.withArgs(this.groupPlanCode)
.returns(this.groupPlan)
.withArgs(this.commonsPlanCode)
.returns(this.commonsPlan)
})
describe('getBestSubscription', function () {
it('should return a free plan when user has no subscription or affiliation', async function () {
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
assert.deepEqual(usersBestSubscription, { type: 'free' })
})
describe('with a individual subscription only', function () {
it('should return a individual subscription when user has an active one', async function () {
this.SubscriptionLocator.promises.getUsersSubscription
.withArgs(this.user)
.resolves(this.individualSubscription)
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
assert.deepEqual(usersBestSubscription, {
type: 'individual',
subscription: this.individualSubscription,
plan: this.plan,
remainingTrialDays: -1,
})
})
it('should return a individual subscription with remaining free trial days', async function () {
const threeDaysLater = new Date()
threeDaysLater.setDate(threeDaysLater.getDate() + 3)
this.individualSubscription.recurly.trialEndsAt = threeDaysLater
this.SubscriptionLocator.promises.getUsersSubscription
.withArgs(this.user)
.resolves(this.individualSubscription)
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
assert.deepEqual(usersBestSubscription, {
type: 'individual',
subscription: this.individualSubscription,
plan: this.plan,
remainingTrialDays: 3,
})
})
it('should return a individual subscription with free trial on last day', async function () {
const threeHoursLater = new Date()
threeHoursLater.setTime(threeHoursLater.getTime() + 3 * 60 * 60 * 1000)
this.individualSubscription.recurly.trialEndsAt = threeHoursLater
this.SubscriptionLocator.promises.getUsersSubscription
.withArgs(this.user)
.resolves(this.individualSubscription)
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
assert.deepEqual(usersBestSubscription, {
type: 'individual',
subscription: this.individualSubscription,
plan: this.plan,
remainingTrialDays: 1,
})
})
it('should update subscription if recurly data is missing', async function () {
this.individualSubscriptionWithoutRecurly = {
planCode: this.planCode,
plan: this.plan,
recurlySubscription_id: this.recurlySubscription_id,
}
this.recurlySubscription = {
state: 'active',
}
this.SubscriptionLocator.promises.getUsersSubscription
.withArgs(this.user)
.onCall(0)
.resolves(this.individualSubscriptionWithoutRecurly)
.withArgs(this.user)
.onCall(1)
.resolves(this.individualSubscription)
this.RecurlyWrapper.promises.getSubscription
.withArgs(this.individualSubscription.recurlySubscription_id, {
includeAccount: true,
})
.resolves(this.recurlySubscription)
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
sinon.assert.calledWith(
this.RecurlyWrapper.promises.getSubscription,
this.individualSubscriptionWithoutRecurly.recurlySubscription_id,
{ includeAccount: true }
)
sinon.assert.calledWith(
this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly,
this.recurlySubscription,
this.individualSubscriptionWithoutRecurly
)
assert.deepEqual(usersBestSubscription, {
type: 'individual',
subscription: this.individualSubscription,
plan: this.plan,
remainingTrialDays: -1,
})
})
})
it('should return a group subscription when user has one', async function () {
this.SubscriptionLocator.promises.getMemberSubscriptions
.withArgs(this.user)
.resolves([this.groupSubscription])
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
assert.deepEqual(usersBestSubscription, {
type: 'group',
subscription: this.groupSubscription,
plan: this.groupPlan,
remainingTrialDays: -1,
})
})
it('should return a commons subscription when user has an institution affiliation', async function () {
this.InstitutionsGetter.promises.getCurrentInstitutionsWithLicence
.withArgs(this.user._id)
.resolves([this.commonsSubscription])
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
assert.deepEqual(usersBestSubscription, {
type: 'commons',
subscription: this.commonsSubscription,
plan: this.commonsPlan,
})
})
})
describe('with multiple subscriptions', function () {
beforeEach(function () {
this.SubscriptionLocator.promises.getUsersSubscription
.withArgs(this.user)
.resolves(this.individualSubscription)
this.SubscriptionLocator.promises.getMemberSubscriptions
.withArgs(this.user)
.resolves([this.groupSubscription])
this.InstitutionsGetter.promises.getCurrentInstitutionsWithLicence
.withArgs(this.user._id)
.resolves([this.commonsSubscription])
})
it('should return individual when the individual subscription has the best feature set', async function () {
this.commonsPlan.features = {
compileGroup: 'standard',
collaborators: 1,
compileTimeout: 60,
}
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
assert.deepEqual(usersBestSubscription, {
type: 'individual',
subscription: this.individualSubscription,
plan: this.plan,
remainingTrialDays: -1,
})
})
it('should return group when the group subscription has the best feature set', async function () {
this.plan.features = {
compileGroup: 'standard',
collaborators: 1,
compileTimeout: 60,
}
this.commonsPlan.features = {
compileGroup: 'standard',
collaborators: 1,
compileTimeout: 60,
}
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
assert.deepEqual(usersBestSubscription, {
type: 'group',
subscription: this.groupSubscription,
plan: this.groupPlan,
remainingTrialDays: -1,
})
})
it('should return commons when the commons affiliation has the best feature set', async function () {
this.plan.features = {
compileGroup: 'priority',
collaborators: 5,
compileTimeout: 240,
}
this.groupPlan.features = {
compileGroup: 'standard',
collaborators: 1,
compileTimeout: 60,
}
this.commonsPlan.features = {
compileGroup: 'priority',
collaborators: -1,
compileTimeout: 240,
}
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
assert.deepEqual(usersBestSubscription, {
type: 'commons',
subscription: this.commonsSubscription,
plan: this.commonsPlan,
})
})
it('should return individual with equal feature sets', async function () {
this.plan.features = {
compileGroup: 'priority',
collaborators: -1,
compileTimeout: 240,
}
this.groupPlan.features = {
compileGroup: 'priority',
collaborators: -1,
compileTimeout: 240,
}
this.commonsPlan.features = {
compileGroup: 'priority',
collaborators: -1,
compileTimeout: 240,
}
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
assert.deepEqual(usersBestSubscription, {
type: 'individual',
subscription: this.individualSubscription,
plan: this.plan,
remainingTrialDays: -1,
})
})
it('should return group over commons with equal feature sets', async function () {
this.plan.features = {
compileGroup: 'standard',
collaborators: 1,
compileTimeout: 60,
}
this.groupPlan.features = {
compileGroup: 'priority',
collaborators: -1,
compileTimeout: 240,
}
this.commonsPlan.features = {
compileGroup: 'priority',
collaborators: -1,
compileTimeout: 240,
}
const usersBestSubscription =
await this.SubscriptionViewModelBuilder.promises.getBestSubscription(
this.user
)
assert.deepEqual(usersBestSubscription, {
type: 'group',
subscription: this.groupSubscription,
plan: this.groupPlan,
remainingTrialDays: -1,
})
})
})
})