diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js
index c24a7a2a97..efe691bab2 100644
--- a/services/web/app/src/Features/Project/ProjectController.js
+++ b/services/web/app/src/Features/Project/ProjectController.js
@@ -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 =
diff --git a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js
index 0a243c9898..65d9bf7417 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js
@@ -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) {
diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
index dfc91aaee8..6683d74f00 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
@@ -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,
},
}
diff --git a/services/web/app/src/models/Subscription.js b/services/web/app/src/models/Subscription.js
index f8f6459484..a557d61873 100644
--- a/services/web/app/src/models/Subscription.js
+++ b/services/web/app/src/models/Subscription.js
@@ -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
diff --git a/services/web/app/views/project/list/_current_plan_mixins.pug b/services/web/app/views/project/list/_current_plan_mixins.pug
new file mode 100644
index 0000000000..f1f99cb831
--- /dev/null
+++ b/services/web/app/views/project/list/_current_plan_mixins.pug
@@ -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
diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug
index 74f86f4653..f8ed41c481 100644
--- a/services/web/app/views/project/list/project-list.pug
+++ b/services/web/app/views/project/list/project-list.pug
@@ -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(
diff --git a/services/web/frontend/stylesheets/app/project-list.less b/services/web/frontend/stylesheets/app/project-list.less
index c9c5c46ee0..2c796e412f 100644
--- a/services/web/frontend/stylesheets/app/project-list.less
+++ b/services/web/frontend/stylesheets/app/project-list.less
@@ -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;
+ }
+}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 97e8dfdcf2..ca4a3e100f 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -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": "You’re on the free plan",
+ "free_plan_tooltip": "Click to find out how you could benefit from Overleaf premium features!",
+ "premium_plan_label": "You’re using Overleaf Premium",
+ "plan_tooltip": "You’re 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": "You’re 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 Overleaf Premium trial",
+ "trial_remaining_days": "__days__ more days on your Overleaf Premium trial"
}
diff --git a/services/web/test/unit/src/Project/ProjectControllerTests.js b/services/web/test/unit/src/Project/ProjectControllerTests.js
index 0fcf34e855..b45c3b8b5f 100644
--- a/services/web/test/unit/src/Project/ProjectControllerTests.js
+++ b/services/web/test/unit/src/Project/ProjectControllerTests.js
@@ -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'
diff --git a/services/web/test/unit/src/Subscription/SubscriptionViewModelBuilderTests.js b/services/web/test/unit/src/Subscription/SubscriptionViewModelBuilderTests.js
new file mode 100644
index 0000000000..94d1441bec
--- /dev/null
+++ b/services/web/test/unit/src/Subscription/SubscriptionViewModelBuilderTests.js
@@ -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,
+ })
+ })
+ })
+})