mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
[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:
parent
c30ec5fa7c
commit
57114c4503
10 changed files with 665 additions and 57 deletions
|
@ -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 =
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
91
services/web/app/views/project/list/_current_plan_mixins.pug
Normal file
91
services/web/app/views/project/list/_current_plan_mixins.pug
Normal 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
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <b>free plan</b>",
|
||||
"free_plan_tooltip": "Click to find out how you could benefit from Overleaf premium features!",
|
||||
"premium_plan_label": "You’re using <b>Overleaf Premium</b>",
|
||||
"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 <b>Overleaf Premium</b> trial",
|
||||
"trial_remaining_days": "__days__ more days on your <b>Overleaf Premium</b> trial"
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue